diff --git a/.dockerignore b/.dockerignore index 4a0b2a7..fd0f560 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,3 +4,10 @@ uploads/ .gitignore Dockerfile .dockerignore +beam +*_test.go +.vscode/ +.github/ +.clinerules +CLAUDE.md +.claude/ diff --git a/.gitignore b/.gitignore index 4e23bf4..bb46564 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ beam *.dll *.so *.dylib +.vscode/ diff --git a/Dockerfile b/Dockerfile index 585aa8a..121344d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ RUN apk add --no-cache mailcap \ USER beam WORKDIR /app -COPY --from=builder /build/index.html.tmpl /build/beam . +COPY --from=builder /build/beam . EXPOSE 8080 diff --git a/README.md b/README.md index df2dfe8..a7f62ec 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,130 @@ # beam -File transfer service via http. +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: + +```sh +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: + +```sh +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: + +```json +{"urls": ["https://beam.example.com/x123abcx/a.txt"], "failed": ["b.txt"]} +``` + +Errors use a consistent shape: + +```json +{"error": "Upload exceeds maximum size of 5368709120 bytes"} +``` + +Download a file, or just open the URL in a browser: + +```sh +curl https://beam.example.com/x123abcx/myfile.txt -o myfile.txt +``` + +Downloads support range requests, so an interrupted transfer can be resumed: + +```sh +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_CLEAN` defaults to `true`.** Until you set `DRYRUN_CLEAN=false`, +> expired files are never actually deleted — the service only logs +> `Dry 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_SIZE` limits 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_GRACE` should be aligned with the pod's + `terminationGracePeriodSeconds`. If an upload is still running when the grace + period elapses, remaining connections are closed and the process still exits + cleanly. + +## Running + +```sh +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 + +```sh +go build ./... && go vet ./... && gofmt -l . && go test ./... +``` + +Run locally: + +```sh +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 -The Dockerfile owns the base application version with the `VERSION` build argument. Docker 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` 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. ```sh ./build.sh diff --git a/index.html.tmpl b/index.html.tmpl index 4ff1b0e..3d74cc3 100644 --- a/index.html.tmpl +++ b/index.html.tmpl @@ -6,15 +6,19 @@
curl --upload-file myfile.txt {{ .Proto }}://{{ .Host }}/myfile.txt
- The server will respond with a download URL.
-Maximum upload size: {{ .MaxUploadSize }} bytes
+The server responds with the download URL.
+Maximum upload size: {{ .MaxUploadSize }} bytes. + Uploaded files are deleted after {{ .Expiry }}.
curl {{ .Proto }}://{{ .Host }}/x123abcx/myfile.txt -o myfile.txt
- Or simply open the URL in your browser.
+Or simply open the URL in your browser.
📁 Click to select files or drag and drop here
-Maximum file size:
+Click to select files or drag and drop here
+Maximum total size: · deleted after {{ .Expiry }}
- + +