Skip to main content

Zero-dependency Python CLI for sharing local directories over HTTP with authentication, file browsing, and ZIP downloads

Project description

neev

neev

Zero-dependency Python CLI for sharing local directories over HTTP — with auth, file browsing, ZIP downloads, and uploads.

PyPI version Python versions PyPI downloads License CI Publish Zero dependencies Coverage Ruff uv


Table of Contents


Why neev?

python -m http.server is great, but it has no authentication, no way to download a folder, no uploads, and exposes dotfiles by default. neev is the drop-in replacement with the things you actually need:

  • HTTP Basic Auth — constant-time credential comparison
  • ZIP folder downloads — streamed on the fly, no temp files
  • File uploads — opt-in, with size limits and path-traversal protection
  • Clean browser UI — dark/light theme, Markdown preview, syntax-aware file icons
  • HTTP Range support — resumable downloads, video/audio seeking
  • Concurrent requests — threaded server, not single-request-at-a-time
  • Secure defaults — localhost-only, no writes, no hidden files
  • Zero dependencies — pure Python stdlib, Python 3.11+

Built for developers sharing build artifacts, teams exchanging files on a LAN, and anyone who wants http.server but grown up.


Install

From PyPI (recommended)

# with uv (fastest)
uv tool install neev

# or pipx
pipx install neev

# or plain pip
pip install neev

Run without installing

uvx neev --dir ./public

From source

git clone https://github.com/prabhuakshay/neev
cd neev
uv sync
uv run neev --help

Requires Python 3.11 or newer. Works on Linux, macOS, and Windows.


Quick Start

# Serve the current directory on http://127.0.0.1:8000
neev

# Serve a specific directory
neev ./public

# With auth, on all interfaces, custom port
neev ./share --host 0.0.0.0 --port 8080 --auth alice:s3cret

# Full-featured: auth + uploads + zip downloads
neev ./share --auth alice:s3cret --enable-upload --enable-zip-download

Open the URL printed on startup. You'll see a file browser. If auth is enabled, your browser will prompt for credentials.


CLI Reference

neev [DIRECTORY] [OPTIONS]
neev share <PATH> [--expires SECONDS] [--write] [-d DIRECTORY]

Positional arguments

Argument Default Description
directory . Directory to serve. Must exist. Resolved to its real path (symlinks followed).

Options

Flag Default Description
--host HOST 127.0.0.1 Address to bind. Use 0.0.0.0 to expose on LAN.
--port PORT, -p PORT 8000 TCP port (1–65535).
--auth USER:PASS (none) Enable HTTP Basic Auth.
--show-hidden / --no-show-hidden off Show dotfiles and neev.toml in listings.
--enable-zip-download / --no-enable-zip-download off Allow folders to be downloaded as streamed ZIP.
--max-zip-size MB 100 Maximum ZIP archive size in MB. Rejected if exceeded.
--enable-upload / --no-enable-upload off Allow multipart file uploads from the browser.
--read-only / --no-read-only off Force-disable uploads (overrides --enable-upload).
--banner TEXT (none) Message displayed at the top of directory listings.
--public-url URL (none) External base URL when running behind a reverse proxy.
-h, --help Show help and exit.

All boolean flags use argparse.BooleanOptionalAction, so --no-<flag> works too — useful for overriding neev.toml from the CLI.

Examples

# Read-only share even if TOML enables uploads
neev ./build --read-only

# Serve on LAN with auth, show dotfiles, custom banner
neev ./code --host 0.0.0.0 --auth me:pw --show-hidden --banner "Internal only"

# Raise ZIP size cap to 500 MB
neev ./data --enable-zip-download --max-zip-size 500

neev share subcommand

Mint a signed, time-limited URL that grants scoped access to one file or folder. See the share recipe for the full flow.

