Skip to main content

litestar-tus

CI CD PyPI version GHCR

TUS v1.0.0 resumable upload protocol plugin for Litestar with pluggable storage backends.

Installation

pip install litestar-tus

# With S3 support
pip install litestar-tus[s3]

Standalone Server

litestar-tus ships a standalone server that can be configured entirely via environment variables, following tusd's configuration conventions.

Run with uvx

# File backend (default)
uvx litestar-tus[server]

# S3 backend
TUS_S3_BUCKET=my-bucket TUS_S3_ENDPOINT=http://localhost:9000 \
  AWS_ACCESS_KEY_ID=minioadmin AWS_SECRET_ACCESS_KEY=minioadmin \
  uvx litestar-tus[server]

Run with Docker

docker run -p 8080:8080 ghcr.io/elohmeier/litestar-tus

# S3 backend
docker run -p 8080:8080 \
  -e TUS_S3_BUCKET=my-bucket \
  -e TUS_S3_ENDPOINT=http://minio:9000 \
  -e AWS_ACCESS_KEY_ID=minioadmin \
  -e AWS_SECRET_ACCESS_KEY=minioadmin \
  ghcr.io/elohmeier/litestar-tus

Environment Variables

Variable Default Description
TUS_UPLOAD_DIR ./data File backend storage directory
TUS_BASE_PATH /files/ URL path prefix for TUS endpoints
TUS_MAX_SIZE 0 Maximum upload size in bytes (0 = unlimited)
TUS_BEHIND_PROXY false Respect X-Forwarded headers
TUS_HOST 0.0.0.0 Bind address
TUS_PORT 8080 Bind port
TUS_S3_BUCKET — S3 bucket name (activates S3 backend)
TUS_S3_OBJECT_PREFIX — S3 key prefix for uploads
TUS_S3_ENDPOINT — Custom S3 endpoint (MinIO, etc.)
TUS_S3_PART_SIZE 10485760 S3 multipart part size (10 MiB)

AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION) are read by boto3 directly.

The server exposes a /health endpoint returning {"status": "ok"}.

Quick Start

from litestar import Litestar
from litestar_tus import TUSPlugin, TUSConfig

app = Litestar(
    plugins=[TUSPlugin(TUSConfig(path_prefix="/uploads", max_size=5 * 1024**3))]
)

This registers TUS protocol endpoints at /uploads/ supporting resumable file uploads.

Features

  • TUS v1.0.0 protocol compliance
  • Extensions: creation, creation-with-upload, termination, expiration, checksum, concatenation
  • Storage backends: local filesystem (default) and S3 (via boto3)
  • Concurrency safety: POSIX file locks (file backend) and S3 conditional writes via ETags (S3 backend)
  • Checksum verification: streaming SHA-1, SHA-256, and MD5 validation
  • Lifecycle events: hook into upload creation, progress, completion, and termination via Litestar's event system
  • Streaming: request bodies are streamed directly to storage — the S3 backend uses a rolling buffer that flushes multipart parts incrementally without buffering the full upload in memory

Configuration

TUSConfig(
    path_prefix="/uploads",       # URL prefix for TUS endpoints
    upload_dir="./uploads",       # Local storage directory (file backend)
    max_size=1024 * 1024 * 100,   # Maximum upload size in bytes (optional)
    expiration_seconds=86400,     # Upload expiration in seconds (default: 24h, None to disable)
    extensions=(                  # Protocol extensions to enable
        "creation",
        "creation-with-upload",
        "termination",
        "expiration",
        "checksum",
        "concatenation",
    ),
    storage_backend=None,         # Custom StorageBackend instance (default: FileStorageBackend)
    metadata_override=None,       # Optional hook to override Upload-Metadata based on the Request
)

Request Body Size

Litestar's built-in request_max_body_size defaults to ~9.5 MiB. If your TUS client sends chunks larger than this, PATCH requests will be rejected with 413 Content Too Large before reaching litestar-tus. Raise the limit on the Litestar app to match your expected chunk sizes:

app = Litestar(
    plugins=[TUSPlugin(TUSConfig(...))],
    request_max_body_size=1024 * 1024 * 100,  # 100 MiB
)

Storage Backends

File Backend (default)

Stores uploads on the local filesystem under upload_dir. Each upload produces three files:

File Purpose
<id> Upload data
<id>.info JSON metadata (offset, size, expiration, etc.)
<id>.lock POSIX advisory lock file

Concurrency is handled with fcntl.flock — the lock file is acquired exclusively before every write, and metadata is re-read under the lock to prevent TOCTOU races.

Limitations: fcntl.flock is POSIX-only (Linux/macOS) and only guarantees exclusive access on a single node. NFS and other network filesystems do not reliably support fcntl advisory locks. For multi-worker or multi-node deployments, use the S3 backend instead.

S3 Backend

Uses S3 multipart uploads with a rolling buffer to stream data into parts without full-stream buffering.

import boto3
from litestar_tus import TUSConfig, TUSPlugin
from litestar_tus.backends.s3 import S3StorageBackend

s3_client = boto3.client("s3")
backend = S3StorageBackend(
    client=s3_client,
    bucket="my-bucket",
    key_prefix="uploads/",
    part_size=10 * 1024 * 1024,  # 10 MiB (default), minimum 5 MiB
)

app = Litestar(
    plugins=[TUSPlugin(TUSConfig(storage_backend=backend))]
)

