Architecture¶
qrtransfer is a small, dependency-light Python package under src/qrtransfer/.
It serves files with the standard library's http.server and renders QR codes in
the terminal.
Module map¶
src/qrtransfer/
├── __init__.py # __version__ (single source of truth)
├── __main__.py # enables `python -m qrtransfer`
├── api.py # public library surface (start/stop + re-exports)
├── cli.py # argparse, orchestration, session lifecycle
├── session.py # Session dataclass (token, limits, expiry, ...)
├── server.py # TransferServer + TransferHandler, auth flow
├── network.py # interface listing, IP detection, free-port lookup
├── config.py # persistent interface/port settings (JSON)
├── history.py # last-200-transfers JSON store
├── zipper.py # temporary zip creation
├── upload.py # streaming multipart parser + filename sanitization
├── web.py # dependency-free send/receive HTML pages
├── qr.py # QR rendering (qrcode-terminal)
└── tls.py # self-signed cert generation + SSL context
Session object¶
Every run builds one mutable Session dataclass (session.py) that carries:
- the unguessable
token(fromsecrets.token_urlsafe(8)), - the
ip,port, andscheme, password,expire, and the computedexpires_at,- the download counter and limits (
max_downloads,max_clients,max_upload_size), - file/directory paths and the
mode(sendorreceive).
The HTTP handler reads everything from this object — there are no module globals, which keeps tests deterministic and parallel-safe.
Request flow¶
- CLI (
cli.py) parses arguments, resolves the interface and port (interactively prompting when ambiguous, remembering the choice), builds aSession, prints the QR code and URL, then calls_serve. - Server (
server.py) —create_serverbinds aThreadingHTTPServeron0.0.0.0:<port>with a handler bound to the session. An optional--max-clientssemaphore rejects excess concurrent connections. - Authorization (
TransferHandler._authorize) checks, in order: - Expiry →
410ifnow > expires_at. - Token →
404if the URL path doesn't match the token. A wrong token is indistinguishable from a missing page. - Password →
401if?passed=doesn't match and theX-Passwordheader doesn't match. - Limit →
403ifmax_downloadsis already reached. Only then is the download counter incremented (send mode). - Serving — for a valid token,
do_GETrewrites the path to the real filename and delegates toSimpleHTTPRequestHandler, which streams the file. Headers are hardened withContent-Disposition,Cache-Control: no-store, andX-Content-Type-Options: nosniff. - Shutdown —
Enter/Ctrl+C,SIGTERM, expiry, or reaching the download limit stops the server. Temporary zip/text files are removed and one history entry is appended.
Receive mode¶
/ serves the receive page (web.receive_page) with a drag-and-drop client. The
page POSTs multipart/form-data to the token URL. upload.MultipartStreamReader
parses the body incrementally from the socket, so a large upload never has to
be buffered in memory. Filenames are sanitized (upload.sanitize_filename) to
defeat path traversal, uploads require Content-Length, and a 413 is returned
above --max-upload-size.
Network detection¶
network.py lists interfaces with psutil, auto-detects the IP by connecting a
UDP socket to 8.8.8.8 (no traffic is sent), and skips loopback/link-local
addresses. IPv6 mode (--ipv6) skips fe80:: link-local addresses. Free ports are
found by binding to port 0.
TLS¶
tls.py wraps the listening socket in an ssl.SSLContext. Without
--cert/--key it generates (and caches) a self-signed certificate for the
advertised IP using cryptography, with SANs for localhost, 127.0.0.1, and the
LAN IP.