StorageUploadTaskV2.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. #if COCOAPODS
  16. import GTMSessionFetcher
  17. #else
  18. import GTMSessionFetcherCore
  19. #endif
  20. /**
  21. * `StorageUploadTaskV2` implements resumable uploads to a file in Firebase Storage.
  22. * Uploads can be done via an async/await function or with a completion callback with a
  23. * Swift task return value.
  24. * Uploads can be initialized from `Data` in memory, or a URL to a file on disk.
  25. * Uploads are performed on a background queue, and callbacks are raised on the developer
  26. * specified `callbackQueue` in Storage, or the main queue if unspecified.
  27. * Currently all uploads must be initiated and managed on the main queue.
  28. */
  29. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  30. internal class StorageUploadTaskV2: StorageTask {
  31. /**
  32. * Prepares a GTMSessionFetcher task and does an upload.
  33. */
  34. internal func upload() async throws -> StorageMetadata {
  35. if progress.isCancelled {
  36. throw StorageError.cancelled
  37. }
  38. if let contentValidationError = isContentToUploadInvalid() {
  39. throw StorageError.swiftConvert(objcError: contentValidationError)
  40. }
  41. var request = baseRequest
  42. request.httpMethod = "POST"
  43. request.timeoutInterval = reference.storage.maxUploadRetryTime
  44. let dataRepresentation = uploadMetadata.dictionaryRepresentation()
  45. let bodyData = try? JSONSerialization.data(withJSONObject: dataRepresentation)
  46. request.httpBody = bodyData
  47. request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type")
  48. if let count = bodyData?.count {
  49. request.setValue("\(count)", forHTTPHeaderField: "Content-Length")
  50. }
  51. var components = URLComponents(url: request.url!, resolvingAgainstBaseURL: false)
  52. if components?.host == "www.googleapis.com",
  53. let path = components?.path {
  54. components?.percentEncodedPath = "/upload\(path)"
  55. }
  56. guard let path = GCSEscapedString(uploadMetadata.path) else {
  57. fatalError("Internal error enqueueing a Storage task")
  58. }
  59. components?.percentEncodedQuery = "uploadType=resumable&name=\(path)"
  60. request.url = components?.url
  61. guard let contentType = uploadMetadata.contentType else {
  62. fatalError("Internal error enqueueing a Storage task")
  63. }
  64. let uploadFetcher = GTMSessionUploadFetcher(
  65. request: request,
  66. uploadMIMEType: contentType,
  67. chunkSize: Int64.max,
  68. fetcherService: fetcherService
  69. )
  70. if let data = uploadData {
  71. uploadFetcher.uploadData = data
  72. uploadFetcher.comment = "Data UploadTask"
  73. } else if let fileURL = fileURL {
  74. uploadFetcher.uploadFileURL = fileURL
  75. uploadFetcher.comment = "File UploadTask"
  76. }
  77. uploadFetcher.maxRetryInterval = reference.storage.maxUploadRetryInterval
  78. uploadFetcher.stopFetchingTriggersCompletionHandler = true
  79. if let progressBlock = progressBlock {
  80. uploadFetcher.sendProgressBlock = { (bytesSent: Int64, totalBytesSent: Int64,
  81. totalBytesExpectedToSend: Int64) in
  82. self.progress.completedUnitCount = totalBytesSent
  83. self.progress.totalUnitCount = totalBytesExpectedToSend
  84. self.metadata = self.uploadMetadata
  85. progressBlock(self.progress)
  86. if self.progress.isCancelled {
  87. uploadFetcher.stopFetching()
  88. }
  89. }
  90. }
  91. let (data, error) = await beginFetch(uploadFetcher: uploadFetcher)
  92. defer {
  93. uploadFetcher.stopFetching()
  94. }
  95. // Handle potential issues with upload
  96. if let error = error {
  97. throw StorageError.swiftConvert(objcError: StorageErrorCode.error(
  98. withServerError: error as NSError, ref: reference
  99. ))
  100. }
  101. guard let data = data else {
  102. fatalError("Internal Error: fetcherCompletion returned with nil data and nil error")
  103. }
  104. if let responseDictionary = try? JSONSerialization
  105. .jsonObject(with: data) as? [String: AnyHashable] {
  106. let metadata = StorageMetadata(dictionary: responseDictionary)
  107. metadata.fileType = .file
  108. return metadata
  109. } else {
  110. throw StorageError.swiftConvert(objcError: StorageErrorCode.error(withInvalidRequest: data))
  111. }
  112. }
  113. private func beginFetch(uploadFetcher: GTMSessionUploadFetcher) async -> (Data?, Error?) {
  114. return await withCheckedContinuation { continuation in
  115. uploadFetcher.beginFetch { data, error in
  116. continuation.resume(returning: (data, error))
  117. }
  118. }
  119. }
  120. private let uploadMetadata: StorageMetadata
  121. private let uploadData: Data?
  122. private let progressBlock: ((Progress) -> Void)?
  123. /**
  124. * The file to download to or upload from
  125. */
  126. private let fileURL: URL?
  127. // MARK: - Internal Implementations
  128. internal init(reference: StorageReference,
  129. service: GTMSessionFetcherService,
  130. queue: DispatchQueue,
  131. file: URL? = nil,
  132. data: Data? = nil,
  133. metadata: StorageMetadata,
  134. progress: Progress? = nil,
  135. progressBlock: ((Progress) -> Void)? = nil) {
  136. uploadMetadata = metadata
  137. uploadData = data
  138. fileURL = file
  139. self.progressBlock = progressBlock
  140. super.init(reference: reference, service: service, queue: queue)
  141. if let progress = progress {
  142. self.progress = progress
  143. }
  144. if uploadMetadata.contentType == nil {
  145. uploadMetadata.contentType = StorageUtils.MIMETypeForExtension(file?.pathExtension)
  146. }
  147. }
  148. internal func isContentToUploadInvalid() -> NSError? {
  149. // TODO: - Does checkResourceIsReachableAndReturnError need to be ported here?
  150. if uploadData != nil {
  151. return nil
  152. }
  153. if let resourceValues = try? fileURL?.resourceValues(forKeys: [.isRegularFileKey]),
  154. let isFile = resourceValues.isRegularFile,
  155. isFile == true {
  156. return nil
  157. }
  158. let userInfo = [NSLocalizedDescriptionKey:
  159. "File at URL: \(fileURL?.absoluteString ?? "") is not reachable."
  160. + " Ensure file URL is not a directory, symbolic link, or invalid url."]
  161. return NSError(
  162. domain: StorageErrorDomain,
  163. code: StorageErrorCode.unknown.rawValue,
  164. userInfo: userInfo
  165. )
  166. }
  167. private func GCSEscapedString(_ input: String?) -> String? {
  168. guard let input = input else {
  169. return nil
  170. }
  171. let GCSObjectAllowedCharacterSet =
  172. "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$'()*,=:@"
  173. let allowedCharacters = CharacterSet(charactersIn: GCSObjectAllowedCharacterSet)
  174. return input.addingPercentEncoding(withAllowedCharacters: allowedCharacters)
  175. }
  176. }