Argument / flag Default Description
path File or folder under the served directory.
-d, --directory . Served directory — the scope that gives path meaning.
--expires SECONDS 86400 Token validity window in seconds.
--write off Token authorizes POST/upload. Server must still have --enable-upload.

The secret is read from neev.toml's share-secret key. It is required — neev share errors out if no secret is configured, since an ephemeral one would not match the running server's secret.


Configuration File (neev.toml)

neev reads neev.toml from two locations:

  1. Localneev.toml in the served directory. Project-specific settings.
  2. User — per-user defaults at:
    • $XDG_CONFIG_HOME/neev/neev.toml if XDG_CONFIG_HOME is set, or
    • %APPDATA%\neev\neev.toml on Windows, or
    • ~/.config/neev/neev.toml elsewhere (Linux, macOS, BSD).

Both files use the same schema. A missing file is silently skipped; a malformed file logs a warning and is ignored.

Example

# ./neev.toml
host = "0.0.0.0"
port = 9000
show-hidden = false
enable-zip-download = true
max-zip-size = 250
enable-upload = false
read-only = false
banner = "Build artifacts — ask #devops for access"

Recognized keys

TOML key Type Notes
host string Same as --host.
port integer Same as --port.
auth string "user:pass". Same as --auth.
show-hidden bool Same as --show-hidden.
enable-zip-download bool Same as --enable-zip-download.
max-zip-size integer MB. Same as --max-zip-size.
enable-upload bool Same as --enable-upload.
read-only bool Same as --read-only.
banner string Same as --banner.
public-url string Same as --public-url.
share-secret string Hex-encoded HMAC key (≥ 32 bytes after decode) for neev share tokens. Required to enable share-link auth — leave unset to disable the feature.

Denied keys: directory is never read from TOML (the served directory is always set by CLI, to avoid surprise path changes).

Unknown keys are logged at WARN level and ignored. Malformed TOML is logged and the file is skipped — neev keeps running with CLI defaults.

The neev.toml file itself is hidden from listings unless --show-hidden is set.

Credentials in user config

~/.config/neev/neev.toml is the intended home for per-user credentials (auth = "user:pass"). It's in your home directory — chmod it to 600 so other users on the machine can't read it:

chmod 600 ~/.config/neev/neev.toml

neev does not read any NEEV_* environment variables. Flags, local neev.toml, and user neev.toml are the only config sources.


Configuration Precedence

From highest to lowest — later sources only fill in values the earlier ones left unset:

  1. CLI flags (explicit --foo bar)
  2. Local neev.toml in the served directory
  3. User neev.toml at ~/.config/neev/neev.toml (or XDG / %APPDATA% equivalent)
  4. Built-in defaults (see CLI reference table)

The startup banner prints every neev.toml that contributed values, so you can always see where a setting came from.


Features

File browser

  • Directory listing with sortable columns (name, size, modified).
  • Per-file icons by extension/type.
  • Breadcrumb navigation.
  • In-browser preview for text files, Markdown (rendered), images, and PDFs.
  • Correct MIME types and charset detection per file.

ZIP folder downloads

  • Triggered by a "Download as ZIP" button on any folder (if --enable-zip-download).
  • Streamed — no temp files, constant memory usage.
  • Capped at --max-zip-size MB; oversized archives return 413 Payload Too Large.
  • Hidden files excluded unless --show-hidden.

Uploads

  • Opt-in via --enable-upload. Disabled under --read-only.
  • Multipart upload form on each directory page.
  • Filename sanitization — path traversal, null bytes, and absolute paths rejected.
  • Writes land in the directory currently being viewed.
  • Authentication (if configured) is required.

HTTP Range requests

Partial content (206) supported for all file downloads — resumable downloads, video/audio seeking, curl -C -.

Concurrency

Threaded HTTPServer — multiple clients can browse, download, and upload simultaneously.

Authentication

  • HTTP Basic Auth.
  • Credentials compared with hmac.compare_digest (constant-time).
  • Failed auth returns 401 with a WWW-Authenticate: Basic challenge.

