Skip to content

Add tus resumable upload support - #84

Merged
raluaces merged 13 commits into
mainfrom
tus-upload-support
Jul 20, 2026
Merged

Add tus resumable upload support#84
raluaces merged 13 commits into
mainfrom
tus-upload-support

Conversation

@raluaces

Copy link
Copy Markdown
Member

Summary

  • Adds a tus (tus.io) client (Django Files/API/Tus.swift) that uploads via chunked POST/PATCH requests to /tus/ instead of one large multipart request, avoiding Cloudflare's 100MB request cap and resuming after a dropped connection mid-transfer.
  • DFAPI.uploadFileResumable is the new entry point: it tries tus first, and transparently falls back to the existing /api/upload/ endpoint (streamed, so the share extension stays memory-safe) whenever tus isn't usable — pre-tus servers (no /tus/ route) and current servers with TUS_ENABLED=False both degrade to the legacy path with no user-visible error.
  • Per-server "tus unsupported" outcome is cached in-memory for the process lifetime so repeat uploads to an old/disabled server don't retry a doomed tus creation request each time.
  • Since import on the server is async (Celery), completion is confirmed by polling /api/files/ for a matching filename+size for a few seconds after the last chunk lands, rather than trusting the PATCH response body.
  • All existing upload call sites (FileUploadView, UploadMenuButton, the UploadAndCopy share extension) now go through uploadFileResumable in place of uploadFile/uploadFileStreamed.

Built against the tus-uploads branch of django-files (sibling repo), matching its /tus/ hook contract (Upload-Metadata keys: filename, name, authorization, albums, private, strip-exif, strip-gps).

Test plan

  • Upload a small file (photo picker, file picker, clipboard, share extension) against a server on tus-uploads with TUS_ENABLED=True — confirm it completes and the file appears.
  • Upload against the same server with TUS_ENABLED=False — confirm silent fallback to legacy upload, no user-visible error.
  • Upload against a pre-tus main django-files server — confirm silent fallback.
  • Upload a large (>100MB) file through a Cloudflare-fronted dev server — confirm it succeeds where the old endpoint would 413.
  • Kill wifi mid-upload of a large file — confirm the chunked PATCH loop resumes rather than restarting from zero.
  • Share a large video from Photos via the share extension — confirm no extension memory crash on either the tus or fallback path.

https://claude.ai/code/session_01G6xHedhUsopN2Ho5U8QRse

raluaces added 13 commits July 9, 2026 17:38
Adds a tus (tus.io) client so uploads chunk through /tus/ instead of one
large multipart POST, avoiding Cloudflare's 100MB request cap and letting
transfers resume across a dropped connection. Every call site now goes
through DFAPI.uploadFileResumable, which attempts tus first and
transparently falls back to the existing /api/upload/ endpoint (streamed,
to stay memory-safe in the share extension) whenever tus is unavailable —
older django-files servers without the /tus/ route, or newer ones with
TUS_ENABLED off. Server-side import is async, so completion is confirmed
by polling /api/files/ for the new file rather than trusting the PATCH
response body.
Adds a Settings > Uploads screen with a toggle to disable resumable (tus)
uploads entirely and a picker for the chunk size (10-90MB, default 40MB).
Both are backed by the shared app-group UserDefaults suite so the share
extension picks up the same preference the main app sets. Disabling tus
skips straight to the existing streamed legacy upload path.
Adds an UploadPauseGate (actor-based suspend/resume gate) consulted between
tus chunks in Tus.swift's patch loop. uploadFileResumable now reports back
once a given upload is confirmed running over tus (the creation POST
succeeded) via onTusActiveChange, and UploadProgressManager only shows the
pause control once that fires — legacy-path uploads (tus disabled, server
too old, or a fallback after tus failed mid-transfer) have no pause concept,
so the button stays hidden for those. Cancelling a paused upload unblocks
it immediately rather than leaving it stuck waiting for a resume.
Tus.swift is compiled into both the main app and the share extension, and
its uploadFileResumable signature now references UploadPauseGate — but
that file lives in Utils/, which wasn't synced to the extension target,
so it failed to build there with "Cannot find type 'UploadPauseGate'".
Pause was only checked between chunks, but a chunk defaults to tens of MB
so an in-flight PATCH could take a long time to finish on its own — pause
looked broken because nothing visibly happened until that chunk completed.

UploadPauseGate now tracks the active chunk's URLSessionTask and cancels
it immediately on pause. The chunk loop recognizes that specific
cancellation (as opposed to a real failure or a genuine upload cancel),
waits for resume, resyncs the offset via HEAD, and retries the same chunk
without touching the retry budget.
Pausing a large chunk still felt broken in practice — by the time a pause
took effect the chunk (and often the whole file) had already gone through,
so it wasn't a usable control. Reverting the feature (the three commits
that added it) rather than carrying dead UI/plumbing forward.
Shared videos previously fell through to the generic "File ready to
upload" placeholder alongside PDFs and other arbitrary files — there was
no public.movie check at all in loadProvider. Now a video is detected
explicitly, a first-frame thumbnail is generated via AVAssetImageGenerator
(mirroring the existing downsample() pattern used for images), and tapping
it opens a full-screen SwiftUI VideoPlayer for real playback of the same
file that's about to upload — no extra loading, both are built-in
AVFoundation/AVKit APIs.
Sharing more than one image/video previously only showed the first item's
thumbnail (or nothing) with a plain "Upload N Files" label — the rest were
invisible until upload. ShareViewModel now collects a PreviewThumbnail per
image/video item (generic files/text are unaffected), and ShareView shows
a 3-column grid capped at 6 cells with a "+N" badge for the overflow once
more than one is present; single-item shares keep the existing large
preview unchanged. Video cells stay tappable into the same full-screen
player added for the single-video case.
@raluaces
raluaces merged commit 4cd97f1 into main Jul 20, 2026
1 check passed
@raluaces
raluaces deleted the tus-upload-support branch July 20, 2026 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant