Skip to content

xenofex7/transfer

Repository files navigation

transfer

transfer

A self-hosted file transfer service. From one place to another - simple, secure, fast, no limits.

latest tag license go 1.25 container image tests docker build last commit commit activity

Originally based on dutchcoders/transfer.sh; this repository is now developed and maintained independently.


Contents


Quick start

Pull the published container image and run it with a local data directory:

docker run --rm \
  -p 8080:8080 \
  -v $(pwd)/data:/data \
  ghcr.io/xenofex7/transfer:latest

Then, in another shell:

curl --upload-file ./hello.txt http://127.0.0.1:8080/hello.txt

The response body is the download URL. The X-Url-Delete response header contains the deletion URL - keep both.

Heads-up for the bare quick start: this command starts the container with no authentication - anything that can reach port 8080 can upload, download and delete. It also fires the anonymous instance heartbeat within 30 s. For anything beyond a local kick-the-tyres run, use the docker compose stack below, which wires up htpasswd auth and an external reverse proxy.

The container image bakes in sensible defaults (LISTENER=:8080, BASEDIR=/data, TEMP_PATH=/tmp, PURGE_DAYS=360, PURGE_INTERVAL=24); override them via env or CLI flags as needed.

Multi-arch images (linux/amd64, linux/arm64) are published on GHCR with latest, semver (X.Y.Z, X.Y, X) and per-commit (sha-<short>) tags. Pin to a specific version in production.


Self-hosting with docker compose

The deployment stack lives in docker-compose.yml.

# 1. Configuration template
cp .env.example .env
$EDITOR .env

# 2. Auth file (seed at least one user; more can be managed in the UI)
htpasswd -B -c htpasswd alice

# 3. Up
docker compose up -d

After the stack is up, open the admin UI to manage users, inspect uploads, and tweak branding without touching files by hand.

The transfer container exposes port 8080 only inside the compose network. TLS and the public hostname are expected to be handled by your reverse proxy of choice (nginx, Caddy, Traefik). Pass standard proxy headers (X-Forwarded-Host, X-Forwarded-Proto) and set client_max_body_size to at least the value of MAX_UPLOAD_SIZE.


Admin UI

Browser routes live behind a cookie-based login at /login. After signing in (password + TOTP if 2FA is enabled) the following pages become reachable:

Path Purpose
/admin/files Browse stored uploads, filter, manually delete
/admin/settings Tagline, contact email, theme, custom logo / favicon, anonymous-heartbeat toggle
/admin/users Add, reset password, delete - read/write to the mounted htpasswd
/account Enable / disable TOTP, manage API tokens, regenerate recovery codes

Branding uploads land in <BASEDIR>/.branding/; persisted operator settings live in <BASEDIR>/.settings.json; TOTP secrets, recovery codes (bcrypt-hashed) and API token records live in <htpasswd>.meta.json next to the htpasswd file.

Authentication model

  • Passwords: bcrypt-hashed in the standard htpasswd file. Used at the /login form and as the password component of HTTP Basic Auth for uploads.
  • TOTP (recommended): enrol at /account/2fa/setup; works with any RFC 6238 authenticator app (Google Authenticator, 1Password, Authy, etc.). Ten one-shot recovery codes are issued at enrolment.
  • API tokens: per-user named tokens for headless clients. Create them at /account and present them as the password in HTTP Basic Auth (curl --user alice:tk_xxx....). Each token is independently revocable and may carry an expiry of 1 to 3650 days. Use these instead of passwords for curl / scripts — they are the only credential allowed to bypass TOTP on the API path.
  • Session cookies: httpOnly, SameSite=Lax, marked Secure whenever the request is HTTPS or X-Forwarded-Proto: https is set. Sliding 8h idle TTL, capped by a 30d max lifetime; both tunable via --auth-session-ttl and --auth-session-max-lifetime.

Set --auth-require-2fa=false if you want to allow password-only browser sessions (not recommended for publicly reachable instances).


Usage

Upload

curl --upload-file ./hello.txt https://your-instance.example.com/hello.txt

Download

curl https://your-instance.example.com/<token>/hello.txt -o hello.txt

Delete

curl -X DELETE https://your-instance.example.com/<token>/hello.txt/<delete-token>

The <delete-token> is returned in the X-Url-Delete response header on upload.

Per-upload limits

# Auto-delete after N days (overrides the server default)
curl --upload-file ./hello.txt https://your-instance.example.com/hello.txt \
  -H "Max-Days: 7"

# Cap the download count
curl --upload-file ./hello.txt https://your-instance.example.com/hello.txt \
  -H "Max-Downloads: 1"

Link aliases

Direct download (skip the preview page):

https://your-instance.example.com/get/<token>/hello.txt

Inline (open in browser instead of download):

https://your-instance.example.com/inline/<token>/hello.txt

For shell helpers, encryption, bulk archives and more, see examples.md.


Configuration

All flags can be set via CLI args or the matching environment variable.

Network

