Skip to content

Repository files navigation

EasyTransfer

English | 简体中文

EasyTransfer interface in English

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.

Features

  • Creates a tar.gz archive in environment A while preserving relative paths.
  • Creates easytransfer-download-YYYYMMDD-HHMMSS in 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.

How it works

  1. Environment A downloads the upload script and creates a local tar.gz archive from the supplied paths.
  2. 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.
  3. Metadata is stored in SQLite, and the server returns a command containing a random download token.
  4. Environment B downloads the archive, verifies SHA-256, extracts into a new directory, and sends a completion receipt.
  5. Files become unavailable at expiry. With delete-after-download enabled, the completion receipt removes the server file immediately.

Quick start with Docker Compose

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 ps

Open 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 -d

Stop:

docker compose down

Rootless Podman / Quadlet

Requirements: 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 en

Edit 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.service

Run 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

Transferring files

The examples below use <server-ip> for the EasyTransfer server.

Environment A: upload one file

curl -fsSL 'http://<server-ip>:8090/easytransfer.sh' \
  | sh -s -- ./report.pdf

Environment A: upload several files and directories

curl -fsSL 'http://<server-ip>:8090/easytransfer.sh' \
  | sh -s -- ./report.pdf ./photos ./config

Run 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

Environment B: download and extract

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/directory

An explicitly selected directory may already contain files; tar can overwrite names that collide.

Upload script options

--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.

English and Simplified Chinese

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 stable error.code does 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.

Inspect remote scripts first

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 ./photos

The download script can be saved and inspected in the same way.

Web UI settings

  • 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.

Token protection

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 32

Write 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.

Environment variables

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, backup, and restore

/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.

HTTP API

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

Security boundary

  • 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.

Before publishing to GitHub

.gitignore excludes local and sensitive runtime content:

  • .env
  • deployment/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.

Development and tests

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 -v

Container 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 easytransfer

Project layout

easytransfer/
├── 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

About

Temporary file transfer for trusted LANs: upload files or directories with curl, then download, verify, and extract them with one command.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages