Storage.swift 17 KB

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