-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Storage.swift
426 lines (388 loc) · 16.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
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
// 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), app: 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), app: app)
}
/**
* 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
}
}
}