Storage.swift 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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. import FirebaseCore
  16. import FirebaseAppCheckInterop
  17. import FirebaseAuthInterop
  18. #if COCOAPODS
  19. import GTMSessionFetcher
  20. #else
  21. import GTMSessionFetcherCore
  22. #endif
  23. // Avoids exposing internal FirebaseCore APIs to Swift users.
  24. @_implementationOnly import FirebaseCoreExtension
  25. /**
  26. * Storage is a service that supports uploading and downloading binary objects,
  27. * such as images, videos, and other files to Google Cloud Storage. Instances of `Storage`
  28. * are not thread-safe.
  29. *
  30. * If you call `Storage.storage()`, the instance will initialize with the default `FirebaseApp`,
  31. * `FirebaseApp.app()`, and the storage location will come from the provided
  32. * `GoogleService-Info.plist`.
  33. *
  34. * If you provide a custom instance of `FirebaseApp`,
  35. * the storage location will be specified via the `FirebaseOptions.storageBucket` property.
  36. */
  37. @objc(FIRStorage) open class Storage: NSObject {
  38. // MARK: - Public APIs
  39. /**
  40. * The default `Storage` instance.
  41. * - Returns: An instance of `Storage`, configured with the default `FirebaseApp`.
  42. * Instances of Storage are long-lived, so calling this method multiple times
  43. * will return the same instance.
  44. */
  45. @objc(storage:) open class func storage(_ ignored: Any? = nil) -> Storage {
  46. return storage(app: FirebaseApp.app()!)
  47. }
  48. /**
  49. * A method used to create `Storage` instances initialized with a custom storage bucket URL.
  50. * Any `StorageReferences` generated from this instance of `Storage` will reference files
  51. * and directories within the specified bucket.
  52. * - Parameter url The `gs://` URL to your Firebase Storage bucket.
  53. * - Returns: A `Storage` instance, configured with the custom storage bucket.
  54. */
  55. @objc(storageWithURL:) open class func storage(url: String) -> Storage {
  56. return storage(app: FirebaseApp.app()!, url: url)
  57. }
  58. /**
  59. * Creates an instance of `Storage`, configured with a custom `FirebaseApp`. `StorageReference`s
  60. * generated from a resulting instance will reference files in the Firebase project
  61. * associated with custom `FirebaseApp`.
  62. * - Parameter app The custom `FirebaseApp` used for initialization.
  63. * - Returns: A `Storage` instance, configured with the custom `FirebaseApp`.
  64. */
  65. @objc(storageForApp:) open class func storage(app: FirebaseApp) -> Storage {
  66. let provider = ComponentType<StorageProvider>.instance(for: StorageProvider.self,
  67. in: app.container)
  68. return provider.storage(for: Storage.bucket(for: app))
  69. }
  70. /**
  71. * Creates an instance of `Storage`, configured with a custom `FirebaseApp` and a custom storage
  72. * bucket URL.
  73. * - Parameters:
  74. * - app: The custom `FirebaseApp` used for initialization.
  75. * - url: The `gs://` url to your Firebase Storage bucket.
  76. * - Returns: the `Storage` instance, configured with the custom `FirebaseApp` and storage bucket URL.
  77. */
  78. @objc(storageForApp:URL:)
  79. open class func storage(app: FirebaseApp, url: String) -> Storage {
  80. let provider = ComponentType<StorageProvider>.instance(for: StorageProvider.self,
  81. in: app.container)
  82. return provider.storage(for: Storage.bucket(for: app, urlString: url))
  83. }
  84. /**
  85. * The `FirebaseApp` associated with this Storage instance.
  86. */
  87. @objc public let app: FirebaseApp
  88. /**
  89. * The maximum time in seconds to retry an upload if a failure occurs.
  90. * Defaults to 10 minutes (600 seconds).
  91. */
  92. @objc public var maxUploadRetryTime: TimeInterval {
  93. didSet {
  94. maxUploadRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxUploadRetryTime)
  95. }
  96. }
  97. /**
  98. * The maximum time in seconds to retry a download if a failure occurs.
  99. * Defaults to 10 minutes (600 seconds).
  100. */
  101. @objc public var maxDownloadRetryTime: TimeInterval {
  102. didSet {
  103. maxDownloadRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxDownloadRetryTime)
  104. }
  105. }
  106. /**
  107. * The maximum time in seconds to retry operations other than upload and download if a failure occurs.
  108. * Defaults to 2 minutes (120 seconds).
  109. */
  110. @objc public var maxOperationRetryTime: TimeInterval {
  111. didSet {
  112. maxOperationRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxOperationRetryTime)
  113. }
  114. }
  115. /**
  116. * Specify the maximum upload chunk size. Values less than 256K (262144) will be rounded up to 256K. Values
  117. * above 256K will be rounded down to the nearest 256K multiple. The default is no maximum.
  118. */
  119. @objc public var uploadChunkSizeBytes: Int64 = .max
  120. /**
  121. * A `DispatchQueue` that all developer callbacks are fired on. Defaults to the main queue.
  122. */
  123. @objc public var callbackQueue: DispatchQueue {
  124. get {
  125. ensureConfigured()
  126. guard let queue = fetcherService?.callbackQueue else {
  127. fatalError("Internal error: Failed to initialize fetcherService callbackQueue")
  128. }
  129. return queue
  130. }
  131. set(newValue) {
  132. ensureConfigured()
  133. fetcherService?.callbackQueue = newValue
  134. }
  135. }
  136. /**
  137. * Creates a `StorageReference` initialized at the root Firebase Storage location.
  138. * - Returns: An instance of `StorageReference` referencing the root of the storage bucket.
  139. */
  140. @objc open func reference() -> StorageReference {
  141. ensureConfigured()
  142. let path = StoragePath(with: storageBucket)
  143. return StorageReference(storage: self, path: path)
  144. }
  145. /**
  146. * Creates a StorageReference given a `gs://`, `http://`, or `https://` URL pointing to a
  147. * Firebase Storage location. For example, you can pass in an `https://` download URL retrieved from
  148. * `StorageReference.downloadURL(completion:)` or the `gs://` URL from
  149. * `StorageReference.description`.
  150. * - Parameter url A gs:// or https:// URL to initialize the reference with.
  151. * - Returns: An instance of StorageReference at the given child path.
  152. * - Throws: Throws a fatal error if `url` is not associated with the `FirebaseApp` used to initialize
  153. * this Storage instance.
  154. */
  155. @objc open func reference(forURL url: String) -> StorageReference {
  156. ensureConfigured()
  157. do {
  158. let path = try StoragePath.path(string: url)
  159. // If no default bucket exists (empty string), accept anything.
  160. if storageBucket == "" {
  161. return StorageReference(storage: self, path: path)
  162. }
  163. // If there exists a default bucket, throw if provided a different bucket.
  164. if path.bucket != storageBucket {
  165. fatalError("Provided bucket: `\(path.bucket)` does not match the Storage bucket of the current " +
  166. "instance: `\(storageBucket)`")
  167. }
  168. return StorageReference(storage: self, path: path)
  169. } catch let StoragePathError.storagePathError(message) {
  170. fatalError(message)
  171. } catch {
  172. fatalError("Internal error finding StoragePath: \(error)")
  173. }
  174. }
  175. /**
  176. * Creates a StorageReference given a `gs://`, `http://`, or `https://` URL pointing to a
  177. * Firebase Storage location. For example, you can pass in an `https://` download URL retrieved from
  178. * `StorageReference.downloadURL(completion:)` or the `gs://` URL from
  179. * `StorageReference.description`.
  180. * - Parameter url A gs:// or https:// URL to initialize the reference with.
  181. * - Returns: An instance of StorageReference at the given child path.
  182. * - Throws: Throws an Error if `url` is not associated with the `FirebaseApp` used to initialize
  183. * this Storage instance.
  184. */
  185. open func reference(for url: URL) throws -> StorageReference {
  186. ensureConfigured()
  187. var path: StoragePath
  188. do {
  189. path = try StoragePath.path(string: url.absoluteString)
  190. } catch let StoragePathError.storagePathError(message) {
  191. throw StorageError.pathError(message)
  192. } catch {
  193. throw StorageError.pathError("Internal error finding StoragePath: \(error)")
  194. }
  195. // If no default bucket exists (empty string), accept anything.
  196. if storageBucket == "" {
  197. return StorageReference(storage: self, path: path)
  198. }
  199. // If there exists a default bucket, throw if provided a different bucket.
  200. if path.bucket != storageBucket {
  201. throw StorageError
  202. .bucketMismatch("Provided bucket: `\(path.bucket)` does not match the Storage " +
  203. "bucket of the current instance: `\(storageBucket)`")
  204. }
  205. return StorageReference(storage: self, path: path)
  206. }
  207. /**
  208. * Creates a `StorageReference` initialized at a location specified by the `path` parameter.
  209. * - Parameter path A relative path from the root of the storage bucket,
  210. * for instance @"path/to/object".
  211. * - Returns: An instance of `StorageReference` pointing to the given path.
  212. */
  213. @objc(referenceWithPath:) open func reference(withPath path: String) -> StorageReference {
  214. return reference().child(path)
  215. }
  216. /**
  217. * Configures the Storage SDK to use an emulated backend instead of the default remote backend.
  218. * This method should be called before invoking any other methods on a new instance of `Storage`.
  219. */
  220. @objc open func useEmulator(withHost host: String, port: Int) {
  221. guard host.count > 0 else {
  222. fatalError("Invalid host argument: Cannot connect to empty host.")
  223. }
  224. guard port >= 0 else {
  225. fatalError("Invalid port argument: Port must be greater or equal to zero.")
  226. }
  227. guard fetcherService == nil else {
  228. fatalError("Cannot connect to emulator after Storage SDK initialization. " +
  229. "Call useEmulator(host:port:) before creating a Storage " +
  230. "reference or trying to load data.")
  231. }
  232. usesEmulator = true
  233. scheme = "http"
  234. self.host = host
  235. self.port = port
  236. }
  237. // MARK: - NSObject overrides
  238. @objc override open func copy() -> Any {
  239. let storage = Storage(app: app, bucket: storageBucket)
  240. storage.callbackQueue = callbackQueue
  241. return storage
  242. }
  243. @objc override open func isEqual(_ object: Any?) -> Bool {
  244. guard let ref = object as? Storage else {
  245. return false
  246. }
  247. return app == ref.app && storageBucket == ref.storageBucket
  248. }
  249. @objc override public var hash: Int {
  250. return app.hash ^ callbackQueue.hashValue
  251. }
  252. // MARK: - Internal and Private APIs
  253. private var fetcherService: GTMSessionFetcherService?
  254. internal var fetcherServiceForApp: GTMSessionFetcherService {
  255. guard let value = fetcherService else {
  256. fatalError("Internal error: fetcherServiceForApp not yet configured.")
  257. }
  258. return value
  259. }
  260. internal let dispatchQueue: DispatchQueue
  261. internal init(app: FirebaseApp, bucket: String) {
  262. self.app = app
  263. auth = ComponentType<AuthInterop>.instance(for: AuthInterop.self,
  264. in: app.container)
  265. appCheck = ComponentType<AppCheckInterop>.instance(for: AppCheckInterop.self,
  266. in: app.container)
  267. storageBucket = bucket
  268. host = "firebasestorage.googleapis.com"
  269. scheme = "https"
  270. port = 443
  271. fetcherService = nil // Configured in `ensureConfigured()`
  272. // Must be a serial queue.
  273. dispatchQueue = DispatchQueue(label: "com.google.firebase.storage")
  274. maxDownloadRetryTime = 600.0
  275. maxDownloadRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxDownloadRetryTime)
  276. maxOperationRetryTime = 120.0
  277. maxOperationRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxOperationRetryTime)
  278. maxUploadRetryTime = 600.0
  279. maxUploadRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxUploadRetryTime)
  280. }
  281. /// Map of apps to a dictionary of buckets to GTMSessionFetcherService.
  282. private static let fetcherServiceLock = NSObject()
  283. private static var fetcherServiceMap: [String: [String: GTMSessionFetcherService]] = [:]
  284. private static var retryWhenOffline: GTMSessionFetcherRetryBlock = {
  285. (suggestedWillRetry: Bool,
  286. error: Error?,
  287. response: @escaping GTMSessionFetcherRetryResponse) in
  288. var shouldRetry = suggestedWillRetry
  289. // GTMSessionFetcher does not consider being offline a retryable error, but we do, so we
  290. // special-case it here.
  291. if !shouldRetry, error != nil {
  292. shouldRetry = (error as? NSError)?.code == URLError.notConnectedToInternet.rawValue
  293. }
  294. response(shouldRetry)
  295. }
  296. private static func initFetcherServiceForApp(_ app: FirebaseApp,
  297. _ bucket: String,
  298. _ auth: AuthInterop,
  299. _ appCheck: AppCheckInterop)
  300. -> GTMSessionFetcherService {
  301. objc_sync_enter(fetcherServiceLock)
  302. defer { objc_sync_exit(fetcherServiceLock) }
  303. var bucketMap = fetcherServiceMap[app.name]
  304. if bucketMap == nil {
  305. bucketMap = [:]
  306. fetcherServiceMap[app.name] = bucketMap
  307. }
  308. var fetcherService = bucketMap?[bucket]
  309. if fetcherService == nil {
  310. fetcherService = GTMSessionFetcherService()
  311. fetcherService?.isRetryEnabled = true
  312. fetcherService?.retryBlock = retryWhenOffline
  313. fetcherService?.allowLocalhostRequest = true
  314. let authorizer = StorageTokenAuthorizer(
  315. googleAppID: app.options.googleAppID,
  316. fetcherService: fetcherService!,
  317. authProvider: auth,
  318. appCheck: appCheck
  319. )
  320. fetcherService?.authorizer = authorizer
  321. bucketMap?[bucket] = fetcherService
  322. }
  323. return fetcherService!
  324. }
  325. private let auth: AuthInterop
  326. private let appCheck: AppCheckInterop
  327. private let storageBucket: String
  328. private var usesEmulator: Bool = false
  329. internal var host: String
  330. internal var scheme: String
  331. internal var port: Int
  332. internal var maxDownloadRetryInterval: TimeInterval
  333. internal var maxOperationRetryInterval: TimeInterval
  334. internal var maxUploadRetryInterval: TimeInterval
  335. /**
  336. * Performs a crude translation of the user provided timeouts to the retry intervals that
  337. * GTMSessionFetcher accepts. GTMSessionFetcher times out operations if the time between individual
  338. * retry attempts exceed a certain threshold, while our API contract looks at the total observed
  339. * time of the operation (i.e. the sum of all retries).
  340. * @param retryTime A timeout that caps the sum of all retry attempts
  341. * @return A timeout that caps the timeout of the last retry attempt
  342. */
  343. internal static func computeRetryInterval(fromRetryTime retryTime: TimeInterval) -> TimeInterval {
  344. // GTMSessionFetcher's retry starts at 1 second and then doubles every time. We use this
  345. // information to compute a best-effort estimate of what to translate the user provided retry
  346. // time into.
  347. // Note that this is the same as 2 << (log2(retryTime) - 1), but deemed more readable.
  348. var lastInterval = 1.0
  349. var sumOfAllIntervals = 1.0
  350. while sumOfAllIntervals < retryTime {
  351. lastInterval *= 2
  352. sumOfAllIntervals += lastInterval
  353. }
  354. return lastInterval
  355. }
  356. /**
  357. * Configures the storage instance. Freezes the host setting.
  358. */
  359. private func ensureConfigured() {
  360. guard fetcherService == nil else {
  361. return
  362. }
  363. fetcherService = Storage.initFetcherServiceForApp(app, storageBucket, auth, appCheck)
  364. if usesEmulator {
  365. fetcherService?.allowLocalhostRequest = true
  366. fetcherService?.allowedInsecureSchemes = ["http"]
  367. }
  368. }
  369. private static func bucket(for app: FirebaseApp) -> String {
  370. guard let bucket = app.options.storageBucket else {
  371. fatalError("No default Storage bucket found. Did you configure Firebase Storage properly?")
  372. }
  373. if bucket == "" {
  374. return Storage.bucket(for: app, urlString: "")
  375. } else {
  376. return Storage.bucket(for: app, urlString: "gs://\(bucket)/")
  377. }
  378. }
  379. private static func bucket(for app: FirebaseApp, urlString: String) -> String {
  380. if urlString == "" {
  381. return ""
  382. } else {
  383. guard let path = try? StoragePath.path(GSURI: urlString),
  384. path.object == nil || path.object == "" else {
  385. fatalError("Internal Error: Storage bucket cannot be initialized with a path")
  386. }
  387. return path.bucket
  388. }
  389. }
  390. }