Skip to content

Commit ae50bd1

Browse files
authored
Merge pull request #10 from polius/v4.0.0
Ground-up rebuild of the backend. Removes the PeerJS dependency in favor of native WebRTC and a first-party WebSocket signaling server bundled with the app. Received files now stream straight to disk, keeping memory flat even on multi-GB transfers. Connectivity - Native RTCPeerConnection / RTCDataChannel client replacing PeerJS. - Built-in signaling relay at /ws with rate limiting, idle timeouts, and capacity guards; relays only connection setup, never file data. - Relayed transfers fixed behind Docker (TURN relay candidates rewritten in transit). Transfers - Chunked, backpressured protocol keeping sender and receiver memory-bounded. - Three auto-selected save methods: File System Access, Service Worker, Blob. Reliability - Serialized disk writes fix a File System Access corruption case. - Abort, remove, and "cancel all" tear down cleanly; no leaked connections. - Transient network blips tolerated instead of killing a transfer. - Container self-heals if the backend process dies. Security - Server refuses to start without SECRET_KEY. - /api/credentials rate-limited; CORS off by default (opt-in via CORS_ORIGINS). - Fonts self-hosted to satisfy the strict CSP. Tooling & docs - End-to-end Playwright harness (browsers x save methods x connection modes). - Dependabot for Python, Docker, GitHub Actions, and the test harness. - README refreshed. BREAKING CHANGE: upgrading from 3.x requires re-downloading the compose files and setting SECRET_KEY (the app will not start without it). A separate PeerJS server is no longer needed.
2 parents d17cc99 + cf5bb15 commit ae50bd1

35 files changed

Lines changed: 4513 additions & 788 deletions

.github/dependabot.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
version: 2
2+
updates:
3+
# Python backend dependencies (api/requirements.txt)
4+
- package-ecosystem: "pip"
5+
directory: "/api"
6+
schedule:
7+
interval: "daily"
8+
groups:
9+
pip-minor-and-patch:
10+
update-types:
11+
- "minor"
12+
- "patch"
13+
14+
# Container base image (Dockerfile)
15+
- package-ecosystem: "docker"
16+
directory: "/"
17+
schedule:
18+
interval: "daily"
19+
groups:
20+
docker-minor-and-patch:
21+
update-types:
22+
- "minor"
23+
- "patch"
24+
25+
# GitHub Actions used by the release workflow
26+
- package-ecosystem: "github-actions"
27+
directory: "/"
28+
schedule:
29+
interval: "daily"
30+
groups:
31+
actions-minor-and-patch:
32+
update-types:
33+
- "minor"
34+
- "patch"
35+
36+
# End-to-end test harness (Playwright)
37+
- package-ecosystem: "npm"
38+
directory: "/e2e"
39+
schedule:
40+
interval: "daily"
41+
groups:
42+
npm-minor-and-patch:
43+
update-types:
44+
- "minor"
45+
- "patch"

.github/workflows/release.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ jobs:
3333
- name: Set up Docker Buildx
3434
uses: docker/setup-buildx-action@v3
3535

36-
- name: Set tag without first two chars
36+
- name: Derive version tag from the git ref
3737
id: tag
3838
run: |
39-
SHORT_TAG="${GITHUB_REF_NAME:2}"
39+
# Strip a leading "v" and an optional "." so both v4.0.0 and v.4.0.0 -> 4.0.0.
40+
SHORT_TAG="${GITHUB_REF_NAME#v}"
41+
SHORT_TAG="${SHORT_TAG#.}"
4042
echo "SHORT_TAG=$SHORT_TAG" >> $GITHUB_OUTPUT
4143
4244
- name: Build and push Docker image

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__/
2+
*.pyc

Dockerfile

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ RUN find /install/lib/ -path "*/site-packages/tests" -type d -exec rm -rf {} + \
3030
# ==============================
3131
FROM python:3.14-alpine
3232

33-
# Install Nginx runtime
34-
RUN apk add --no-cache nginx
33+
# Install Nginx runtime (+ bash for a reliable `wait -n` in the start command)
34+
RUN apk add --no-cache nginx bash
3535

3636
WORKDIR /filesync
3737

@@ -52,5 +52,11 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
5252
# Expose ports
5353
EXPOSE 80
5454

