| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- // Copyright 2022 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- import Foundation
- import FirebaseCore
- import FirebaseAppCheckInterop
- import FirebaseAuthInterop
- #if COCOAPODS
- import GTMSessionFetcher
- #else
- import GTMSessionFetcherCore
- #endif
- // Avoids exposing internal FirebaseCore APIs to Swift users.
- @_implementationOnly import FirebaseCoreExtension
- /**
- * Firebase Storage is a service that supports uploading and downloading binary objects,
- * such as images, videos, and other files to Google Cloud Storage. Instances of `Storage`
- * are not thread-safe.
- *
- * If you call `Storage.storage()`, the instance will initialize with the default `FirebaseApp`,
- * `FirebaseApp.app()`, and the storage location will come from the provided
- * `GoogleService-Info.plist`.
- *
- * If you provide a custom instance of `FirebaseApp`,
- * the storage location will be specified via the `FirebaseOptions.storageBucket` property.
- */
- @objc(FIRStorage) open class Storage: NSObject {
- // MARK: - Public APIs
- /**
- * The default `Storage` instance.
- * - Returns: An instance of `Storage`, configured with the default `FirebaseApp`.
- */
- @objc(storage) open class func storage() -> Storage {
- return storage(app: FirebaseApp.app()!)
- }
- /**
- * A method used to create `Storage` instances initialized with a custom storage bucket URL.
- * Any `StorageReferences` generated from this instance of `Storage` will reference files
- * and directories within the specified bucket.
- * - Parameter url The `gs://` URL to your Firebase Storage bucket.
- * - Returns: A `Storage` instance, configured with the custom storage bucket.
- */
- @objc(storageWithURL:) open class func storage(url: String) -> Storage {
- return storage(app: FirebaseApp.app()!, url: url)
- }
- /**
- * Creates an instance of `Storage`, configured with a custom `FirebaseApp`. `StorageReference`s
- * generated from a resulting instance will reference files in the Firebase project
- * associated with custom `FirebaseApp`.
- * - Parameter app The custom `FirebaseApp` used for initialization.
- * - Returns: A `Storage` instance, configured with the custom `FirebaseApp`.
- */
- @objc(storageForApp:) open class func storage(app: FirebaseApp) -> Storage {
- let provider = ComponentType<StorageProvider>.instance(for: StorageProvider.self,
- in: app.container)
- return provider.storage(for: Storage.bucket(for: app))
- }
- /**
- * Creates an instance of `Storage`, configured with a custom `FirebaseApp` and a custom storage
- * bucket URL.
- * - Parameters:
- * - app: The custom `FirebaseApp` used for initialization.
- * - url: The `gs://` url to your Firebase Storage bucket.
- * - Returns: the `Storage` instance, configured with the custom `FirebaseApp` and storage bucket URL.
- */
- @objc(storageForApp:URL:)
- open class func storage(app: FirebaseApp, url: String) -> Storage {
- let provider = ComponentType<StorageProvider>.instance(for: StorageProvider.self,
- in: app.container)
- return provider.storage(for: Storage.bucket(for: app, urlString: url))
- }
- /**
- * The `FirebaseApp` associated with this Storage instance.
- */
- @objc public let app: FirebaseApp
- /**
- * The maximum time in seconds to retry an upload if a failure occurs.
- * Defaults to 10 minutes (600 seconds).
- */
- @objc public var maxUploadRetryTime: TimeInterval {
- didSet {
- maxUploadRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxUploadRetryTime)
- }
- }
- /**
- * The maximum time in seconds to retry a download if a failure occurs.
- * Defaults to 10 minutes (600 seconds).
- */
- @objc public var maxDownloadRetryTime: TimeInterval {
- didSet {
- maxDownloadRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxDownloadRetryTime)
- }
- }
- /**
- * The maximum time in seconds to retry operations other than upload and download if a failure occurs.
- * Defaults to 2 minutes (120 seconds).
- */
- @objc public var maxOperationRetryTime: TimeInterval {
- didSet {
- maxOperationRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxOperationRetryTime)
- }
- }
- /**
- * Specify the maximum upload chunk size. Values less than 256K (262144) will be rounded up to 256K. Values
- * above 256K will be rounded down to the nearest 256K multiple. The default is no maximum.
- */
- @objc public var uploadChunkSizeBytes: Int64 = .max
- /**
- * A `DispatchQueue` that all developer callbacks are fired on. Defaults to the main queue.
- */
- @objc public var callbackQueue: DispatchQueue {
- get {
- ensureConfigured()
- guard let queue = fetcherService?.callbackQueue else {
- fatalError("Internal error: Failed to initialize fetcherService callbackQueue")
- }
- return queue
- }
- set(newValue) {
- ensureConfigured()
- fetcherService?.callbackQueue = newValue
- }
- }
- /**
- * Creates a `StorageReference` initialized at the root Firebase Storage location.
- * - Returns: An instance of `StorageReference` referencing the root of the storage bucket.
- */
- @objc open func reference() -> StorageReference {
- ensureConfigured()
- let path = StoragePath(with: storageBucket)
- return StorageReference(storage: self, path: path)
- }
- /**
- * Creates a StorageReference given a `gs://`, `http://`, or `https://` URL pointing to a
- * Firebase Storage location. For example, you can pass in an `https://` download URL retrieved from
- * `StorageReference.downloadURL(completion:)` or the `gs://` URL from
- * `StorageReference.description`.
- * - Parameter url A gs:// or https:// URL to initialize the reference with.
- * - Returns: An instance of StorageReference at the given child path.
- * - Throws: Throws a fatal error if `url` is not associated with the `FirebaseApp` used to initialize
- * this Storage instance.
- */
- @objc open func reference(forURL url: String) -> StorageReference {
- ensureConfigured()
- do {
- let path = try StoragePath.path(string: url)
- // If no default bucket exists (empty string), accept anything.
- if storageBucket == "" {
- return StorageReference(storage: self, path: path)
- }
- // If there exists a default bucket, throw if provided a different bucket.
- if path.bucket != storageBucket {
- fatalError("Provided bucket: `\(path.bucket)` does not match the Storage bucket of the current " +
- "instance: `\(storageBucket)`")
- }
- return StorageReference(storage: self, path: path)
- } catch let StoragePathError.storagePathError(message) {
- fatalError(message)
- } catch {
- fatalError("Internal error finding StoragePath: \(error)")
- }
- }
- /**
- * Creates a StorageReference given a `gs://`, `http://`, or `https://` URL pointing to a
- * Firebase Storage location. For example, you can pass in an `https://` download URL retrieved from
- * `StorageReference.downloadURL(completion:)` or the `gs://` URL from
- * `StorageReference.description`.
- * - Parameter url A gs:// or https:// URL to initialize the reference with.
- * - Returns: An instance of StorageReference at the given child path.
- * - Throws: Throws an Error if `url` is not associated with the `FirebaseApp` used to initialize
- * this Storage instance.
- */
- open func reference(for url: URL) throws -> StorageReference {
- ensureConfigured()
- var path: StoragePath
- do {
- path = try StoragePath.path(string: url.absoluteString)
- } catch let StoragePathError.storagePathError(message) {
- throw StorageError.pathError(message)
- } catch {
- throw StorageError.pathError("Internal error finding StoragePath: \(error)")
- }
- // If no default bucket exists (empty string), accept anything.
- if storageBucket == "" {
- return StorageReference(storage: self, path: path)
- }
- // If there exists a default bucket, throw if provided a different bucket.
- if path.bucket != storageBucket {
- throw StorageError
- .bucketMismatch("Provided bucket: `\(path.bucket)` does not match the Storage " +
- "bucket of the current instance: `\(storageBucket)`")
- }
- return StorageReference(storage: self, path: path)
- }
- /**
- * Creates a `StorageReference` initialized at a location specified by the `path` parameter.
- * - Parameter path A relative path from the root of the storage bucket,
- * for instance @"path/to/object".
- * - Returns: An instance of `StorageReference` pointing to the given path.
- */
- @objc(referenceWithPath:) open func reference(withPath path: String) -> StorageReference {
- return reference().child(path)
- }
- /**
- * Configures the Storage SDK to use an emulated backend instead of the default remote backend.
- * This method should be called before invoking any other methods on a new instance of `Storage`.
- */
- @objc open func useEmulator(withHost host: String, port: Int) {
- guard host.count > 0 else {
- fatalError("Invalid host argument: Cannot connect to empty host.")
- }
- guard port >= 0 else {
- fatalError("Invalid port argument: Port must be greater or equal to zero.")
- }
- guard fetcherService == nil else {
- fatalError("Cannot connect to emulator after Storage SDK initialization. " +
- "Call useEmulator(host:port:) before creating a Storage " +
- "reference or trying to load data.")
- }
- usesEmulator = true
- scheme = "http"
- self.host = host
- self.port = port
- }
- // MARK: - NSObject overrides
- @objc override open func copy() -> Any {
- let storage = Storage(app: app, bucket: storageBucket)
- storage.callbackQueue = callbackQueue
- return storage
- }
- @objc override open func isEqual(_ object: Any?) -> Bool {
- guard let ref = object as? Storage else {
- return false
- }
- return app == ref.app && storageBucket == ref.storageBucket
- }
- @objc override public var hash: Int {
- return app.hash ^ callbackQueue.hashValue
- }
- // MARK: - Internal and Private APIs
- private var fetcherService: GTMSessionFetcherService?
- internal var fetcherServiceForApp: GTMSessionFetcherService {
- guard let value = fetcherService else {
- fatalError("Internal error: fetcherServiceForApp not yet configured.")
- }
- return value
- }
- internal let dispatchQueue: DispatchQueue
- internal init(app: FirebaseApp, bucket: String) {
- self.app = app
- auth = ComponentType<AuthInterop>.instance(for: AuthInterop.self,
- in: app.container)
- appCheck = ComponentType<AppCheckInterop>.instance(for: AppCheckInterop.self,
- in: app.container)
- storageBucket = bucket
- host = "firebasestorage.googleapis.com"
- scheme = "https"
- port = 443
- fetcherService = nil // Configured in `ensureConfigured()`
- // Must be a serial queue.
- dispatchQueue = DispatchQueue(label: "com.google.firebase.storage")
- maxDownloadRetryTime = 600.0
- maxDownloadRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxDownloadRetryTime)
- maxOperationRetryTime = 120.0
- maxOperationRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxOperationRetryTime)
- maxUploadRetryTime = 600.0
- maxUploadRetryInterval = Storage.computeRetryInterval(fromRetryTime: maxUploadRetryTime)
- }
- /// Map of apps to a dictionary of buckets to GTMSessionFetcherService.
- private static let fetcherServiceLock = NSObject()
- private static var fetcherServiceMap: [String: [String: GTMSessionFetcherService]] = [:]
- private static var retryWhenOffline: GTMSessionFetcherRetryBlock = {
- (suggestedWillRetry: Bool,
- error: Error?,
- response: @escaping GTMSessionFetcherRetryResponse) in
- var shouldRetry = suggestedWillRetry
- // GTMSessionFetcher does not consider being offline a retryable error, but we do, so we
- // special-case it here.
- if !shouldRetry, error != nil {
- shouldRetry = (error as? NSError)?.code == URLError.notConnectedToInternet.rawValue
- }
- response(shouldRetry)
- }
- private static func initFetcherServiceForApp(_ app: FirebaseApp,
- _ bucket: String,
- _ auth: AuthInterop,
- _ appCheck: AppCheckInterop)
- -> GTMSessionFetcherService {
- objc_sync_enter(fetcherServiceLock)
- defer { objc_sync_exit(fetcherServiceLock) }
- var bucketMap = fetcherServiceMap[app.name]
- if bucketMap == nil {
- bucketMap = [:]
- fetcherServiceMap[app.name] = bucketMap
- }
- var fetcherService = bucketMap?[bucket]
- if fetcherService == nil {
- fetcherService = GTMSessionFetcherService()
- fetcherService?.isRetryEnabled = true
- fetcherService?.retryBlock = retryWhenOffline
- fetcherService?.allowLocalhostRequest = true
- let authorizer = StorageTokenAuthorizer(
- googleAppID: app.options.googleAppID,
- fetcherService: fetcherService!,
- authProvider: auth,
- appCheck: appCheck
- )
- fetcherService?.authorizer = authorizer
- bucketMap?[bucket] = fetcherService
- }
- return fetcherService!
- }
- private let auth: AuthInterop
- private let appCheck: AppCheckInterop
- private let storageBucket: String
- private var usesEmulator: Bool = false
- internal var host: String
- internal var scheme: String
- internal var port: Int
- internal var maxDownloadRetryInterval: TimeInterval
- internal var maxOperationRetryInterval: TimeInterval
- internal var maxUploadRetryInterval: TimeInterval
- /**
- * Performs a crude translation of the user provided timeouts to the retry intervals that
- * GTMSessionFetcher accepts. GTMSessionFetcher times out operations if the time between individual
- * retry attempts exceed a certain threshold, while our API contract looks at the total observed
- * time of the operation (i.e. the sum of all retries).
- * @param retryTime A timeout that caps the sum of all retry attempts
- * @return A timeout that caps the timeout of the last retry attempt
- */
- internal static func computeRetryInterval(fromRetryTime retryTime: TimeInterval) -> TimeInterval {
- // GTMSessionFetcher's retry starts at 1 second and then doubles every time. We use this
- // information to compute a best-effort estimate of what to translate the user provided retry
- // time into.
- // Note that this is the same as 2 << (log2(retryTime) - 1), but deemed more readable.
- var lastInterval = 1.0
- var sumOfAllIntervals = 1.0
- while sumOfAllIntervals < retryTime {
- lastInterval *= 2
- sumOfAllIntervals += lastInterval
- }
- return lastInterval
- }
- /**
- * Configures the storage instance. Freezes the host setting.
- */
- private func ensureConfigured() {
- guard fetcherService == nil else {
- return
- }
- fetcherService = Storage.initFetcherServiceForApp(app, storageBucket, auth, appCheck)
- if usesEmulator {
- fetcherService?.allowLocalhostRequest = true
- fetcherService?.allowedInsecureSchemes = ["http"]
- }
- }
- private static func bucket(for app: FirebaseApp) -> String {
- guard let bucket = app.options.storageBucket else {
- fatalError("No default Storage bucket found. Did you configure Firebase Storage properly?")
- }
- if bucket == "" {
- return Storage.bucket(for: app, urlString: "")
- } else {
- return Storage.bucket(for: app, urlString: "gs://\(bucket)/")
- }
- }
- private static func bucket(for app: FirebaseApp, urlString: String) -> String {
- if urlString == "" {
- return ""
- } else {
- guard let path = try? StoragePath.path(GSURI: urlString),
- path.object == nil || path.object == "" else {
- fatalError("Internal Error: Storage bucket cannot be initialized with a path")
- }
- return path.bucket
- }
- }
- }
|