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:
- start with the configured
tmp_prefix– otherwisevalidate_upload_keyraisesInvalidUploadKey(this prefix check is the security boundary: it keepsfinalizefrom reading or deleting files outside the temporary upload area); - end with the original filename – otherwise the finalized file loses its name and extension
(
finalizederives the name fromupload_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
tusdwith-upload-dirset to your DjangoMEDIA_ROOT; the hook's relative path (tmp/uploads/<uuid>/<filename>) then resolves toMEDIA_ROOT/tmp/uploads/<uuid>/<filename>, which is exactly the storage-relativeupload_key. The Django app andtusdmust share that directory (e.g. the same mounted volume), so the objecttusdwrites is visible tofinalize. Thefilestorebackend writes a small<key>.infometadata file next to each object; it stays undertmp_prefix,finalizeignores it, and it is removed by the temp cleanup below. - S3. Point
tusd'ss3storeat the same bucket the field's storage uses. The resulting object key must equalupload_key; finalize then server-side copies it to the field location like thes3-multipartprovider.
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/