55-
# Start FastAPI + Nginx
56-
CMD ["sh", "-c", "python3 -m uvicorn api.main:app --host 0.0.0.0 --port 8000 & nginx -g 'daemon off;'"]
55+
# Start FastAPI + Nginx. bash's `wait -n` returns as soon as EITHER process exits, so the
56+
# container stops (and `restart: unless-stopped` restarts it) if the backend dies —
57+
# instead of nginx keeping a half-dead container alive indefinitely. (busybox sh's
58+
# `wait -n` does not reliably fire for a backgrounded child as PID 1, so we use bash.)
59+
# --ws-max-size caps WebSocket frames at the transport (uvicorn's default is 16 MiB,
60+
# which would be fully buffered before the app's own 32 KiB payload check runs);
61+
# 64 KiB leaves headroom over the signaling _MAX_PAYLOAD_BYTES limit.
62+
CMD ["bash", "-c", "python3 -m uvicorn api.main:app --host 0.0.0.0 --port 8000 --ws-max-size 65536 & nginx -g 'daemon off;' & wait -n"]

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 Pol Alzina
3+
Copyright (c) 2025-2026 Pol Alzina
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 95 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,178 +2,169 @@
22
<img src="web/assets/icon.png" alt="FileSync Logo" width="80">
33
<h1 align="center">FileSync</h1>
44

5-
**Send files from one device to many in real-time**
5+
**Send files from one device to many, in real time — private, peer-to-peer, with no size limit.**
66

77
<p align="center">
8-
<a href="https://github.com/polius/filesync/actions/workflows/release.yml"><img src="https://github.com/polius/filesync/actions/workflows/release.yml/badge.svg"></a>&nbsp;<a href="https://github.com/polius/filesync/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/polius/filesync"></a>&nbsp;<a href="https://hub.docker.com/r/poliuscorp/filesync"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/poliuscorp/filesync"></a>
8+
<a href="https://github.com/polius/filesync/actions/workflows/release.yml"><img src="https://github.com/polius/filesync/actions/workflows/release.yml/badge.svg"></a>&nbsp;<a href="https://github.com/polius/filesync/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/polius/filesync"></a>&nbsp;<a href="https://hub.docker.com/r/poliuscorp/filesync"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/poliuscorp/filesync"></a>&nbsp;<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
99
</p>
1010

1111
<br>
1212

13-
<p align="center">
14-
<b>FileSync</b> is a file sharing web application that allows users to transfer files between multiple devices with end-to-end encryption.
15-
</p>
13+
![FileSync](web/assets/filesync.png?v=4.0.0)
1614

17-
<br>
15+
</div>
1816

19-
![FileSync](web/assets/filesync.png?v=3.4.0)
17+
## Features
2018

