beam
File transfer service via http. Upload with curl or a browser, share the
returned URL, and the file is deleted automatically once it expires.
Usage
Upload a single file with PUT; the response body is the download URL:
curl --upload-file myfile.txt https://beam.example.com/myfile.txt
# https://beam.example.com/x123abcx/myfile.txt
Upload one or more files as a multipart form. This is the endpoint the web UI uses, and it responds with JSON:
curl -F 'files=@a.txt' -F 'files=@b.txt' https://beam.example.com/upload
# {"urls":["https://beam.example.com/x123abcx/a.txt", "..."]}
If some parts could not be stored, their names are returned alongside the successful URLs:
{"urls": ["https://beam.example.com/x123abcx/a.txt"], "failed": ["b.txt"]}
Errors use a consistent shape:
{"error": "Upload exceeds maximum size of 5368709120 bytes"}
Download a file, or just open the URL in a browser:
curl https://beam.example.com/x123abcx/myfile.txt -o myfile.txt
Downloads support range requests, so an interrupted transfer can be resumed:
curl -C - -O https://beam.example.com/x123abcx/myfile.txt
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Web UI with usage instructions |
PUT |
/{filename} |
Upload one file, returns the URL as text |
POST |
/upload |
Multipart upload, returns JSON |
GET/HEAD |
/{id}/{filename} |
Download, supports Range |
GET |
/healthz |
Liveness/readiness probe |
Configuration
Configuration is via environment variables only. Invalid values are rejected at startup.
| Variable | Default | Description |
|---|---|---|
UPLOAD_DIR |
/uploads |
Where uploaded files are stored. |
PORT |
8080 |
Port to listen on. |
FILE_DURATION |
168h |
How long a file is kept. Set to 0 to disable cleanup entirely. |
DRYRUN_CLEAN |
true |
When true, cleanup only logs what it would delete. See the note below. |
CLEANUP_INTERVAL |
6h |
How often the cleanup sweep runs. |
MAX_UPLOAD_SIZE |
5368709120 |
Maximum bytes per request (5 GiB), covering all files in one upload. |
SHUTDOWN_GRACE |
15s |
How long to wait for in-flight requests on SIGTERM. |
DRYRUN_CLEANdefaults totrue. Until you setDRYRUN_CLEAN=false, expired files are never actually deleted — the service only logsDry run: Would remove expired file ...while the disk keeps filling up.
Durations use Go's time.ParseDuration syntax (30m, 12h, 168h).
MAX_UPLOAD_SIZE is a plain byte count.
Notes on behaviour worth knowing:
MAX_UPLOAD_SIZElimits the whole request, so a multipart upload of several files is measured by their combined size, not per file.- A single multipart request may contain at most 100 files.
- Expiry is based on the file's modification time; downloading does not extend a file's life.
SHUTDOWN_GRACEshould be aligned with the pod'sterminationGracePeriodSeconds. If an upload is still running when the grace period elapses, remaining connections are closed and the process still exits cleanly.
Running
docker run -p 8080:8080 \
-e DRYRUN_CLEAN=false \
-e FILE_DURATION=168h \
-v /srv/beam-uploads:/uploads \
docker.sdal.tech/beam:latest
The container runs as the non-root user beam (uid 1000), which must be able
to write to whatever is mounted at /uploads.
Development
go build ./... && go vet ./... && gofmt -l . && go test ./...
Run locally:
UPLOAD_DIR=/tmp/beam-uploads PORT=8080 DRYRUN_CLEAN=false go run .
Note that without a reverse proxy setting X-Forwarded-Host, generated
download URLs fall back to localhost:8080. In production beam runs behind a
TLS-terminating ingress that sets X-Forwarded-Proto and X-Forwarded-Host,
which is where the URLs come from.
Versioning
build.sh owns the application version via the VERSION variable and passes
it to the Docker build as a build argument. Builds append the current git short
hash with GIT_SHA so the homepage can display the exact image version near the
bottom-left edge.
./build.sh
To push docker.sdal.tech/beam:latest after building:
./build.sh push
Local non-Docker builds display vdev unless a version is injected manually:
go build -ldflags "-X main.version=0.1.0-dev" -o beam .