Each upload produces these S3 objects:

Object Purpose
<prefix><id> Assembled upload data (after multipart completion)
<prefix><id>.info JSON metadata
<prefix><id>.pending Temporary buffer for bytes not yet flushed as a part

Rolling Buffer

Incoming data accumulates in a buffer. Whenever the buffer reaches part_size, a multipart part is flushed to S3. Leftover bytes smaller than part_size are persisted as a .pending object and prepended to the buffer on the next write_chunk call. On finish(), any remaining pending data is flushed as the final part and complete_multipart_upload is called.

Optimistic Concurrency Control

The S3 backend uses two layers of concurrency protection:

  1. Process-local anyio.Lock — serializes concurrent writes to the same upload within a single worker process, avoiding unnecessary S3 round-trips.
  2. S3 conditional writes via ETags — provides cross-process and cross-node safety. The ETag of the .info object is tracked and passed as IfMatch on every put_object call. If another process modified the .info object in the meantime, S3 returns 412 Precondition Failed and the write is rejected with HTTP 409. New uploads use IfNoneMatch: * to prevent duplicate creation.

This means the S3 backend is safe to run with multiple worker processes without sticky sessions or external locks.

Checksum Verification

When the checksum extension is enabled (default), clients can send an Upload-Checksum header with PATCH or creation-with-upload requests:

Upload-Checksum: sha256 <base64-encoded-digest>

Supported algorithms: sha1, sha256, md5.

The digest is computed incrementally as data streams through — no extra buffering pass required. A mismatch returns HTTP 460 per the TUS protocol specification.

Concatenation

The concatenation extension (enabled by default) allows clients to upload file parts in parallel and then combine them into a single final upload. This is used by tus-js-client's parallelUploads option.

Protocol flow:

  1. Client creates N partial uploads with Upload-Concat: partial
  2. Client uploads data to each partial via PATCH (can be done in parallel)
  3. Client creates a final upload with Upload-Concat: final;/uploads/id1 /uploads/id2 ...
  4. Server concatenates all partial data in order; the final upload is immediately complete

Partial uploads support creation-with-upload (sending data in the POST body). Final uploads cannot be modified via PATCH after creation.

The S3 backend uses upload_part_copy to concatenate partials server-side without downloading and re-uploading data, running all copy operations in parallel. Partials smaller than 5 MiB (S3's minimum part size for copy operations) fall back to download and re-upload automatically.

Expiration

When expiration_seconds is set (default: 86400 / 24 hours), each upload receives an expires_at timestamp. Expired uploads are rejected with HTTP 410 (Gone) on HEAD, PATCH, and DELETE requests. The Upload-Expires header is included in responses so clients know the deadline.

Note: expired uploads are not automatically cleaned up from storage. Implement a background job or use S3 lifecycle rules to remove stale objects.

Events

Listen to upload lifecycle events:

from litestar.events import listener
from litestar_tus import TUSEvent, UploadInfo

@listener(TUSEvent.POST_FINISH)
async def on_upload_complete(upload_info: UploadInfo) -> None:
    print(f"Upload {upload_info.id} completed ({upload_info.offset} bytes)")

app = Litestar(
    plugins=[TUSPlugin()],
    listeners=[on_upload_complete],
)

Available events:

Event When
PRE_CREATE Before upload is created
POST_CREATE After upload is created
POST_RECEIVE After a data chunk is written
PRE_FINISH Before completing (assembling) the upload
POST_FINISH After the upload is completed
PRE_TERMINATE Before deleting an upload
POST_TERMINATE After an upload is deleted

All events receive upload_info: UploadInfo as a keyword argument.

Metadata Override

Override or inject Upload-Metadata using the incoming request before the upload is created:

from litestar import Request
from litestar_tus import TUSConfig, TUSPlugin

async def metadata_override(request: Request, metadata: dict[str, bytes]) -> dict[str, bytes]:
    metadata["user_id"] = request.headers.get("authorization", "").encode()
    return metadata

app = Litestar(
    plugins=[TUSPlugin(TUSConfig(metadata_override=metadata_override))],
)

License

MIT

Metadata

Release files for litestar-tus 1.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for litestar-tus 1.3.0
File Size Uploaded Provenance
litestar_tus-1.3.0.tar.gz 19.2 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for litestar-tus 1.3.0
File Interpreter ABI Platform
litestar_tus-1.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 43.3 kB

Release files / litestar_tus-1.3.0.tar.gz

Download URL litestar_tus-1.3.0.tar.gz
Size 19.2 kB
Tags Source
SHA-256 checksum
How to use checksums
2f720113d2eb74e0ecd7c4855812b59b42d640a6c8a8fe4fefe8c697019b9c59
BLAKE2b-256 checksum
How to use checksums
41996a4a9f6a57e96068aed61dde0f6608b9d94187dedd95050b2a8325544fb1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release files / litestar_tus-1.3.0-py3-none-any.whl

Download URL litestar_tus-1.3.0-py3-none-any.whl
Size 24.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5b996fbd3d5556a755f0873f023ee06a4c2158ad545c116943b082e2f557443c
BLAKE2b-256 checksum
How to use checksums
c58c497efda56e5ccc7afb3f9c9576b27d5a0c6cd5d22de2d2ea782e739fbdec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Security for litestar-tus 1.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for litestar_tus-1.3.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page