Skip to content

Case study, open source, Python

Open Transfer

AirDrop for every device, straight from the browser.

Role
Author
Period
2024 to 2026
Status
v2.0.0, stable
License
MIT
Latest
Updated Sep 2026

Source on GitHub (opens in a new tab)

Overview

Share files between a phone, a laptop and anything else on the same Wi‑Fi. Run it on one computer; every other device opens a link or scans a QR code. No app, no account, no cloud. Files never leave the network.

3
runtime dependencies: Flask, cheroot, segno
0
external requests: works on a LAN with no internet
7
CI test jobs: Python 3.10 to 3.14 on Linux, plus macOS and Windows
88
unit and API tests, plus 6 Playwright browser tests

Problem

Moving a file from a phone to a laptop usually means a cable, a messaging app that recompresses it, or a cloud drive that needs an account and sends the file off the network. AirDrop solves it, but only when every device is Apple.

Version 1 was a small Flask script. Version 2.0 rewrote it into an installable product, and fixed a path-traversal hole, silently overwritten uploads and a crashing shutdown route along the way.

What I built

  1. A one-command start on macOS, Linux and Windows (run.sh or run.ps1) that prints the address and a QR code in the terminal, plus pipx, Docker and a standalone PyInstaller binary.
  2. A build-free single-page app: drag and drop anywhere, paste, three parallel uploads with live speed and ETA, cancel and retry, thumbnails, search, Download all as a ZIP, delete with Undo, and automatic dark mode.
  3. Live updates on every connected device through ETag polling, with offline and reconnecting states.
  4. Optional PIN, plus receive-only, read-only and no-delete modes. Every flag also works as an OPEN_TRANSFER_* environment variable.
  5. A small JSON API for scripts and curl, documented alongside architecture and self-hosting guides.

The product

Open Transfer on a desktop: a drop zone and a list of shared files with sizes, times and download buttonsThe same app on a phone
Screenshots of Open Transfer on desktop and phone, dark mode.

Architecture

One Python process. cheroot streams request bodies to disk, guards check host, origin and PIN, and a storage layer publishes each upload atomically once it is complete.

One process, from browser to disk
  1. Any browser

    phone, laptop, TV

    Nothing to install on the receiving side. Files are sent as the raw request body with the name in a header, so upload progress comes for free from xhr.upload.onprogress.

  2. curl and scripts

    JSON API

    Scripts use the same API: POST /api/files, /api/info, /api/archive. Multipart uploads are still accepted for curl -F.

  3. cheroot

    streaming WSGI server

    Most WSGI servers buffer the whole request body before the app sees it: 20 GB in /tmp for a 20 GB video. cheroot hands the app a streaming body instead.

  4. Guards

    host, origin, PIN

    One before_request hook: a host allow-list against DNS rebinding (421), Origin and Sec-Fetch-Site on writes against CSRF (403), then the PIN session. PIN attempts are limited to 5 a minute and compared in constant time.

  5. Flask app

    pages, API, downloads

    File lists carry an ETag fingerprint of name, size and mtime, so an idle client polling every 3 s gets a 304 of a few hundred bytes.

  6. Storage

    sanitise, stream, dedupe

    Uploads stream in 1 MiB chunks to a hidden .part file. Only a complete upload is published, under a lock, with os.replace, to a unique name like photo (1).jpg. Deletes sit in a trash for 30 s so Undo works.

  7. Shared folder

    .part → os.replace

    Names are sanitised (../../etc/passwd → passwd), hidden files and symlinks are never served, and every path is checked to sit inside the shared folder. Leftover .part files are removed on start-up.

  8. archive.py

    streaming ZIP64

    Download all streams a ZIP straight to the client through a non-seekable sink, with no temp file and no size limit. Stored, not deflated: shared media rarely compresses.

Decisions

  1. Stream the body, never buffer it

    Raw-body uploads through cheroot keep memory constant and write each file exactly once, so multi-GB videos over Wi‑Fi don’t fill RAM or /tmp.

  2. Nothing half-written is ever visible

    An upload is only published once its byte count matches Content-Length, atomically and under a lock. A crash leaves a .part file that is cleaned up on the next start, never a broken file in the list.

  3. Polling over WebSockets, on purpose

    ETag polling with 304 responses keeps the server stateless and friendly to thread-per-connection serving. SSE or WebSockets would pin a server thread for every open tab.

  4. Safe by default on a shared network

    DNS-rebinding and CSRF checks, a strict Content-Security-Policy with no inline script, and downloads always served as sandboxed attachments, never rendered as HTML.

  5. No service worker, deliberately

    Browsers only allow service workers on HTTPS or localhost, and a stale cached app shell is a worse failure than none for a LAN tool. Version 1’s service worker cached the home page forever; 2.0 removed it.

Result

Version 2.0.0 shipped on 24 September 2026 as a complete, installable product, with the old URLs still working.

CI runs on Linux, macOS and Windows across Python 3.10 to 3.14, plus Playwright browser tests, a Docker smoke test, CodeQL and dependency audits.

Known limitations

  • Built for trusted local networks, not for exposure to the internet.
  • Without a PIN, anyone on the network with the link can see, download and delete files.
  • Plain HTTP on the LAN; encryption needs an HTTPS reverse proxy.
  • Folder uploads and automatic mDNS discovery are still on the roadmap.

Stack

Python, Flask, cheroot, JavaScript, Docker, GitHub Actions, Playwright