Skip to content

[Storage] Rewrite Firebase.Storage in pure C# - #1555

Open
AustinBenoit wants to merge 17 commits into
mainfrom
ab-pure-csharp-storage
Open

AustinBenoit wants to merge 17 commits into
mainfrom
ab-pure-csharp-storage

Conversation

@AustinBenoit

@AustinBenoit AustinBenoit commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

Rewrites Firebase.Storage in pure C# on top of the Firebase Storage REST API, removing the dependency on the C++ SWIG layer and native platform Storage libraries (FirebaseCppStorage, Android firebase-storage-unity, and iOS FirebaseStorage).

  • Packaging & Dependencies: Ships Firebase.Storage as source under Assets/Firebase/FirebaseStorage with Firebase.Storage.asmdef (matching Firebase.Functions and Firebase.FirebaseAI), and removes the hard package dependency on Firebase.Auth (Auth and App Check tokens are attached via FirebaseInterops when present).
  • Resumable Transfers & Reliability:
    • Uploads use the GCS resumable upload protocol with adaptive chunking (256 KB to 32 MB) and populate UploadState.UploadSessionUri so interrupted uploads can be resumed via previousSessionUri on all platforms.
    • Downloads support automatic HTTP Range resumption with ETag verification, and GetStreamAsync() now returns a live, forward-only network stream instead of buffering the entire object into a MemoryStream.
    • Adds a 60s per-attempt stall watchdog and exponential backoff bounded by MaxUploadRetryTime, MaxDownloadRetryTime, and MaxOperationRetryTime.
  • Pause / Resume / Cancel: New StorageController (mirrors the C++ SDK's firebase::storage::Controller and the pause()/resume()/cancel() on Android/iOS/Web transfer tasks). Pass one as the new trailing optional controller parameter of PutBytesAsync / PutFileAsync / PutStreamAsync / GetBytesAsync / GetFileAsync / GetStreamAsync(Action<Stream>, ...). Paused uploads resume from the server-committed offset; paused downloads resume with a Range request. CancellationToken continues to work as before.
  • Public API Compatibility: Preserves 100% of the existing Firebase.Storage public API surface; the only additions are StorageController and the trailing optional controller parameter above (source-compatible with all existing calls).

Changelog

  • Storage: Rewrote the internal implementation in C#, removing the dependency on the C++ and native platform Storage libraries.
  • Storage: Populated UploadState.UploadSessionUri on all platforms so interrupted uploads can be resumed via the previousSessionUri parameter of PutBytesAsync, PutFileAsync, and PutStreamAsync.
  • Storage: Added StorageController, which can be passed to the upload and download methods on StorageReference to pause, resume or cancel a transfer in progress.
  • Storage: Behavior Change StorageReference.GetStreamAsync() now returns a live, forward-only network stream instead of a buffered MemoryStream (CanSeek is false).

Testing


Type of Change

Place an x the applicable box:

  • Bug fix. Add the issue # below if applicable.
  • New feature. A non-breaking change which adds functionality.
  • Other, such as a build process or documentation change.

- Replace C++ SWIG wrapper (storage.i / FirebaseCppStorage) with a pure C#
  implementation in storage/src/ targeting Unity 2022.3+ (.NET Standard 2.1).
- Extend shared Firebase.Internal (app/src/internal/FirebaseInterops.cs and
  HttpHelpers.cs) with AppDisposed reflection helpers, Storage REST headers
  (X-Firebase-Storage-Version, Authorization, x-firebase-appcheck, x-firebase-gmpid),
  and non-blocking exponential backoff with jitter.
- Implement adaptive GCS resumable uploads (256 KB -> 32 MB chunks with
  ArrayPool<byte>.Shared, session URI exposure/resumption, and cancel support)
  and resumable Range + ETag validated downloads.
- Enforce SemaphoreSlim concurrency gates (2 uploads, 3 downloads, 5 commands)
  and add 5 new pure-C# tests to UIHandlerAutomated.cs (45 total tests).
- GetDownloadUrlAsync: route the returned Uri through
  FirebaseStorage.ConstructFormattedUri so "%2F" in the object path is
  preserved by Uri.ToString()/PathAndQuery (matches the pre-rewrite SDK).
- ResumableUploadTask: fix SynchronizeBufferOrStreamAsync so an
  out-of-buffer server offset re-reads a fresh chunk from the synchronized
  stream position (seekable and non-seekable), accounts for the lookahead
  byte, and fails clearly if a non-seekable stream ends before the server
  offset. Handle "final" query responses with an empty body by fetching
  metadata (ResolveFinalizedMetadataAsync).
- ResumableDownloadTask: ETag mismatch on a streaming reconnect now fails
  fast instead of retrying until MaxDownloadRetryTime; handle a server
  that ignores Range and returns 200 by draining already-consumed bytes
  (erroring on short bodies); dispose the response if the body stream
  cannot be opened; delete a newly created destination file when
  GetFileAsync fails or is canceled.
- ResolveLocalFilePath (upload + download): do not unescape Uri.LocalPath
  a second time.
- TransferStateUpdater: log progress-handler exceptions directly instead of
  leaving them in ExceptionAggregator thread-local storage.
- ModuleLogger: drop the unused strong-reference children list that leaked
  one logger per StorageReference; StorageReference.Logger is now lazy.
- FirebaseStorage.GetInstance(app, url): tolerate a null app again.
- unity_packer/debug_single_export_json/storage.json: match exports.json.
- Tests: TestUploadSessionUriAndResume now actually resumes from the
  captured session URI; add TestUploadPartialSessionResume which interrupts
  a non-seekable stream upload after the first chunk and resumes it.
  Mono desktop suite: 46/46 passing.
- Release download concurrency slot after response headers in GetStreamAsync() and add finalizer to ResumableDownloadStream (B1)
- Dispatch IProgress<T> callbacks on main thread via FirebaseHandler.RunOnMainThreadAsync with coalescing and FlushAsync (B2)
- Add 60s per-attempt StallWatchdog and WatchdogByteArrayContent for transfer stall detection (B3)
- Reject operations on RootReference and empty buckets with StorageException (B4)
- Document 14.0.0 Storage rewrite and migration notes in docs/readme.md and XML comments (B5)
- Preserve null values in MetadataChange/StorageMetadata to support clearing fields and deleting custom metadata keys (S1)
- Increase HttpClientHandler.MaxConnectionsPerServer to 16 (S2)
- Preserve inner network exceptions on retry budget exhaustion and rethrow non-network exceptions (S3)
- Respect initial Stream.Position for seekable streams in PutStreamAsync (S5)
- Route non-user OperationCanceledExceptions through backoff instead of pause branch (S6)
- Add ModuleLogger diagnostics and consolidate StorageHttpClient.SendCommandAsync (S7)
- Subscribe directly to FirebaseApp.AppDisposed, restore GetInstance(FirebaseApp, string = null) overload, and remove dead reflection fallback (S8, S9, S12)
- Cap ETag mismatch restarts and write GetFileAsync via atomic .fbdownload temp file (S10, S11)
- Add integration tests for root reference rejection, metadata deletion, undisposed GetStreamAsync, non-zero stream position, and GetFileAsync failure preservation (S13)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request rewrites the internal implementation of the Firebase Storage Unity SDK in pure C#, completely removing the dependency on the C++ SDK and native platform Storage libraries. It introduces new C# classes to handle HTTP REST communication, resumable uploads/downloads, concurrency scheduling, and stall watchdogs. Additionally, it updates metadata, progress tracking, and exception handling to work natively in C#, and adds comprehensive integration tests. Feedback on the changes highlights a potential race condition in 'FirebaseStorage.cs' between 'Dispose()' and 'GetInstance()' when accessing the cached instances dictionary, and suggests synchronizing these operations with a lock.

Comment thread storage/src/FirebaseStorage.cs Resolved
Comment thread storage/src/FirebaseStorage.cs Resolved
@AustinBenoit AustinBenoit added the tests-requested: full Trigger a FULL set of integration tests (uses expanded test matrix). label Oct 29, 2026
…isposed outside cache lock, unique GetFile temp names, 404 bucket/project mapping, exponential backoff for transport errors, multi-chunk test
- Retry budgets (MaxUploadRetryTime / MaxDownloadRetryTime) now measure time
  spent failing rather than wall-clock time: the stopwatch starts on the first
  failure and resets when bytes flow or after a pause, so long healthy
  transfers and long pauses no longer exhaust the budget on the next blip.
- Paused transfers hand their scheduler slot back (StorageTaskScheduler.Slot)
  so two paused uploads / three paused downloads cannot starve other transfers.
- Pause() now aborts an in-flight read on GetStreamAsync streams too.
- Cancellation always wins over the retry budget in
  SendWithExponentialBackoffAsync (Mono reports a cancelled request as
  WebException).
- StorageTask caches its CancellationToken so it stays queryable after Dispose.
- StorageController records Pause()/Cancel() made after its previous transfer
  finished, matching the documented pause-before-start behavior.
- ResumableDownloadStream registers a weak cancel callback (undisposed streams
  stay collectable) and never throws from its finalizer.
- Per-instance FirebaseStorage logger (tagged with the instance URL) restored.
- Removed legacy C++ error-code mapping and other dead code; dropped unrelated
  app folder GUIDs from guids.json and aligned internal.meta with guids.json.
- Fixed TestGetFileAsyncPreservesExistingFileOnFailure temp-file assertion and
  added TestPausedTransfersDoNotBlockOthers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests-requested: full Trigger a FULL set of integration tests (uses expanded test matrix).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant