Skip to main content
Premium

TUS Server Setup

Run a tusd service and point HABARICO_UPLOADS["TUS"]["ENDPOINT"] at it. The frontend uploads to tusd via the TUS resumable protocol; tusd stores the object in the same Django default_storage that Habarico reads from, and Habarico moves it to the field location on save (TusUploadProvider.finalize).

For this to line up, the temporary object must land at a storage key that Habarico accepts. The frontend derives that key (upload_key) from the TUS upload URL, i.e. it is exactly the upload ID that tusd assigns. Habarico's finalize then requires the key to:

  1. start with the configured tmp_prefix – otherwise validate_upload_key raises InvalidUploadKey (this prefix check is the security boundary: it keeps finalize from reading or deleting files outside the temporary upload area);
  2. end with the original filename – otherwise the finalized file loses its name and extension (finalize derives the name from upload_key.rsplit("/", 1)[-1]).

tusd assigns a random, prefix-less, name-less ID by default, which satisfies neither. A pre-create hook rewrites the upload ID (and storage path) into the required shape:

<tmp_prefix><uuid>/<original_filename> e.g. tmp/uploads/9f3a.../photo.jpg

Pre-create hook​

A ready-to-use file hook lives in the repository at scripts/tusd/pre-create. It is an operational artifact for the tusd container (not part of the installed Python package), so copy it from the source tree at the tag matching your installed habarico version into your tusd -hooks-dir. It reads the TUS event from stdin, takes filename from the upload metadata (stripped to its basename to defuse path traversal), generates a UUID, and returns a ChangeFileInfo response setting both the upload ID and the storage Path to <tmp_prefix><uuid>/<filename>. Uploads without a filename metadata value are rejected with HTTP 400 (a name-less object would lose its extension).

The prefix is read from the HABARICO_TUS_TMP_PREFIX environment variable (default tmp/uploads/). It must match HABARICO_UPLOADS["TUS"]["TMP_PREFIX"] in your Django settings.

The hook needs a POSIX shell and jq in the tusd image.

Storage mapping​

tusd must write to the same storage and key that Habarico's default_storage.open(upload_key) reads from.

  • Local filesystem (the typical TUS case). Run tusd with -upload-dir set to your Django MEDIA_ROOT; the hook's relative path (tmp/uploads/<uuid>/<filename>) then resolves to MEDIA_ROOT/tmp/uploads/<uuid>/<filename>, which is exactly the storage-relative upload_key. The Django app and tusd must share that directory (e.g. the same mounted volume), so the object tusd writes is visible to finalize. The filestore backend writes a small <key>.info metadata file next to each object; it stays under tmp_prefix, finalize ignores it, and it is removed by the temp cleanup below.
  • S3. Point tusd's s3store at the same bucket the field's storage uses. The resulting object key must equal upload_key; finalize then server-side copies it to the field location like the s3-multipart provider.

CORS​

The admin frontend talks to tusd cross-origin, so allow its origin:

tusd -cors-allow-origin 'https?://admin\.example\.com'
# add -cors-allow-credentials if the frontend sends cookies/auth to tusd

tusd already exposes the TUS protocol response headers the frontend needs (Location, Upload-Offset, Upload-Length, Tus-Resumable, ...); use -cors-expose-headers only for extra custom headers.

Temporary file cleanup​

Finalized objects are moved out of tmp_prefix on save (the temp original, and its .info sidecar on filestore, are then stale). Clean the tmp_prefix periodically: an S3 lifecycle rule expiring objects under the prefix, or a scheduled job pruning old files in MEDIA_ROOT/tmp/uploads/ for filesystem storage. Uploads the user never finishes also accumulate there and are covered by the same cleanup.

Example (local filesystem)​

# MEDIA_ROOT is a volume shared between the Django app and tusd.
tusd \
-upload-dir "$MEDIA_ROOT" \
-hooks-dir /opt/tusd-hooks \ # contains the pre-create hook
-cors-allow-origin 'https?://admin\.example\.com' \
-base-path /files/
# with HABARICO_TUS_TMP_PREFIX=tmp/uploads/ in tusd's environment, matching
# HABARICO_UPLOADS["TUS"]["TMP_PREFIX"], and the endpoint set to .../files/