HTTP API

neev is primarily browser-driven, but every interaction is a plain HTTP request — scriptable with curl, wget, httpie, etc.

Listing / serving files

GET /path/to/dir/      → HTML directory listing
GET /path/to/file.txt  → file contents (with Range support)

Trailing slash → directory; no slash → file. Requests to a path without a trailing slash that resolves to a directory are redirected with 301 to the canonical slashed URL.

ZIP download

GET /path/to/dir/?zip=1

Returns application/zip stream. Filename is <dirname>.zip. Disabled unless --enable-zip-download.

File preview (rendered)

GET /path/to/file.md?preview=1

Returns HTML with Markdown rendered server-side. Works for text and Markdown files.

Upload

POST /path/to/dir/
Content-Type: multipart/form-data; boundary=...

<file field named "file">

Returns 303 See Other redirect to the directory on success, 4xx on validation failure. Disabled unless --enable-upload and not --read-only.

Example with curl:

curl -u "$USER_PASS" -F "file=@./report.pdf" http://localhost:8000/uploads/

Authentication

Every endpoint honors Basic Auth when enabled:

curl -u "$USER_PASS" http://localhost:8000/
# or (equivalent) — build the header yourself
curl -H "Authorization: Basic $(printf '%s' "$USER_PASS" | base64)" http://localhost:8000/

Status codes

Code When
200 OK Successful GET of file/listing.
206 Partial Content Successful Range request.
301 Moved Permanently Directory URL missing trailing slash.
303 See Other Successful upload.
400 Bad Request Malformed request / upload.
401 Unauthorized Auth required or invalid credentials.
403 Forbidden Path traversal, access denied, or writes on read-only.
404 Not Found Path doesn't exist or hidden without --show-hidden.
405 Method Not Allowed e.g. POST when uploads disabled.
413 Payload Too Large ZIP exceeds --max-zip-size.
416 Range Not Satisfiable Invalid Range header.
500 Internal Server Error Unexpected server fault.

Security Model

What neev protects against:

  • Path traversal — every request resolves os.path.realpath() and verifies the result is a descendant of the served directory. ../, symlink escapes, and absolute paths are rejected with 403.
  • Credential leaks in timinghmac.compare_digest for both username and password.
  • Upload filename injection — rejects traversal, null bytes, and absolute paths.
  • Accidental exposure — defaults are localhost-only, no writes, no dotfiles, no ZIPs.
  • Oversized ZIP abuse — streaming ZIPs are bounded by --max-zip-size.

What neev does NOT do:

  • No HTTPS/TLS. Run behind a reverse proxy (nginx, Caddy) for internet-facing deployments.
  • No rate limiting. Use a reverse proxy or firewall.
  • No user management — single Basic Auth pair, one shared credential.
  • No virus scanning on uploads.
  • No audit log beyond stdout request logging.

If exposing to a network or internet:

  1. Always set --auth.
  2. Use a strong password (treat as a bearer token).
  3. Put it behind a TLS-terminating proxy.
  4. Prefer --read-only unless uploads are required.
  5. Keep --show-hidden off.

Recipes

Share a build artifact with a coworker

neev ./dist --host 0.0.0.0 --auth reviewer:pleasecheck
# share: http://<your-ip>:8000/

Drop box — let someone upload files to you

mkdir -p ~/inbox
neev ~/inbox --host 0.0.0.0 --auth sender:pw --enable-upload

Static preview site with Markdown rendering

neev ./docs --enable-zip-download --banner "Project docs"

Share a file with an expiring link

Hand someone a URL that lets them fetch one file (or browse one folder) without handing out your password. The link carries an HMAC signed with your server's share-secret, a path scope, and an expiry.

