EasyTransfer is a temporary file transfer service for trusted local networks. Environment A uploads one or more files or directories with a single curl | sh command. The service returns another complete command that environment B can run to download, verify, and extract the transfer.
No permanent client is required in A or B. It works well across computers, containers, virtual machines, and other environments that can reach the LAN service.
- Creates a
tar.gzarchive in environment A while preserving relative paths. - Creates
easytransfer-download-YYYYMMDD-HHMMSSin B by default, keeping extracted files contained. - Uploads one file, one directory, or several mixed paths at once.
- Verifies SHA-256 before extraction and sends a completion receipt afterward.
- Random download tokens, expiry, delete-after-download, and manual deletion.
- Automatic expiry cleanup and one-click purging of all records marked
deleted. - SQLite persistence, HTTP Range support, and archive safety checks.
- Optional admin and upload tokens.
- Complete Simplified Chinese and English Web UI, shell help, progress, errors, and results.
- English is the default service language; first-time visitors must choose English or Simplified Chinese and can change it later in Settings.
- Docker Compose and rootless Podman/Quadlet deployment.
- Environment A downloads the upload script and creates a local
tar.gzarchive from the supplied paths. - The server streams the archive to disk, calculates SHA-256, and checks for path traversal, escaping links, device entries, excessive entries, and excessive declared expanded size.
- Metadata is stored in SQLite, and the server returns a command containing a random download token.
- Environment B downloads the archive, verifies SHA-256, extracts into a new directory, and sends a completion receipt.
- Files become unavailable at expiry. With delete-after-download enabled, the completion receipt removes the server file immediately.
Requirements: Docker Engine and the Docker Compose plugin.
cd easytransfer
cp .env.example .env
mkdir -p data
chown "${EASYTRANSFER_UID:-1000}:${EASYTRANSFER_GID:-1000}" data
docker compose build
docker compose up -d
docker compose psOpen http://<server-lan-ip>:8090/.
The service starts in English. On the first visit, a required language picker lets you choose English or Simplified Chinese; the choice is synchronized to the service's single language setting. You can change it later in Settings → Language.
If the host user that owns the mounted directory is not UID/GID 1000, edit EASYTRANSFER_UID and EASYTRANSFER_GID in .env, then give data to that user.
Update:
docker compose build --pull
docker compose up -dStop:
docker compose downRequirements: rootless Podman, user systemd, and linger enabled for that user. The first run only creates the local environment file:
cd easytransfer
sh deployment/deploy-rootless.sh --lang enEdit deployment/easytransfer.env if needed, then run the same command again to build the image, install the Quadlet, start the service, and verify its health:
sh deployment/deploy-rootless.sh --lang en
systemctl --user status easytransfer.serviceRun the script as the regular user that will own the rootless container, never as root. The default data directory is data/ under the project. Override these values when needed:
| Variable | Default | Purpose |
|---|---|---|
EASYTRANSFER_HOST_DATA_DIR |
<project>/data |
Persistent host directory |
EASYTRANSFER_ENV_FILE |
deployment/easytransfer.env |
Container environment file |
EASYTRANSFER_IMAGE |
localhost/easytransfer:1.1.0 |
Local image name |
EASYTRANSFER_PORT |
8090 |
Published host port |
EASYTRANSFER_DEPLOY_LANGUAGE |
en |
Deployment output: zh-CN or en |
The examples below use <server-ip> for the EasyTransfer server.
curl -fsSL 'http://<server-ip>:8090/easytransfer.sh' \
| sh -s -- ./report.pdfcurl -fsSL 'http://<server-ip>:8090/easytransfer.sh' \
| sh -s -- ./report.pdf ./photos ./configRun from the common parent of the content and pass relative paths for the clearest extracted layout. The script first creates an archive under ${TMPDIR:-/tmp}, so A needs enough temporary space.
An English upload result looks like this:
Upload complete.
Name: photos
Size: 123456 bytes
Expires at: 2026-09-02 12:00:00 UTC
Delete after successful extraction: no
Run in environment B:
curl -fsSL 'http://<server-ip>:8090/d/<random-token>?lang=en' | sh
Run the returned command. By default, it creates:
./easytransfer-download-20260901-183045/
├── report.pdf
├── photos/
└── config/
If the timestamped directory already exists, the script appends -1, -2, and so on.
Choose an extraction directory:
curl -fsSL 'http://<server-ip>:8090/d/<random-token>?lang=en' \
| sh -s -- --dir /target/directoryAn explicitly selected directory may already contain files; tar can overwrite names that collide.
--expires 30m|24h|7d Set expiry; uses the server default when omitted
--name NAME Set the transfer name shown in the Web UI
--delete-after-download Delete the server file after B verifies and extracts it
--keep-after-download Override the server default and keep it until expiry
--token TOKEN Supply the upload token required by the server
-h, --help Show help
The upload token can also be supplied with EASYTRANSFER_UPLOAD_TOKEN.
EasyTransfer uses one service-level language setting:
- A new service starts in English.
- On a first visit, a required language picker lets the visitor choose English or Simplified Chinese.
- Settings → Language switches between English and Simplified Chinese. The choice is written to server-side SQLite and kept in the browser for a fast next render.
- API errors: localized from
Accept-Language; the stableerror.codedoes not change.
The default service language is English. You can override it for one script request:
# English
curl -fsSL 'http://<server-ip>:8090/easytransfer.sh?lang=en' | sh -s -- ./
# Simplified Chinese
curl -fsSL 'http://<server-ip>:8090/easytransfer.sh?lang=zh-CN' | sh -s -- ./The upload script sends its language to the server, so its upload result and returned download command stay in the same language.
curl | sh executes the server response immediately. A more cautious workflow downloads and inspects it first:
curl -fsSL 'http://<server-ip>:8090/easytransfer.sh?lang=en' -o easytransfer.sh
less easytransfer.sh
sh easytransfer.sh ./report.pdf ./photosThe download script can be saved and inspected in the same way.
- Default and maximum expiry.
- Automatic cleanup interval and history retention.
- Default delete-after-download behavior.
- Maximum upload size, expanded size, and archive entry count.
- Public base URL for reverse proxies or hosts with multiple interfaces.
- One shared service language for the Web UI and command-line scripts.
Except for tokens and reverse-proxy trust, settings are saved in /data/easytransfer.db without a restart.
By default, EasyTransfer assumes a trusted LAN and leaves admin and upload tokens empty. For a more complex network, generate two different tokens:
openssl rand -hex 32
openssl rand -hex 32Write them to .env for Compose or deployment/easytransfer.env for Quadlet:
EASYTRANSFER_ADMIN_TOKEN=<admin-token>
EASYTRANSFER_UPLOAD_TOKEN=<different-upload-token>The admin token protects records, settings, deletion, and cleanup. The upload token protects uploads. Download authorization is the unguessable download URL itself. The Web UI stores the admin token only in the current tab's sessionStorage, and the server never reveals environment tokens through the API.
| Variable | Default | Description |
|---|---|---|
EASYTRANSFER_DATA_DIR |
/data |
SQLite and transfer archive directory |
EASYTRANSFER_LISTEN |
0.0.0.0:8090 |
Listen address inside the container |
EASYTRANSFER_ADMIN_TOKEN |
Empty | Bearer token for admin APIs |
EASYTRANSFER_UPLOAD_TOKEN |
Empty | Bearer token for the upload API |
EASYTRANSFER_TRUST_PROXY |
false |
Trust X-Forwarded-*; enable only behind a trusted proxy |
EASYTRANSFER_LOG_LEVEL |
INFO |
Python log level |
TZ |
Asia/Shanghai |
Container timezone (Compose default) |
/data/easytransfer.db SQLite database
/data/easytransfer.db-wal SQLite WAL, sometimes present at runtime
/data/files/*.tar.gz Available transfer archives
For a consistent backup, stop the container and copy the whole data directory. SQLite can also create an online backup:
sqlite3 data/easytransfer.db ".backup '/safe/location/easytransfer-backup.db'"To restore, stop the container, restore both the database and files, and keep ownership correct.
| Method and path | Purpose | Protection |
|---|---|---|
GET / |
Bilingual Web UI | None |
GET /healthz |
Health check | None |
GET /easytransfer.sh?lang=zh-CN|en |
Upload script for A | None |
POST /api/uploads |
Upload a gzip tar archive | Optional upload token |
GET /d/<token>?lang=zh-CN|en |
Download script for B | Download link token |
GET /api/downloads/<token>/archive |
Archive download with one Range | Download link token |
POST /api/downloads/<token>/complete |
Successful extraction receipt | Download link token |
GET /api/dashboard |
Statistics and transfer records | Optional admin token |
GET/PUT /api/settings |
Read or update settings | Optional admin token |
DELETE /api/transfers/<id> |
Delete a transfer | Optional admin token |
POST /api/cleanup |
Purge every record marked deleted |
Optional admin token |
- EasyTransfer is designed for trusted LANs and does not terminate TLS. Do not expose its port directly to the Internet.
- Across an untrusted network, use an HTTPS reverse proxy and enable both admin and upload tokens.
- A download URL is a bearer capability: anyone with it can download until expiry.
- The server rejects path traversal, escaping links, device entries, and oversized archives, but the uploader remains the trust source for file contents.
- Extracted content may contain executable files, and an explicit target directory may contain names that are overwritten.
- Delete-after-download depends on the official script's completion receipt. A raw archive download without a receipt remains until expiry.
.gitignore excludes local and sensitive runtime content:
.envdeployment/easytransfer.env- SQLite databases, archives, and other runtime files under
data/ - Python and test caches
Do not force-add those files with git add -f. Review the staged content before pushing:
git status --short
git diff --cached
git grep -nE 'BEGIN .*PRIVATE KEY|Bearer [A-Za-z0-9._-]{20,}|192\.168\.' -- ':!tests/*'Example files contain empty tokens and generic addresses only. They do not include the author's IP address, username, or absolute host paths.
The runtime uses only the Python standard library and has no third-party Python dependency.
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src \
python3 -m unittest discover -s tests -vContainer test:
docker build --target test -t easytransfer:test .Local server:
mkdir -p .local-data
EASYTRANSFER_DATA_DIR=./.local-data \
EASYTRANSFER_LISTEN=127.0.0.1:8090 \
PYTHONPATH=src python3 -m easytransfereasytransfer/
├── compose.yaml
├── Dockerfile
├── deployment/ # Generic rootless Podman/Quadlet deployment
├── src/easytransfer/ # Server, script generators, and bilingual Web UI
├── tests/ # Unit and end-to-end tests
├── README.md # English documentation
└── README_ZH.md # Simplified Chinese documentation
