Add tus resumable upload support - #84
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Django Files/API/Tus.swift) that uploads via chunkedPOST/PATCHrequests to/tus/instead of one large multipart request, avoiding Cloudflare's 100MB request cap and resuming after a dropped connection mid-transfer.DFAPI.uploadFileResumableis 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 withTUS_ENABLED=Falseboth degrade to the legacy path with no user-visible error./api/files/for a matching filename+size for a few seconds after the last chunk lands, rather than trusting the PATCH response body.FileUploadView,UploadMenuButton, theUploadAndCopyshare extension) now go throughuploadFileResumablein place ofuploadFile/uploadFileStreamed.Built against the
tus-uploadsbranch of django-files (sibling repo), matching its/tus/hook contract (Upload-Metadatakeys:filename,name,authorization,albums,private,strip-exif,strip-gps).Test plan
tus-uploadswithTUS_ENABLED=True— confirm it completes and the file appears.TUS_ENABLED=False— confirm silent fallback to legacy upload, no user-visible error.maindjango-files server — confirm silent fallback.https://claude.ai/code/session_01G6xHedhUsopN2Ho5U8QRse