Skip to content

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 (from secrets.token_urlsafe(8)),
  • the ip, port, and scheme,
  • password, expire, and the computed expires_at,
  • the download counter and limits (max_downloads, max_clients, max_upload_size),
  • file/directory paths and the mode (send or receive).

The HTTP handler reads everything from this object — there are no module globals, which keeps tests deterministic and parallel-safe.

Request flow

  1. CLI (cli.py) parses arguments, resolves the interface and port (interactively prompting when ambiguous, remembering the choice), builds a Session, prints the QR code and URL, then calls _serve.
  2. Server (server.py) — create_server binds a ThreadingHTTPServer on 0.0.0.0:<port> with a handler bound to the session. An optional --max-clients semaphore rejects excess concurrent connections.
  3. Authorization (TransferHandler._authorize) checks, in order:
  4. Expiry410 if now > expires_at.
  5. Token404 if the URL path doesn't match the token. A wrong token is indistinguishable from a missing page.
  6. Password401 if ?passed= doesn't match and the X-Password header doesn't match.
  7. Limit403 if max_downloads is already reached. Only then is the download counter incremented (send mode).
  8. Serving — for a valid token, do_GET rewrites the path to the real filename and delegates to SimpleHTTPRequestHandler, which streams the file. Headers are hardened with Content-Disposition, Cache-Control: no-store, and X-Content-Type-Options: nosniff.
  9. ShutdownEnter/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.

Back to README