Resumable Uploads (TUS 1.0.0)

Anvil implements the TUS 1.0.0 resumable upload protocol so large files survive transient failures without restarting. The threshold for switching from single-shot upload to TUS is whatever the client decides - both Hammer and the SDKs default to 5 MiB.

Protocol overview

StepEndpointHeaders
InitiatePOST /storage/v1/upload/resumableTus-Resumable, Upload-Length, Upload-Metadata
Append chunkPATCH /storage/v1/upload/resumable/{id}Upload-Offset, Content-Type=application/offset+octet-stream
Query offsetHEAD /storage/v1/upload/resumable/{id}Tus-Resumable
AbortDELETE /storage/v1/upload/resumable/{id}Tus-Resumable
DiscoveryOPTIONS /storage/v1/upload/resumableReturns Tus-Version, Tus-Extension, Tus-Max-Size

Extensions advertised

ExtensionBehaviour
creationPOST initiates a session
expirationUpload-Expires header; expired sessions return 410
checksumUpload-Checksum: sha256 <base64> per chunk
terminationDELETE removes the session + temp file

Walk-through

# 1. Create session - Upload-Metadata carries bucket/path/mime as base64.
curl -i -X POST http://localhost:7474/storage/v1/upload/resumable \
  -H "Authorization: Bearer $TOKEN" \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Length: 10485760" \
  -H "Upload-Metadata: bucket dmlkZW9z,path Y2xpcC5tcDQ=,mime dmlkZW8vbXA0"

# 201 Created
# Location: /storage/v1/upload/resumable/<session-id>

# 2. Append a chunk (any size up to MAX_CHUNK_BYTES = 1 GiB).
curl -i -X PATCH http://localhost:7474/storage/v1/upload/resumable/<session-id> \
  -H "Authorization: Bearer $TOKEN" \
  -H "Tus-Resumable: 1.0.0" \
  -H "Content-Type: application/offset+octet-stream" \
  -H "Upload-Offset: 0" \
  --data-binary @part0.bin

# 204 No Content
# Upload-Offset: <new offset>
# When offset == Upload-Length, also:
#   Location: /storage/v1/object/<bucket>/<path>
#   X-Anvil-Content-Hash: <sha256>

Resume after failure

If a chunk fails, issue HEAD on the session URL to discover the server's current Upload-Offset, then resume PATCHing from there. The Anvil client SDKs and the Hammer upload zone do this transparently.

Garbage collection

Expired upload sessions are cleaned up by the same background loop that handles snapshot checkpoints. The expiry window comes from file_storage.upload_expiry.