Skip to content

feat(examples): showcase multipart, TUS resumable, and background downloads in Storage examples#989

Open
grdsdev wants to merge 14 commits into
claude/vibrant-carson-674f4afrom
claude/nostalgic-meninsky-4fba6f
Open

feat(examples): showcase multipart, TUS resumable, and background downloads in Storage examples#989
grdsdev wants to merge 14 commits into
claude/vibrant-carson-674f4afrom
claude/nostalgic-meninsky-4fba6f

Conversation

@grdsdev
Copy link
Copy Markdown
Contributor

@grdsdev grdsdev commented May 6, 2026

Summary

  • Adds backgroundDownloadSessionIdentifier to StorageOptions and threads it through SupabaseClientStorageClientConfiguration, so apps using SupabaseClient can opt into background download sessions without needing a standalone StorageClient.
  • Configures the Examples app with a background-download session identifier and wires application(_:handleEventsForBackgroundURLSession:completionHandler:) in AppDelegate to supabase.storage.handleBackgroundEvents(forSessionIdentifier:completionHandler:).
  • Full rewrite of FileUploadView — showcases all three upload engines with real progress from the AsyncStream<TransferEvent> API:
    • Smart — auto-selects multipart (≤6 MB) or TUS (>6 MB)
    • Multipart — always multipart regardless of file size
    • Resumable (TUS) — live Pause / Resume / Cancel buttons backed by a retained StorageUploadTask
  • Full rewrite of FileDownloadView — showcases both download paths with real progress:
    • To Memory (downloadData) — loads bytes in-process, image/text preview
    • To Disk (download) — saves to a temp file, background-session capable, noted in the UI
  • Updated StorageExamplesView navigation rows to reflect the new capabilities.

Test plan

  • Open the Examples app → Storage → Upload Files
    • Switch between Smart / Multipart / Resumable, upload a small (<6 MB) and large (>6 MB) file and confirm progress updates in real time.
    • In Resumable mode: tap Pause mid-upload and confirm the bar freezes; tap Resume and confirm it continues; tap Cancel and confirm state resets.
  • Storage → Download Files
    • Switch between To Memory and To Disk, download an image and a text file; confirm preview appears for both modes.
    • In To Disk mode confirm the temporary file URL is displayed.
  • Confirm the app compiles without warnings on iOS (make PLATFORM=IOS XCODEBUILD_ARGUMENT=build xcodebuild).

🤖 Generated with Claude Code

@grdsdev grdsdev requested a review from a team as a code owner May 6, 2026 13:05
@grdsdev grdsdev force-pushed the claude/nostalgic-meninsky-4fba6f branch from 27a5031 to a4ccb0b Compare May 6, 2026 14:07
@grdsdev
Copy link
Copy Markdown
Contributor Author

grdsdev commented May 6, 2026

Code review

Found 2 issues:

  1. FileDownloadView.swift and FileUploadView.swift are missing the Created by Author Name on DD/MM/YY. line from their file headers. AGENTS.md specifies the required format explicitly, and every other file in the Examples/ directory includes this line.

//
// FileDownloadView.swift
// Examples
//
// Demonstrates both download engines:
// - To Memory (downloadData): loads file bytes in-process
// - To Disk (download): saves to a temporary file; background-session capable, survives app suspension
//

//
// FileUploadView.swift
// Examples
//
// Demonstrates all three upload engines:
// - Smart (auto): multipart for files ≤6 MB, TUS for larger files
// - Multipart: standard HTTP multipart, always
// - Resumable (TUS): pause / resume / cancel support, ideal for large files
//

  1. FileDownloadView reuses isDownloading for both loadFiles() (bucket file listing) and downloadFile() (actual transfer). This means tapping "Load Files" renders the "Downloading… 0%" progress bar and disables all download buttons while only fetching a directory listing. If a download is in progress and the user manually taps "Load Files", loadFiles()'s defer { isDownloading = false } will prematurely clear the flag and hide the transfer UI mid-download. A separate @State private var isLoadingFiles = false flag in loadFiles() would fix both problems.

if isDownloading {
transferSection
}

do {
error = nil
isDownloading = true
defer { isDownloading = false }
files = try await supabase.storage.from(selectedBucket).list()
} catch {
self.error = error
}
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

grdsdev and others added 13 commits May 7, 2026 05:41
Adds `DownloadEngine` — an actor-based state machine that manages
URLSessionDownloadTask lifecycle with pause/resume/cancel support.

When the server returns `Accept-Ranges: bytes`, pausing captures resume
data via `cancel(byProducingResumeData:)` so the next `resume()` call
restarts from the last received byte. If the server does not support
range requests the download restarts from byte 0.

Changes:
- New `DownloadEngine` actor with states: idle, starting, downloading,
  paused, completed, failed, cancelled
- `DownloadSessionDelegate` refactored to callback-based routing
  (`DownloadTaskCallbacks`) instead of holding continuations directly;
  temp-file move is now done synchronously inside the delegate method so
  URLSession cannot reclaim the file before the move completes
- `StorageFileAPI.download()` now wires through `DownloadEngine.makeTask`
  and documents pause/resume behavior
- `StorageTransferTask.pause()` / `resume()` docs updated to cover
  downloads alongside TUS uploads
- New `DownloadEngineTests` (Darwin-only) covering basic download,
  pause-then-resume, cancel-yields-cancelled-error, and network-error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lude downloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers happy path, pause/resume cycles, cancel semantics, error
handling, and concurrent download scenarios using a live Supabase
instance via the existing withUploadedFile helper pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Task-identity race in DownloadEngine: after pause() cancels the active
   URLSessionDownloadTask (ID=X) and resume() starts a new one (ID=Y),
   the stale URLError.cancelled from X could arrive while state is
   .downloading(Y) and incorrectly call cancel(). Fix: capture the task
   identifier at callback-registration time and guard every engine callback
   against a mismatch with the current active task.

2. HTTP error responses delivered as completed downloads: URLSession calls
   didFinishDownloadingTo for 4xx/5xx responses too, saving the error JSON
   as the "downloaded" file. Fix: check the HTTP status code in the delegate
   and parse the Supabase error envelope into a StorageError, delivering
   .failed instead of .success.

Also adds StorageError.from(httpResponse:data:) as an internal factory
shared by the delegate, and removes LockIsolated (unavailable in
IntegrationTests target) from the multi-pause-cycle test in favour of
a local actor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nloads in Storage examples

- Add `backgroundDownloadSessionIdentifier` to `StorageOptions` and wire it through
  `SupabaseClient` → `StorageClientConfiguration` so apps can opt into background
  download sessions via `SupabaseClientOptions`.
- Configure the Examples app with a background-download session identifier and forward
  `application(_:handleEventsForBackgroundURLSession:completionHandler:)` in `AppDelegate`
  to `supabase.storage.handleBackgroundEvents(forSessionIdentifier:completionHandler:)`.
- Rewrite `FileUploadView`: segmented picker for Smart / Multipart / Resumable (TUS)
  engines; real progress driven by `for await event in task.events`; Pause / Resume /
  Cancel buttons active for Resumable uploads via a retained `StorageUploadTask`.
- Rewrite `FileDownloadView`: segmented picker for To Memory (`downloadData`) vs To Disk
  (`download`); real progress from event stream; disk mode notes background-session support.
- Update `StorageExamplesView` navigation labels to reflect the new capabilities.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…on row

The buttons were nested inside the same VStack as the ProgressView, making
them a single List row whose height was driven by the ProgressView label.
The buttons were clipped and never visible.

Moving the HStack into a sibling view at the Section content level gives it
its own List row and guaranteed vertical space, matching the intended UI.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…view

After a To Disk download completed, the view read the entire file with
Data(contentsOf:) regardless of size. For a 5 GB file this caused a
matching 5 GB memory spike — exactly what the disk download mode is
designed to avoid.

Fix: only attempt an in-memory preview when the file is ≤ 10 MB.
Larger files show the file URL and size instead, with a note explaining
why the preview was skipped.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Preview required reading the downloaded bytes into memory, which
defeats the purpose of demonstrating the download API. Both modes
now show only metadata (size, path, mode) after a successful download.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add missing 'Created by' line to FileDownloadView and FileUploadView
  headers per AGENTS.md convention
- Introduce isLoadingFiles flag in FileDownloadView so that listing
  bucket files does not collide with the isDownloading transfer state;
  prevents the progress bar from flashing during listing and avoids
  prematurely clearing download progress when loadFiles() completes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add NSAllowsLocalNetworking to bypass ATS for LAN IP addresses,
enabling the app to connect to a local Supabase instance via the
Mac's IP address when testing on a physical device.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@grdsdev grdsdev force-pushed the claude/nostalgic-meninsky-4fba6f branch from 39ad389 to aa78086 Compare May 7, 2026 08:50
Both download modes (To Memory and To Disk) now expose Pause/Resume/Cancel
controls via a type-erased DownloadController that wraps StorageTransferTask.
Uses the new pause/resume support added in feat/storage-resumable-downloads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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