No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-28 16:31:42 +02:00
docs docs: hero screenshot + punchy README; ui: inline favicon 2026-08-28 16:31:38 +02:00
src/drop docs: hero screenshot + punchy README; ui: inline favicon 2026-08-28 16:31:38 +02:00
.gitignore chore: gitignore playwright artifacts 2026-08-28 16:31:42 +02:00
AGENTS.md docs: add AGENTS.md + CLAUDE.md, enrich README (dev/contributing sections) 2026-08-28 11:17:59 +02:00
CLAUDE.md docs: add AGENTS.md + CLAUDE.md, enrich README (dev/contributing sections) 2026-08-28 11:17:59 +02:00
pyproject.toml drop: ephemeral drag&drop file receiver (domain/sslip/local modes) 2026-08-28 11:06:34 +02:00
README.md docs: hero screenshot + punchy README; ui: inline favicon 2026-08-28 16:31:38 +02:00
uv.lock drop: ephemeral drag&drop file receiver (domain/sslip/local modes) 2026-08-28 11:06:34 +02:00

⬇ drop

Send a file to a remote server by dragging it into a browser tab.

Run drop on the machine you want to receive the file (SSH session, VPS, anything Python runs on), it prints a share URL, and anyone who opens that URL can drag&drop files straight into the folder where you ran the command — no auth, no client install, no size limit.

drop share page: two files being received, one in progress
  1. drop on the server → prints https://<uid>.example.com
  2. open the URL from your laptop browser
  3. drag files → they land in the folder, streamed in 64 KiB chunks

Works on a laptop, a VPS, a Raspberry Pi, a container — with or without Caddy.

Install

cd ~/dev/py/drop
uv sync            # stdlib only — zero runtime deps
uv run drop --help

Optional global entrypoint (so drop works from anywhere):

uv tool install .   # or: pipx install .

Usage

drop                          # auto-detect best publication mode, 1h window
drop --hours 3                # 3h window
drop --max-size 2G            # reject uploads larger than 2 GiB
drop list                     # active sessions
drop down <uid|port>          # close (graceful: waits for in-flight uploads)
drop down <uid> --force       # close now (SIGKILL, no grace)

The printed URL (8 hex chars) is the shared secret — give it to whoever needs to send you a file. Sessions auto-expire (--hours N, default 1 h) and the server cleans up its own Caddy route on shutdown.

Publication modes

drop auto-detects the best way to publish the URL. Override with a flag:

Flag When URL TLS
(auto) picks the best below — —
--domain D host runs Caddy + you own *.D https://<uid>.D Let's Encrypt via Caddy
--sslip public IP, no domain https://<uid>.<ip>.sslip.io via Caddy if present, else HTTP
--local behind NAT / no public IP http://localhost:<port> via ssh -L tunnel
--direct bind all interfaces, HTTP http://<ip>:<port> none

Mode --domain

Needs Caddy with admin API on localhost:2019 and a wildcard DNS (*.D) pointing at the host, plus Let's Encrypt. drop adds an in-memory reverse_proxy route via the Caddy admin API (no Caddyfile edit, no reload) and removes it on shutdown. The first hit triggers cert issuance (~3–8 s); drop probes and prints ✓ TLS prêt.

# set the wildcard domain once, or pass --domain each time
DROP_DOMAIN=example.com drop --hours 1
# → https://<uid>.example.com

Any host running Caddy with a wildcard cert works — not specific to one VPS.

Mode --sslip (public IP, no domain)

Uses the free sslip.io wildcard DNS, which resolves <anything>.<ip>.sslip.io to <ip>. If Caddy is present, you get real HTTPS (cert on the sslip host). If not, it binds 0.0.0.0 and serves HTTP directly on a public port (open the firewall). The public IP is auto-detected via an echo service.

Mode --local (no public IP / NAT)

The server binds 127.0.0.1 only. Forward it through your existing SSH session:

# on the remote host:
drop --local
# prints:  http://localhost:57625
#          ssh -L 57625:127.0.0.1:57625 <user>@<serveur>

# on your laptop, in another terminal:
ssh -L 57625:127.0.0.1:57625 ubuntu@server
# then open http://localhost:57625

No infra needed — the SSH channel carries it. Most secure option (SSH = auth).

Large files

  • No protocol size limit. Uploads stream in 64 KiB chunks; a 10 GiB file uses ~64 KiB of RAM server-side. Caddy (domain/sslip-caddy modes) imposes no body limit by default.
  • Graceful shutdown. drop down (or auto-expiry) waits up to 10 min for in-flight uploads to finish before cutting, so a multi-GB transfer isn't truncated. --force skips the wait.
  • No resume. If the connection drops mid-transfer, the upload restarts from zero. For unstable links + very large files, prefer croc or oneshot.

How it works

browser ──HTTPS──> Caddy (TLS, route added via admin API) ──> 127.0.0.1:<port> ──> drop server ──> file in $PWD
  • src/drop/server.py — threaded HTTP server (stdlib http.server): serves the drag&drop page, streams POST /upload to disk in chunks, tracks active uploads. On SIGTERM/expiry, waits for uploads then removes the Caddy route.
  • src/drop/caddy.py — minimal Caddy admin API client (stdlib urllib).
  • src/drop/cli.py — mode detection, port/uid generation, state, lifecycle.

State: ~/.config/drop/state.json (DROP_STATE to override). Caddy admin: http://localhost:2019 (DROP_ADMIN to override).

Security

The URL is public during the window (no auth) — the random 8-hex-char uid is the secret, and the time window bounds exposure, exactly like a PairDrop code. Don't leave a session open longer than needed. For sensitive long-lived needs, put it behind Caddy basic_auth in the Caddyfile instead.

Filenames are sanitized to a basename (no path traversal, no clobbering: duplicate names get .1, .2, …).

Development

This project is stdlib-only (zero runtime dependencies) and uses uv:

uv sync                          # create/update the venv
uv run drop --hours 1            # run in dev
uv run python -c "import drop.cli, drop.server, drop.caddy"  # compile check

Manual smoke test (no formal test suite yet) — cover the three main modes:

uv run drop --domain example.com   # or --sslip / --local
# then upload via the printed URL (curl or browser), check md5sum integrity
uv run drop down <uid>                          # then verify the Caddy route is gone

Before committing: run the compile check + uv run drop --help, and make sure no test artifacts (*.bin) leak into the commit (covered by .gitignore, but check git status).

Contributing

See AGENTS.md for the project rules and design principles (also loaded as CLAUDE.md for agent harness compatibility). Key invariants to preserve:

  • Stdlib only — dependencies = [] in pyproject.toml. Discuss before adding any runtime dependency.
  • Single Python process owns the server + expiry + Caddy route cleanup. No external timer/wrapper scripts.
  • Fixed-memory streaming — 64 KiB chunks to disk, never read a whole body into RAM.
  • Graceful shutdown — the server waits for in-flight uploads (≤ 10 min) and removes its own Caddy route on exit.
  • Launch via python -m drop.server so package imports resolve.
  • The URL is the secret — no auth by default; the random uid + time window are the security boundary (PairDrop model).

License

MIT