Share files over a single WiFi/LAN network. Recipients just scan a QR code (phone) or type the IP + a 4-digit code (laptop). No cloud, no accounts, and recipients don't need to install anything — just a browser.
uv run lanshare ~/Desktop/video.mp4With no arguments, the server starts empty — zero files exposed. You add paths later while
the server keeps running (see the next section). This is deliberate: defaulting to the current
folder is a trap that could expose your source code or home folder without you noticing. Want to
share the current folder anyway? Say so explicitly: uv run lanshare .
Type uv run lanshare (with a trailing space), then drag files/folders from Finder into the
terminal window — the path pastes itself in, spaces already escaped. Drop as many as you like,
mixing files and folders is fine, then hit Enter.
uv run lanshare ~/Desktop/video.mp4 ~/Documents/report.pdf ~/Holiday\ Photos/What shows up in the terminal:
◆ share·lan — running. Ctrl-C to stop.
Shared: 3 items
• video.mp4 (412 MB)
• report.pdf (2.1 MB)
• Holiday Photos/ (128 files, 3.4 GB)
Address: http://192.168.1.7:8000 ← used in the QR
Also : http://macbook.local:8000 (survives an IP change)
Code : 4815
[ QR code ]
→ To a phone : scan the QR above
→ To a laptop: give them the address + code above
To a phone: point the built-in camera at the QR — the access code rides along in the URL, so zero typing is needed. To a laptop: tell them the address + the 4 digits, they type it into a browser.
The server can start empty and get filled in later — the QR is scannable from the very start:
uv run lanshareOnce it's running, the terminal becomes the control surface. Drag a file/folder from Finder into the terminal window and hit Enter, or type the path directly:
+ video.mp4 (412 MB)
Now sharing 1 item.
ls show what's currently shared
rm <name> remove one (you can use its list index, e.g. rm 2)
qr reprint the address + QR
q stop the server
A page that's already open on someone's phone updates itself within a few seconds — no need
to ask them to refresh, and a QR they already scanned stays valid. If you rm something, access
to it is cut immediately.
The only thing the server stores is the path. A 200 GB file in ~/Documents stays right there
in ~/Documents — it's read straight from that location whenever someone downloads it, never
moved, copied, or placed in the folder you launched the server from. The only thing that actually
gets written to disk is whatever someone else uploads to you, and that lands in whichever
folder the recipient has open when they upload — never dropped at the root.
If what you dropped was a single file, the QR points straight at that file — scan it and the download starts immediately, no listing page in between.
- Browse folders, search for files, list or grid view with thumbnails
- Download a single file, or a whole folder at once as a ZIP
- Preview right in the browser — click the eye icon: PDF, images, video, audio, and text
open in the browser's built-in viewer; Office formats (docx, xlsx, pptx, odt, rtf, etc.) render
to HTML via GroupDocs.Viewer when it's
installed (
uv pip install groupdocs-viewer-netoruv run --extra viewer) - Click the QR button on any row → a large QR just for that item (great for pointing one file at someone's phone)
- Stream video/audio right in the browser without downloading the whole file
- Send files back to you (drag & drop, can be disabled with
--read-only) - Grab the SHA-256 of a single file, or a folder's
SHA256SUMS, to verify the transfer
A folder that's mostly images opens as a gallery, complete with thumbnails — not a plain text list:
Every row has its own QR button. Click it → a large QR appears in the middle of the screen, hand the phone over, and their download starts instantly:
The theme follows the system, and light mode is a real design pass — not just inverted colors:
On a phone, the listing reflows into one or two columns with larger touch targets:
Single-file downloads always resume. It uses Accept-Ranges + sendfile (zero-copy in the
kernel), so a connection that drops at 180 GB just continues instead of starting over.
Folder ZIPs have two modes, chosen automatically based on size:
| Folder size | Mode | Resumable? |
|---|---|---|
≤ 20 GB (--zip-resume-limit) |
CRCs precomputed, ZIP layout planned exactly | Yes |
| > 20 GB | Streaming | No |
For anything above the threshold, the UI is upfront about it and offers a "Copy URL list" button. Paste the result into a file, then on the recipient's end:
aria2c -i list.txt # parallel + per-file resume
wget -c -i list.txt # alternativeFor hundreds of GB, this is the sanest approach: parallel downloads, and if one file fails, only that one needs retrying.
lanshare [PATH ...]
(no PATH: server starts empty, paths are added later via the terminal)
--port N port (default 8000, hops to the next free one if taken)
--host ADDR bind address (default 0.0.0.0)
--ip ADDR force the address shown in the QR
--code CODE custom access code (default: random)
--code-len N length of the random code (default 4)
--max-upload SIZE limit per upload, e.g. 2G
--read-only disable uploads
--hidden include hidden files/folders
--no-qr don't print the QR in the terminal
--zip-resume-limit N threshold for a resumable ZIP (default 20G)
--no-caffeinate don't prevent the Mac from sleeping
- Plain HTTP, unencrypted. Fine for a home/office WiFi network; don't use it for sensitive data on public WiFi (cafes, airports).
- The 4-digit code is protected by rate-limiting, not by length. 5 wrong guesses locks that IP
for 60 seconds, and each further failed round doubles the lockout (up to 15 minutes). Since
changing IP on a LAN is trivial, there's also a global counter: 50 failures anywhere → the code
is rotated automatically and a new QR is printed. Need it stricter?
--code-len 6. - Every path is validated back against the folders you shared —
../and symlinks pointing outside are rejected. - Uploaded filenames are sanitized (path components,
.., control characters, Windows reserved names), plus a disk-space check. - The access code is not a substitute for a VPN — it only stops casual snooping from someone scanning ports on the same network.
- The server never moves or copies anything. Files stay exactly where they are and are only read when someone downloads them. Ctrl-C = access is cut immediately.
- Make sure both devices are on the same WiFi network.
- macOS will ask for firewall permission the first time it runs → choose Allow.
- Got a VPN/Docker/Tailscale running? The address picked might belong to that interface. The
banner shows alternatives — try another one, or force it with
--ip. - Office/hotel routers sometimes enable "AP isolation," which blocks devices from talking to each other.
The server is a plain Python package (folder lanshare/, one small module per responsibility —
mounts, auth, HTTP, ZIP, pages, etc.), with dependencies (qrcode, pillow, pypdfium2)
declared in pyproject.toml — uv handles installing them. Pillow is optional: without it,
thumbnails are disabled and the UI falls back to icons, everything else works normally.
groupdocs-viewer-net is also optional (--extra viewer): without it, Office-document preview is
disabled and PDF/image/media still preview via the browser's built-in viewer. Everything else is
stdlib.
Don't have this repo on your machine yet? This one-liner installs lanshare directly, needing
only Python (no uv, git, or anything else required):
macOS / Ubuntu / Fedora / other Linux:
curl -LsSf https://raw.githubusercontent.com/harizinside/share-lan/main/install.sh | shWindows (PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/harizinside/share-lan/main/install.ps1 | iex"After that, just call sharelan (or the old alias lanshare) from any terminal.
Starting with v0.0.2, update an existing install to the latest GitHub release:
sharelan update
sharelan --versionlanshare update works too. If you're still on v0.0.1, re-run the one-line installer above once
to get this new command.
The update uses whichever Python installation is currently running; it needs an internet
connection. Once it's done, restart the server so the new code takes effect.
Editable install from the repo: use git pull then uv sync.
Docker: use docker compose pull then docker compose up -d.
To share a file/folder literally named update, write sharelan ./update.
pyproject.toml registers lanshare as a global command. Install it once with uv tool install,
and it can then be called directly from anywhere without uv run:
uv tool install --editable .
lanshare ~/Desktop/video.mp4--editable means code changes take effect immediately, with no reinstall needed.
The official image is published to GHCR via GitHub Actions on every push to main/a release tag
(.github/workflows/docker-publish.yml) — the docker-compose.yaml in the repo root is ready to
use as-is:
mkdir -p shared && cp file-to-share shared/
docker compose up -d
docker compose logs -f # see the QR + 4-digit code hereThe host folder ./shared is mounted to /data inside the container; edit command: in the
compose file to add other flags (--read-only, --code, --max-upload, etc. — see
Options).
Networking note: only port 8000 is published by default, so share-lan runs fine, but the address
it auto-detects for the QR/banner may be Docker's internal address rather than the host's real LAN
IP. If that happens, uncomment network_mode: host below (Linux only) so share-lan sees the host
network directly, or force it manually with --ip <host-lan-ip>.
Want to build from source instead of pulling from GHCR? Change image: to build: . in the
compose file, or directly:
docker build -t share-lan .
docker run --rm -p 8000:8000 -v "$(pwd)/shared:/data" share-lanThe web UI's language toggle reads from lanshare/locales/ — one flat JSON file per language
(en.json, id.json, ...). To add a language: copy en.json, translate the values, save it as
<code>.json in that folder, and it shows up in the toggle automatically — no code changes
needed. See lanshare/locales/README.md for details; a partial translation is fine, since any key
you skip just falls back to English.
Linting, formatting, and tests use the Astral tools (ruff + pytest, managed by uv):
uv run --group dev ruff check . # lint
uv run --group dev ruff format . # format
uv run --group dev pytest # tests109 tests, running in ~3 seconds, split across four files:
| File | Covers |
|---|---|
tests/test_unit.py |
Size formatting, --max-upload parsing, upload-name sanitization, Range header parsing, duplicate mount naming |
tests/test_paths.py |
Path security: ../, absolute paths, symlinks pointing outside, dotfiles, .part files |
tests/test_zip.py |
Exact ZIP byte layout, chunked fetches must be identical to a from-scratch fetch (this is what guarantees resume), ZIP64, unicode, CRC caching |
tests/test_http.py |
End-to-end over HTTP: auth, scan flow, rate limiting, Range/416/multi-range, ZIP resume, streaming mode, uploads, size limits, read-only, /sums, /urls, /qr, /thumb |
Configuration lives in pyproject.toml.