21-
</div>
19+
- 🔒 **Private by design** — files travel directly between browsers over encrypted WebRTC. The server only brokers the initial handshake; it never sees your file contents.
20+
- 🚀 **No size limit** — received files stream straight to disk, so even multi-gigabyte transfers use almost no memory (over HTTPS — see [how files are saved](#how-received-files-are-saved)).
21+
- 👥 **One-to-many** — share a room link or QR code and send to as many devices at once.
22+
- 🌐 **Works across networks** — connects directly when possible, with automatic STUN/TURN relay fallback for restrictive NATs and firewalls.
23+
- 🪄 **No installs, no accounts** — recipients just open a link in any modern browser. Optional password protection per room.
24+
- 🐳 **Self-hosted** — a single Docker image you run yourself.
2225

23-
# Installation
26+
## Table of contents
2427

25-
## Prerequisites
28+
- [Features](#features)
29+
- [Table of contents](#table-of-contents)
30+
- [How to use](#how-to-use)
31+
- [Self-hosting](#self-hosting)
32+
- [Prerequisites](#prerequisites)
33+
- [Option A — HTTP (local network / quick start)](#option-a--http-local-network--quick-start)
34+
- [Option B — HTTPS (public domain, recommended)](#option-b--https-public-domain-recommended)
35+
- [Stopping FileSync](#stopping-filesync)
36+
- [Required ports](#required-ports)
37+
- [Customizing ports](#customizing-ports)
38+
- [Configuration](#configuration)
39+
- [How received files are saved](#how-received-files-are-saved)
40+
- [Under the hood](#under-the-hood)
41+
- [License](#license)
2642

27-
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/) installed
28-
- Python 3 (for generating the secret key)
43+
## How to use
2944

30-
## Quick Start
45+
1. **Open** your FileSync URL — you land in a room with a unique link and a QR code.
46+
2. **Share** the link (or QR) with the people or devices you want to send to.
47+
3. **Drop in your files** — drag and drop them (or click **Send Files**). Recipients see them appear and can download instantly.
3148

32-
### Option 1: HTTP (Local Development)
49+
> Want to restrict access? Click **Add password** to protect the room before sharing the link.
3350
34-
**1. Download the required files**
51+
## Self-hosting
3552

36-
Get [docker-compose.yml](deploy/docker-compose.yml) from the `deploy` folder.
53+
### Prerequisites
3754

38-
**2. Generate a secret key**
55+
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
56+
- Python 3 (only to generate the secret key below)
3957

40-
Run the following command to generate a secure 32-byte base64-encoded secret:
58+
Every deployment needs a **secret key** (it signs the TURN credentials used for NAT traversal). Generate one with:
4159

4260
```bash
4361
python3 -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())"
4462
```
4563

46-
**3. Configure the secret**
47-
48-
Open `docker-compose.yml` and replace **both occurrences** of `<SECRET_KEY>` with the generated value.
49-
50-
Example:
51-
```yaml
52-
...
53-
- --static-auth-secret=/RaFOHJQQPAAXRNdaDhfBghvX9+o9UJEazKgIopK3TI=
54-
...
55-
- SECRET_KEY=/RaFOHJQQPAAXRNdaDhfBghvX9+o9UJEazKgIopK3TI=
56-
...
57-
```
58-
59-
**4. Start FileSync**
60-
61-
```bash
62-
docker-compose up -d
63-
```
64+
> ⚠️ Use your **own** generated value — never ship the examples shown below.
6465
65-
Access FileSync at `http://localhost:80`
66+
### Option A — HTTP (local network / quick start)
6667

67-
### Option 2: HTTPS (Production with Custom Domain)
68+
Best for trying FileSync or running it on a trusted LAN. Note that large transfers (>~500 MB) are unreliable over plain HTTP — for those, use [Option B](#option-b--https-public-domain-recommended).
6869

69-
**1. Download the required files**
70+
**1. Download** [`deploy/docker-compose.yml`](deploy/docker-compose.yml).
7071

71-
Get [docker-compose-ssl.yml](deploy/docker-compose-ssl.yml) and [Caddyfile](deploy/Caddyfile) from the `deploy` folder.
72+
**2. Set your secret.** Replace **both** `<SECRET_KEY>` placeholders with your generated value:
7273

73-
**2. Generate a secret key**
74+
```yaml
75+
# (use your own generated value — these are illustrative)
76+
- --static-auth-secret=Hs9k…your-generated-key…=
77+
- SECRET_KEY=Hs9k…your-generated-key…=
78+
```
7479
75-
Run the following command to generate a secure 32-byte base64-encoded secret:
80+
**3. Start it:**
7681
7782
```bash
78-
python3 -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())"
83+
docker compose up -d
7984
```
8085

81-
**3. Configure the secret**
86+
Open **`http://localhost`** (or your server's IP).
8287

83-
Open `docker-compose-ssl.yml` and replace **both occurrences** of `<SECRET_KEY>` with the generated value.
88+
### Option B — HTTPS (public domain, recommended)
8489

85-
Example:
86-
```yaml
87-
...
88-
- --static-auth-secret=/RaFOHJQQPAAXRNdaDhfBghvX9+o9UJEazKgIopK3TI=
89-
...
90-
- SECRET_KEY=/RaFOHJQQPAAXRNdaDhfBghvX9+o9UJEazKgIopK3TI=
91-
...
92-
```
90+
Caddy obtains and renews a Let's Encrypt certificate automatically. HTTPS also unlocks memory-safe streaming for files of any size.
9391

94-
**4. Configure your domain**
92+
**1. Download** [`deploy/docker-compose-ssl.yml`](deploy/docker-compose-ssl.yml) and [`deploy/Caddyfile`](deploy/Caddyfile).
9593

96-
Open `Caddyfile` and replace `yourdomain.com` with your actual domain.
94+
**2. Set your secret.** Replace **both** `<SECRET_KEY>` placeholders (same as Option A).
9795

98-
Example:
99-
```
100-
filesync.app {
101-
reverse_proxy filesync:80
96+
**3. Set your domain.** In `Caddyfile`, replace `yourdomain.com` with your domain:
97+
98+
```caddyfile
99+
filesync.example.com {
100+
reverse_proxy filesync:80
102101
}
103102
```
104103

105-
**5. Start FileSync**
104+
**4. Start it:**
106105

107106
```bash
108-
docker-compose -f docker-compose-ssl.yml up -d
107+
docker compose -f docker-compose-ssl.yml up -d
109108
```
110109

111-
Caddy will automatically obtain and manage SSL certificates from Let's Encrypt.
110+
Open **`https://yourdomain.com`**.
112111

113-
Access FileSync at `https://yourdomain.com`
114-
115-
## Stopping FileSync
112+
### Stopping FileSync
116113

117114
```bash
118-
# For HTTP setup
119-
docker-compose down
120-
121-
# For HTTPS setup
122-
docker-compose -f docker-compose-ssl.yml down
115+
docker compose down # HTTP setup
116+
docker compose -f docker-compose-ssl.yml down # HTTPS setup
123117
```
124118

125-
## Required Ports
126-
127-
To expose FileSync to the internet, ensure the following ports are open on your server/firewall:
128-
129-
### HTTP Setup
130-
- **Port 80** (TCP) - Web interface
131-
- **Port 3478** (TCP + UDP) - STUN/TURN server for WebRTC
119+
## Required ports
132120

133-
### HTTPS Setup
134-
- **Port 443** (TCP) - Web interface (HTTPS)
135-
- **Port 3478** (TCP + UDP) - STUN/TURN server for WebRTC
121+
Open these on your server/firewall to expose FileSync to the internet:
136122

137-
> **Note:** Port 3478 is essential for establishing peer-to-peer connections, especially when devices are behind NAT/firewalls.
123+
| Port | Protocol | Purpose |
124+
|---|---|---|
125+
| `80` *(HTTP)* / `443` *(HTTPS)* | TCP | Web interface |
126+
| `3478` | TCP + UDP | STUN/TURN — peer-to-peer connection setup |
127+
| `50000–50100` | UDP | TURN relay range — used when a direct connection isn't possible |
138128

139-
## Customizing Ports
129+
> Port `3478` handles connection setup. The `50000–50100` UDP range carries relayed traffic for the ~5–10% of connections that can't go direct (typically a peer behind symmetric NAT or a UDP-blocking firewall).
140130
141-
### Changing the HTTP Port
131+
## Customizing ports
142132

143-
By default, FileSync uses port 80. To use a different port (e.g., 8080):
144-
145-
**1. Edit `docker-compose.yml`**
146-
147-
Find the `ports` section under the `filesync` service:
133+
**HTTP port.** In `docker-compose.yml`, change the **first** number of the `filesync` port mapping (the second is the internal container port — leave it as `80`):
148134

149135
```yaml
150136
ports:
151-
- "80:80"
137+
- "8080:80" # serve on http://localhost:8080
152138
```
153139
154-
Change the **first** port number to your desired port:
140+
**HTTPS port.** Keep Caddy on `443` and put your own reverse proxy (Nginx, Traefik, standalone Caddy) in front if you need a non-standard external port — terminate TLS there and forward to FileSync's internal HTTP port.
155141

156-
```yaml
157-
ports:
158-
- "8080:80"
159-
```
142+
## Configuration
160143

161-
**2. Access FileSync**
144+
Environment variables on the `filesync` service:
162145

163-
Access FileSync at `http://localhost:8080` (or your server IP with the new port).
146+
| Variable | Required | Default | Description |
147+
|---|---|---|---|
148+
| `SECRET_KEY` | **Yes** | — | Signs TURN credentials and tokens. Must match the coturn `--static-auth-secret`. The app won't start without it. |
164149

165-
> **Note:** The second port number (80) should remain unchanged as it refers to the internal container port.
150+
## How received files are saved
166151

167-
### Changing the HTTPS Port
152+
FileSync writes received files to disk as the bytes arrive, so transfers use almost no memory regardless of size. It uses the first method the browser supports, falling back in order:
168153

169-
To use a custom external port, deploy a separate reverse proxy (Nginx, Traefik, or standalone Caddy) that:
170-
- Listens on port 443 with SSL termination
171-
- Forwards traffic to FileSync on your custom internal HTTP port
154+
1. **File System Access API** — streams straight to a file you pick. Desktop Chromium browsers (Chrome, Edge, Brave, Opera) over HTTPS.
155+
2. **Service Worker** — streams into a normal browser download. All modern browsers over HTTPS.
156+
3. **Blob** — buffers the whole file in memory, then saves it. Last resort; the only option over plain HTTP, and unreliable past ~500 MB.
172157

173-
This approach maintains standard HTTPS on port 443 while allowing flexible internal port configuration.
158+
The first two require a secure context (HTTPS, or `localhost`), so **serving FileSync over HTTPS is recommended** — it enables memory-safe transfers of any size.
174159

175160
## Under the hood
176161

177-
FileSync uses [PeerJS](https://github.com/peers/peerjs) (a WebRTC wrapper) to transfer files between multiple devices. Files shared are peer-to-peer, which means there is a direct file transfer between the sender and receiver without any intermediate server. Your files remain private and secure throughout the entire transfer process.
162+
FileSync uses native [WebRTC](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API) to transfer files directly between devices — peer-to-peer, with no intermediate server in the data path. Your files stay private throughout.
163+
164+
A lightweight WebSocket signaling server (served at `/ws` by the FileSync app itself) assists only with the initial connection setup — relaying SDP offers/answers and ICE candidates between peers. Once a peer-to-peer connection is established, the signaling server steps back and file bytes flow directly between browsers. **The server never has access to file contents.**
165+
166+
![File Transfer - https://xkcd.com/949](web/assets/comic.png)
167+
168+
## License
178169

179-
![File Transfer - https://xkcd.com/949](web/assets/comic.png)
170+
Released under the [MIT License](LICENSE).

0 commit comments

Comments
 (0)