Easy and fast file sharing from the command line — and a clean web UI when you need it.
You have a file. Someone else needs it. You want a URL you can paste into chat, drop into a CI log, or curl down from a production box five minutes later — then have it disappear. send.to does exactly that, and nothing else.
curl --upload-file ./build.tar.gz https://send.to/build.tar.gz
# → https://send.to/aB3cD4eF/build.tar.gz
curl https://send.to/aB3cD4eF/build.tar.gz -o build.tar.gzOne static Go binary. One 52 MB Docker image. No database. No account. Your files.
- Features
- Quick start
- Usage
- HTTP API
- Configuration
- Deployment
- Architecture
- Benchmarks
- FAQ
- Contributing
- License
| Single static binary | CGO_ENABLED=0, runs on scratch as non-root UID 10001 |
| Pluggable storage | local filesystem, S3 (Minio / DO Spaces), Google Drive, Storj |
| Server-side encryption | OpenPGP AES-256 via X-Encrypt-Password header |
| Client-friendly | Works with curl, wget, HTTPie, PowerShell, any HTTP client |
| Auto-expiry | Per-file Max-Days / Max-Downloads headers + scheduled purge |
| Virus scanning | Optional ClamAV prescan and VirusTotal submission |
| TLS | Bring-your-own cert, or automatic via Let's Encrypt |
| Auth | HTTP Basic, htpasswd, IP allow/deny lists |
| Hardened | Strict CSP, COOP/CORP, HSTS, slow-loris timeouts, constant-time auth compare, per-IP rate limiting |
| Graceful shutdown | SIGINT/SIGTERM → Shutdown(ctx) → in-flight uploads complete |
| Modern web UI | Astro 5 + React 19 + Tailwind 4, drag-and-drop, ETA, abortable upload, i18n (English / 中文 / 日本語) |
Pick whichever fits your setup. All three produce the same running service on http://localhost:8080.
Requires only Docker.
git clone https://github.com/sooua/send.to && cd send.to
./scripts/docker.sh upThat's it. The wrapper script:
- Copies
.env.example→.envif missing (edit to customise). - Runs
docker compose up -d --build. - Probes
/health.htmlfor up to 30 s and prints the URL once ready.
| Command | What it does |
|---|---|
./scripts/docker.sh up |
Build & start in background |
./scripts/docker.sh down |
Stop & remove containers (volume preserved) |
./scripts/docker.sh purge |
Stop & delete the data volume (asks first) |
./scripts/docker.sh logs |
Follow container logs |
The image is ≈ 52 MB, non-root, read-only root filesystem, no shell, with a built-in HEALTHCHECK.
Requires Go 1.25+ and Node 20+.
Linux / macOS / WSL
./scripts/deploy.sh # foreground, Ctrl+C for graceful shutdown
./scripts/deploy.sh --daemon # background; logs in build/sendto.log
./scripts/deploy.sh --stop # stop a daemonised instance
PORT=9000 ./scripts/deploy.sh # override portWindows (PowerShell)
.\scripts\deploy.ps1 # foreground
.\scripts\deploy.ps1 -Daemon # background
.\scripts\deploy.ps1 -Stop # stop
$env:PORT="9000"; .\scripts\deploy.ps1Everything lives under ./build/ and ./data/ — uninstall is rm -rf build data web/dist web/node_modules.
make build # Go binary + web bundle
./send.to --provider local --basedir ./data --web-path ./web/distOr run make dev to get the Go server and the Astro dev server hot-reloading in parallel.
# Plain upload
curl --upload-file ./notes.md https://send.to/notes.md
# Encrypted upload (server stores ciphertext)
curl -H "X-Encrypt-Password: s3cret" \
--upload-file ./notes.md https://send.to/notes.md
# Limit to 5 downloads, expire in 7 days
curl -H "Max-Downloads: 5" -H "Max-Days: 7" \
--upload-file ./notes.md https://send.to/notes.md
# Multipart: many files in one request
curl -F file1=@a.txt -F file2=@b.txt https://send.to/curl https://send.to/<token>/notes.md -o notes.md
# Decrypt on the fly
curl -H "X-Decrypt-Password: s3cret" \
https://send.to/<token>/notes.md -o notes.md
# Resumable (Range requests)
curl -C - -o big.iso https://send.to/<token>/big.iso# Combine multiple stored files into one stream
curl https://send.to/(tokenA/a.txt,tokenB/b.txt).zip -o bundle.zip
curl https://send.to/(tokenA/a.txt,tokenB/b.txt).tar.gz -o bundle.tgzThe upload response includes an X-Url-Delete header — hit it with DELETE.
curl -X DELETE https://send.to/<token>/notes.md/<deletion-token>Drop this in your ~/.bashrc / ~/.zshrc:
send() { curl --progress-bar --upload-file "$1" "https://send.to/$(basename "$1")"; }
# then: send ./report.pdf| Method | Path | Purpose |
|---|---|---|
PUT |
/{filename} |
Upload a single file |
POST |
/ |
Multipart upload (one or many) |
GET |
/{token}/{filename} |
Download or preview |
HEAD |
/{token}/{filename} |
Metadata only |
GET |
/({files}).{zip,tar,tar.gz} |
Combine files into an archive |
DELETE |
/{token}/{filename}/{delToken} |
Delete a file |
PUT |
/{filename}/scan |
ClamAV scan |
PUT |
/{filename}/virustotal |
VirusTotal submission |
GET |
/health.html |
Health check |
| Request header | Purpose |
|---|---|
Max-Days |
Auto-expire after N days |
Max-Downloads |
Cap downloads at N |
X-Encrypt-Password |
Server encrypts payload (OpenPGP AES-256) |
X-Decrypt-Password |
Provide password on download |
| Response header | Meaning |
|---|---|
X-Url-Delete |
Authenticated URL to DELETE this upload |
X-Remaining-Days |
Days until auto-expiry |
X-Remaining-Downloads |
Downloads remaining under the cap |
A live reference is rendered in the web UI at /api-docs.
Every CLI flag has an environment-variable equivalent (--listener ↔ LISTENER). Run ./send.to --help for the full matrix. The most relevant knobs:
| Flag / Env | Default | Notes |
|---|---|---|
--listener / LISTENER |
:8080 |
Plain HTTP bind address |
--tls-listener / TLS_LISTENER |
empty | Enable native HTTPS |
--provider / PROVIDER |
— | local | s3 | gdrive | storj |
--basedir / BASEDIR |
— | Storage root for the local provider |
--max-upload-size |
0 |
KB per upload; 0 = unlimited |
--rate-limit |
0 |
Requests / minute / IP (PUT/POST/GET) |
--purge-days |
0 |
Delete uploads older than N days |
--shutdown-timeout |
30s |
Grace period for in-flight requests on quit |
--http-auth-user / _pass |
empty | HTTP Basic Auth credentials |
--cors-domains |
empty | Comma-separated Allow-Origin list |
--clamav-host |
empty | e.g. tcp://clamav:3310 |
--virustotal-key |
empty | Enables /{file}/virustotal |
--lets-encrypt-hosts |
empty | Comma-separated hostnames for ACME |
The docker-compose.yml + .env.example combo documents everything you need for container deployments.
Two production patterns:
1. Reverse proxy (most common) — keep send.to on plain HTTP and let Caddy / Nginx / Traefik / Cloudflare terminate TLS:
files.example.com {
reverse_proxy 127.0.0.1:8080
}Make sure the proxy forwards X-Forwarded-Proto: https so the HSTS / CSP headers kick in.
2. Native TLS
./send.to --tls-listener :8443 \
--tls-cert-file fullchain.pem --tls-private-key privkey.pemOr automatic Let's Encrypt:
./send.to --lets-encrypt-hosts files.example.com- Set
MAX_UPLOAD_SIZEto a sane value — protects disk / S3 bill. - Set
RATE_LIMIT— protects against a single abusive IP. - Always front with HTTPS (browsers refuse
clipboard.writeTexton insecure origins). - Enable Basic Auth on instances that must not accept anonymous uploads.
- Mount the storage volume on a partition with a quota.
- Monitor the
HEALTHCHECKstatus (docker ps) or poll/health.html.
git pull
./scripts/docker.sh up # rebuilds & rolls the container
# or:
./scripts/deploy.sh --stop && ./scripts/deploy.sh --daemonGraceful shutdown is bounded by SHUTDOWN_TIMEOUT, so in-flight uploads up to that deadline complete before the old instance exits.
┌──────────────┐ HTTPS ┌────────────────┐
│ Browser / │ ─────────────────▶ │ Reverse Proxy │
│ curl / │ │ (optional) │
│ HTTPie │ ◀───────────────── │ │
└──────────────┘ └────────┬───────┘
│ HTTP
▼
┌────────────────┐
│ send.to │
│ Go binary │
│ │
│ • mux router │
│ • rate limit │
│ • CSP / HSTS │
│ • OpenPGP enc │
│ • ClamAV / │
│ VirusTotal │
└────────┬───────┘
│
┌──────────────┬───────────┴───────────┬──────────────┐
▼ ▼ ▼ ▼
local FS S3 / Minio Google Drive Storj
.
├── cmd/ CLI flag parsing (urfave/cli)
├── server/ HTTP handlers, auth, security, storage backends
│ └── storage/ local | s3 | gdrive | storj
├── internal/clamd/ Embedded ClamAV daemon client
├── web/ Astro 5 + React 19 frontend
├── scripts/ deploy.sh · deploy.ps1 · docker.sh (one-click ops)
├── test/ End-to-end smoke test + Windows signal helper
├── Dockerfile 3-stage build: web bundle → Go binary → scratch
├── docker-compose.yml Production-ready compose (healthcheck, read-only FS)
├── Makefile build · test · lint · vuln · smoke · docker
└── .github/workflows/ CI: Go test+race+coverage, lint, govulncheck, web build
Approximate numbers from a single-core laptop run (Windows 11, WSL2 off, local filesystem backend, empty pre-warmed cache):
| Metric | Value |
|---|---|
| Cold-start time | < 50 ms |
| Image size | 51.8 MB |
| RSS at idle | ~ 20 MB |
| Upload throughput (local FS) | line-rate (~ disk) |
| Concurrent uploads (10×) | all succeed, zero errors, distinct tokens |
| Graceful shutdown overhead | bounded by SHUTDOWN_TIMEOUT (default 30 s) |
A self-contained smoke test exercising 22 assertions (health, security headers, round-trip, encryption, limits, concurrency, graceful shutdown) is in test/smoke/. Run it locally with make smoke.
Is there a size limit?
No hard-coded limit. Set --max-upload-size (KB) to bound uploads. With no limit the only cap is disk space on the storage backend.
How long are files kept?
Clients choose per upload via the Max-Days and Max-Downloads headers. Operators set a server-wide floor with --purge-days. Reach the cap → file is deleted on next access or next purge run.
Are files encrypted at rest?
Only if the uploader sends X-Encrypt-Password (OpenPGP AES-256). Otherwise files are stored as-is. For S3 / Storj backends, use their server-side encryption features in addition.
Can I run it behind a reverse proxy on a sub-path?
Yes. Set --proxy-path /send (or PROXY_PATH=/send) and point the proxy at the container. All URLs in responses will be rewritten accordingly.
How do I back up uploads?
For local provider: everything is in --basedir. Snapshot that directory or use filesystem-level tools (rsync, ZFS snapshots, etc.). For S3 / Storj, use the backend's replication / snapshot features.
Does it work offline / on an air-gapped network?
Yes. The binary ships with no runtime deps, the image has none, and the web UI is a fully static Astro build. Only use of the network is outbound to the configured storage backend (and optional ClamAV / VirusTotal endpoints).
See CONTRIBUTING.md for dev setup, build / test commands, and PR conventions.
For security issues, please follow SECURITY.md — don't open public issues for vulnerabilities.
Third-party license attributions are consolidated in THIRD_PARTY_LICENSES.md.