Repository: firebase/firebase-ios-sdk (language: cpp)

Issue:
[FR]: Set chunk size on upload task
### Description

I may be missing something very obvious, but I see no way to set the upload chunk size from swift code for a 'FIRStorageUploadTask' created via FIRStorageReference putFile.
e.g. 
```swift
let uploadTask = remoteRef.putFile(from: fileURL, metadata: metadata)
```

1. Use case: allow custom chunk sizes so resumable uploads work by default
2. Currently the chunk size seems hard coded to a large value 
3. I propose allowing custom chunk sizes

In  [FIRStorageUploadTask.m](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseStorageInternal/Sources/FIRStorageUploadTask.m#L122) the chunk size seems to be set from the constant `kGTMSessionUploadFetcherStandardChunkSize` which is set to LLONG_MAX; 

```swift
    GTMSessionUploadFetcher *uploadFetcher =
        [GTMSessionUploadFetcher uploadFetcherWithRequest:request
                                           uploadMIMEType:strongSelf->_uploadMetadata.contentType
                                                chunkSize:kGTMSessionUploadFetcherStandardChunkSize
                                           fetcherService:self.fetcherService];
```

Perhaps there is a way to modify this somewhere, but I can't find any documentation on it.

Thanks for any support.




### API Proposal

```swift
let uploadTask = remoteRef.putFile(from: fileURL, metadata: metadata, chunkSize: myChunkSize )
```


### Firebase Product(s)

Storage


Discussion / hints:
I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.
@cclaan Thank for the feature request.  You're correct that there's currently no way to change `chunkSize` from LLONG_MAX

We've begun work on a Swift-only V2 API for Storage in #10161. `chunkSize` could work well as a default argument in the new API. 
@cclaan Would you provide more description about the value of being able to set the `chunkSize`.  Why do you want to be able to manage the upload in smaller chunks?

My use case is uploading ~20mb files from a phone with potentially poor network connection. Using smaller chunks is required so that the upload does not restart from scratch every time the network drops. ( Previously chunkSize was LLONG_MAX )

Fix patch:
diff --git a/FirebaseStorage/CHANGELOG.md b/FirebaseStorage/CHANGELOG.md
index f38471936e5..a790e04b930 100644
--- a/FirebaseStorage/CHANGELOG.md
+++ b/FirebaseStorage/CHANGELOG.md
@@ -1,3 +1,6 @@
+# 10.5.0
+- [added] Added Storage API to limit upload chunk size. (#10137)
+
 # 10.3.0
 - [fixed] Use dedicated serial queue for Storage uploads and downloads instead of a (concurrent) global queue.
   Fixes regression introduced in 10.0.0. (#10487)
diff --git a/FirebaseStorage/Sources/Storage.swift b/FirebaseStorage/Sources/Storage.swift
index 6e3c34b0211..8c453eeb403 100644
--- a/FirebaseStorage/Sources/Storage.swift
+++ b/FirebaseStorage/Sources/Storage.swift
@@ -123,6 +123,12 @@ import FirebaseAuthInterop
     }
   }
 
+  /**
+   * Specify the maximum upload chunk size. Values less than 256K (262144) will be rounded up to 256K. Values
+   * above 256K will be rounded down to the nearest 256K multiple. The default is no maximum.
+   */
+  @objc public var uploadChunkSizeBytes: Int64 = .max
+
   /**
    * A `DispatchQueue` that all developer callbacks are fired on. Defaults to the main queue.
    */
diff --git a/FirebaseStorage/Sources/StorageUploadTask.swift b/FirebaseStorage/Sources/StorageUploadTask.swift
index 2a4ecf278e7..e078633ac7e 100644
--- a/FirebaseStorage/Sources/StorageUploadTask.swift
+++ b/FirebaseStorage/Sources/StorageUploadTask.swift
@@ -76,7 +76,7 @@ import Foundation
       let uploadFetcher = GTMSessionUploadFetcher(
         request: request,
         uploadMIMEType: contentType,
-        chunkSize: Int64.max,
+        chunkSize: self.reference.storage.uploadChunkSizeBytes,
         fetcherService: self.fetcherService
       )
       if let data = self.uploadData {
diff --git a/FirebaseStorage/Tests/Integration/StorageIntegration.swift b/FirebaseStorage/Tests/Integration/StorageIntegration.swift
index 7732c92285f..16543b23170 100644
--- a/FirebaseStorage/Tests/Integration/StorageIntegration.swift
+++ b/FirebaseStorage/Tests/Integration/StorageIntegration.swift
@@ -265,6 +265,106 @@ class StorageResultTests: StorageIntegrationCommon {
     waitForExpectations()
   }
 
+  func testPutFileLimitedChunk() throws {
+    defer {
+      // Reset since tests share storage instance.
+      storage.uploadChunkSizeBytes = Int64.max
+    }
+    let expectation = self.expectation(description: #function)
+    let putFileExpectation = self.expectation(description: "putFile")
+    let ref = storage.reference(withPath: "ios/public/testPutFilePauseResume")
+    let bundle = Bundle(for: StorageIntegrationCommon.self)
+    let filePath = try XCTUnwrap(bundle.path(forResource: "1mb", ofType: "dat"),
+                                 "Failed to get filePath")
+    let data = try XCTUnwrap(try Data(contentsOf: URL(fileURLWithPath: filePath)),
+                             "Failed to load file")
+    let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory())
+    let fileURL = tmpDirURL.appendingPathComponent("LargePutFile.txt")
+    var progressCount = 0
+
+    try data.write(to: fileURL, options: .atomicWrite)
+
+    // Limit the upload chunk size
+    storage.uploadChunkSizeBytes = 256_000
+
+    let task = ref.putFile(from: fileURL) { result in
+      XCTAssertGreaterThanOrEqual(progressCount, 4)
+      self.assertResultSuccess(result)
+      putFileExpectation.fulfill()
+    }
+
+    task.observe(StorageTaskStatus.success) { snapshot in
+      XCTAssertEqual(snapshot.description, "<State: Success>")
+      expectation.fulfill()
+    }
+
+    var uploadedBytes: Int64 = -1
+
+    task.observe(StorageTaskStatus.progress) { snapshot in
+      XCTAssertTrue(snapshot.description.starts(with: "<State: Progress") ||
+        snapshot.description.starts(with: "<State: Resume"))
+      guard let progress = snapshot.progress else {
+        XCTFail("Failed to get snapshot.progress")
+        return
+      }
+      progressCount = progressCount + 1
+      XCTAssertGreaterThanOrEqual(progress.completedUnitCount, uploadedBytes)
+      uploadedBytes = progress.completedUnitCount
+    }
+    waitForExpectations()
+  }
+
+  func testPutFileTinyChunk() throws {
+    defer {
+      // Reset since tests share storage instance.
+      storage.uploadChunkSizeBytes = Int64.max
+    }
+    let expectation = self.expectation(description: #function)
+    let putFileExpectation = self.expectation(description: "putFile")
+    let ref = storage.reference(withPath: "ios/public/testPutFilePauseResume")
+    let bundle = Bundle(for: StorageIntegrationCommon.self)
+    let filePath = try XCTUnwrap(bundle.path(forResource: "1mb", ofType: "dat"),
+                                 "Failed to get filePath")
+    let data = try XCTUnwrap(try Data(contentsOf: URL(fileURLWithPath: filePath)),
+                             "Failed to load file")
+    let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory())
+    let fileURL = tmpDirURL.appendingPathComponent("LargePutFile.txt")
+    var progressCount = 0
+
+    try data.write(to: fileURL, options: .atomicWrite)
+
+    // Limit the upload chunk size. This should behave exactly like the previous
+    // test since small chunk sizes are rounded up to 256K.
+    storage.uploadChunkSizeBytes = 1
+
+    let task = ref.putFile(from: fileURL) { result in
+      XCTAssertGreaterThanOrEqual(progressCount, 4)
+      XCTAssertLessThanOrEqual(progressCount, 6)
+      self.assertResultSuccess(result)
+      putFileExpectation.fulfill()
+    }
+
+    task.observe(StorageTaskStatus.success) { snapshot in
+      XCTAssertEqual(snapshot.description, "<State: Success>")
+      expectation.fulfill()
+    }
+
+    var uploadedBytes: Int64 = -1
+
+    task.observe(StorageTaskStatus.progress) { snapshot in
+      XCTAssertTrue(snapshot.description.starts(with: "<State: Progress") ||
+        snapshot.description.starts(with: "<State: Resume"))
+      guard let progress = snapshot.progress else {
+        XCTFail("Failed to get snapshot.progress")
+        return
+      }
+      progressCount = progressCount + 1
+      XCTAssertGreaterThanOrEqual(progress.completedUnitCount, uploadedBytes)
+      uploadedBytes = progress.completedUnitCount
+    }
+    waitForExpectations()
+  }
+
   func testAttemptToUploadDirectoryShouldFail() throws {
     // This `.numbers` file is actually a directory.
     let fileName = "HomeImprovement.numbers"


Test patch: