StorageReference.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. // Copyright 2022 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import FirebaseCoreInternal
  15. import Foundation
  16. /// `StorageReference` represents a reference to a Google Cloud Storage object. Developers can
  17. /// upload and download objects, as well as get/set object metadata, and delete an object at the
  18. /// path. See the [Cloud docs](https://cloud.google.com/storage/) for more details.
  19. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  20. @objc(FIRStorageReference) open class StorageReference: NSObject,
  21. @unchecked Sendable /* TODO: sendable */ {
  22. // MARK: - Public APIs
  23. /// The `Storage` service object which created this reference.
  24. @objc public let storage: Storage
  25. /// The name of the Google Cloud Storage bucket associated with this reference.
  26. /// For example, in `gs://bucket/path/to/object.txt`, the bucket would be 'bucket'.
  27. @objc public var bucket: String {
  28. return path.bucket
  29. }
  30. /// The full path to this object, not including the Google Cloud Storage bucket.
  31. /// In `gs://bucket/path/to/object.txt`, the full path would be: `path/to/object.txt`.
  32. @objc public var fullPath: String {
  33. return path.object ?? ""
  34. }
  35. /// The short name of the object associated with this reference.
  36. ///
  37. /// In `gs://bucket/path/to/object.txt`, the name of the object would be `object.txt`.
  38. @objc public var name: String {
  39. return (path.object as? NSString)?.lastPathComponent ?? ""
  40. }
  41. /// Creates a new `StorageReference` pointing to the root object.
  42. /// - Returns: A new `StorageReference` pointing to the root object.
  43. @objc open func root() -> StorageReference {
  44. return StorageReference(storage: storage, path: path.root())
  45. }
  46. /// Creates a new `StorageReference` pointing to the parent of the current reference
  47. /// or `nil` if this instance references the root location.
  48. /// ```
  49. /// For example:
  50. /// path = foo/bar/baz parent = foo/bar
  51. /// path = foo parent = (root)
  52. /// path = (root) parent = nil
  53. /// ```
  54. /// - Returns: A new `StorageReference` pointing to the parent of the current reference.
  55. @objc open func parent() -> StorageReference? {
  56. guard let parentPath = path.parent() else {
  57. return nil
  58. }
  59. return StorageReference(storage: storage, path: parentPath)
  60. }
  61. /// Creates a new `StorageReference` pointing to a child object of the current reference.
  62. /// ```
  63. /// path = foo child = bar newPath = foo/bar
  64. /// path = foo/bar child = baz ntask.impl.snapshotwPath = foo/bar/baz
  65. /// All leading and trailing slashes will be removed, and consecutive slashes will be
  66. /// compressed to single slashes. For example:
  67. /// child = /foo/bar newPath = foo/bar
  68. /// child = foo/bar/ newPath = foo/bar
  69. /// child = foo///bar newPath = foo/bar
  70. /// ```
  71. ///
  72. /// - Parameter path: The path to append to the current path.
  73. /// - Returns: A new `StorageReference` pointing to a child location of the current reference.
  74. @objc(child:) open func child(_ path: String) -> StorageReference {
  75. return StorageReference(storage: storage, path: self.path.child(path))
  76. }
  77. // MARK: - Uploads
  78. /// Asynchronously uploads data to the currently specified `StorageReference`,
  79. /// without additional metadata.
  80. /// This is not recommended for large files, and one should instead upload a file from disk.
  81. /// - Parameters:
  82. /// - uploadData: The data to upload.
  83. /// - metadata: `StorageMetadata` containing additional information (MIME type, etc.)
  84. /// about the object being uploaded.
  85. /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the
  86. /// upload.
  87. @objc(putData:metadata:)
  88. @discardableResult
  89. open func putData(_ uploadData: Data, metadata: StorageMetadata? = nil) -> StorageUploadTask {
  90. return putData(uploadData, metadata: metadata, completion: nil)
  91. }
  92. /// Asynchronously uploads data to the currently specified `StorageReference`.
  93. /// This is not recommended for large files, and one should instead upload a file from disk.
  94. /// - Parameter uploadData The data to upload.
  95. /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the
  96. /// upload.
  97. @objc(putData:) @discardableResult open func __putData(_ uploadData: Data) -> StorageUploadTask {
  98. return putData(uploadData, metadata: nil, completion: nil)
  99. }
  100. /// Asynchronously uploads data to the currently specified `StorageReference`.
  101. /// This is not recommended for large files, and one should instead upload a file from disk.
  102. /// - Parameters:
  103. /// - uploadData: The data to upload.
  104. /// - metadata: `StorageMetadata` containing additional information (MIME type, etc.)
  105. /// about the object being uploaded.
  106. /// - completion: A closure that either returns the object metadata on success,
  107. /// or an error on failure.
  108. /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the
  109. /// upload.
  110. @objc(putData:metadata:completion:) @discardableResult
  111. open func putData(_ uploadData: Data,
  112. metadata: StorageMetadata? = nil,
  113. completion: ((_: StorageMetadata?, _: Error?) -> Void)?) -> StorageUploadTask {
  114. let putMetadata = metadata ?? StorageMetadata()
  115. if let path = path.object {
  116. putMetadata.path = path
  117. putMetadata.name = (path as NSString).lastPathComponent as String
  118. }
  119. let task = StorageUploadTask(reference: self,
  120. queue: storage.dispatchQueue,
  121. data: uploadData,
  122. metadata: putMetadata)
  123. startAndObserveUploadTask(task: task, completion: completion)
  124. return task
  125. }
  126. /// Asynchronously uploads a file to the currently specified `StorageReference`.
  127. /// - Parameters:
  128. /// - fileURL: A URL representing the system file path of the object to be uploaded.
  129. /// - metadata: `StorageMetadata` containing additional information (MIME type, etc.)
  130. /// about the object being uploaded.
  131. /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the
  132. /// upload.
  133. @objc(putFile:metadata:) @discardableResult
  134. open func putFile(from fileURL: URL, metadata: StorageMetadata? = nil) -> StorageUploadTask {
  135. return putFile(from: fileURL, metadata: metadata, completion: nil)
  136. }
  137. /// Asynchronously uploads a file to the currently specified `StorageReference`,
  138. /// without additional metadata.
  139. /// @param fileURL A URL representing the system file path of the object to be uploaded.
  140. /// @return An instance of StorageUploadTask, which can be used to monitor or manage the upload.
  141. @objc(putFile:) @discardableResult open func __putFile(from fileURL: URL) -> StorageUploadTask {
  142. return putFile(from: fileURL, metadata: nil, completion: nil)
  143. }
  144. /// Asynchronously uploads a file to the currently specified `StorageReference`.
  145. /// - Parameters:
  146. /// - fileURL: A URL representing the system file path of the object to be uploaded.
  147. /// - metadata: `StorageMetadata` containing additional information (MIME type, etc.)
  148. /// about the object being uploaded.
  149. /// - completion: A completion block that either returns the object metadata on success,
  150. /// or an error on failure.
  151. /// - Returns: An instance of `StorageUploadTask`, which can be used to monitor or manage the
  152. /// upload.
  153. @objc(putFile:metadata:completion:) @discardableResult
  154. open func putFile(from fileURL: URL,
  155. metadata: StorageMetadata? = nil,
  156. completion: ((_: StorageMetadata?, _: Error?) -> Void)?) -> StorageUploadTask {
  157. let putMetadata: StorageMetadata = metadata ?? StorageMetadata()
  158. if let path = path.object {
  159. putMetadata.path = path
  160. putMetadata.name = (path as NSString).lastPathComponent as String
  161. }
  162. let task = StorageUploadTask(reference: self,
  163. queue: storage.dispatchQueue,
  164. file: fileURL,
  165. metadata: putMetadata)
  166. startAndObserveUploadTask(task: task, completion: completion)
  167. return task
  168. }
  169. // MARK: - Downloads
  170. /// Asynchronously downloads the object at the `StorageReference` to a `Data` instance in memory.
  171. /// A `Data` buffer of the provided max size will be allocated, so ensure that the device has
  172. /// enough free
  173. /// memory to complete the download. For downloading large files, `write(toFile:)` may be a better
  174. /// option.
  175. /// - Parameters:
  176. /// - maxSize: The maximum size in bytes to download. If the download exceeds this size,
  177. /// the task will be cancelled and an error will be returned.
  178. /// - completion: A completion block that either returns the object data on success,
  179. /// or an error on failure.
  180. /// - Returns: A `StorageDownloadTask` that can be used to monitor or manage the download.
  181. @objc(dataWithMaxSize:completion:) @discardableResult
  182. open func getData(maxSize: Int64,
  183. completion: @escaping ((_: Data?, _: Error?) -> Void)) -> StorageDownloadTask {
  184. let task = StorageDownloadTask(reference: self,
  185. queue: storage.dispatchQueue,
  186. file: nil)
  187. task.completionData = completion
  188. let callbackQueue = storage.callbackQueue
  189. task.observe(.success) { snapshot in
  190. let error = self.checkSizeOverflow(task: snapshot.task, maxSize: maxSize)
  191. callbackQueue.async {
  192. if let completion = task.completionData {
  193. let data = error == nil ? task.downloadData : nil
  194. completion(data, error)
  195. task.completionData = nil
  196. }
  197. }
  198. }
  199. task.observe(.failure) { snapshot in
  200. callbackQueue.async {
  201. task.completionData?(nil, snapshot.error)
  202. task.completionData = nil
  203. }
  204. }
  205. task.observe(.progress) { snapshot in
  206. if let error = self.checkSizeOverflow(task: snapshot.task, maxSize: maxSize) {
  207. task.cancel(withError: error)
  208. }
  209. }
  210. task.enqueue()
  211. return task
  212. }
  213. /// Asynchronously retrieves a long lived download URL with a revokable token.
  214. /// This can be used to share the file with others, but can be revoked by a developer
  215. /// in the Firebase Console.
  216. /// - Parameter completion: A completion block that either returns the URL on success,
  217. /// or an error on failure.
  218. @objc(downloadURLWithCompletion:)
  219. open func downloadURL(completion: @escaping (@Sendable (_: URL?, _: Error?) -> Void)) {
  220. StorageGetDownloadURLTask.getDownloadURLTask(reference: self,
  221. queue: storage.dispatchQueue,
  222. completion: completion)
  223. }
  224. /// Asynchronously retrieves a long lived download URL with a revokable token.
  225. /// This can be used to share the file with others, but can be revoked by a developer
  226. /// in the Firebase Console.
  227. /// - Throws: An error if the download URL could not be retrieved.
  228. /// - Returns: The URL on success.
  229. open func downloadURL() async throws -> URL {
  230. return try await withCheckedThrowingContinuation { continuation in
  231. self.downloadURL { result in
  232. continuation.resume(with: result)
  233. }
  234. }
  235. }
  236. /// Asynchronously downloads the object at the current path to a specified system filepath.
  237. /// - Parameter fileURL: A file system URL representing the path the object should be downloaded
  238. /// to.
  239. /// - Returns A `StorageDownloadTask` that can be used to monitor or manage the download.
  240. @objc(writeToFile:) @discardableResult
  241. open func write(toFile fileURL: URL) -> StorageDownloadTask {
  242. return write(toFile: fileURL, completion: nil)
  243. }
  244. /// Asynchronously downloads the object at the current path to a specified system filepath.
  245. /// - Parameters:
  246. /// - fileURL: A file system URL representing the path the object should be downloaded to.
  247. /// - completion: A closure that fires when the file download completes, passed either
  248. /// a URL pointing to the file path of the downloaded file on success,
  249. /// or an error on failure.
  250. /// - Returns: A `StorageDownloadTask` that can be used to monitor or manage the download.
  251. @objc(writeToFile:completion:) @discardableResult
  252. open func write(toFile fileURL: URL,
  253. completion: ((_: URL?, _: Error?) -> Void)?) -> StorageDownloadTask {
  254. let task = StorageDownloadTask(reference: self,
  255. queue: storage.dispatchQueue,
  256. file: fileURL)
  257. if let completion {
  258. task.completionURL = completion
  259. let callbackQueue = storage.callbackQueue
  260. task.observe(.success) { snapshot in
  261. callbackQueue.async {
  262. task.completionURL?(fileURL, nil)
  263. task.completionURL = nil
  264. }
  265. }
  266. task.observe(.failure) { snapshot in
  267. callbackQueue.async {
  268. task.completionURL?(nil, snapshot.error)
  269. task.completionURL = nil
  270. }
  271. }
  272. }
  273. task.enqueue()
  274. return task
  275. }
  276. // MARK: - List Support
  277. /// Lists all items (files) and prefixes (folders) under this `StorageReference`.
  278. ///
  279. /// This is a helper method for calling `list()` repeatedly until there are no more results.
  280. ///
  281. /// Consistency of the result is not guaranteed if objects are inserted or removed while this
  282. /// operation is executing. All results are buffered in memory.
  283. ///
  284. /// `listAll(completion:)` is only available for projects using Firebase Rules Version 2.
  285. /// - Parameter completion: A completion handler that will be invoked with all items and prefixes
  286. /// under
  287. /// the current `StorageReference`.
  288. @objc(listAllWithCompletion:)
  289. open func listAll(completion: @escaping @Sendable (_: StorageListResult?, _: Error?) -> Void) {
  290. // Making this a closure changes its self-capture semantics, which causes
  291. // a compiler error.
  292. @Sendable func paginatedCompletion(_ listResult: StorageListResult?, _ error: Error?) {
  293. var prefixes = [StorageReference]()
  294. var items = [StorageReference]()
  295. if let error {
  296. completion(nil, error)
  297. return
  298. }
  299. guard let listResult = listResult else {
  300. fatalError("internal error: both listResult and error are nil")
  301. }
  302. prefixes.append(contentsOf: listResult.prefixes)
  303. items.append(contentsOf: listResult.items)
  304. if let pageToken = listResult.pageToken {
  305. StorageListTask.listTask(reference: self,
  306. queue: storage.dispatchQueue,
  307. pageSize: nil,
  308. previousPageToken: pageToken,
  309. completion: paginatedCompletion)
  310. } else {
  311. let result = StorageListResult(withPrefixes: prefixes, items: items, pageToken: nil)
  312. completion(result, nil)
  313. }
  314. }
  315. StorageListTask.listTask(reference: self,
  316. queue: storage.dispatchQueue,
  317. pageSize: nil,
  318. previousPageToken: nil,
  319. completion: paginatedCompletion)
  320. }
  321. /// Lists all items (files) and prefixes (folders) under this StorageReference.
  322. /// This is a helper method for calling list() repeatedly until there are no more results.
  323. /// Consistency of the result is not guaranteed if objects are inserted or removed while this
  324. /// operation is executing. All results are buffered in memory.
  325. /// `listAll()` is only available for projects using Firebase Rules Version 2.
  326. /// - Throws: An error if the list operation failed.
  327. /// - Returns: All items and prefixes under the current `StorageReference`.
  328. open func listAll() async throws -> StorageListResult {
  329. return try await withCheckedThrowingContinuation { continuation in
  330. self.listAll { result in
  331. continuation.resume(with: result)
  332. }
  333. }
  334. }
  335. /// List up to `maxResults` items (files) and prefixes (folders) under this StorageReference.
  336. ///
  337. /// "/" is treated as a path delimiter. Firebase Storage does not support unsupported object
  338. /// paths that end with "/" or contain two consecutive "/"s. All invalid objects in GCS will be
  339. /// filtered.
  340. ///
  341. /// Only available for projects using Firebase Rules Version 2.
  342. /// - Parameters:
  343. /// - maxResults: The maximum number of results to return in a single page. Must be
  344. /// greater than 0 and at most 1000.
  345. /// - completion: A completion handler that will be invoked with up to `maxResults` items and
  346. /// prefixes under the current `StorageReference`.
  347. @objc(listWithMaxResults:completion:)
  348. open func list(maxResults: Int64,
  349. completion: @escaping (@Sendable (_: StorageListResult?, _: Error?) -> Void)) {
  350. if maxResults <= 0 || maxResults > 1000 {
  351. completion(nil, StorageError.invalidArgument(
  352. message: "Argument 'maxResults' must be between 1 and 1000 inclusive."
  353. ))
  354. } else {
  355. StorageListTask.listTask(reference: self,
  356. queue: storage.dispatchQueue,
  357. pageSize: maxResults,
  358. previousPageToken: nil,
  359. completion: completion)
  360. }
  361. }
  362. /// Resumes a previous call to `list(maxResults:completion:)`, starting after a pagination token.
  363. ///
  364. /// Returns the next set of items (files) and prefixes (folders) under this `StorageReference`.
  365. ///
  366. /// "/" is treated as a path delimiter. Storage does not support unsupported object
  367. /// paths that end with "/" or contain two consecutive "/"s. All invalid objects in GCS will be
  368. /// filtered.
  369. ///
  370. /// `list(maxResults:pageToken:completion:)`is only available for projects using Firebase Rules
  371. /// Version 2.
  372. /// - Parameters:
  373. /// - maxResults: The maximum number of results to return in a single page. Must be greater
  374. /// than 0 and at most 1000.
  375. /// - pageToken: A page token from a previous call to list.
  376. /// - completion: A completion handler that will be invoked with the next items and prefixes
  377. /// under the current StorageReference.
  378. @objc(listWithMaxResults:pageToken:completion:)
  379. open func list(maxResults: Int64,
  380. pageToken: String,
  381. completion: @escaping (@Sendable (_: StorageListResult?, _: Error?) -> Void)) {
  382. if maxResults <= 0 || maxResults > 1000 {
  383. completion(nil, StorageError.invalidArgument(
  384. message: "Argument 'maxResults' must be between 1 and 1000 inclusive."
  385. ))
  386. } else {
  387. StorageListTask.listTask(reference: self,
  388. queue: storage.dispatchQueue,
  389. pageSize: maxResults,
  390. previousPageToken: pageToken,
  391. completion: completion)
  392. }
  393. }
  394. // MARK: - Metadata Operations
  395. /// Retrieves metadata associated with an object at the current path.
  396. /// - Parameter completion: A completion block which returns the object metadata on success,
  397. /// or an error on failure.
  398. @objc(metadataWithCompletion:)
  399. open func getMetadata(completion: @escaping @Sendable (_: StorageMetadata?, _: Error?) -> Void) {
  400. StorageGetMetadataTask.getMetadataTask(reference: self,
  401. queue: storage.dispatchQueue,
  402. completion: completion)
  403. }
  404. /// Retrieves metadata associated with an object at the current path.
  405. /// - Throws: An error if the object metadata could not be retrieved.
  406. /// - Returns: The object metadata on success.
  407. open func getMetadata() async throws -> StorageMetadata {
  408. return try await withCheckedThrowingContinuation { continuation in
  409. self.getMetadata { result in
  410. continuation.resume(with: result)
  411. }
  412. }
  413. }
  414. /// Updates the metadata associated with an object at the current path.
  415. /// - Parameters:
  416. /// - metadata: A `StorageMetadata` object with the metadata to update.
  417. /// - completion: A completion block which returns the `StorageMetadata` on success,
  418. /// or an error on failure.
  419. @objc(updateMetadata:completion:)
  420. open func updateMetadata(_ metadata: StorageMetadata,
  421. completion: (@Sendable (_: StorageMetadata?, _: Error?) -> Void)?) {
  422. StorageUpdateMetadataTask.updateMetadataTask(reference: self,
  423. queue: storage.dispatchQueue,
  424. metadata: metadata,
  425. completion: completion)
  426. }
  427. /// Updates the metadata associated with an object at the current path.
  428. /// - Parameter metadata: A `StorageMetadata` object with the metadata to update.
  429. /// - Throws: An error if the metadata update operation failed.
  430. /// - Returns: The object metadata on success.
  431. open func updateMetadata(_ metadata: StorageMetadata) async throws -> StorageMetadata {
  432. return try await withCheckedThrowingContinuation { continuation in
  433. self.updateMetadata(metadata) { result in
  434. continuation.resume(with: result)
  435. }
  436. }
  437. }
  438. // MARK: - Delete
  439. /// Deletes the object at the current path.
  440. /// - Parameter completion: A completion block which returns a nonnull error on failure.
  441. @objc(deleteWithCompletion:)
  442. open func delete(completion: (@Sendable (_: Error?) -> Void)?) {
  443. let completionWrap: @Sendable (Data?, Error?) -> Void = { (_: Data?, error: Error?) in
  444. if let completion {
  445. completion(error)
  446. }
  447. }
  448. StorageDeleteTask.deleteTask(reference: self,
  449. queue: storage.dispatchQueue,
  450. completion: completionWrap)
  451. }
  452. /// Deletes the object at the current path.
  453. /// - Throws: An error if the delete operation failed.
  454. open func delete() async throws {
  455. return try await withCheckedThrowingContinuation { continuation in
  456. self.delete { error in
  457. if let error {
  458. continuation.resume(throwing: error)
  459. } else {
  460. continuation.resume()
  461. }
  462. }
  463. }
  464. }
  465. // MARK: - NSObject overrides
  466. /// NSObject override
  467. @objc override open func copy() -> Any {
  468. return StorageReference(storage: storage, path: path)
  469. }
  470. /// NSObject override
  471. @objc override open func isEqual(_ object: Any?) -> Bool {
  472. guard let ref = object as? StorageReference else {
  473. return false
  474. }
  475. return storage == ref.storage && path == ref.path
  476. }
  477. /// NSObject override
  478. @objc override public var hash: Int {
  479. return storage.hash ^ path.bucket.hashValue
  480. }
  481. /// NSObject override
  482. @objc override public var description: String {
  483. return "gs://\(path.bucket)/\(path.object ?? "")"
  484. }
  485. // MARK: - Internal APIs
  486. /// The current path which points to an object in the Google Cloud Storage bucket.
  487. let path: StoragePath
  488. override init() {
  489. storage = Storage.storage()
  490. let storageBucket = storage.app.options.storageBucket!
  491. path = StoragePath(with: storageBucket)
  492. }
  493. init(storage: Storage, path: StoragePath) {
  494. self.storage = storage
  495. self.path = path
  496. }
  497. /// For maxSize API, return an error if the size is exceeded.
  498. private func checkSizeOverflow(task: StorageTask, maxSize: Int64) -> NSError? {
  499. if task.progress.totalUnitCount > maxSize || task.progress.completedUnitCount > maxSize {
  500. return StorageError.downloadSizeExceeded(
  501. total: task.progress.totalUnitCount,
  502. maxSize: maxSize
  503. ) as NSError
  504. }
  505. return nil
  506. }
  507. private func startAndObserveUploadTask(task: StorageUploadTask,
  508. completion: ((_: StorageMetadata?, _: Error?) -> Void)?) {
  509. if let completion {
  510. task.completionMetadata = completion
  511. let callbackQueue = storage.callbackQueue
  512. task.observe(.success) { snapshot in
  513. callbackQueue.async {
  514. task.completionMetadata?(snapshot.metadata, nil)
  515. task.completionMetadata = nil
  516. }
  517. }
  518. task.observe(.failure) { snapshot in
  519. callbackQueue.async {
  520. task.completionMetadata?(nil, snapshot.error)
  521. task.completionMetadata = nil
  522. }
  523. }
  524. }
  525. task.enqueue()
  526. }
  527. }