Take out the trash. Serve the rest beautifully.
A single-binary local file server with two hand-crafted web UIs (a nerdy terminal aesthetic + an authentic Windows XP Luna look), SFTP, resumable uploads, a ratatui TUI, click-sound feedback, and a 10-foot TV mode. Built in Rust.
# Serve current directory (read-only, LAN-visible)
trashhttp .
# Serve with uploads + writes enabled
trashhttp --write .
# Or also expose SFTP on port 2222 (requires --auth)
trashhttp --write --ssh --auth me:hunter2 .Open the URL printed in the banner. The page auto-picks the right theme/mode for your device:
- Desktop / mobile → Bitline (default, dark terminal + phosphor-green accent)
- TV / set-top box (Tizen, WebOS, GoogleTV, Roku, …) → Bitline auto-enters TV mode
- Console (PlayStation, Xbox, Nintendo) → Bitline + controller (D-pad navigable)
Switch theme any time via the Bitline / WinXP segmented pill in the title bar. Toggle TV mode with the persistent TV icon next to it.
| Theme | Vibe |
|---|---|
| Bitline (default) | Deep terminal black #0a0a0a, phosphor green #00ff9d, JetBrains Mono, hairline 1px borders, faint dot-grid overlay |
| WinXP | Authentic Luna chrome — blue gradient title bar, Tahoma/Segoe UI, task-pane sidebar with Luna-blue panel headers, white file list with Name/Size/Kind/Modified columns + Aero-style buttons |
All chrome is pure CSS variables — the two themes share the exact same HTML markup. No frameworks. No node_modules at runtime. Single self-contained web/build/index.html (~145 KB) embedded via rust-embed.
Always-visible 280 px sidebar on the right. Three states:
- No selection → folder summary (items / folders / files / total size / current path with click-to-copy)
- Single item → big glyph + name + kind + size + modified + MIME + path + Download / Copy URL buttons
- 2+ items selected → "N items selected · total size · Download as ZIP · Clear (ESC)"
- Click = single select + show in right pane
- Cmd/Ctrl + click = toggle individual
- Shift + click = range select from anchor
- Cmd/Ctrl + A = select everything (files + folders)
- Esc = clear selection
- 3 entry points for batch download: toolbar pill
⬇ Download N, bottom floating sel-bar, details-pane button. All hitGET /__trash/api/zip?paths=a|b|c(server-side streaming Zip, each path throughfs::safe_join).
- Classic blue Luna title bar with white shadowed brand
- File-Explorer task pane on the left (Pinned + Tools panels with the iconic blue gradient headers)
- Two view toggles:
Details(Name / Size / Kind / Modified columns, default) andList(icon + filename, compact) - WinXP-themed Aero buttons (
Back / Forward / Up / Refresh) - Address bar with
Address →+ Go button
Click the TV icon in the title bar (or hit ?mode=tv). The whole window scales up for TV viewing distance:
- 22 px base font, 60 px row height, 48 px buttons
- 4 px phosphor-green focus ring + 24 px outer glow on the focused row
- Sidebar + details pane hidden, statusbar simplified
- Grain / dot-grid overlays auto-disabled
- Statusbar shows D-pad hint:
↑↓ navigate · ENTER open · ESC exit TV - Persists to
localStorage["trashhttp:mode"]. Auto-enables for set-top-box UAs.
Two-layer synth click (filtered noise burst + decaying sine) plays on every interactive control. Three timbres:
click— buttons, tabs, rows (40 ms)navigate— folder open /loadPath(rising sweep, 120 ms)error— failed API call (descending sweep, 240 ms)
Theme-aware: Bitline = drier high-pitch tick (2100 Hz + 3600 Hz bandpass noise). WinXP = softer Luna click-tock (1500 Hz + 2200 Hz). Persistent speaker icon next to the TV toggle mutes / unmutes; respects prefers-reduced-motion: reduce. Persists to localStorage["trashhttp:sound"].
Real <input type="search"> in the toolbar with magnifying-glass background icon. Debounced 120 ms substring filter over the loaded list (case-insensitive). Esc clears + blurs.
- HTTP file serve with
Range(206 Partial Content),ETag, conditionalIf-None-Match - Force-download flag:
?download=1setsContent-Disposition: attachment(single-click on a file in the UI sends this so browsers don't auto-render JSON/MD/HTML) - Streaming Zip archive endpoint:
?path=folder— recursive Zip of one directory?paths=a|b|c— pipe-separated, each throughsafe_join, includes folders + files
- JPEG thumbnail endpoint with on-disk cache (
/__trash/api/thumb?path=...&size=320) - Server-Sent Events live request log (
/__trash/api/events) — http requests + SSH connects + SFTP ops in one stream - HTTP Basic Auth middleware (when
--auth USER:PASSis set) — constant-time compare viasubtle,401 + WWW-Authenticate: Basicon failure, no-op when--authabsent - Path-traversal guard (
fs::path_guard::safe_join) — proptest + adversarial table cover symlink escape,.., NUL, Windows reserved names - Read-only by default; write ops gated on
--write - Refuse-to-start on
--ssh + no --auth + no --ssh-authorized-keys
- russh 0.60 + russh-sftp 2.1
- SFTP-only — shell, exec, pty, port-forwarding all rejected with a friendly banner
- Password auth — constant-time compare, reuses
--auth USER:PASS - Public-key auth —
--ssh-authorized-keys <PATH>loads an OpenSSHauthorized_keysfile; SHA-256 fingerprint match - ed25519 host key auto-generated on first run, persisted to
$XDG_DATA_HOME/trashhttp/ssh/ed25519(mode0600on Unix) - Subcommands:
trashhttp gen-ssh-host-key -f,trashhttp ssh-fingerprint - Same
fs::path_guard::safe_joinchokepoint as HTTP — SFTP/foo/bartraversal stops at the served root - Writes gated on
--write: atomic temp→final rename onclose,mkdir/rmdir/remove/renameall routed through the guard - All SSH/SFTP events publish to the same
EventBusthat feeds the TUI + SSE
sftp -P 2222 me@laptop.local
# put / get / mkdir / rename / rm all work- Warm coffee-colored startup banner with QR code (auto-detects LAN IP — phones on the same Wi-Fi can scan to join)
- ratatui TUI via
--tui: header (URL / LAN / theme / uptime) + live request log + footer hotkeys - Tracing subscriber muted when TUI owns the terminal
- Shell completions for bash / zsh / fish / powershell / elvish via
trashhttp completions <shell>
< 1024 px— right details pane hidden< 720 px— sidebar collapses to a slide-in drawer (hamburger ≡), toolbar stacks vertically, file-list columns shrink< 420 px— theme pill collapses to just the two color dots, TV-toggle icon-only
- Web Gamepad API poll loop fires synthetic
ArrowUp / Down / Left / Right/Enter/Escapeevents on the focused element (D-pad + face buttons) - Loop only runs while a pad is connected — zero idle CPU when disconnected
cargo test24+ unit tests covering path_guard, range, SFTP path stripping, longname formatting, tus metadata parsing, ed25519 fingerprint round-trip, constant-time password compare, basic-auth middleware.
trashhttp [PATH]
trashhttp serve [PATH] [FLAGS]
trashhttp gen-ssh-host-key [-f]
trashhttp ssh-fingerprint
trashhttp gen-cert [-f]
trashhttp trust-cert
trashhttp completions <shell>
trashhttp version
| Flag | Default | What it does |
|---|---|---|
-p, --port <PORT> |
8080 |
Auto-walks 8080-8090 if busy |
-H, --host <HOST> |
0.0.0.0 |
All interfaces. Set 127.0.0.1 for loopback only |
--strict-port |
off | Don't auto-walk |
-w, --write |
off | Enable mkdir, tus uploads, SFTP writes |
-a, --auth USER:PASS |
— | HTTP Basic Auth + SSH password creds |
-t, --theme <NAME> |
bitline |
UI's initial theme; respects ?theme= and localStorage |
--no-banner / --no-qr / -q, --silent |
— | Suppress startup chrome |
--tui |
off | Take over the terminal with the ratatui dashboard |
-v, --verbose (-vv, -vvv) |
warn | Bump tracing verbosity |
-L, --follow-symlinks |
off | Otherwise symlinks return PermissionDenied |
-o, --open |
off | Launch browser at startup |
--cors |
off | Permissive CORS headers |
--max-upload-size <BYTES> |
16 GiB | tus upload cap |
--api-prefix <PATH> |
/__trash |
API namespace |
-s, --ssh |
off | Enable SSH + SFTP (requires --auth or --ssh-authorized-keys) |
-S, --ssh-port <PORT> |
2222 |
Separate port from --port |
--ssh-host <HOST> |
= --host |
SSH bind address |
--ssh-authorized-keys <PATH> |
— | OpenSSH authorized_keys file for pubkey auth |
--ssh-keepalive <SECS> |
30 | SSH keepalive interval |
GET / → SPA shell (or JSON if Accept: application/json)
GET /*path[?download=1] → file (Range/206), or sub-dir browse, or force-attachment
HEAD /*path → headers only
GET /__trash/api/list?path=&sort=&order=&hidden=
POST /__trash/api/mkdir {path} → 204 (needs --write)
GET /__trash/api/zip?path= → streaming single-folder Zip
GET /__trash/api/zip?paths=a|b|c → streaming multi-entry Zip (folders + files)
GET /__trash/api/thumb?path=&size= → JPEG thumb (cached on disk)
GET /__trash/api/events → SSE live log
GET /__trash/api/info → server info + theme registry
GET /__trash/api/healthz → {ok:true,version:"..."}
OPTIONS /__trash/api/upload/tus → tus 1.0.0 discovery
POST /__trash/api/upload/tus → create session (Location header)
HEAD /__trash/api/upload/tus/:id → current Upload-Offset
PATCH /__trash/api/upload/tus/:id → append bytes at offset
DELETE /__trash/api/upload/tus/:id → cancel
git clone https://github.com/somchandra17/trashhttp
cd trashhttp
cargo build --release
./target/release/trashhttp .The web UI lives at web/build/index.html as a single self-contained file (two themes + the override script that wires every button to the API). It's committed to the repo and embedded into the binary via rust-embed at compile time — no Node, no pnpm, no Vite required to build.
Skip the pnpm build during cargo install:
TRASHHTTP_SKIP_WEB_BUILD=1 cargo build --releaseRelease binary is ~7 MB on Apple Silicon. Single static binary, zero runtime deps.
- Read-only by default; no writes anywhere until
--write - Path-traversal guard: every fs op canonicalizes + ancestor-checks; symlinks denied by default (opt-in via
-L) - HTTP Basic Auth: constant-time password compare via
subtle::ConstantTimeEq - SSH:
auth_rejection_time = 2 sslows brute force; host key persisted at0600on Unix X-Content-Type-Options: nosniff, server header set- Refuse-to-start:
--ssh + no auth methodexits 2 - Warns on
0.0.0.0 + --write + no --auth(and skips banner if--silent)
See SECURITY.md for the threat model.
Honest list — promised in the roadmap but not yet wired into the runtime:
- HTTPS /
--tls—rustls + rcgencompiled, CSS + cert-gen subcommands work, but the runtime doesn't bind viaaxum_server::bind_rustlsyet - WebDAV —
dav-serverintegration compiled; not mounted at/davin the runtime - HTTP delete / rename endpoints — SFTP has them; HTTP API does not
- tar.gz archives — Zip only
- In-browser preview overlay (image lightbox, PDF.js, CodeMirror editor)
- mDNS broadcast (
_trashhttp._tcp) - Homebrew tap —
dist-workspace.tomlexists; tap repo not bootstrapped
Dual-licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.