-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathStorage.swift
350 lines (307 loc) · 14.2 KB
/
Storage.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
// 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 FirebaseAppCheckInterop
import FirebaseAuthInterop
import FirebaseCore
// Avoids exposing internal FirebaseCore APIs to Swift users.
internal 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, but can be accessed from any thread.
///
/// 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.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
@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 {
return storage(app: app, bucket: 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 {
return storage(app: app, bucket: Storage.bucket(for: app, urlString: url))
}
private class func storage(app: FirebaseApp, bucket: String) -> Storage {
return InstanceCache.shared.storage(app: app, bucket: bucket)
}
/// 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 = .main
/// Creates a `StorageReference` initialized at the root Firebase Storage location.
/// - Returns: An instance of `StorageReference` referencing the root of the storage bucket.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
@objc open func reference() -> StorageReference {
configured = true
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.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
@objc open func reference(forURL url: String) -> StorageReference {
configured = true
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 {
configured = true
var path: StoragePath
do {
path = try StoragePath.path(string: url.absoluteString)
} catch let StoragePathError.storagePathError(message) {
throw StorageError.pathError(message: message)
} catch {
throw StorageError.pathError(message: "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(message: "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`.
/// - Parameter host: A string specifying the host.
/// - Parameter port: The port specified as an `Int`.
@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 configured == false 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 final class InstanceCache: @unchecked Sendable {
static let shared = InstanceCache()
/// A map of active instances, grouped by app. Keys are FirebaseApp names and values are
/// instances of Storage associated with the given app.
private var instances: [String: Storage] = [:]
/// Lock to manage access to the instances array to avoid race conditions.
private var instancesLock: os_unfair_lock = .init()
private init() {}
func storage(app: FirebaseApp, bucket: String) -> Storage {
os_unfair_lock_lock(&instancesLock)
defer { os_unfair_lock_unlock(&instancesLock) }
if let instance = instances[bucket] {
return instance
}
let newInstance = FirebaseStorage.Storage(app: app, bucket: bucket)
instances[bucket] = newInstance
return newInstance
}
}
let dispatchQueue: DispatchQueue
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
// 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)
}
let auth: AuthInterop?
let appCheck: AppCheckInterop?
let storageBucket: String
var usesEmulator = false
/// Once `configured` is true, the emulator can no longer be enabled.
var configured = false
var host: String
var scheme: String
var port: Int
var maxDownloadRetryInterval: TimeInterval
var maxOperationRetryInterval: TimeInterval
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
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
}
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
}
}
}