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
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
- A one-command start on macOS, Linux and Windows (
run.shorrun.ps1) that prints the address and a QR code in the terminal, plus pipx, Docker and a standalone PyInstaller binary. - 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.
- Live updates on every connected device through
ETagpolling, with offline and reconnecting states. - Optional PIN, plus receive-only, read-only and no-delete modes. Every flag also works as an
OPEN_TRANSFER_*environment variable. - A small JSON API for scripts and
curl, documented alongside architecture and self-hosting guides.
The product


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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
Polling over WebSockets, on purpose
ETagpolling 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.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.
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