Flag Env Default Description
--listener LISTENER 127.0.0.1:8080 (binary) / :8080 (container) Address the HTTP server binds to
--proxy-path PROXY_PATH - Path prefix when behind a reverse proxy
--proxy-port PROXY_PORT - External port of the reverse proxy
--cors-domains CORS_DOMAINS - Comma-separated list of CORS origins

Storage

Flag Env Default Description
--basedir BASEDIR required (the container image presets it to /data) Path to the local storage directory
--temp-path TEMP_PATH OS temp dir (/tmp in the container) Path used for in-flight uploads

Lifecycle

Flag Env Default Description
--purge-days PURGE_DAYS 360 Days after which uploads are purged
--purge-interval PURGE_INTERVAL 24 Hours between purge runs
--max-upload-size MAX_UPLOAD_SIZE 0 (no limit) Per-file limit in KB
--rate-limit RATE_LIMIT 0 (off) Requests per minute
--random-token-length RANDOM_TOKEN_LENGTH 10 URL token length

Authentication & access control

Flag Env Description
--http-auth-user / --http-auth-pass HTTP_AUTH_USER / HTTP_AUTH_PASS Single-user basic auth
--http-auth-htpasswd HTTP_AUTH_HTPASSWD Path to a htpasswd file (multi-user)
--http-auth-ip-whitelist HTTP_AUTH_IP_WHITELIST CIDRs that may upload without auth
--auth-require-2fa AUTH_REQUIRE_2FA Force every user to enrol TOTP before reaching protected routes (default true)
--auth-session-ttl AUTH_SESSION_TTL Idle timeout for the browser session cookie (default 8h)
--auth-session-max-lifetime AUTH_SESSION_MAX_LIFETIME Hard upper bound on a session cookie regardless of activity (default 720h)
--ip-whitelist IP_WHITELIST CIDRs allowed at the connection level
--ip-blacklist IP_BLACKLIST CIDRs denied at the connection level

Frontend / misc

Flag Env Description
--tagline TAGLINE Subtitle shown beneath the hostname on the homepage; empty hides it. The admin UI overrides this at runtime.
--email-contact EMAIL_CONTACT Address rendered in the "Contact" link
--web-path WEB_PATH Override the bundled web frontend directory (development only)
--log LOG Log file path (defaults to stderr)

Webhooks

Best-effort, async, no-retry POSTs for upload, download and delete events.

Flag Env Description
--upload-webhook-url UPLOAD_WEBHOOK_URL URL that receives JSON events. Empty disables webhooks.
--webhook-token WEBHOOK_TOKEN Optional bearer token added as Authorization: Bearer <token>

Body shape (event is one of upload, download, delete; optional fields are omitted when empty):

{
  "event": "upload",
  "filename": "hello.txt",
  "content_type": "text/plain",
  "size": 123,
  "url": "https://your-instance.example.com/<token>/hello.txt",
  "delete_url": "https://your-instance.example.com/<token>/hello.txt/<delete-token>",
  "user": "alice",
  "downloads": 0
}

Analytics

Visitor analytics (optional, off by default)

Setting both UMAMI_SCRIPT_URL and UMAMI_WEBSITE_ID injects the Umami tracker into user-facing pages (/, download views, 404). Admin pages (/admin/*) never carry the tag. Self-hosted Umami works as well as umami.is.

Flag Env Description
--umami-script-url UMAMI_SCRIPT_URL URL to your Umami script.js
--umami-website-id UMAMI_WEBSITE_ID Umami site UUID (data-website-id)

Anonymous instance heartbeat

Transfer sends one anonymous heartbeat per day so I can see how many instances are running and which versions are in the wild. The payload is just the running version - no user data, no IP, no upload metadata, no usernames, no request tracking, not even an instance ID. Events go to a privately-hosted Umami instance.

Caveat: the request unavoidably reveals this server's source IP to the Umami host at the HTTP layer (that's how every outbound HTTP call works); IP logging on that Umami is disabled.

To opt out, toggle it off in /admin/settings, or set UMAMI_HEARTBEAT=off in your .env. The exact JSON the server would send is exposed at /admin/settings/heartbeat/payload for inspection.

To redirect the beat to your own Umami instead, set UMAMI_HEARTBEAT_URL and UMAMI_HEARTBEAT_WEBSITE_ID.


Development

Requires Go 1.25+.

git clone git@github.com:xenofex7/transfer.git
cd transfer
go run . --listener 127.0.0.1:8080 --basedir ./tmp/storage --temp-path ./tmp

Or, for a production-style binary (the Go module path is kept on dutchcoders/transfer.sh for compatibility):

go build -tags netgo \
  -ldflags "-X github.com/dutchcoders/transfer.sh/cmd.Version=dev -s -w" \
  -o transfer ./

Run the tests and the linter:

go test -race ./...
golangci-lint run --config .golangci.yml

CI runs both on every push (see .github/workflows/test.yml). The roadmap and planned work live in ROADMAP.md.


Credits

Built on top of the original work by:

  • Remco Verhoef & Uvis Grinfelds - original creators of transfer.sh
  • Andrea Spacca & Stefan Benten - long-time upstream maintainers

The upstream copyright notice is kept intact and the project ships under the same MIT license.

About

Like WeTransfer - but it stays at your place

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors