Skip to content

Repository files navigation

trashhttp

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.

License: MIT OR Apache-2.0


Quick start

# 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.


What you get

Two themes, one HTML structure

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.

Right details pane (Bitline)

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)"

Multi-select like macOS Finder

  • 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 hit GET /__trash/api/zip?paths=a|b|c (server-side streaming Zip, each path through fs::safe_join).

Authentic WinXP File Explorer

  • 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) and List (icon + filename, compact)
  • WinXP-themed Aero buttons (Back / Forward / Up / Refresh)
  • Address bar with Address → + Go button

10-foot TV mode

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.

Click sounds (WinXP-flavor, Web Audio synth, no assets)

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"].

Live filter input

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.

Server features

  • HTTP file serve with Range (206 Partial Content), ETag, conditional If-None-Match
  • Force-download flag: ?download=1 sets Content-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 through safe_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:PASS is set) — constant-time compare via subtle, 401 + WWW-Authenticate: Basic on failure, no-op when --auth absent
  • 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

SSH + SFTP (--ssh)

  • 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 OpenSSH authorized_keys file; SHA-256 fingerprint match
  • ed25519 host key auto-generated on first run, persisted to $XDG_DATA_HOME/trashhttp/ssh/ed25519 (mode 0600 on Unix)
  • Subcommands: trashhttp gen-ssh-host-key -f, trashhttp ssh-fingerprint
  • Same fs::path_guard::safe_join chokepoint as HTTP — SFTP /foo/bar traversal stops at the served root
  • Writes gated on --write: atomic temp→final rename on close, mkdir/rmdir/remove/rename all routed through the guard
  • All SSH/SFTP events publish to the same EventBus that feeds the TUI + SSE
sftp -P 2222 me@laptop.local
# put / get / mkdir / rename / rm all work

Terminal

  • 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>

Mobile responsiveness

  • < 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

Gamepad

  • Web Gamepad API poll loop fires synthetic ArrowUp / Down / Left / Right / Enter / Escape events on the focused element (D-pad + face buttons)
  • Loop only runs while a pad is connected — zero idle CPU when disconnected

Tests

cargo test

24+ 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.


CLI

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

API

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

Building from source

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 --release

Release binary is ~7 MB on Apple Silicon. Single static binary, zero runtime deps.


Security defaults

  • 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 s slows brute force; host key persisted at 0600 on Unix
  • X-Content-Type-Options: nosniff, server header set
  • Refuse-to-start: --ssh + no auth method exits 2
  • Warns on 0.0.0.0 + --write + no --auth (and skips banner if --silent)

See SECURITY.md for the threat model.


What's deferred

Honest list — promised in the roadmap but not yet wired into the runtime:

  • HTTPS / --tlsrustls + rcgen compiled, CSS + cert-gen subcommands work, but the runtime doesn't bind via axum_server::bind_rustls yet
  • WebDAVdav-server integration compiled; not mounted at /dav in 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 tapdist-workspace.toml exists; tap repo not bootstrapped

License

Dual-licensed under either of:

at your option.

About

Single-binary Rust local file server with two themed UIs (Bitline + WinXP), SFTP, TUI, TV mode

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages