Storage.swift 14 KB

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