Zero-dependency Python CLI for sharing local directories over HTTP with authentication, file browsing, and ZIP downloads
Project description
neev
Zero-dependency Python CLI for sharing local directories over HTTP — with auth, file browsing, ZIP downloads, and uploads.
Table of Contents
- Why neev?
- Install
- Quick Start
- CLI Reference
- Configuration File (
neev.toml) - Environment Variables
- Configuration Precedence
- Features
- HTTP API
- Security Model
- Recipes
- Architecture
- Development
- Troubleshooting
- FAQ
- Contributing
- License
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:
- Local —
neev.tomlin the served directory. Project-specific settings. - User — per-user defaults at:
$XDG_CONFIG_HOME/neev/neev.tomlifXDG_CONFIG_HOMEis set, or%APPDATA%\neev\neev.tomlon Windows, or~/.config/neev/neev.tomlelsewhere (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:
- CLI flags (explicit
--foo bar) - Local
neev.tomlin the served directory - User
neev.tomlat~/.config/neev/neev.toml(or XDG /%APPDATA%equivalent) - 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-sizeMB; oversized archives return413 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
401with aWWW-Authenticate: Basicchallenge.
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 with403. - Credential leaks in timing —
hmac.compare_digestfor 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:
- Always set
--auth. - Use a strong password (treat as a bearer token).
- Put it behind a TLS-terminating proxy.
- Prefer
--read-onlyunless uploads are required. - Keep
--show-hiddenoff.
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
Refererheader may carry?share=...to that third party. Prefer short--expiresfor browser-facing links, or instruct recipients to usecurl/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:
- Stdlib only. Every feature is built on
http.server,zipfile,argparse,html,tomllib,base64,hmac. - Defense-in-depth on paths. Every filesystem op goes through
fs.pywhich resolves and verifies containment. - Frozen config.
Configis built once at startup; request handlers never mutate configuration state. - No dynamic imports, no plugin system. Small, auditable surface area.
- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e61698c49eab2256249d8bf231a1c94c899a578ed37e0e9425e4b4fa890907b7
|
|
| MD5 |
12b3130bff547ebf863084717a24ba5b
|
|
| BLAKE2b-256 |
cb38554f7ba3f1a3c3a88b0444e0f5a2578b656a9a7231535dfd0687fb2cca10
|
Provenance
The following attestation bundles were made for neev-0.2.0.tar.gz:
Publisher:
publish.yml on prabhuakshay/neev
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
neev-0.2.0.tar.gz -
Subject digest:
e61698c49eab2256249d8bf231a1c94c899a578ed37e0e9425e4b4fa890907b7 - Sigstore transparency entry: 1764246770
- Sigstore integration time:
-
Permalink:
prabhuakshay/neev@5a44838e89273dfa4b848a80554b9b382e0c20d7 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/prabhuakshay
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5a44838e89273dfa4b848a80554b9b382e0c20d7 -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dcfc909ce95a6fac694fcdbdea5b46131a98e6a3d2a79eb8a94f1dcfe192b12f
|
|
| MD5 |
2990c5de58671b9a51ba13e2fc7b3f44
|
|
| BLAKE2b-256 |
71e455ea29ed46424db543a75867811a8106b7e58659821fd608f5d794eed656
|
Provenance
The following attestation bundles were made for neev-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on prabhuakshay/neev
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
neev-0.2.0-py3-none-any.whl -
Subject digest:
dcfc909ce95a6fac694fcdbdea5b46131a98e6a3d2a79eb8a94f1dcfe192b12f - Sigstore transparency entry: 1764246976
- Sigstore integration time:
-
Permalink:
prabhuakshay/neev@5a44838e89273dfa4b848a80554b9b382e0c20d7 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/prabhuakshay
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5a44838e89273dfa4b848a80554b9b382e0c20d7 -
Trigger Event:
release
-
Statement type: