StorageReference.swift 24 KB

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