# First, pin a share-secret so tokens survive restarts.
# Generate 32 random bytes of hex and add to neev.toml:
python -c "import secrets; print(secrets.token_hex(32))"
# neev.toml
share-secret = "abc...paste-your-hex-here..."
public-url   = "https://share.example.com"   # so generated URLs are externally reachable

Then ask neev to mint a URL:

# 2 hours of read access to one file
neev share ./releases/v1.zip --expires 7200
# → https://share.example.com/releases/v1.zip?share=<token>

# Folder-scoped token: covers every file under ./public-builds for a day
neev share ./public-builds --expires 86400

# Drop-off link: lets the recipient upload into ./inbox for 30 minutes
neev share ./inbox --expires 1800 --write
# (only works if the server is run with --enable-upload)

Tokens are self-contained — the server does not keep any per-token state. An expired token returns 403, a tampered token returns 403, and a token for /releases will not grant access to /releases-private. Rotate share-secret in neev.toml to invalidate every outstanding token at once.

Share-link auth is opt-in: neev share (and the server's token check) requires share-secret set in neev.toml. Without it, neev share exits with a clear error rather than silently minting tokens that won't validate against a separately-running server.

Treat the URL as a bearer credential. Anyone with the link has the access it grants until expiry. A few practical notes:

  • The token sits in the query string. If the recipient opens the URL in a browser and clicks an external link, the Referer header may carry ?share=... to that third party. Prefer short --expires for browser-facing links, or instruct recipients to use curl/wget.
  • neev redacts share= values in its own request log, but TLS-terminating proxies, browser history, and any server access log between you and the recipient will not. Put neev behind HTTPS for any non-LAN use.
  • File-scoped tokens (minted for a single file) only authorize that exact path. Folder-scoped tokens authorize every file underneath the folder — pick the smaller scope when possible.

Behind Caddy with TLS

share.example.com {
    reverse_proxy localhost:8000
}
neev /srv/share --auth alice:s3cret --enable-zip-download \
    --public-url https://share.example.com

Pass --public-url so the startup banner and any server-generated absolute URLs use the externally reachable URL rather than the 127.0.0.1:8000 bind address.

Systemd service

# /etc/systemd/system/neev.service
[Unit]
Description=neev file server
After=network.target

[Service]
ExecStart=/usr/local/bin/neev /srv/share --host 127.0.0.1 --port 8000 --auth alice:s3cret --enable-zip-download
Restart=on-failure
User=neev

[Install]
WantedBy=multi-user.target

Docker one-liner

docker run --rm -p 8000:8000 -v "$PWD:/srv:ro" python:3.13-slim \
  sh -c "pip install neev && neev /srv --host 0.0.0.0 --read-only"

Architecture

src/neev/
├── cli.py                # argparse, validation, entry point
├── config.py             # frozen Config dataclass
├── toml_config.py        # neev.toml loader + CLI merge
├── server.py             # HTTPServer wiring (threaded)
├── server_core.py        # main request dispatcher
├── server_auth.py        # HTTP Basic Auth
├── server_preview.py     # file/Markdown preview
├── server_upload.py      # multipart upload handler
├── server_zip.py         # streaming ZIP response
├── server_assets.py      # bundled static assets (CSS, JS)
├── server_utils.py       # Range, redirects, MIME helpers
├── fs.py                 # path-safe filesystem ops
├── auth.py               # constant-time credential check
├── upload.py             # upload validation
├── upload_multipart.py   # multipart body parsing
├── zip.py                # streaming zip generator
├── url_utils.py          # URL encoding/decoding
├── log.py                # logging helpers + ANSI styling
├── html*.py              # HTML templating (no jinja — pure stdlib)
└── static/               # compiled CSS bundled with package

Design pillars:

  1. Stdlib only. Every feature is built on http.server, zipfile, argparse, html, tomllib, base64, hmac.
  2. Defense-in-depth on paths. Every filesystem op goes through fs.py which resolves and verifies containment.
  3. Frozen config. Config is built once at startup; request handlers never mutate configuration state.
  4. No dynamic imports, no plugin system. Small, auditable surface area.
  5. Files under 300 lines. Enforced by convention — keeps modules focused.

Development

Uses uv for everything.

Setup

git clone https://github.com/prabhuakshay/neev
cd neev
uv sync

Run

uv run neev --help
uv run neev ./tests/fixtures --enable-zip-download

Test

uv run pytest               # full suite with coverage (≥95% enforced)
uv run pytest -k upload     # filter
uv run pytest --no-cov -x   # fast iteration

Lint & type check

uv run ruff check .
uv run ruff format .
uv run ty check

Pre-commit hooks (prek)

uv run prek install
uv run prek run --all-files

The pre-commit config also recompiles the Tailwind CSS bundle when any html_*.py changes.

Build & publish

uv build                    # produces sdist + wheel in dist/
# Publishing is automated: create a GitHub Release → PyPI workflow uploads.

Troubleshooting

Symptom Likely cause Fix
Address already in use Port taken Pick a different --port or kill the prior process.
Browser keeps re-prompting for password Wrong credentials or auth off on restart Verify --auth or the auth key in your local/user neev.toml, clear browser cache.
403 Forbidden on a file that exists Path-traversal guard / symlink escape The file isn't inside the served directory's real path.
ZIP download returns 413 Folder exceeds --max-zip-size Raise the cap or download individual files.
Uploads return 405 --enable-upload not set, or --read-only on Enable uploads and disable read-only.
Dotfiles don't appear Hidden by default Use --show-hidden.
neev.toml takes effect unexpectedly Merged from served directory Check startup banner; override with CLI flags.

FAQ

Does neev support HTTPS? No. Use a reverse proxy (Caddy, nginx, Traefik) for TLS termination.

Can I serve multiple directories? Not in one process. Run multiple neev instances on different ports.

Is it safe to expose to the internet? Only behind HTTPS + auth + ideally --read-only. For anything mission-critical, reach for a proper server.

Why "neev"? "Neev" (नींव) means foundation in Hindi — a simple base to build file sharing on.

Does it work on Windows? Yes. Python 3.11+ required.

What about WebDAV / S3 / FTP? Out of scope. neev is HTTP + browser UI only.


Contributing

Issues and PRs welcome at github.com/prabhuakshay/neev.

Before submitting:

uv run ruff check . && uv run ruff format --check .
uv run ty check
uv run pytest

See CLAUDE.md for the full development philosophy (stability > features, stdlib only, files < 300 lines, etc.).


License

MIT © Akshay Prabhu

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

neev-0.2.0.tar.gz (87.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

neev-0.2.0-py3-none-any.whl (104.4 kB view details)

Uploaded Python 3

File details

Details for the file neev-0.2.0.tar.gz.

File metadata

  • Download URL: neev-0.2.0.tar.gz
  • Upload date:
  • Size: 87.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for neev-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e61698c49eab2256249d8bf231a1c94c899a578ed37e0e9425e4b4fa890907b7
MD5 12b3130bff547ebf863084717a24ba5b
BLAKE2b-256 cb38554f7ba3f1a3c3a88b0444e0f5a2578b656a9a7231535dfd0687fb2cca10

See more details on using hashes here.

Provenance

The following attestation bundles were made for neev-0.2.0.tar.gz:

Publisher: publish.yml on prabhuakshay/neev

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file neev-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: neev-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 104.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for neev-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dcfc909ce95a6fac694fcdbdea5b46131a98e6a3d2a79eb8a94f1dcfe192b12f
MD5 2990c5de58671b9a51ba13e2fc7b3f44
BLAKE2b-256 71e455ea29ed46424db543a75867811a8106b7e58659821fd608f5d794eed656

See more details on using hashes here.

Provenance

The following attestation bundles were made for neev-0.2.0-py3-none-any.whl:

Publisher: publish.yml on prabhuakshay/neev

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page