diff options
author | Paul Beusterien <paulbeusterien@google.com> | 2017-05-15 12:27:07 -0700 |
---|---|---|
committer | Paul Beusterien <paulbeusterien@google.com> | 2017-05-15 12:27:07 -0700 |
commit | 98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch) | |
tree | 131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Firebase/Storage | |
parent | 32461366c9e204a527ca05e6e9b9404a2454ac51 (diff) |
Initial
Diffstat (limited to 'Firebase/Storage')
44 files changed, 4539 insertions, 0 deletions
diff --git a/Firebase/Storage/FIRStorage.h b/Firebase/Storage/FIRStorage.h new file mode 100644 index 0000000..3b37a0e --- /dev/null +++ b/Firebase/Storage/FIRStorage.h @@ -0,0 +1,130 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +#import "FIRStorageConstants.h" +#import "FIRStorageSwiftNameSupport.h" + +@class FIRApp; +@class FIRStorageReference; + +NS_ASSUME_NONNULL_BEGIN + +/** Project version string for FirebaseStorage. */ +FOUNDATION_EXPORT const unsigned char *const FIRStorageVersionString; + +/** + * FirebaseStorage is a service that supports uploading and downloading binary objects, + * such as images, videos, and other files to Google Cloud Storage. + * + * If you call [FIRStorage storage], the instance will initialize with the default FIRApp, + * [FIRApp defaultApp], and the storage location will come from the provided + * GoogleService-Info.plist. + * + * If you call [FIRStorage storageForApp:] and provide a custom instance of FIRApp, + * the storage location will be specified via the FIROptions#storageBucket property. + */ +FIR_SWIFT_NAME(Storage) +@interface FIRStorage : NSObject + +/** + * Creates an instance of FIRStorage, configured with the default FIRApp. + * @return the FIRStorage instance, initialized with the default FIRApp. + */ ++ (instancetype)storage FIR_SWIFT_NAME(storage()); + +/** + * Creates an instance of FIRStorage, configured with the custom FIRApp @a app. + * @param app The custom FIRApp used for initialization. + * @return the FIRStorage instance, initialized with the custom FIRApp. + */ ++ (instancetype)storageForApp:(FIRApp *)app FIR_SWIFT_NAME(storage(app:)); + +/** + * Creates an instance of FIRStorage, configured with a custom storage bucket @a url. + * @param url The gs:// url to your Firebase Storage Bucket. + * @return the FIRStorage instance, initialized with the custom FIRApp. + */ ++ (instancetype)storageWithURL:(NSString *)url FIR_SWIFT_NAME(storage(url:)); + +/** + * Creates an instance of FIRStorage, configured with a custom FIRApp @a app and a custom storage + * bucket @a url. + * @param app The custom FIRApp used for initialization. + * @param url The gs:// url to your Firebase Storage Bucket. + * @return the FIRStorage instance, initialized with the custom FIRApp. + */ ++ (instancetype)storageForApp:(FIRApp *)app + URL:(NSString *)url FIR_SWIFT_NAME(storage(app:url:)); + +/** + * The Firebase App associated with this Firebase Storage instance. + */ +@property(strong, nonatomic, readonly) FIRApp *app; + +/** + * Maximum time in seconds to retry an upload if a failure occurs. + * Defaults to 10 minutes (600 seconds). + */ +@property NSTimeInterval maxUploadRetryTime; + +/** + * Maximum time in seconds to retry a download if a failure occurs. + * Defaults to 10 minutes (600 seconds). + */ +@property NSTimeInterval maxDownloadRetryTime; + +/** + * Maximum time in seconds to retry operations other than upload and download if a failure occurs. + * Defaults to 2 minutes (120 seconds). + */ +@property NSTimeInterval maxOperationRetryTime; + +/** + * Queue that all developer callbacks are fired on. Defaults to the main queue. + */ +@property(strong, nonatomic) dispatch_queue_t callbackQueue; + +/** + * Creates a FIRStorageReference initialized at the root Firebase Storage location. + * @return An instance of FIRStorageReference initialized at the root. + */ +- (FIRStorageReference *)reference; + +/** + * Creates a FIRStorageReference given a gs:// or https:// URL pointing to a Firebase Storage + * location. For example, you can pass in an https:// download URL retrieved from + * [FIRStorageReference downloadURLWithCompletion] or the gs:// URI from + * [FIRStorageReference description]. + * @param string A gs:// or https:// URL to initialize the reference with. + * @return An instance of FIRStorageReference at the given child path. + * @throws Throws an exception if passed in URL is not associated with the FIRApp used to initialize + * this FIRStorage. + */ +- (FIRStorageReference *)referenceForURL:(NSString *)string; + +/** + * Creates a FIRStorageReference initialized at a child Firebase Storage location. + * @param string A relative path from the root to initialize the reference with, + * for instance @"path/to/object". + * @return An instance of FIRStorageReference at the given child path. + */ +- (FIRStorageReference *)referenceWithPath:(NSString *)string; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/FIRStorage.m b/Firebase/Storage/FIRStorage.m new file mode 100644 index 0000000..dd11391 --- /dev/null +++ b/Firebase/Storage/FIRStorage.m @@ -0,0 +1,233 @@ +// Copyright 2017 Google +// +// 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 "FIRStorage.h" + +#import "FIRStorageConstants_Private.h" +#import "FIRStoragePath.h" +#import "FIRStorageReference.h" +#import "FIRStorageReference_Private.h" +#import "FIRStorageTokenAuthorizer.h" +#import "FIRStorageUtils.h" +#import "FIRStorage_Private.h" + +#import "FIRApp.h" +#import "FIROptions.h" + +#import <GTMSessionFetcher/GTMSessionFetcher.h> +#import <GTMSessionFetcher/GTMSessionFetcherLogging.h> + +static NSMutableDictionary< + NSString * /* app name */, + NSMutableDictionary<NSString * /* bucket */, GTMSessionFetcherService *> *> *_fetcherServiceMap; +static GTMSessionFetcherRetryBlock _retryWhenOffline; + +@implementation FIRStorage + ++ (void)initialize { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _retryWhenOffline = ^(BOOL suggestedWillRetry, + NSError * GTM_NULLABLE_TYPE error, + GTMSessionFetcherRetryResponse response) { + bool shouldRetry = suggestedWillRetry; + // GTMSessionFetcher does not consider being offline a retryable error, but we do, so we + // special-case it here. + if (!shouldRetry && error) { + shouldRetry = error.code == NSURLErrorNotConnectedToInternet; + } + response(shouldRetry); + }; + _fetcherServiceMap = [[NSMutableDictionary alloc] init]; + }); +} + ++ (GTMSessionFetcherService *)fetcherServiceForApp:(FIRApp *)app bucket:(NSString *)bucket { + @synchronized(_fetcherServiceMap) { + NSMutableDictionary *bucketMap = _fetcherServiceMap[app.name]; + if (!bucketMap) { + bucketMap = [[NSMutableDictionary alloc] init]; + _fetcherServiceMap[app.name] = bucketMap; + } + + GTMSessionFetcherService *fetcherService = bucketMap[bucket]; + if (!fetcherService) { + fetcherService = [[GTMSessionFetcherService alloc] init]; + [fetcherService setRetryEnabled:YES]; + [fetcherService setRetryBlock:_retryWhenOffline]; + FIRStorageTokenAuthorizer *authorizer = + [[FIRStorageTokenAuthorizer alloc] initWithApp:app fetcherService:fetcherService]; + [fetcherService setAuthorizer:authorizer]; + bucketMap[bucket] = fetcherService; + } + return fetcherService; + } +} + ++ (void)setGTMSessionFetcherLoggingEnabled:(BOOL)isLoggingEnabled { + [GTMSessionFetcher setLoggingEnabled:isLoggingEnabled]; +} + ++ (instancetype)storage { + return [self storageForApp:[FIRApp defaultApp]]; +} + ++ (instancetype)storageForApp:(FIRApp *)app { + NSString* url; + + if (app.options.storageBucket) { + url = [app.options.storageBucket isEqualToString:@""] ? @"" + : [@"gs://" stringByAppendingString:app.options.storageBucket]; + } + + return [self storageForApp:app URL:url]; +} + ++ (instancetype)storageWithURL:(NSString *)url { + return [self storageForApp:[FIRApp defaultApp] URL:url]; +} + ++ (instancetype)storageForApp:(FIRApp *)app URL:(NSString *)url { + if (!url) { + NSString *const kAppNotConfiguredMessage = + @"No default Storage bucket found. Did you configure Firebase Storage properly?"; + [NSException raise:NSInvalidArgumentException format:kAppNotConfiguredMessage]; + } + + NSString *bucket; + if ([url isEqualToString:@""]) { + bucket = @""; + } else { + FIRStoragePath *path; + + @try { + path = [FIRStoragePath pathFromGSURI:url]; + } @catch (NSException *e) { + [NSException raise:NSInternalInconsistencyException + format:@"URI must be in the form of gs://<bucket>/"]; + } + + if (path.object != nil && ![path.object isEqualToString:@""]) { + [NSException raise:NSInternalInconsistencyException + format:@"Storage bucket cannot be initialized with a path"]; + } + + bucket = path.bucket; + } + + return [[self alloc] initWithApp:app bucket:bucket]; +} + +- (instancetype)initWithApp:(FIRApp *)app bucket:(NSString *)bucket { + self = [super init]; + if (self) { + _app = app; + _storageBucket = bucket; + _fetcherServiceForApp = [FIRStorage fetcherServiceForApp:_app bucket:bucket]; + _maxDownloadRetryTime = 600.0; + _maxOperationRetryTime = 120.0; + _maxUploadRetryTime = 600.0; + } + return self; +} + +#pragma mark - NSObject overrides + +- (instancetype)copyWithZone:(NSZone *)zone { + FIRStorage *storage = [[[self class] allocWithZone:zone] initWithApp:_app bucket:_storageBucket]; + storage.callbackQueue = _callbackQueue; + return storage; +} + +// Two FIRStorage objects are equal if they use the same app +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[FIRStorage class]]) { + return NO; + } + + BOOL isEqualObject = [self isEqualToFIRStorage:(FIRStorage *)object]; + return isEqualObject; +} + +- (BOOL)isEqualToFIRStorage:(FIRStorage *)storage { + BOOL isEqual = [_app isEqual:storage->_app]; + return isEqual; +} + +- (NSUInteger)hash { + NSUInteger hash = [_app hash] ^ [_callbackQueue hash]; + return hash; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p: %@", [self class], self, _app]; +} + +#pragma mark - Public methods + +- (FIRStorageReference *)reference { + FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:_storageBucket object:nil]; + return [[FIRStorageReference alloc] initWithStorage:self path:path]; +} + +- (FIRStorageReference *)referenceForURL:(NSString *)string { + FIRStoragePath *path = [FIRStoragePath pathFromString:string]; + + // If no default bucket exists (empty string), accept anything. + if ([_storageBucket isEqual:@""]) { + FIRStorageReference *reference = [[FIRStorageReference alloc] initWithStorage:self path:path]; + return reference; + } + + // If there exists a default bucket, throw if provided a different bucket. + if (![path.bucket isEqual:_storageBucket]) { + NSString *const kInvalidBucketFormat = + @"Provided bucket: %@ does not match the Storage bucket of the current instance: %@"; + [NSException raise:NSInvalidArgumentException + format:kInvalidBucketFormat, path.bucket, _storageBucket]; + } + + FIRStorageReference *reference = [[FIRStorageReference alloc] initWithStorage:self path:path]; + return reference; +} + +- (FIRStorageReference *)referenceWithPath:(NSString *)string { + FIRStorageReference *reference = [[self reference] child:string]; + return reference; +} + +- (void)setCallbackQueue:(dispatch_queue_t)callbackQueue { + _fetcherServiceForApp.callbackQueue = callbackQueue; +} + +#pragma mark - Background tasks + ++ (void)enableBackgroundTasks:(BOOL)isEnabled { + [NSException raise:NSGenericException format:@"enableBackgroundTasks not implemented"]; +} + +- (NSArray<FIRStorageUploadTask *> *)uploadTasks { + [NSException raise:NSGenericException format:@"getUploadTasks not implemented"]; + return nil; +} + +- (NSArray<FIRStorageDownloadTask *> *)downloadTasks { + [NSException raise:NSGenericException format:@"getDownloadTasks not implemented"]; + return nil; +} +@end diff --git a/Firebase/Storage/FIRStorageConstants.h b/Firebase/Storage/FIRStorageConstants.h new file mode 100644 index 0000000..cf6c3b8 --- /dev/null +++ b/Firebase/Storage/FIRStorageConstants.h @@ -0,0 +1,173 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +#import "FIRStorageSwiftNameSupport.h" + +@class FIRStorageDownloadTask; +@class FIRStorageMetadata; +@class FIRStorageTaskSnapshot; +@class FIRStorageUploadTask; + +NS_ASSUME_NONNULL_BEGIN + +/** + * NSString typedef representing a task listener handle. + */ +typedef NSString *FIRStorageHandle FIR_SWIFT_NAME(StorageHandle); + +/** + * Block typedef typically used when downloading data. + * @param data The data returned by the download, or nil if no data available or download failed. + * @param error The error describing failure, if one occurred. + */ +typedef void (^FIRStorageVoidDataError)(NSData *_Nullable data, NSError *_Nullable error) + FIR_SWIFT_NAME(StorageVoidDataError); + +/** + * Block typedef typically used when performing "binary" async operations such as delete, + * where the operation either succeeds without an error or fails with an error. + * @param error The error describing failure, if one occurred. + */ +typedef void (^FIRStorageVoidError)(NSError *_Nullable error) FIR_SWIFT_NAME(StorageVoidError); + +/** + * Block typedef typically used when retrieving metadata. + * @param metadata The metadata returned by the operation, if metadata exists. + */ +typedef void (^FIRStorageVoidMetadata)(FIRStorageMetadata *_Nullable metadata) + FIR_SWIFT_NAME(StorageVoidMetadata); + +/** + * Block typedef typically used when retrieving metadata with the possibility of an error. + * @param metadata The metadata returned by the operation, if metadata exists. + * @param error The error describing failure, if one occurred. + */ +typedef void (^FIRStorageVoidMetadataError)(FIRStorageMetadata *_Nullable metadata, + NSError *_Nullable error) + FIR_SWIFT_NAME(StorageVoidMetadataError); + +/** + * Block typedef typically used to asynchronously return a storage task snapshot. + * @param snapshot The returned task snapshot. + */ +typedef void (^FIRStorageVoidSnapshot)(FIRStorageTaskSnapshot *snapshot) + FIR_SWIFT_NAME(StorageVoidSnapshot); + +/** + * Block typedef typically used when retrieving a download URL. + * @param URL The download URL associated with the operation. + * @param error The error describing failure, if one occurred. + */ +typedef void (^FIRStorageVoidURLError)(NSURL *_Nullable URL, NSError *_Nullable error) + FIR_SWIFT_NAME(StorageVoidURLError); + +/** + * Enum representing the upload and download task status. + */ +typedef NS_ENUM(NSInteger, FIRStorageTaskStatus) { + /** + * Unknown task status. + */ + FIRStorageTaskStatusUnknown, + + /** + * Task is being resumed. + */ + FIRStorageTaskStatusResume, + + /** + * Task reported a progress event. + */ + FIRStorageTaskStatusProgress, + + /** + * Task is paused. + */ + FIRStorageTaskStatusPause, + + /** + * Task has completed successfully. + */ + FIRStorageTaskStatusSuccess, + + /** + * Task has failed and is unrecoverable. + */ + FIRStorageTaskStatusFailure +} FIR_SWIFT_NAME(StorageTaskStatus); + +/** + * Firebase Storage error domain. + */ +FOUNDATION_EXPORT NSString *const FIRStorageErrorDomain FIR_SWIFT_NAME(StorageErrorDomain); + +/** + * Enum representing the errors raised by Firebase Storage. + */ +typedef NS_ENUM(NSInteger, FIRStorageErrorCode) { + /** An unknown error occurred. */ + FIRStorageErrorCodeUnknown = -13000, + + /** No object exists at the desired reference. */ + FIRStorageErrorCodeObjectNotFound = -13010, + + /** No bucket is configured for Firebase Storage. */ + FIRStorageErrorCodeBucketNotFound = -13011, + + /** No project is configured for Firebase Storage. */ + FIRStorageErrorCodeProjectNotFound = -13012, + + /** + * Quota on your Firebase Storage bucket has been exceeded. + * If you're on the free tier, upgrade to a paid plan. + * If you're on a paid plan, reach out to Firebase support. + */ + FIRStorageErrorCodeQuotaExceeded = -13013, + + /** User is unauthenticated. Authenticate and try again. */ + FIRStorageErrorCodeUnauthenticated = -13020, + + /** + * User is not authorized to perform the desired action. + * Check your rules to ensure they are correct. + */ + FIRStorageErrorCodeUnauthorized = -13021, + + /** + * The maximum time limit on an operation (upload, download, delete, etc.) has been exceeded. + * Try uploading again. + */ + FIRStorageErrorCodeRetryLimitExceeded = -13030, + + /** + * File on the client does not match the checksum of the file received by the server. + * Try uploading again. + */ + FIRStorageErrorCodeNonMatchingChecksum = -13031, + + /** + * Size of the downloaded file exceeds the amount of memory allocated for the download. + * Increase memory cap and try downloading again. + */ + FIRStorageErrorCodeDownloadSizeExceeded = -13032, + + /** User cancelled the operation. */ + FIRStorageErrorCodeCancelled = -13040 +} FIR_SWIFT_NAME(StorageErrorCode); + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/FIRStorageConstants.m b/Firebase/Storage/FIRStorageConstants.m new file mode 100644 index 0000000..aa3da1b --- /dev/null +++ b/Firebase/Storage/FIRStorageConstants.m @@ -0,0 +1,83 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageConstants.h" + +#import "FIRStorageConstants_Private.h" + +NSString *const kGCSScheme = @"https"; +NSString *const kGCSHost = @"www.googleapis.com"; +NSString *const kGCSUploadPath = @"upload"; +NSString *const kGCSStorageVersionPath = @"storage/v1"; +NSString *const kGCSBucketPathFormat = @"b/%@"; +NSString *const kGCSObjectPathFormat = @"o/%@"; + +NSString *const kFIRStorageScheme = @"https"; +NSString *const kFIRStorageHost = @"firebasestorage.googleapis.com"; +NSString *const kFIRStorageVersionPath = @"v0"; +NSString *const kFIRStorageBucketPathFormat = @"b/%@"; +NSString *const kFIRStorageObjectPathFormat = @"o/%@"; +NSString *const kFIRStorageFullPathFormat = @"/v0/b/%@/o/%@"; + +NSString *const kFIRStorageAuthTokenFormat = @"Firebase %@"; +NSString *const kFIRStorageDefaultBucketFormat = @"gs://%@"; + +NSString *const kFIRStorageResponseErrorDomain = @"ResponseErrorDomain"; +NSString *const kFIRStorageResponseErrorCode = @"ResponseErrorCode"; +NSString *const kFIRStorageResponseBody = @"ResponseBody"; + +NSString *const FIRStorageErrorDomain = @"FIRStorageErrorDomain"; + +NSString *const kFIRStorageInvalidDataFormat = @"Invalid data returned from the server: %@"; +NSString *const kFIRStorageInvalidObserverStatus = @"Invalid observer status requested, use one " + @"of: FIRStorageTaskStatusPause, Resume, Progress, " @"Complete, or Failure"; + +/** + * String constants mapping GCS Object#resource mappings to metadata fields. + */ +NSString *const kFIRStorageMetadataBucket = @"bucket"; +NSString *const kFIRStorageMetadataCacheControl = @"cacheControl"; +NSString *const kFIRStorageMetadataContentDisposition = @"contentDisposition"; +NSString *const kFIRStorageMetadataContentEncoding = @"contentEncoding"; +NSString *const kFIRStorageMetadataContentLanguage = @"contentLanguage"; +NSString *const kFIRStorageMetadataContentType = @"contentType"; +NSString *const kFIRStorageMetadataCustomMetadata = @"metadata"; +NSString *const kFIRStorageMetadataSize = @"size"; +NSString *const kFIRStorageMetadataDownloadURLs = @"downloadURLs"; +NSString *const kFIRStorageMetadataGeneration = @"generation"; +NSString *const kFIRStorageMetadataMetageneration = @"metageneration"; +NSString *const kFIRStorageMetadataTimeCreated = @"timeCreated"; +NSString *const kFIRStorageMetadataUpdated = @"updated"; +NSString *const kFIRStorageMetadataName = @"name"; +NSString *const kFIRStorageMetadataDownloadTokens = @"downloadTokens"; + +// TODO: add notification support +NSString *const kFIRStorageTaskStatusResumeNotification = + @"kFIRStorageTaskStatusResumeNotification"; +NSString *const kFIRStorageTaskStatusPauseNotification = @"kFIRStorageTaskStatusResumeNotification"; +NSString *const kFIRStorageTaskStatusProgressNotification = + @"kFIRStorageTaskStatusResumeNotification"; +NSString *const kFIRStorageTaskStatusCompleteNotification = + @"kFIRStorageTaskStatusResumeNotification"; +NSString *const kFIRStorageTaskStatusFailureNotification = + @"kFIRStorageTaskStatusResumeNotification"; + +NSString *const kFIRStorageBundleIdentifier = @"com.google.firebase.storage"; + +// The STR and STR_EXPAND macro allow a numeric version passed to he compiler driver +// with a -D to be treated as a string instead of an invalid floating point value. +#define STR(x) STR_EXPAND(x) +#define STR_EXPAND(x) #x +const unsigned char *const FIRStorageVersionString = + (const unsigned char *const) STR(FIRStorage_VERSION); diff --git a/Firebase/Storage/FIRStorageDeleteTask.m b/Firebase/Storage/FIRStorageDeleteTask.m new file mode 100644 index 0000000..4f3f1cc --- /dev/null +++ b/Firebase/Storage/FIRStorageDeleteTask.m @@ -0,0 +1,54 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageDeleteTask.h" + +#import "FIRStorageTask_Private.h" + +@implementation FIRStorageDeleteTask { + @private + FIRStorageVoidError _completion; +} + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + completion:(FIRStorageVoidError)completion { + self = [super initWithReference:reference fetcherService:service]; + if (self) { + _completion = [completion copy]; + } + return self; +} + +- (void)enqueue { + NSMutableURLRequest *request = [self.baseRequest mutableCopy]; + request.HTTPMethod = @"DELETE"; + request.timeoutInterval = self.reference.storage.maxOperationRetryTime; + + FIRStorageVoidError callback = _completion; + _completion = nil; + + GTMSessionFetcher *fetcher = [self.fetcherService fetcherWithRequest:request]; + fetcher.comment = @"DeleteTask"; + [fetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) { + if (!self.error) { + self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference]; + } + if (callback) { + callback(self.error); + } + }]; +} + +@end diff --git a/Firebase/Storage/FIRStorageDownloadTask.h b/Firebase/Storage/FIRStorageDownloadTask.h new file mode 100644 index 0000000..252b910 --- /dev/null +++ b/Firebase/Storage/FIRStorageDownloadTask.h @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +#import "FIRStorageObservableTask.h" +#import "FIRStorageSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * FIRStorageDownloadTask implements resumable downloads from an object in Firebase Storage. + * Downloads can be returned on completion with a completion handler, and can be monitored + * by attaching observers, or controlled by calling FIRStorageTask#pause, FIRStorageTask#resume, + * or FIRStorageTask#cancel. + * Downloads can currently be returned as NSData in memory, or as an NSURL to a file on disk. + * Downloads are performed on a background queue, and callbacks are raised on the developer + * specified callbackQueue in FIRStorage, or the main queue if left unspecified. + * Currently all uploads must be initiated and managed on the main queue. + */ +FIR_SWIFT_NAME(StorageDownloadTask) +@interface FIRStorageDownloadTask : FIRStorageObservableTask<FIRStorageTaskManagement> + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/FIRStorageDownloadTask.m b/Firebase/Storage/FIRStorageDownloadTask.m new file mode 100644 index 0000000..0d71e52 --- /dev/null +++ b/Firebase/Storage/FIRStorageDownloadTask.m @@ -0,0 +1,162 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageDownloadTask.h" + +#import "FIRStorageConstants_Private.h" +#import "FIRStorageDownloadTask_Private.h" +#import "FIRStorageObservableTask_Private.h" +#import "FIRStorageTask_Private.h" + +@implementation FIRStorageDownloadTask + +@synthesize progress = _progress; +@synthesize fetcher = _fetcher; + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + file:(nullable NSURL *)fileURL { + self = [super initWithReference:reference fetcherService:service]; + if (self) { + _fileURL = [fileURL copy]; + _progress = [NSProgress progressWithTotalUnitCount:0]; + } + return self; +} + +- (void)enqueue { + [self enqueueWithData:nil]; +} + +- (void)enqueueWithData:(nullable NSData *)resumeData { + NSAssert([NSThread isMainThread], @"Download attempting to execute on non main queue! Please " + @"only execute this method on the main queue."); + self.state = FIRStorageTaskStateQueueing; + NSMutableURLRequest *request = [self.baseRequest mutableCopy]; + request.HTTPMethod = @"GET"; + request.timeoutInterval = self.reference.storage.maxDownloadRetryTime; + NSURLComponents *components = + [NSURLComponents componentsWithURL:request.URL resolvingAgainstBaseURL:NO]; + [components setQuery:@"alt=media"]; + request.URL = components.URL; + + GTMSessionFetcher *fetcher; + if (resumeData) { + fetcher = [GTMSessionFetcher fetcherWithDownloadResumeData:resumeData]; + fetcher.comment = @"Resuming DownloadTask"; + } else { + fetcher = [self.fetcherService fetcherWithRequest:request]; + fetcher.comment = @"Starting DownloadTask"; + } + + [fetcher setResumeDataBlock:^(NSData *data) { + if (data) { + _downloadData = data; + } + }]; + + fetcher.maxRetryInterval = self.reference.storage.maxDownloadRetryTime; + + if (_fileURL) { + // Handle file downloads + [fetcher setDestinationFileURL:_fileURL]; + [fetcher setDownloadProgressBlock:^(int64_t bytesWritten, int64_t totalBytesWritten, + int64_t totalBytesExpectedToWrite) { + self.state = FIRStorageTaskStateProgress; + self.progress.completedUnitCount = totalBytesWritten; + self.progress.totalUnitCount = totalBytesExpectedToWrite; + FIRStorageTaskSnapshot *snapshot = self.snapshot; + [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:snapshot]; + self.state = FIRStorageTaskStateRunning; + }]; + } else { + // Handle data downloads + [fetcher setReceivedProgressBlock:^(int64_t bytesWritten, int64_t totalBytesWritten) { + self.state = FIRStorageTaskStateProgress; + self.progress.completedUnitCount = totalBytesWritten; + int64_t totalLength = [[self.fetcher response] expectedContentLength]; + self.progress.totalUnitCount = totalLength; + FIRStorageTaskSnapshot *snapshot = self.snapshot; + [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:snapshot]; + self.state = FIRStorageTaskStateRunning; + }]; + } + + _fetcher = fetcher; + + self.state = FIRStorageTaskStateRunning; + [self.fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + // Fire last progress updates + [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:self.snapshot]; + + // Handle potential issues with download + if (error) { + self.state = FIRStorageTaskStateFailed; + self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference]; + [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot]; + [self removeAllObservers]; + return; + } + + // Download completed successfully, fire completion callbacks + self.state = FIRStorageTaskStateSuccess; + + if (data) { + _downloadData = data; + } + + [self fireHandlersForStatus:FIRStorageTaskStatusSuccess snapshot:self.snapshot]; + [self removeAllObservers]; + }]; +} + +#pragma mark - Download Management + +- (void)cancel { + NSError *error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeCancelled]; + [self cancelWithError:error]; +} + +- (void)cancelWithError:(NSError *)error { + NSAssert([NSThread isMainThread], @"Cancel attempting to execute on non main queue! Please only " + @"execute this method on the main queue."); + self.state = FIRStorageTaskStateCancelled; + [self.fetcher stopFetching]; + self.error = error; + [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot]; +} + +- (void)pause { + NSAssert([NSThread isMainThread], @"Pause attempting to execute on non main queue! Please only " + @"execute this method on the main queue."); + self.state = FIRStorageTaskStatePausing; + [self.fetcher stopFetching]; + // Give the resume callback a chance to run (if scheduled) + [self.fetcher waitForCompletionWithTimeout:0.001]; + self.state = FIRStorageTaskStatePaused; + FIRStorageTaskSnapshot *snapshot = self.snapshot; + [self fireHandlersForStatus:FIRStorageTaskStatusPause snapshot:snapshot]; +} + +- (void)resume { + NSAssert([NSThread isMainThread], @"Resume attempting to execute on non main queue! Please only " + @"execute this method on the main queue."); + self.state = FIRStorageTaskStateResuming; + FIRStorageTaskSnapshot *snapshot = self.snapshot; + [self fireHandlersForStatus:FIRStorageTaskStatusResume snapshot:snapshot]; + self.state = FIRStorageTaskStateRunning; + [self enqueueWithData:_downloadData]; +} + +@end diff --git a/Firebase/Storage/FIRStorageErrors.m b/Firebase/Storage/FIRStorageErrors.m new file mode 100644 index 0000000..49a5ffa --- /dev/null +++ b/Firebase/Storage/FIRStorageErrors.m @@ -0,0 +1,172 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageErrors.h" + +#import "FIRStorageConstants_Private.h" +#import "FIRStorageReference.h" +#import "FIRStorageReference_Private.h" + +@implementation FIRStorageErrors + ++ (NSError *)errorWithCode:(FIRStorageErrorCode)code { + return [FIRStorageErrors errorWithCode:code infoDictionary:nil]; +} + ++ (NSError *)errorWithCode:(FIRStorageErrorCode)code + infoDictionary:(nullable NSDictionary *)dictionary { + NSMutableDictionary *errorDictionary; + if (dictionary) { + errorDictionary = [dictionary mutableCopy]; + } else { + errorDictionary = [[NSMutableDictionary alloc] init]; + } + + NSString *errorMessage; + switch (code) { + case FIRStorageErrorCodeObjectNotFound: + errorMessage = + [NSString stringWithFormat:@"Object %@ does not exist.", errorDictionary[@"object"]]; + break; + + case FIRStorageErrorCodeBucketNotFound: + errorMessage = + [NSString stringWithFormat:@"Bucket %@ does not exist.", errorDictionary[@"bucket"]]; + break; + + case FIRStorageErrorCodeProjectNotFound: + errorMessage = + [NSString stringWithFormat:@"Project %@ does not exist.", errorDictionary[@"project"]]; + break; + + case FIRStorageErrorCodeQuotaExceeded: { + NSString *const kQuotaExceededFormat = + @"Quota for bucket %@ exceeded, please view quota on firebase.google.com."; + errorMessage = [NSString stringWithFormat:kQuotaExceededFormat, errorDictionary[@"bucket"]]; + break; + } + + case FIRStorageErrorCodeDownloadSizeExceeded: { + int64_t total = [errorDictionary[@"totalSize"] longLongValue]; + int64_t size = [errorDictionary[@"maxAllowedSize"] longLongValue]; + NSString *totalString = total ? @(total).stringValue : @"unknown"; + NSString *sizeString = total ? @(size).stringValue : @"unknown"; + NSString *const kSizeExceededErrorFormat = + @"Attempeted to download object with size of %@ bytes, " + @"which exceeds the maximum size of %@ bytes. " + @"Consider raising the maximum download size, or using " + @"[FIRStorageReference writeToFile:]"; + errorMessage = [NSString stringWithFormat:kSizeExceededErrorFormat, totalString, sizeString]; + break; + } + + case FIRStorageErrorCodeUnauthenticated: + errorMessage = @"User is not authenticated, please authenticate using Firebase " + @"Authentication and try again."; + break; + + case FIRStorageErrorCodeUnauthorized: { + NSString *bucket = errorDictionary[@"bucket"]; + NSString *object = errorDictionary[@"object"]; + NSString *const kUnauthorizedFormat = @"User does not have permission to access gs://%@/%@."; + errorMessage = [NSString stringWithFormat:kUnauthorizedFormat, bucket, object]; + break; + } + + case FIRStorageErrorCodeRetryLimitExceeded: + errorMessage = @"Max retry time for operation exceeded, please try again."; + break; + + case FIRStorageErrorCodeNonMatchingChecksum: { + // TODO: replace with actual checksum strings when we choose to implement. + NSString *const kChecksumFailedErrorFormat = + @"Uploaded/downloaded object %@ has checksum: %@ " + @"which does not match server checksum: %@. Please retry the upload/download."; + errorMessage = [NSString stringWithFormat:kChecksumFailedErrorFormat, @"object", + @"client checksum", @"server checksum"]; + break; + } + + case FIRStorageErrorCodeCancelled: + errorMessage = @"User cancelled the upload/download."; + break; + + case FIRStorageErrorCodeUnknown: + /* Fall through to default case for unknown errors */ + + default: + errorMessage = @"An unknown error occurred, please check the server response."; + break; + } + + errorDictionary[NSLocalizedDescriptionKey] = errorMessage; + + NSError *err = [NSError errorWithDomain:FIRStorageErrorDomain code:code userInfo:errorDictionary]; + return err; +} + ++ (nullable NSError *)errorWithServerError:(nullable NSError *)error + reference:(nullable FIRStorageReference *)reference { + if (error == nil) { + return nil; + } + + FIRStorageErrorCode errorCode; + switch (error.code) { + case 400: + errorCode = FIRStorageErrorCodeUnknown; + break; + + case 401: + errorCode = FIRStorageErrorCodeUnauthenticated; + break; + + case 402: + errorCode = FIRStorageErrorCodeQuotaExceeded; + break; + + case 403: + errorCode = FIRStorageErrorCodeUnauthorized; + break; + + case 404: + errorCode = FIRStorageErrorCodeObjectNotFound; + break; + + default: + errorCode = FIRStorageErrorCodeUnknown; + break; + } + + NSMutableDictionary *errorDictionary = + [[[NSDictionary alloc] initWithDictionary:error.userInfo] mutableCopy]; + errorDictionary[kFIRStorageResponseErrorDomain] = error.domain; + errorDictionary[kFIRStorageResponseErrorCode] = @(error.code); + + // Turn raw response into a string + NSData *responseData = errorDictionary[@"data"]; + if (responseData) { + NSString *errorString = + [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; + errorDictionary[kFIRStorageResponseBody] = errorString ?: @"No Response from Server."; + } + + errorDictionary[@"bucket"] = reference.path.bucket; + errorDictionary[@"object"] = reference.path.object; + + NSError *clientError = [FIRStorageErrors errorWithCode:errorCode infoDictionary:errorDictionary]; + return clientError; +} + +@end diff --git a/Firebase/Storage/FIRStorageGetMetadataTask.m b/Firebase/Storage/FIRStorageGetMetadataTask.m new file mode 100644 index 0000000..d0e8981 --- /dev/null +++ b/Firebase/Storage/FIRStorageGetMetadataTask.m @@ -0,0 +1,84 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageGetMetadataTask.h" + +#import "FIRStorageConstants.h" +#import "FIRStorageMetadata_Private.h" +#import "FIRStorageTask_Private.h" +#import "FIRStorageUtils.h" + +#import "FirebaseStorage.h" + +@implementation FIRStorageGetMetadataTask { + @private + FIRStorageVoidMetadataError _completion; +} + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + completion:(FIRStorageVoidMetadataError)completion { + self = [super initWithReference:reference fetcherService:service]; + if (self) { + _completion = [completion copy]; + } + return self; +} + +- (void)enqueue { + NSMutableURLRequest *request = [self.baseRequest mutableCopy]; + request.HTTPMethod = @"GET"; + request.timeoutInterval = self.reference.storage.maxDownloadRetryTime; + + FIRStorageVoidMetadataError callback = _completion; + _completion = nil; + + GTMSessionFetcher *fetcher = [self.fetcherService fetcherWithRequest:request]; + fetcher.comment = @"GetMetadataTask"; + [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + if (error) { + if (!self.error) { + self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference]; + } + if (callback) { + callback(nil, self.error); + } + return; + } + + NSDictionary *responseDictionary = [NSDictionary frs_dictionaryFromJSONData:data]; + if (responseDictionary != nil) { + FIRStorageMetadata *metadata = + [[FIRStorageMetadata alloc] initWithDictionary:responseDictionary]; + [metadata setType:FIRStorageMetadataTypeFile]; + if (callback) { + callback(metadata, nil); + } + } else { + NSString *returnedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSString *invalidDataString = + [NSString stringWithFormat:kFIRStorageInvalidDataFormat, returnedData]; + NSDictionary *dict; + if (invalidDataString.length > 0) { + dict = @{NSLocalizedFailureReasonErrorKey : invalidDataString}; + } + self.error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnknown infoDictionary:dict]; + if (callback) { + callback(nil, self.error); + } + } + }]; +} + +@end diff --git a/Firebase/Storage/FIRStorageMetadata.h b/Firebase/Storage/FIRStorageMetadata.h new file mode 100644 index 0000000..8d844f7 --- /dev/null +++ b/Firebase/Storage/FIRStorageMetadata.h @@ -0,0 +1,149 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +#import "FIRStorageSwiftNameSupport.h" + +@class FIRStorageReference; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Class which represents the metadata on an object in Firebase Storage. This metadata is + * returned on successful operations, and can be used to retrieve download URLs, content types, + * and a FIRStorage reference to the object in question. Full documentation can be found at the GCS + * Objects#resource docs. + * @see https://cloud.google.com/storage/docs/json_api/v1/objects#resource + */ +FIR_SWIFT_NAME(StorageMetadata) +@interface FIRStorageMetadata : NSObject<NSCopying> + +/** + * The name of the bucket containing this object. + */ +@property(copy, nonatomic, readonly) NSString *bucket; + +/** + * Cache-Control directive for the object data. + */ +@property(copy, nonatomic, nullable) NSString *cacheControl; + +/** + * Content-Disposition of the object data. + */ +@property(copy, nonatomic, nullable) NSString *contentDisposition; + +/** + * Content-Encoding of the object data. + */ +@property(copy, nonatomic, nullable) NSString *contentEncoding; + +/** + * Content-Language of the object data. + */ +@property(copy, nonatomic, nullable) NSString *contentLanguage; + +/** + * Content-Type of the object data. + */ +@property(copy, nonatomic, nullable) NSString *contentType; + +/** + * The content generation of this object. Used for object versioning. + */ +@property(readonly) int64_t generation; + +/** + * User-provided metadata, in key/value pairs. + */ +@property(copy, nonatomic, nullable) NSDictionary<NSString *, NSString *> *customMetadata; + +/** + * The version of the metadata for this object at this generation. Used + * for preconditions and for detecting changes in metadata. A metageneration number is only + * meaningful in the context of a particular generation of a particular object. + */ +@property(readonly) int64_t metageneration; + +/** + * The name of this object, in gs://bucket/path/to/object.txt, this is object.txt. + */ +@property(copy, nonatomic, readonly, nullable) NSString *name; + +/** + * The full path of this object, in gs://bucket/path/to/object.txt, this is path/to/object.txt. + */ +@property(copy, nonatomic, readonly, nullable) NSString *path; + +/** + * Content-Length of the data in bytes. + */ +@property(readonly) int64_t size; + +/** + * The creation time of the object in RFC 3339 format. + */ +@property(copy, nonatomic, readonly, nullable) NSDate *timeCreated; + +/** + * The modification time of the object metadata in RFC 3339 format. + */ +@property(copy, nonatomic, readonly, nullable) NSDate *updated; + +/** + * A reference to the object in Firebase Storage. + */ +@property(strong, nonatomic, readonly, nullable) FIRStorageReference *storageReference; + +/** + * An array containing all download URLs available for the object. + */ +@property(strong, nonatomic, readonly, nullable) NSArray<NSURL *> *downloadURLs; + +/** + * Creates an instanece of FIRStorageMetadata from the contents of a dictionary. + * @return An instance of FIRStorageMetadata that represents the contents of a dictionary. + */ +- (nullable instancetype)initWithDictionary:(NSDictionary <NSString *, id>*)dictionary + NS_DESIGNATED_INITIALIZER; + +/** + * Creates an NSDictionary from the contents of the metadata. + * @return An NSDictionary that represents the contents of the metadata. + */ +- (NSDictionary <NSString *, id>*)dictionaryRepresentation; + +/** + * Determines if the current metadata represents a "file". + */ +@property(readonly, getter=isFile) BOOL file; + +/** + * Determines if the current metadata represents a "folder". + */ +@property(readonly, getter=isFolder) BOOL folder; + +/** + * Retrieves a download URL for the given object, or nil if none exist. + * Note that if there are many valid download tokens, this will always return the first + * valid token created. + */ +- (nullable NSURL *)downloadURL; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/FIRStorageMetadata.m b/Firebase/Storage/FIRStorageMetadata.m new file mode 100644 index 0000000..6c85bbf --- /dev/null +++ b/Firebase/Storage/FIRStorageMetadata.m @@ -0,0 +1,227 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageMetadata.h" + +#import "FIRStorageConstants.h" +#import "FIRStorageConstants_Private.h" +#import "FIRStorageMetadata_Private.h" +#import "FIRStorageUtils.h" + +// TODO: consider rewriting this using GTLR (GTLRStorageObjects.h) +@implementation FIRStorageMetadata + +#pragma mark - Initializers + +- (instancetype)init { + return [self initWithDictionary:[NSDictionary dictionary]]; +} + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary { + self = [super init]; + if (self) { + _bucket = dictionary[kFIRStorageMetadataBucket]; + _cacheControl = dictionary[kFIRStorageMetadataCacheControl]; + _contentDisposition = dictionary[kFIRStorageMetadataContentDisposition]; + _contentEncoding = dictionary[kFIRStorageMetadataContentEncoding]; + _contentLanguage = dictionary[kFIRStorageMetadataContentLanguage]; + _contentType = dictionary[kFIRStorageMetadataContentType]; + _customMetadata = dictionary[kFIRStorageMetadataCustomMetadata]; + _size = [dictionary[kFIRStorageMetadataSize] longLongValue]; + _downloadURLs = dictionary[kFIRStorageMetadataDownloadURLs]; + _generation = [dictionary[kFIRStorageMetadataGeneration] longLongValue]; + _metageneration = [dictionary[kFIRStorageMetadataMetageneration] longLongValue]; + _timeCreated = [self dateFromRFC3339String:dictionary[kFIRStorageMetadataTimeCreated]]; + _updated = [self dateFromRFC3339String:dictionary[kFIRStorageMetadataUpdated]]; + // GCS "name" is our path, our "name" is just the last path component of the path + _path = dictionary[kFIRStorageMetadataName]; + _name = [_path lastPathComponent]; + NSString *downloadTokens = dictionary[kFIRStorageMetadataDownloadTokens]; + if (downloadTokens) { + NSArray<NSString *> *downloadStringArray = [downloadTokens componentsSeparatedByString:@","]; + NSMutableArray<NSURL *> *downloadURLArray = + [[NSMutableArray alloc] initWithCapacity:[downloadStringArray count]]; + [downloadStringArray enumerateObjectsUsingBlock:^(NSString *_Nonnull token, NSUInteger idx, + BOOL *_Nonnull stop) { + NSURLComponents *components = [[NSURLComponents alloc] init]; + components.scheme = kFIRStorageScheme; + components.host = kFIRStorageHost; + NSString *path = [FIRStorageUtils GCSEscapedString:_path]; + NSString *fullPath = [NSString stringWithFormat:kFIRStorageFullPathFormat, _bucket, path]; + components.percentEncodedPath = fullPath; + components.query = [NSString stringWithFormat:@"alt=media&token=%@", token]; + + [downloadURLArray insertObject:[components URL] atIndex:idx]; + }]; + _downloadURLs = downloadURLArray; + } + } + return self; +} + +#pragma mark - NSObject overrides + +- (instancetype)copyWithZone:(NSZone *)zone { + return [[[self class] allocWithZone:zone] initWithDictionary:[self dictionaryRepresentation]]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[FIRStorageMetadata class]]) { + return NO; + } + + BOOL isEqualObject = [self isEqualToFIRStorageMetadata:(FIRStorageMetadata *)object]; + return isEqualObject; +} + +- (BOOL)isEqualToFIRStorageMetadata:(FIRStorageMetadata *)metadata { + return [[self dictionaryRepresentation] isEqualToDictionary:[metadata dictionaryRepresentation]]; +} + +- (NSUInteger)hash { + NSUInteger hash = [[self dictionaryRepresentation] hash]; + return hash; +} + +- (NSString *)description { + NSDictionary *metadataDictionary = [self dictionaryRepresentation]; + return [NSString stringWithFormat:@"%@ %p: %@", [self class], self, metadataDictionary]; +} + +#pragma mark - Public methods + +- (NSDictionary *)dictionaryRepresentation { + NSMutableDictionary *metadataDictionary = [[NSMutableDictionary alloc] initWithCapacity:13]; + + if (_bucket) { + metadataDictionary[kFIRStorageMetadataBucket] = _bucket; + } + + if (_cacheControl) { + metadataDictionary[kFIRStorageMetadataCacheControl] = _cacheControl; + } + + if (_contentDisposition) { + metadataDictionary[kFIRStorageMetadataContentDisposition] = _contentDisposition; + } + + if (_contentEncoding) { + metadataDictionary[kFIRStorageMetadataContentEncoding] = _contentEncoding; + } + + if (_contentLanguage) { + metadataDictionary[kFIRStorageMetadataContentLanguage] = _contentLanguage; + } + + if (_contentType) { + metadataDictionary[kFIRStorageMetadataContentType] = _contentType; + } + + if (_customMetadata) { + metadataDictionary[kFIRStorageMetadataCustomMetadata] = _customMetadata; + } + + if (_downloadURLs) { + NSMutableArray *downloadTokens = [[NSMutableArray alloc] init]; + [_downloadURLs + enumerateObjectsUsingBlock:^(NSURL *_Nonnull URL, NSUInteger idx, BOOL *_Nonnull stop) { + NSArray *queryItems = [URL.query componentsSeparatedByString:@"&"]; + [queryItems enumerateObjectsUsingBlock:^(NSString *queryString, NSUInteger idx, + BOOL *_Nonnull stop) { + NSString *key; + NSString *value; + NSScanner *scanner = [NSScanner scannerWithString:queryString]; + [scanner scanUpToString:@"=" intoString:&key]; + [scanner scanString:@"=" intoString:NULL]; + [scanner scanUpToString:@"\n" intoString:&value]; + if ([key isEqual:@"token"]) { + [downloadTokens addObject:value]; + *stop = YES; + } + }]; + }]; + NSString *downloadTokenString = [downloadTokens componentsJoinedByString:@","]; + metadataDictionary[kFIRStorageMetadataDownloadTokens] = downloadTokenString; + } + + if (_generation) { + NSString *generationString = [NSString stringWithFormat:@"%lld", _generation]; + metadataDictionary[kFIRStorageMetadataGeneration] = generationString; + } + + if (_metageneration) { + NSString *metagenerationString = [NSString stringWithFormat:@"%lld", _metageneration]; + metadataDictionary[kFIRStorageMetadataMetageneration] = metagenerationString; + } + + if (_timeCreated) { + metadataDictionary[kFIRStorageMetadataTimeCreated] = [self RFC3339StringFromDate:_timeCreated]; + } + + if (_updated) { + metadataDictionary[kFIRStorageMetadataUpdated] = [self RFC3339StringFromDate:_updated]; + } + + if (_path) { + metadataDictionary[kFIRStorageMetadataName] = _path; + } + + return [metadataDictionary copy]; +} + +- (BOOL)isFile { + return _type == FIRStorageMetadataTypeFile; +} + +- (BOOL)isFolder { + return _type == FIRStorageMetadataTypeFolder; +} + +- (nullable NSURL *)downloadURL { + return [_downloadURLs firstObject]; +} + +#pragma mark - RFC 3339 conversions + +static NSDateFormatter *sRFC3339DateFormatter; + +static void setupDateFormatterOnce(void) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sRFC3339DateFormatter = [[NSDateFormatter alloc] init]; + NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + + [sRFC3339DateFormatter setLocale:enUSPOSIXLocale]; + [sRFC3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSSZZZZZ"]; + [sRFC3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + }); +} + +- (nullable NSDate *)dateFromRFC3339String:(NSString *)dateString { + setupDateFormatterOnce(); + NSDate *rfc3339Date = [sRFC3339DateFormatter dateFromString:dateString]; + return rfc3339Date; +} + +- (nullable NSString *)RFC3339StringFromDate:(NSDate *)date { + setupDateFormatterOnce(); + NSString *rfc3339String = [sRFC3339DateFormatter stringFromDate:date]; + return rfc3339String; +} + +@end diff --git a/Firebase/Storage/FIRStorageObservableTask.h b/Firebase/Storage/FIRStorageObservableTask.h new file mode 100644 index 0000000..502aba5 --- /dev/null +++ b/Firebase/Storage/FIRStorageObservableTask.h @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRStorageSwiftNameSupport.h" +#import "FIRStorageTask.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRStorageReference; +@class FIRStorageTaskSnapshot; + +/** + * Extends FIRStorageTask to provide observable semantics such as adding and removing observers. + * Observers produce a FIRStorageHandle, which is used to keep track of and remove specific + * observers at a later date. + * This class is currently not thread safe and can only be called on the main thread. + */ +FIR_SWIFT_NAME(StorageObservableTask) +@interface FIRStorageObservableTask : FIRStorageTask + +/** + * Observes changes in the upload status: Resume, Pause, Progress, Success, and Failure. + * @param status The FIRStorageTaskStatus change to observe. + * @param handler A callback that fires every time the status event occurs, + * returns a FIRStorageTaskSnapshot containing the state of the task. + * @return A task handle that can be used to remove the observer at a later date. + */ +- (FIRStorageHandle)observeStatus:(FIRStorageTaskStatus)status + handler:(void (^)(FIRStorageTaskSnapshot *snapshot))handler; + +/** + * Removes the single observer with the provided handle. + * @param handle The handle of the task to remove. + */ +- (void)removeObserverWithHandle:(FIRStorageHandle)handle; + +/** + * Removes all observers for a single status. + * @param status A FIRStorageTaskStatus to remove listeners for. + */ +- (void)removeAllObserversForStatus:(FIRStorageTaskStatus)status; + +/** + * Removes all observers. + */ +- (void)removeAllObservers; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/FIRStorageObservableTask.m b/Firebase/Storage/FIRStorageObservableTask.m new file mode 100644 index 0000000..bac5924 --- /dev/null +++ b/Firebase/Storage/FIRStorageObservableTask.m @@ -0,0 +1,216 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageObservableTask.h" +#import "FIRStorageObservableTask_Private.h" +#import "FIRStorageTask_Private.h" + +@implementation FIRStorageObservableTask { + @private + // Handlers for pause, resume, progress, success, and failure callbacks + NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_resumeHandlers; + NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_pauseHandlers; + NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_progressHandlers; + NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_successHandlers; + NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *_failureHandlers; + // Reverse map of fetcher handles to status types + NSMutableDictionary<NSString *, NSNumber *> *_handleToStatusMap; +} + +@synthesize state = _state; + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service { + self = [super initWithReference:reference fetcherService:service]; + if (self) { + _pauseHandlers = [[NSMutableDictionary alloc] init]; + _resumeHandlers = [[NSMutableDictionary alloc] init]; + _progressHandlers = [[NSMutableDictionary alloc] init]; + _successHandlers = [[NSMutableDictionary alloc] init]; + _failureHandlers = [[NSMutableDictionary alloc] init]; + _handleToStatusMap = [[NSMutableDictionary alloc] init]; + } + return self; +} + +#pragma mark - Observers + +- (FIRStorageHandle)observeStatus:(FIRStorageTaskStatus)status + handler:(FIRStorageVoidSnapshot)handler { + FIRStorageVoidSnapshot callback = handler; + handler = nil; + + // Note: self.snapshot is synchronized + FIRStorageTaskSnapshot *snapshot = self.snapshot; + // TODO: use an increasing counter instead of a random UUID + NSString *UUIDString = [[NSUUID UUID] UUIDString]; + switch (status) { + case FIRStorageTaskStatusPause: + @synchronized(self) { + [_pauseHandlers setValue:callback forKey:UUIDString]; + } // @synchronized(self) + if (_state == FIRStorageTaskStatePausing || _state == FIRStorageTaskStatePaused) { + [self fireHandlers:_pauseHandlers snapshot:snapshot]; + } + break; + + case FIRStorageTaskStatusResume: + @synchronized(self) { + [_resumeHandlers setValue:callback forKey:UUIDString]; + } // @synchronized(self) + if (_state == FIRStorageTaskStateResuming || _state == FIRStorageTaskStateRunning) { + [self fireHandlers:_resumeHandlers snapshot:snapshot]; + } + break; + + case FIRStorageTaskStatusProgress: + @synchronized(self) { + [_progressHandlers setValue:callback forKey:UUIDString]; + } // @synchronized(self) + if (_state == FIRStorageTaskStateRunning || _state == FIRStorageTaskStateProgress) { + [self fireHandlers:_progressHandlers snapshot:snapshot]; + } + break; + + case FIRStorageTaskStatusSuccess: + @synchronized(self) { + [_successHandlers setValue:callback forKey:UUIDString]; + } // @synchronized(self) + if (_state == FIRStorageTaskStateSuccess) { + [self fireHandlers:_successHandlers snapshot:snapshot]; + } + break; + + case FIRStorageTaskStatusFailure: + @synchronized(self) { + [_failureHandlers setValue:callback forKey:UUIDString]; + } // @synchronized(self) + if (_state == FIRStorageTaskStateFailing || _state == FIRStorageTaskStateFailed) { + [self fireHandlers:_failureHandlers snapshot:snapshot]; + } + break; + + case FIRStorageTaskStatusUnknown: + // Fall through to exception case if an unknown status is passed + + default: + [NSException raise:NSInternalInconsistencyException + format:kFIRStorageInvalidObserverStatus, nil]; + break; + } + + @synchronized(self) { + _handleToStatusMap[UUIDString] = @(status); + } // @synchronized(self) + + return UUIDString; +} + +- (void)removeObserverWithHandle:(FIRStorageHandle)handle { + FIRStorageTaskStatus status = [_handleToStatusMap[handle] intValue]; + NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *observerDictionary = + [self handlerDictionaryForStatus:status]; + + @synchronized(self) { + [observerDictionary removeObjectForKey:handle]; + [_handleToStatusMap removeObjectForKey:handle]; + } // @synchronized(self) +} + +- (void)removeAllObserversForStatus:(FIRStorageTaskStatus)status { + NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *observerDictionary = + [self handlerDictionaryForStatus:status]; + [self removeHandlersFromStatusMapForDictionary:observerDictionary]; + + @synchronized(self) { + [observerDictionary removeAllObjects]; + } // @synchronized(self) +} + +- (void)removeAllObservers { + @synchronized(self) { + [_pauseHandlers removeAllObjects]; + [_resumeHandlers removeAllObjects]; + [_progressHandlers removeAllObjects]; + [_successHandlers removeAllObjects]; + [_failureHandlers removeAllObjects]; + [_handleToStatusMap removeAllObjects]; + } // @synchronized(self) +} + +- (NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *)handlerDictionaryForStatus: + (FIRStorageTaskStatus)status { + switch (status) { + case FIRStorageTaskStatusPause: + return _pauseHandlers; + + case FIRStorageTaskStatusResume: + return _resumeHandlers; + + case FIRStorageTaskStatusProgress: + return _progressHandlers; + + case FIRStorageTaskStatusSuccess: + return _successHandlers; + + case FIRStorageTaskStatusFailure: + return _failureHandlers; + + case FIRStorageTaskStatusUnknown: + return [NSMutableDictionary dictionary]; + + default: + [NSException raise:NSInternalInconsistencyException + format:kFIRStorageInvalidObserverStatus, nil]; + return nil; + } +} + +- (void)removeHandlersFromStatusMapForDictionary: + (NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *)dict { + @synchronized(self) { + [_handleToStatusMap removeObjectsForKeys:dict.allKeys]; + } // @synchronized(self) +} + +- (void)fireHandlersForStatus:(FIRStorageTaskStatus)status + snapshot:(FIRStorageTaskSnapshot *)snapshot { + NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *observerDictionary = + [self handlerDictionaryForStatus:status]; + [self fireHandlers:observerDictionary snapshot:snapshot]; +} + +- (void)fireHandlers:(NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *)handlers + snapshot:(FIRStorageTaskSnapshot *)snapshot { + dispatch_queue_t callbackQueue = self.fetcherService.callbackQueue; + if (!callbackQueue) { + callbackQueue = dispatch_get_main_queue(); + } + + // TODO: iterate over this list in a consistent order + NSMutableDictionary<NSString *, FIRStorageVoidSnapshot> *handlersCopy; + @synchronized(self) { + handlersCopy = [handlers copy]; + } // @synchronized(self) + [handlersCopy enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, + FIRStorageVoidSnapshot _Nonnull handler, + BOOL *_Nonnull stop) { + + dispatch_async(callbackQueue, ^{ + handler(snapshot); + }); + }]; +} + +@end diff --git a/Firebase/Storage/FIRStoragePath.m b/Firebase/Storage/FIRStoragePath.m new file mode 100644 index 0000000..7188ab6 --- /dev/null +++ b/Firebase/Storage/FIRStoragePath.m @@ -0,0 +1,199 @@ +// Copyright 2017 Google +// +// 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 "FIRStoragePath.h" + +#import "FIRStorageConstants_Private.h" + +@implementation FIRStoragePath + +#pragma mark - Class methods + ++ (nullable FIRStoragePath *)pathFromString:(NSString *)string { + if ([string hasPrefix:@"gs://"]) { + // "gs://bucket/path/to/object" + return [FIRStoragePath pathFromGSURI:string]; + } else if ([string hasPrefix:@"http://"] || [string hasPrefix:@"https://"]) { + // "http[s]://firebasestorage.googleapis.com/bucket/path/to/object?signed_url_params" + return [FIRStoragePath pathFromHTTPURL:string]; + } else { + // Invalid scheme, raise an exception! + [NSException raise:NSInternalInconsistencyException + format:@"URL scheme must be one of gs://, http://, or https:// "]; + return nil; + } +} + ++ (nullable FIRStoragePath *)pathFromGSURI:(NSString *)aURIString { + NSString *bucketName; + NSString *objectName; + NSScanner *scanner = [NSScanner scannerWithString:aURIString]; + BOOL isGSURI = [scanner scanString:@"gs://" intoString:NULL]; + BOOL hasBucket = [scanner scanUpToString:@"/" intoString:&bucketName]; + [scanner scanString:@"/" intoString:NULL]; + [scanner scanUpToString:@"\n" intoString:&objectName]; + + if (!isGSURI || !hasBucket) { + [NSException raise:NSInternalInconsistencyException + format:@"URI must be in the form of gs://<bucket>/<path/to/object>"]; + return nil; + } + + return [[self alloc] initWithBucket:bucketName object:objectName]; +} + ++ (nullable FIRStoragePath *)pathFromHTTPURL:(NSString *)aURLString { + NSString *bucketName; + NSString *objectName; + NSURL *httpsURL = [NSURL URLWithString:aURLString]; + NSArray *pathComponents = httpsURL.pathComponents; // [/, v0, b, <bucket>, o, <objects/...>] + + if ([httpsURL.host isEqual:kFIRStorageHost]) { + // Have a bucket name + if ([pathComponents count] > 3) { + bucketName = pathComponents[3]; + } + + // Have an object name + if ([pathComponents count] > 5) { + NSRange objectRange = NSMakeRange(5, [pathComponents count] - 5); + objectName = [[pathComponents subarrayWithRange:objectRange] componentsJoinedByString:@"/"]; + } + } + + if (bucketName.length == 0) { + [NSException raise:NSInternalInconsistencyException + format:@"URL must be in the form of " + @"http[s]://firebasestorage.googleapis.com/v0/b/<bucket>/o/<path/to/" + @"object>[?token=signed_url_params]"]; + return nil; + } + + if (objectName.length == 0) { + objectName = nil; + } + + return [[self alloc] initWithBucket:bucketName object:objectName]; +} + +#pragma mark - Initializers + +- (instancetype)initWithBucket:(NSString *)bucket object:(nullable NSString *)object { + self = [super init]; + if (self) { + _bucket = [bucket copy]; + _object = [self standardizedPathForString:[object copy]]; + } + return self; +} + +#pragma mark - NSObject overrides + +- (instancetype)copyWithZone:(NSZone *)zone { + return [[[self class] allocWithZone:zone] initWithBucket:_bucket object:_object]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[FIRStoragePath class]]) { + return NO; + } + + BOOL isObjectEqual = [self isEqualToFIRStoragePath:(FIRStoragePath *)object]; + return isObjectEqual; +} + +- (BOOL)isEqualToFIRStoragePath:(FIRStoragePath *)path { + BOOL isBucketEqual = _bucket == nil && path->_bucket == nil; + BOOL isObjectEqual = _object == nil && path->_object == nil; + + if (_bucket && path->_bucket) { + isBucketEqual = [_bucket isEqual:path->_bucket]; + } + + if (_object && path.object) { + isObjectEqual = [_object isEqual:path->_object]; + } + + BOOL isEqual = isBucketEqual && isObjectEqual; + return isEqual; +} + +- (NSUInteger)hash { + // "...because in those days, you could XOR anything with anything and get something useful..." + // https://www.usenix.org/system/files/1309_14-17_mickens.pdf + NSUInteger hash = [_bucket hash] ^ [_object hash]; + return hash; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %p: %@", [self class], self, [self stringValue]]; +} + +- (NSString *)stringValue { + return [NSString stringWithFormat:@"gs://%@/%@", _bucket, _object ?: @""]; +} + +#pragma mark - Public methods + +- (FIRStoragePath *)child:(NSString *)path { + if (path.length == 0) { + return [self copy]; // Return a copy of the same path, nothing happened + } + + NSString *childObject; + if (_object == nil) { + childObject = path; + } else { + childObject = [_object stringByAppendingPathComponent:path]; + } + + FIRStoragePath *childPath = [[FIRStoragePath alloc] initWithBucket:_bucket object:childObject]; + return childPath; +} + +- (nullable FIRStoragePath *)parent { + if (_object.length == 0) { + return nil; + } + + NSString *parentObject = [_object stringByDeletingLastPathComponent]; + FIRStoragePath *parentPath = [[FIRStoragePath alloc] initWithBucket:_bucket object:parentObject]; + return parentPath; +} + +- (FIRStoragePath *)root { + FIRStoragePath *rootPath = [[FIRStoragePath alloc] initWithBucket:_bucket object:nil]; + return rootPath; +} + +#pragma mark - Private methods + +// Removes leading and trailing slashes, and compresses multiple slashes +// to create a canonical representation. +// Example: /foo//bar///baz//// -> foo/bar/baz +- (NSString *)standardizedPathForString:(NSString *)string { + NSMutableArray *components = [[string componentsSeparatedByString:@"/"] mutableCopy]; + NSIndexSet *removedPaths = + [components indexesOfObjectsPassingTest:^BOOL(NSString *string, NSUInteger idx, BOOL *stop) { + return (string.length == 0); + }]; + [components removeObjectsAtIndexes:removedPaths]; + return [components componentsJoinedByString:@"/"]; +} + +@end diff --git a/Firebase/Storage/FIRStorageReference.h b/Firebase/Storage/FIRStorageReference.h new file mode 100644 index 0000000..0b85267 --- /dev/null +++ b/Firebase/Storage/FIRStorageReference.h @@ -0,0 +1,244 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +#import "FIRStorage.h" +#import "FIRStorageConstants.h" +#import "FIRStorageDownloadTask.h" +#import "FIRStorageMetadata.h" +#import "FIRStorageSwiftNameSupport.h" +#import "FIRStorageTask.h" +#import "FIRStorageUploadTask.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * FIRStorageReference represents a reference to a Google Cloud Storage object. Developers can + * upload and download objects, as well as get/set object metadata, and delete an object at the + * path. + * @see https://cloud.google.com/storage/ + */ +FIR_SWIFT_NAME(StorageReference) +@interface FIRStorageReference : NSObject + +/** + * The FIRStorage service object which created this reference. + */ +@property(nonatomic, readonly) FIRStorage *storage; + +/** + * The name of the Google Cloud Storage bucket associated with this reference, + * in gs://bucket/path/to/object.txt, the bucket would be: 'bucket' + */ +@property(nonatomic, readonly) NSString *bucket; + +/** + * The full path to this object, not including the Google Cloud Storage bucket. + * In gs://bucket/path/to/object.txt, the full path would be: 'path/to/object.txt' + */ +@property(nonatomic, readonly) NSString *fullPath; + +/** + * The short name of the object associated with this reference, + * in gs://bucket/path/to/object.txt, the name of the object would be: 'object.txt' + */ +@property(nonatomic, readonly) NSString *name; + +#pragma mark - Path Operations + +/** + * Creates a new FIRStorageReference pointing to the root object. + * @return A new FIRStorageReference pointing to the root object. + */ +- (FIRStorageReference *)root; + +/** + * Creates a new FIRStorageReference pointing to the parent of the current reference + * or nil if this instance references the root location. + * For example: + * path = foo/bar/baz parent = foo/bar + * path = foo parent = (root) + * path = (root) parent = nil + * @return A new FIRStorageReference pointing to the parent of the current reference. + */ +- (nullable FIRStorageReference *)parent; + +/** + * Creates a new FIRStorageReference pointing to a child object of the current reference. + * path = foo child = bar newPath = foo/bar + * path = foo/bar child = baz newPath = foo/bar/baz + * All leading and trailing slashes will be removed, and consecutive slashes will be + * compressed to single slashes. For example: + * child = /foo/bar newPath = foo/bar + * child = foo/bar/ newPath = foo/bar + * child = foo///bar newPath = foo/bar + * @param path Path to append to the current path. + * @return A new FIRStorageReference pointing to a child location of the current reference. + */ +- (FIRStorageReference *)child:(NSString *)path; + +#pragma mark - Uploads + +/** + * Asynchronously uploads data to the currently specified FIRStorageReference, + * without additional metadata. + * This is not recommended for large files, and one should instead upload a file from disk. + * @param uploadData The NSData to upload. + * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload. + */ +- (FIRStorageUploadTask *)putData:(NSData *)uploadData FIR_SWIFT_NAME(putData(_:)); + +/** + * Asynchronously uploads data to the currently specified FIRStorageReference. + * This is not recommended for large files, and one should instead upload a file from disk. + * @param uploadData The NSData to upload. + * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.) + * about the object being uploaded. + * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload. + */ +- (FIRStorageUploadTask *)putData:(NSData *)uploadData + metadata:(nullable FIRStorageMetadata *)metadata + FIR_SWIFT_NAME(putData(_:metadata:)); + +/** + * Asynchronously uploads data to the currently specified FIRStorageReference. + * This is not recommended for large files, and one should instead upload a file from disk. + * @param uploadData The NSData to upload. + * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.) + * about the object being uploaded. + * @param completion A completion block that either returns the object metadata on success, + * or an error on failure. + * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload. + */ +- (FIRStorageUploadTask *)putData:(NSData *)uploadData + metadata:(nullable FIRStorageMetadata *)metadata + completion:(nullable void (^)(FIRStorageMetadata *_Nullable metadata, + NSError *_Nullable error))completion + FIR_SWIFT_NAME(putData(_:metadata:completion:)); + +/** + * Asynchronously uploads a file to the currently specified FIRStorageReference, + * without additional metadata. + * @param fileURL A URL representing the system file path of the object to be uploaded. + * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload. + */ +- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL FIR_SWIFT_NAME(putFile(from:)); + +/** + * Asynchronously uploads a file to the currently specified FIRStorageReference. + * @param fileURL A URL representing the system file path of the object to be uploaded. + * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.) + * about the object being uploaded. + * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload. + */ +- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL metadata:(nullable FIRStorageMetadata *)metadata + FIR_SWIFT_NAME(putFile(from:metadata:)); + +/** + * Asynchronously uploads a file to the currently specified FIRStorageReference. + * @param fileURL A URL representing the system file path of the object to be uploaded. + * @param metadata FIRStorageMetadata containing additional information (MIME type, etc.) + * about the object being uploaded. + * @param completion A completion block that either returns the object metadata on success, + * or an error on failure. + * @return An instance of FIRStorageUploadTask, which can be used to monitor or manage the upload. + */ +- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL + metadata:(nullable FIRStorageMetadata *)metadata + completion:(nullable void (^)(FIRStorageMetadata *_Nullable metadata, + NSError *_Nullable error))completion + FIR_SWIFT_NAME(putFile(from:metadata:completion:)); + +#pragma mark - Downloads + +/** + * Asynchronously downloads the object at the FIRStorageReference to an NSData object in memory. + * An NSData of the provided max size will be allocated, so ensure that the device has enough free + * memory to complete the download. For downloading large files, writeToFile may be a better option. + * @param size The maximum size in bytes to download. If the download exceeds this size + * the task will be cancelled and an error will be returned. + * @param completion A completion block that either returns the object data on success, + * or an error on failure. + * @return An FIRStorageDownloadTask that can be used to monitor or manage the download. + */ +- (FIRStorageDownloadTask *)dataWithMaxSize:(int64_t)size + completion:(void (^)(NSData *_Nullable data, + NSError *_Nullable error))completion + FIR_SWIFT_NAME(getData(maxSize:completion:)); + +/** + * Asynchronously retrieves a long lived download URL with a revokable token. + * This can be used to share the file with others, but can be revoked by a developer + * in the Firebase Console if desired. + * @param completion A completion block that either returns the URL on success, + * or an error on failure. + */ +- (void)downloadURLWithCompletion:(void (^)(NSURL *_Nullable URL, + NSError *_Nullable error))completion; + +/** + * Asynchronously downloads the object at the current path to a specified system filepath. + * @param fileURL A file system URL representing the path the object should be downloaded to. + * @return An FIRStorageDownloadTask that can be used to monitor or manage the download. + */ +- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL; + +/** + * Asynchronously downloads the object at the current path to a specified system filepath. + * @param fileURL A file system URL representing the path the object should be downloaded to. + * @param completion A completion block that fires when the file download completes. + * Returns an NSURL pointing to the file path of the downloaded file on success, + * or an error on failure. + * @return An FIRStorageDownloadTask that can be used to monitor or manage the download. + */ +- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL + completion:(nullable void (^)(NSURL *_Nullable URL, + NSError *_Nullable error))completion; + +#pragma mark - Metadata Operations + +/** + * Retrieves metadata associated with an object at the current path. + * @param completion A completion block which returns the object metadata on success, + * or an error on failure. + */ +- (void)metadataWithCompletion:(void (^)(FIRStorageMetadata *_Nullable metadata, + NSError *_Nullable error))completion + FIR_SWIFT_NAME(getMetadata(completion:)); + +/** + * Updates the metadata associated with an object at the current path. + * @param metadata An FIRStorageMetadata object with the metadata to update. + * @param completion A completion block which returns the FIRStorageMetadata on success, + * or an error on failure. + */ +- (void)updateMetadata:(FIRStorageMetadata *)metadata + completion:(nullable void (^)(FIRStorageMetadata *_Nullable metadata, + NSError *_Nullable error))completion + FIR_SWIFT_NAME(updateMetadata(_:completion:)); + +#pragma mark - Delete + +/** + * Deletes the object at the current path. + * @param completion A completion block which returns nil on success, or an error on failure. + */ +- (void)deleteWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/FIRStorageReference.m b/Firebase/Storage/FIRStorageReference.m new file mode 100644 index 0000000..6e8105c --- /dev/null +++ b/Firebase/Storage/FIRStorageReference.m @@ -0,0 +1,364 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageReference.h" + +#import "FIRStorageConstants_Private.h" +#import "FIRStorageDeleteTask.h" +#import "FIRStorageDownloadTask_Private.h" +#import "FIRStorageGetMetadataTask.h" +#import "FIRStorageMetadata_Private.h" +#import "FIRStorageReference_Private.h" +#import "FIRStorageTaskSnapshot.h" +#import "FIRStorageTaskSnapshot_Private.h" +#import "FIRStorageTask_Private.h" +#import "FIRStorageUpdateMetadataTask.h" +#import "FIRStorageUploadTask_Private.h" +#import "FIRStorageUtils.h" +#import "FIRStorage_Private.h" + +#import "FIRApp.h" +#import "FIROptions.h" + +#import <GTMSessionFetcher/GTMSessionFetcher.h> +#import "GTMSessionFetcherService.h" + +@implementation FIRStorageReference + +- (instancetype)init { + FIRStorage *storage = [FIRStorage storage]; + NSString *storageBucket = storage.app.options.storageBucket; + FIRStoragePath *path = [[FIRStoragePath alloc] initWithBucket:storageBucket object:nil]; + FIRStorageReference *reference = [self initWithStorage:storage path:path]; + return reference; +} + +- (instancetype)initWithStorage:(FIRStorage *)storage path:(FIRStoragePath *)path { + self = [super init]; + if (self) { + _storage = storage; + _path = path; + } + return self; +} + +#pragma mark - NSObject overrides + +- (instancetype)copyWithZone:(NSZone *)zone { + FIRStorageReference *copiedReference = + [[[self class] allocWithZone:zone] initWithStorage:_storage path:_path]; + return copiedReference; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[FIRStorageReference class]]) { + return NO; + } + + BOOL isObjectEqual = [self isEqualToFIRStorageReference:(FIRStorageReference *)object]; + return isObjectEqual; +} + +- (BOOL)isEqualToFIRStorageReference:(FIRStorageReference *)reference { + BOOL isEqual = [_storage isEqual:reference.storage] && [_path isEqual:reference.path]; + return isEqual; +} + +- (NSUInteger)hash { + NSUInteger hash = [_storage hash] ^ [_path hash]; + return hash; +} + +- (NSString *)description { + return [self stringValue]; +} + +- (NSString *)stringValue { + NSString *value = [NSString stringWithFormat:@"gs://%@/%@", _path.bucket, _path.object ?: @""]; + return value; +} + +#pragma mark - Property Getters + +- (NSString *)bucket { + NSString *bucket = _path.bucket; + return bucket; +} + +- (NSString *)fullPath { + NSString *path = _path.object; + if (!path) { + path = @""; + } + return path; +} + +- (NSString *)name { + NSString *name = [_path.object lastPathComponent]; + if (!name) { + name = @""; + } + return name; +} + +#pragma mark - Path Operations + +- (FIRStorageReference *)root { + FIRStoragePath *rootPath = [_path root]; + FIRStorageReference *rootReference = + [[FIRStorageReference alloc] initWithStorage:_storage path:rootPath]; + return rootReference; +} + +- (nullable FIRStorageReference *)parent { + FIRStoragePath *parentPath = [_path parent]; + if (!parentPath) { + return nil; + } + + FIRStorageReference *parentReference = + [[FIRStorageReference alloc] initWithStorage:_storage path:parentPath]; + return parentReference; +} + +- (FIRStorageReference *)child:(NSString *)path { + FIRStoragePath *childPath = [_path child:path]; + FIRStorageReference *childReference = + [[FIRStorageReference alloc] initWithStorage:_storage path:childPath]; + return childReference; +} + +#pragma mark - Uploads + +- (FIRStorageUploadTask *)putData:(NSData *)uploadData { + return [self putData:uploadData metadata:nil completion:nil]; +} + +- (FIRStorageUploadTask *)putData:(NSData *)uploadData + metadata:(nullable FIRStorageMetadata *)metadata { + return [self putData:uploadData metadata:metadata completion:nil]; +} + +- (FIRStorageUploadTask *)putData:(NSData *)uploadData + metadata:(nullable FIRStorageMetadata *)metadata + completion:(nullable FIRStorageVoidMetadataError)completion { + if (!metadata) { + metadata = [[FIRStorageMetadata alloc] init]; + } + + metadata.path = _path.object; + metadata.name = [_path.object lastPathComponent]; + FIRStorageUploadTask *task = + [[FIRStorageUploadTask alloc] initWithReference:self + fetcherService:_storage.fetcherServiceForApp + data:uploadData + metadata:metadata]; + + if (completion) { + dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue; + if (!callbackQueue) { + callbackQueue = dispatch_get_main_queue(); + } + + [task observeStatus:FIRStorageTaskStatusSuccess + handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) { + dispatch_async(callbackQueue, ^{ + completion(snapshot.metadata, nil); + }); + }]; + [task observeStatus:FIRStorageTaskStatusFailure + handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) { + dispatch_async(callbackQueue, ^{ + completion(nil, snapshot.error); + }); + }]; + } + [task enqueue]; + return task; +} + +- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL { + return [self putFile:fileURL metadata:nil completion:nil]; +} + +- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL + metadata:(nullable FIRStorageMetadata *)metadata { + return [self putFile:fileURL metadata:metadata completion:nil]; +} + +- (FIRStorageUploadTask *)putFile:(NSURL *)fileURL + metadata:(nullable FIRStorageMetadata *)metadata + completion:(nullable FIRStorageVoidMetadataError)completion { + if (!metadata) { + metadata = [[FIRStorageMetadata alloc] init]; + } + + metadata.path = _path.object; + metadata.name = [_path.object lastPathComponent]; + FIRStorageUploadTask *task = + [[FIRStorageUploadTask alloc] initWithReference:self + fetcherService:_storage.fetcherServiceForApp + file:fileURL + metadata:metadata]; + + if (completion) { + dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue; + if (!callbackQueue) { + callbackQueue = dispatch_get_main_queue(); + } + + [task observeStatus:FIRStorageTaskStatusSuccess + handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) { + dispatch_async(callbackQueue, ^{ + completion(snapshot.metadata, nil); + }); + }]; + [task observeStatus:FIRStorageTaskStatusFailure + handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) { + dispatch_async(callbackQueue, ^{ + completion(nil, snapshot.error); + }); + }]; + } + [task enqueue]; + return task; +} + +#pragma mark - Downloads + +- (FIRStorageDownloadTask *)dataWithMaxSize:(int64_t)size + completion:(FIRStorageVoidDataError)completion { + FIRStorageDownloadTask *task = + [[FIRStorageDownloadTask alloc] initWithReference:self + fetcherService:_storage.fetcherServiceForApp + file:nil]; + + dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue; + if (!callbackQueue) { + callbackQueue = dispatch_get_main_queue(); + } + + [task observeStatus:FIRStorageTaskStatusSuccess + handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) { + FIRStorageDownloadTask *task = snapshot.task; + dispatch_async(callbackQueue, ^{ + completion(task.downloadData, nil); + }); + }]; + [task observeStatus:FIRStorageTaskStatusFailure + handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) { + dispatch_async(callbackQueue, ^{ + completion(nil, snapshot.error); + }); + }]; + [task observeStatus:FIRStorageTaskStatusProgress + handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) { + FIRStorageDownloadTask *task = snapshot.task; + if (task.progress.totalUnitCount > size || + task.progress.completedUnitCount > size) { + NSDictionary *infoDictionary = @{ + @"totalSize" : @(task.progress.totalUnitCount), + @"maxAllowedSize" : @(size) + }; + NSError *error = + [FIRStorageErrors errorWithCode:FIRStorageErrorCodeDownloadSizeExceeded + infoDictionary:infoDictionary]; + [task cancelWithError:error]; + } + }]; + [task enqueue]; + return task; +} + +- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL { + return [self writeToFile:fileURL completion:nil]; +} + +- (FIRStorageDownloadTask *)writeToFile:(NSURL *)fileURL + completion:(FIRStorageVoidURLError)completion { + FIRStorageDownloadTask *task = + [[FIRStorageDownloadTask alloc] initWithReference:self + fetcherService:_storage.fetcherServiceForApp + file:fileURL]; + if (completion) { + dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue; + if (!callbackQueue) { + callbackQueue = dispatch_get_main_queue(); + } + + [task observeStatus:FIRStorageTaskStatusSuccess + handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) { + dispatch_async(callbackQueue, ^{ + completion(fileURL, nil); + }); + }]; + [task observeStatus:FIRStorageTaskStatusFailure + handler:^(FIRStorageTaskSnapshot *_Nonnull snapshot) { + dispatch_async(callbackQueue, ^{ + completion(nil, snapshot.error); + }); + }]; + } + [task enqueue]; + return task; +} + +- (void)downloadURLWithCompletion:(FIRStorageVoidURLError)completion { + dispatch_queue_t callbackQueue = _storage.fetcherServiceForApp.callbackQueue; + if (!callbackQueue) { + callbackQueue = dispatch_get_main_queue(); + } + + return [self metadataWithCompletion:^(FIRStorageMetadata *metadata, NSError *error) { + dispatch_async(callbackQueue, ^{ + completion(metadata.downloadURL, error); + }); + }]; +} + +#pragma mark - Metadata Operations + +- (void)metadataWithCompletion:(FIRStorageVoidMetadataError)completion { + FIRStorageGetMetadataTask *task = + [[FIRStorageGetMetadataTask alloc] initWithReference:self + fetcherService:_storage.fetcherServiceForApp + completion:completion]; + [task enqueue]; +} + +- (void)updateMetadata:(FIRStorageMetadata *)metadata + completion:(nullable FIRStorageVoidMetadataError)completion { + FIRStorageUpdateMetadataTask *task = + [[FIRStorageUpdateMetadataTask alloc] initWithReference:self + fetcherService:_storage.fetcherServiceForApp + metadata:metadata + completion:completion]; + [task enqueue]; +} + +#pragma mark - Delete + +- (void)deleteWithCompletion:(nullable FIRStorageVoidError)completion { + FIRStorageDeleteTask *task = + [[FIRStorageDeleteTask alloc] initWithReference:self + fetcherService:_storage.fetcherServiceForApp + completion:completion]; + [task enqueue]; +} + +@end diff --git a/Firebase/Storage/FIRStorageSwiftNameSupport.h b/Firebase/Storage/FIRStorageSwiftNameSupport.h new file mode 100644 index 0000000..529adf4 --- /dev/null +++ b/Firebase/Storage/FIRStorageSwiftNameSupport.h @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#ifndef FIR_SWIFT_NAME + +#import <Foundation/Foundation.h> + +// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK. +// // Wrap it in our own macro if it's a non-compatible SDK. +#ifdef __IPHONE_9_3 +#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X) +#else +#define FIR_SWIFT_NAME(X) // Intentionally blank. +#endif // #ifdef __IPHONE_9_3 + +#endif // FIR_SWIFT_NAME
\ No newline at end of file diff --git a/Firebase/Storage/FIRStorageTask.h b/Firebase/Storage/FIRStorageTask.h new file mode 100644 index 0000000..0428220 --- /dev/null +++ b/Firebase/Storage/FIRStorageTask.h @@ -0,0 +1,76 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +#import "FIRStorageConstants.h" +#import "FIRStorageMetadata.h" +#import "FIRStorageSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * A superclass to all FIRStorage*Tasks, including FIRStorageUploadTask + * and FIRStorageDownloadTask, to provide state transitions, event raising, and common storage + * or metadata and errors. + * Callbacks are always fired on the developer specified callback queue. + * If no queue is specified by the developer, it defaults to the main queue. + * Currently not thread safe, so only call methods on the main thread. + */ +FIR_SWIFT_NAME(StorageTask) +@interface FIRStorageTask : NSObject + +/** + * An immutable view of the task and associated metadata, progress, error, etc. + */ +@property(strong, readonly, nonatomic, nonnull) FIRStorageTaskSnapshot *snapshot; + +@end + +/** + * Defines task operations such as pause, resume, cancel, and enqueue for all tasks. + * All tasks are required to implement enqueue, which begins the task, and may optionally + * implement pause, resume, and cancel, which operate on the task to pause, resume, and cancel + * operations. + */ +FIR_SWIFT_NAME(StorageTaskManagement) +@protocol FIRStorageTaskManagement<NSObject> + +@required +/** + * Prepares a task and begins execution. + */ +- (void)enqueue; + +@optional +/** + * Pauses a task currently in progress. + */ +- (void)pause; + +/** + * Cancels a task currently in progress. + */ +- (void)cancel; + +/** + * Resumes a task that is paused. + */ +- (void)resume; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/FIRStorageTask.m b/Firebase/Storage/FIRStorageTask.m new file mode 100644 index 0000000..3c1cf6f --- /dev/null +++ b/Firebase/Storage/FIRStorageTask.m @@ -0,0 +1,65 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageTask.h" + +#import "FIRStorage.h" +#import "FIRStorage_Private.h" +#import "FIRStorageReference.h" +#import "FIRStorageReference_Private.h" +#import "FIRStorageTaskSnapshot.h" +#import "FIRStorageTaskSnapshot_Private.h" +#import "FIRStorageTask_Private.h" + +#import "GTMSessionFetcherService.h" + +@implementation FIRStorageTask + +- (instancetype)init { + FIRStorage *storage = [FIRStorage storage]; + FIRStorageReference *reference = [storage reference]; + FIRStorageTask *task = + [self initWithReference:reference fetcherService:storage.fetcherServiceForApp]; + return task; +} + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service { + self = [super init]; + if (self) { + _reference = reference; + _baseRequest = [FIRStorageUtils defaultRequestForPath:reference.path]; + _fetcherService = service; + _fetcherService.maxRetryInterval = _reference.storage.maxOperationRetryTime; + } + return self; +} + +- (FIRStorageTaskSnapshot *)snapshot { + @synchronized(self) { + NSProgress *progress = + [NSProgress progressWithTotalUnitCount:self.progress.totalUnitCount]; + progress.completedUnitCount = self.progress.completedUnitCount; + FIRStorageTaskSnapshot *snapshot = + [[FIRStorageTaskSnapshot alloc] initWithTask:self + state:self.state + metadata:self.metadata + reference:self.reference + progress:progress + error:[self.error copy]]; + return snapshot; + } +} + +@end diff --git a/Firebase/Storage/FIRStorageTaskSnapshot.h b/Firebase/Storage/FIRStorageTaskSnapshot.h new file mode 100644 index 0000000..b654c09 --- /dev/null +++ b/Firebase/Storage/FIRStorageTaskSnapshot.h @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +#import "FIRStorageConstants.h" +#import "FIRStorageSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRStorageMetadata; +@class FIRStorageReference; +@class FIRStorageTask; + +/** + * FIRStorageTaskSnapshot represents an immutable view of a task. + * A Snapshot contains a task, storage reference, metadata (if it exists), + * progress, and an error (if one occurred). + */ +FIR_SWIFT_NAME(StorageTaskSnapshot) +@interface FIRStorageTaskSnapshot : NSObject + +/** + * Subclass of FIRStorageTask this snapshot represents. + */ +@property(readonly, copy, nonatomic) __kindof FIRStorageTask *task; + +/** + * Metadata returned by the task, or nil if no metadata returned. + */ +@property(readonly, copy, nonatomic, nullable) FIRStorageMetadata *metadata; + +/** + * FIRStorageReference this task is operates on. + */ +@property(readonly, copy, nonatomic) FIRStorageReference *reference; + +/** + * NSProgress object which tracks the progess of an upload or download. + */ +@property(readonly, strong, nonatomic, nullable) NSProgress *progress; + +/** + * Error during task execution, or nil if no error occurred. + */ +@property(readonly, copy, nonatomic, nullable) NSError *error; + +/** + * Status of the task. + */ +@property(readonly, nonatomic) FIRStorageTaskStatus status; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/FIRStorageTaskSnapshot.m b/Firebase/Storage/FIRStorageTaskSnapshot.m new file mode 100644 index 0000000..050d05c --- /dev/null +++ b/Firebase/Storage/FIRStorageTaskSnapshot.m @@ -0,0 +1,88 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageTaskSnapshot.h" +#import "FIRStorageTaskSnapshot_Private.h" + +#import "FIRStorageTask_Private.h" + +@implementation FIRStorageTaskSnapshot + +- (instancetype)initWithTask:(__kindof FIRStorageTask *)task + state:(FIRStorageTaskState)state + metadata:(nullable FIRStorageMetadata *)metadata + reference:(FIRStorageReference *)reference + progress:(nullable NSProgress *)progress + error:(nullable NSError *)error { + self = [super init]; + if (self) { + _task = task; + _metadata = metadata; + _reference = reference; + _progress = progress; + _error = error; + + switch (state) { + case FIRStorageTaskStateQueueing: + case FIRStorageTaskStateRunning: + case FIRStorageTaskStateResuming: + _status = FIRStorageTaskStatusResume; + break; + + case FIRStorageTaskStateProgress: + _status = FIRStorageTaskStatusProgress; + break; + + case FIRStorageTaskStatePaused: + case FIRStorageTaskStatePausing: + _status = FIRStorageTaskStatusPause; + break; + + case FIRStorageTaskStateSuccess: + case FIRStorageTaskStateCompleting: + _status = FIRStorageTaskStatusSuccess; + break; + + case FIRStorageTaskStateCancelled: + case FIRStorageTaskStateFailing: + case FIRStorageTaskStateFailed: + _status = FIRStorageTaskStatusFailure; + break; + + default: + _status = FIRStorageTaskStatusUnknown; + } + } + return self; +} + + +-(NSString *)description { + switch (_status) { + case FIRStorageTaskStatusResume: + return @"<State: Resume>"; + case FIRStorageTaskStatusProgress: + return [NSString stringWithFormat:@"<State: Progress, Progress: %@>", _progress]; + case FIRStorageTaskStatusPause: + return @"<State: Paused>"; + case FIRStorageTaskStatusSuccess: + return @"<State: Success>"; + case FIRStorageTaskStatusFailure: + return [NSString stringWithFormat:@"<State: Failed, Error: %@>", _error]; + default: + return @"<State: Unknown>"; + }; +} + +@end diff --git a/Firebase/Storage/FIRStorageTokenAuthorizer.m b/Firebase/Storage/FIRStorageTokenAuthorizer.m new file mode 100644 index 0000000..36b94a9 --- /dev/null +++ b/Firebase/Storage/FIRStorageTokenAuthorizer.m @@ -0,0 +1,131 @@ +// Copyright 2017 Google +// +// 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 "../../Firebase/Core/Private/FIRAppInternal.h" + +#import "FIRStorageTokenAuthorizer.h" + +#import "FIRStorageConstants.h" +#import "FIRStorageConstants_Private.h" +#import "FIRStorageErrors.h" + +#import "FirebaseStorage.h" + +#import "FIRApp.h" +#import "FIROptions.h" + +@implementation FIRStorageTokenAuthorizer { + @private + // Firebase App which vends tokens + FIRApp *_app; +} + +@synthesize fetcherService = _fetcherService; + +- (instancetype)initWithApp:(FIRApp *)app fetcherService:(GTMSessionFetcherService *)service { + self = [super init]; + if (self) { + _app = app; + _fetcherService = service; + } + return self; +} + +#pragma mark - GTMFetcherAuthorizationProtocol methods + +- (void)authorizeRequest:(NSMutableURLRequest *)request + delegate:(id)delegate + didFinishSelector:(SEL)sel { + // Set version header on each request + NSString *versionString = [NSString stringWithFormat:@"ios/%s", FIRStorageVersionString]; + [request setValue:versionString forHTTPHeaderField:@"x-firebase-storage-version"]; + + // Set GMP ID on each request + NSString *GMPAppId = _app.options.googleAppID; + [request setValue:GMPAppId forHTTPHeaderField:@"x-firebase-gmpid"]; + + if (delegate && sel) { + id selfParam = self; + NSMethodSignature *sig = [delegate methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; + [invocation setSelector:sel]; + [invocation setTarget:delegate]; + [invocation setArgument:&selfParam atIndex:2]; + [invocation setArgument:&request atIndex:3]; + + dispatch_queue_t callbackQueue = self.fetcherService.callbackQueue; + if (!callbackQueue) { + callbackQueue = dispatch_get_main_queue(); + } + + [invocation retainArguments]; + if (_app.getTokenImplementation) { + [_app getTokenForcingRefresh:NO + withCallback:^(NSString *_Nullable token, NSError *_Nullable error) { + if (error) { + NSMutableDictionary *errorDictionary = + [NSMutableDictionary dictionaryWithDictionary:error.userInfo]; + errorDictionary[kFIRStorageResponseErrorDomain] = error.domain; + errorDictionary[kFIRStorageResponseErrorCode] = @(error.code); + + NSError *tokenError = + [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnauthenticated + infoDictionary:errorDictionary]; + [invocation setArgument:&tokenError atIndex:4]; + } else if (token) { + NSString *firebaseToken = + [NSString stringWithFormat:kFIRStorageAuthTokenFormat, token]; + [request setValue:firebaseToken forHTTPHeaderField:@"Authorization"]; + } + dispatch_async(callbackQueue, ^{ + [invocation invoke]; + }); + }]; + } else { + dispatch_async(callbackQueue, ^{ + [invocation invoke]; + }); + } + } +} + +// Note that stopAuthorization, isAuthorizingRequest, and userEmail +// aren't relevant with the Firebase App/Auth implementation of tokens, +// and thus aren't implemented. Token refresh is handled transparently +// for us, and we don't allow the auth request to be stopped. +// Auth is also not required so the world doesn't stop. +- (void)stopAuthorization { + // Noop +} + +- (void)stopAuthorizationForRequest:(NSURLRequest *)request { + // Noop +} + +- (BOOL)isAuthorizingRequest:(NSURLRequest *)request { + return NO; +} + +- (BOOL)isAuthorizedRequest:(NSURLRequest *)request { + NSString *authHeader = request.allHTTPHeaderFields[@"Authorization"]; + BOOL isFirebaseToken = [authHeader hasPrefix:@"Firebase"]; + return isFirebaseToken; +} + +- (NSString *)userEmail { + // Noop + return nil; +} + +@end diff --git a/Firebase/Storage/FIRStorageUpdateMetadataTask.m b/Firebase/Storage/FIRStorageUpdateMetadataTask.m new file mode 100644 index 0000000..dbd276b --- /dev/null +++ b/Firebase/Storage/FIRStorageUpdateMetadataTask.m @@ -0,0 +1,91 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageUpdateMetadataTask.h" + +#import "FIRStorageMetadata_Private.h" +#import "FIRStorageTask_Private.h" + +@implementation FIRStorageUpdateMetadataTask { + @private + FIRStorageVoidMetadataError _completion; + // Metadata used in the update request + FIRStorageMetadata *_updateMetadata; +} + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + metadata:(FIRStorageMetadata *)metadata + completion:(FIRStorageVoidMetadataError)completion { + self = [super initWithReference:reference fetcherService:service]; + if (self) { + _updateMetadata = [metadata copy]; + _completion = [completion copy]; + } + return self; +} + +- (void)enqueue { + NSMutableURLRequest *request = [self.baseRequest mutableCopy]; + NSDictionary *updateDictionary = [_updateMetadata dictionaryRepresentation]; + NSData *updateData = [NSData frs_dataFromJSONDictionary:updateDictionary]; + request.HTTPMethod = @"PATCH"; + request.timeoutInterval = self.reference.storage.maxUploadRetryTime; + request.HTTPBody = updateData; + NSString *typeString = @"application/json; charset=UTF-8"; + [request setValue:typeString forHTTPHeaderField:@"Content-Type"]; + NSString *lengthString = [NSString stringWithFormat:@"%zu", (unsigned long)[updateData length]]; + [request setValue:lengthString forHTTPHeaderField:@"Content-Length"]; + + FIRStorageVoidMetadataError callback = _completion; + _completion = nil; + + GTMSessionFetcher *fetcher = [self.fetcherService fetcherWithRequest:request]; + fetcher.comment = @"UpdateMetadataTask"; + [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + if (error) { + if (!self.error) { + self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference]; + } + if (callback) { + callback(nil, self.error); + } + return; + } + + NSDictionary *responseDictionary = [NSDictionary frs_dictionaryFromJSONData:data]; + if (responseDictionary) { + FIRStorageMetadata *metadata = + [[FIRStorageMetadata alloc] initWithDictionary:responseDictionary]; + [metadata setType:FIRStorageMetadataTypeFile]; + if (callback){ + callback(metadata, nil); + } + } else { + NSString *returnedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSString *invalidDataString = + [NSString stringWithFormat:kFIRStorageInvalidDataFormat, returnedData]; + NSDictionary *dict; + if (invalidDataString.length > 0) { + dict = @{NSLocalizedFailureReasonErrorKey : invalidDataString}; + } + self.error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnknown infoDictionary:dict]; + if (callback) { + callback(nil, self.error); + } + } + }]; +} + +@end diff --git a/Firebase/Storage/FIRStorageUploadTask.h b/Firebase/Storage/FIRStorageUploadTask.h new file mode 100644 index 0000000..cdf1d29 --- /dev/null +++ b/Firebase/Storage/FIRStorageUploadTask.h @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +#import "FIRStorageObservableTask.h" +#import "FIRStorageSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * FIRStorageUploadTask implements resumable uploads to a file in Firebase Storage. + * Uploads can be returned on completion with a completion callback, and can be monitored + * by attaching observers, or controlled by calling FIRStorageTask#pause, FIRStorageTask#resume, + * or FIRStorageTask#cancel. + * Uploads can take NSData in memory, or an NSURL to a file on disk. + * Uploads are performed on a background queue, and callbacks are raised on the developer + * specified callbackQueue in FIRStorage, or the main queue if left unspecified. + * Currently all uploads must be initiated and managed on the main queue. + */ +FIR_SWIFT_NAME(StorageUploadTask) +@interface FIRStorageUploadTask : FIRStorageObservableTask<FIRStorageTaskManagement> + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/FIRStorageUploadTask.m b/Firebase/Storage/FIRStorageUploadTask.m new file mode 100644 index 0000000..74741b0 --- /dev/null +++ b/Firebase/Storage/FIRStorageUploadTask.m @@ -0,0 +1,199 @@ +// Copyright 2017 Google +// +// 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 "FIRStorageUploadTask.h" + +#import "FIRStorageConstants_Private.h" +#import "FIRStorageMetadata_Private.h" +#import "FIRStorageObservableTask_Private.h" +#import "FIRStorageTask_Private.h" +#import "FIRStorageUploadTask_Private.h" + +#import "GTMSessionUploadFetcher.h" + +@implementation FIRStorageUploadTask + +@synthesize progress = _progress; + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + data:(NSData *)uploadData + metadata:(FIRStorageMetadata *)metadata { + self = [super initWithReference:reference fetcherService:service]; + if (self) { + _uploadMetadata = [metadata copy]; + _uploadData = [uploadData copy]; + _progress = [NSProgress progressWithTotalUnitCount:[_uploadData length]]; + + if (!_uploadMetadata.contentType) { + _uploadMetadata.contentType = @"application/octet-stream"; + } + } + return self; +} + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + file:(NSURL *)fileURL + metadata:(FIRStorageMetadata *)metadata { + self = [super initWithReference:reference fetcherService:service]; + if (self) { + _uploadMetadata = [metadata copy]; + _fileURL = [fileURL copy]; + _progress = [NSProgress progressWithTotalUnitCount:0]; + + NSString *mimeType = [FIRStorageUtils MIMETypeForExtension:[_fileURL pathExtension]]; + + if (!_uploadMetadata.contentType) { + _uploadMetadata.contentType = mimeType ?: @"application/octet-stream"; + } + } + return self; +} + +- (void)enqueue { + NSAssert([NSThread isMainThread], @"Upload attempting to execute on non main queue! Please only " + @"execute this method on the main queue."); + self.state = FIRStorageTaskStateQueueing; + + NSMutableURLRequest *request = [self.baseRequest mutableCopy]; + request.HTTPMethod = @"POST"; + request.timeoutInterval = self.reference.storage.maxUploadRetryTime; + NSData *bodyData = [NSData frs_dataFromJSONDictionary:[_uploadMetadata dictionaryRepresentation]]; + request.HTTPBody = bodyData; + [request setValue:@"application/json; charset=UTF-8" forHTTPHeaderField:@"Content-Type"]; + NSString *contentLengthString = [NSString stringWithFormat:@"%zu", + (unsigned long)[bodyData length]]; + [request setValue:contentLengthString forHTTPHeaderField:@"Content-Length"]; + + NSURLComponents *components = + [NSURLComponents componentsWithURL:request.URL resolvingAgainstBaseURL:NO]; + + if ([components.host isEqual:kGCSHost]) { + [components setPercentEncodedPath:[@"/upload" stringByAppendingString:components.path]]; + } + + NSDictionary *queryParams = @{ @"uploadType" : @"resumable", @"name" : self.uploadMetadata.path }; + [components setPercentEncodedQuery:[FIRStorageUtils queryStringForDictionary:queryParams]]; + request.URL = components.URL; + + GTMSessionUploadFetcher *uploadFetcher = + [GTMSessionUploadFetcher uploadFetcherWithRequest:request + uploadMIMEType:_uploadMetadata.contentType + chunkSize:kGTMSessionUploadFetcherStandardChunkSize + fetcherService:self.fetcherService]; + + if (_uploadData) { + [uploadFetcher setUploadData:_uploadData]; + uploadFetcher.comment = @"Data UploadTask"; + } else if (_fileURL) { + [uploadFetcher setUploadFileURL:_fileURL]; + uploadFetcher.comment = @"File UploadTask"; + } + + uploadFetcher.maxRetryInterval = self.reference.storage.maxUploadRetryTime; + + [uploadFetcher setSendProgressBlock:^(int64_t bytesSent, int64_t totalBytesSent, + int64_t totalBytesExpectedToSend) { + self.state = FIRStorageTaskStateProgress; + self.progress.completedUnitCount = totalBytesSent; + self.progress.totalUnitCount = totalBytesExpectedToSend; + self.metadata = _uploadMetadata; + [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:self.snapshot]; + self.state = FIRStorageTaskStateRunning; + }]; + + _uploadFetcher = uploadFetcher; + + // Process fetches + self.state = FIRStorageTaskStateRunning; + [_uploadFetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data, + NSError *_Nullable error) { + // Fire last progress updates + [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:self.snapshot]; + + // Handle potential issues with upload + if (error) { + self.state = FIRStorageTaskStateFailed; + self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference]; + self.metadata = _uploadMetadata; + [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot]; + [self removeAllObservers]; + return; + } + + // Upload completed successfully, fire completion callbacks + self.state = FIRStorageTaskStateSuccess; + + NSDictionary *responseDictionary = [NSDictionary frs_dictionaryFromJSONData:data]; + if (responseDictionary) { + FIRStorageMetadata *metadata = + [[FIRStorageMetadata alloc] initWithDictionary:responseDictionary]; + [metadata setType:FIRStorageMetadataTypeFile]; + self.metadata = metadata; + } else { + NSString *returnedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSString *invalidDataString = + [NSString stringWithFormat:kFIRStorageInvalidDataFormat, returnedData]; + NSDictionary *dict; + if (invalidDataString.length > 0) { + dict = @{NSLocalizedFailureReasonErrorKey : invalidDataString}; + } + self.error = + [FIRStorageErrors errorWithCode:FIRStorageErrorCodeUnknown infoDictionary:dict]; + } + + [self fireHandlersForStatus:FIRStorageTaskStatusSuccess snapshot:self.snapshot]; + [self removeAllObservers]; + }]; +} + +#pragma mark - Upload Management + +- (void)cancel { + NSAssert([NSThread isMainThread], @"Cancel attempting to execute on non main queue! Please only " + @"execute this method on the main queue."); + self.state = FIRStorageTaskStateCancelled; + [_uploadFetcher stopFetching]; + if (self.state != FIRStorageTaskStateSuccess) { + self.metadata = _uploadMetadata; + } + self.error = [FIRStorageErrors errorWithCode:FIRStorageErrorCodeCancelled]; + [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot]; +} + +- (void)pause { + NSAssert([NSThread isMainThread], @"Pause attempting to execute on non main queue! Please only " + @"execute this method on the main queue."); + self.state = FIRStorageTaskStatePaused; + [_uploadFetcher pauseFetching]; + if (self.state != FIRStorageTaskStateSuccess) { + self.metadata = _uploadMetadata; + } + [self fireHandlersForStatus:FIRStorageTaskStatusPause snapshot:self.snapshot]; +} + +- (void)resume { + NSAssert([NSThread isMainThread], @"Resume attempting to execute on non main queue! Please only " + @"execute this method on the main queue."); + self.state = FIRStorageTaskStateResuming; + [_uploadFetcher resumeFetching]; + if (self.state != FIRStorageTaskStateSuccess) { + self.metadata = _uploadMetadata; + } + [self fireHandlersForStatus:FIRStorageTaskStatusResume snapshot:self.snapshot]; + self.state = FIRStorageTaskStateRunning; +} + +@end diff --git a/Firebase/Storage/FIRStorageUtils.m b/Firebase/Storage/FIRStorageUtils.m new file mode 100644 index 0000000..e0abe0a --- /dev/null +++ b/Firebase/Storage/FIRStorageUtils.m @@ -0,0 +1,121 @@ +// Copyright 2017 Google +// +// 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 <MobileCoreServices/MobileCoreServices.h> + +#import "FIRStorageUtils.h" + +#import "FIRStorageConstants_Private.h" +#import "FIRStoragePath.h" + +#import "FirebaseStorage.h" + +#import "GTMSessionFetcher.h" + +// This is the list at https://cloud.google.com/storage/docs/json_api/ without & and +. +NSString *const kGCSObjectAllowedCharacterSet = + @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~!$'()*,;=:@"; + +@implementation FIRStorageUtils + ++ (nullable NSString *)GCSEscapedString:(NSString *)string { + NSCharacterSet *allowedCharacters = + [NSCharacterSet characterSetWithCharactersInString:kGCSObjectAllowedCharacterSet]; + + return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters]; +} + ++ (nullable NSString *)MIMETypeForExtension:(NSString *)extension { + if (extension == nil) { + return nil; + } + + CFStringRef pathExtension = (__bridge_retained CFStringRef)extension; + CFStringRef type = + UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension, NULL); + NSString *mimeType = + (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(type, kUTTagClassMIMEType); + CFRelease(pathExtension); + if (type != NULL) { + CFRelease(type); + } + + return mimeType; +} + ++ (NSString *)queryStringForDictionary:(nullable NSDictionary *)dictionary { + if (!dictionary) { + return @""; + } + + __block NSMutableArray *queryItems = [[NSMutableArray alloc] initWithCapacity:[dictionary count]]; + [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull name, NSString *_Nonnull value, + BOOL *_Nonnull stop) { + NSString *item = + [FIRStorageUtils GCSEscapedString:[NSString stringWithFormat:@"%@=%@", name, value]]; + [queryItems addObject:item]; + }]; + return [queryItems componentsJoinedByString:@"&"]; +} + ++ (NSURLRequest *)defaultRequestForPath:(FIRStoragePath *)path { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + NSURLComponents *components = [[NSURLComponents alloc] init]; + [components setScheme:kFIRStorageScheme]; + [components setHost:kFIRStorageHost]; + NSString *encodedPath = [self encodedURLForPath:path]; + [components setPercentEncodedPath:encodedPath]; + [request setURL:components.URL]; + return request; +} + ++ (NSString *)encodedURLForPath:(FIRStoragePath *)path { + NSString *bucketName = [FIRStorageUtils GCSEscapedString:path.bucket]; + NSString *objectName = [FIRStorageUtils GCSEscapedString:path.object]; + NSString *bucketFormat = [NSString stringWithFormat:kFIRStorageBucketPathFormat, bucketName]; + NSString *urlPath = [@"/" stringByAppendingPathComponent:bucketFormat]; + if (objectName) { + NSString *objectFormat = [NSString stringWithFormat:kFIRStorageObjectPathFormat, objectName]; + urlPath = [urlPath stringByAppendingFormat:@"/%@", objectFormat]; + } else { + urlPath = [urlPath stringByAppendingString:@"/o"]; + } + return [@"/" stringByAppendingString:[kFIRStorageVersionPath stringByAppendingString:urlPath]]; +} + +@end + +@implementation NSDictionary (FIRStorageNSDictionaryJSONHelpers) + ++ (nullable instancetype)frs_dictionaryFromJSONData:(nullable NSData *)data { + if (!data) { + return nil; + } + return [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:nil]; +} + +@end + +@implementation NSData (FIRStorageNSDataJSONHelpers) + ++ (nullable instancetype)frs_dataFromJSONDictionary:(nullable NSDictionary *)dictionary { + if (!dictionary) { + return nil; + } + return [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil]; +} + +@end
\ No newline at end of file diff --git a/Firebase/Storage/FirebaseStorage.h b/Firebase/Storage/FirebaseStorage.h new file mode 100644 index 0000000..1e5dfa4 --- /dev/null +++ b/Firebase/Storage/FirebaseStorage.h @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRStorage.h" +#import "FIRStorageConstants.h" +#import "FIRStorageDownloadTask.h" +#import "FIRStorageMetadata.h" +#import "FIRStorageObservableTask.h" +#import "FIRStorageReference.h" +#import "FIRStorageTask.h" +#import "FIRStorageTaskSnapshot.h" +#import "FIRStorageUploadTask.h" diff --git a/Firebase/Storage/FirebaseStorage.podspec b/Firebase/Storage/FirebaseStorage.podspec new file mode 100644 index 0000000..69c6ddc --- /dev/null +++ b/Firebase/Storage/FirebaseStorage.podspec @@ -0,0 +1,44 @@ +# This podspec is not intended to be deployed. It is solely for the static +# library framework build process at +# https://github.com/firebase/firebase-ios-sdk/tree/master/BuildFrameworks + +Pod::Spec.new do |s| + s.name = 'FirebaseStorage' + s.version = '2.0.0' + s.summary = 'Firebase Open Source Libraries for iOS.' + + s.description = <<-DESC +Simplify your iOS development, grow your user base, and monetize more effectively with Firebase. + DESC + + s.homepage = 'https://firebase.google.com' + s.license = { :type => 'Apache', :file => '../../LICENSE' } + s.authors = 'Google, Inc.' + + # NOTE that the FirebaseDev pod is neither publicly deployed nor yet interchangeable with the + # Firebase pod + s.source = { :git => 'https://github.com/firebase/firebase-ios-sdk.git', :tag => s.version.to_s } + s.social_media_url = 'https://twitter.com/Firebase' + s.ios.deployment_target = '7.0' + + s.source_files = '**/*.[mh]' + s.public_header_files = + 'FirebaseStorage.h', + 'FIRStorage.h', + 'FIRStorageConstants.h', + 'FIRStorageDownloadTask.h', + 'FIRStorageMetadata.h', + 'FIRStorageObservableTask.h', + 'FIRStorageReference.h', + 'FIRStorageSwiftNameSupport.h', + 'FIRStorageTask.h', + 'FIRStorageTaskSnapshot.h', + 'FIRStorageUploadTask.h' + + s.framework = 'MobileCoreServices' +# s.dependency 'FirebaseDev/Core' + s.dependency 'GTMSessionFetcher/Core', '~> 1.1' + s.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => + '$(inherited) ' + + 'FIRStorage_VERSION=' + s.version.to_s } +end diff --git a/Firebase/Storage/Private/FIRStorageConstants_Private.h b/Firebase/Storage/Private/FIRStorageConstants_Private.h new file mode 100644 index 0000000..50addb1 --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageConstants_Private.h @@ -0,0 +1,145 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +@class FIRStorageMetadata; + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXPORT NSString *const kGCSScheme; +FOUNDATION_EXPORT NSString *const kGCSHost; +FOUNDATION_EXPORT NSString *const kGCSUploadPath; +FOUNDATION_EXPORT NSString *const kGCSStorageVersionPath; +FOUNDATION_EXPORT NSString *const kGCSBucketPathFormat; +FOUNDATION_EXPORT NSString *const kGCSObjectPathFormat; + +FOUNDATION_EXPORT NSString *const kFIRStorageScheme; +FOUNDATION_EXPORT NSString *const kFIRStorageHost; +FOUNDATION_EXPORT NSString *const kFIRStorageVersionPath; +FOUNDATION_EXPORT NSString *const kFIRStorageBucketPathFormat; +FOUNDATION_EXPORT NSString *const kFIRStorageObjectPathFormat; +FOUNDATION_EXPORT NSString *const kFIRStorageFullPathFormat; + +FOUNDATION_EXPORT NSString *const kFIRStorageAuthTokenFormat; +FOUNDATION_EXPORT NSString *const kFIRStorageDefaultBucketFormat; + +FOUNDATION_EXPORT NSString *const kFIRStorageResponseErrorDomain; +FOUNDATION_EXPORT NSString *const kFIRStorageResponseErrorCode; +FOUNDATION_EXPORT NSString *const kFIRStorageResponseBody; + +FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusResumeNotification; +FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusPauseNotification; +FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusProgressNotification; +FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusCompleteNotification; +FOUNDATION_EXPORT NSString *const kFIRStorageTaskStatusFailureNotification; + +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataBucket; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataCacheControl; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentDisposition; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentEncoding; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentLanguage; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataContentType; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataCustomMetadata; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataSize; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataDownloadURLs; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataGeneration; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataMetageneration; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataTimeCreated; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataUpdated; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataName; +FOUNDATION_EXPORT NSString *const kFIRStorageMetadataDownloadTokens; + +FOUNDATION_EXPORT NSString *const kFIRStorageInvalidDataFormat; +FOUNDATION_EXPORT NSString *const kFIRStorageInvalidObserverStatus; + +FOUNDATION_EXPORT NSString *const kFIRStorageBundleIdentifier; + +/** + * Enum representing the internal state of an upload or download task. + */ +typedef NS_ENUM(NSInteger, FIRStorageTaskState) { + /** + * Unknown task state + */ + FIRStorageTaskStateUnknown, + + /** + * Task is being queued is ready to run + */ + FIRStorageTaskStateQueueing, + + /** + * Task is resuming from a paused state + */ + FIRStorageTaskStateResuming, + + /** + * Task is currently running + */ + FIRStorageTaskStateRunning, + + /** + * Task reporting a progress event + */ + FIRStorageTaskStateProgress, + + /** + * Task is pausing + */ + FIRStorageTaskStatePausing, + + /** + * Task is completing successfully + */ + FIRStorageTaskStateCompleting, + + /** + * Task is failing unrecoverably + */ + FIRStorageTaskStateFailing, + + /** + * Task paused successfully + */ + FIRStorageTaskStatePaused, + + /** + * Task cancelled successfully + */ + FIRStorageTaskStateCancelled, + + /** + * Task completed successfully + */ + FIRStorageTaskStateSuccess, + + /** + * Task failed unrecoverably + */ + FIRStorageTaskStateFailed +}; + +/** + * Represents the various types of metadata: Files or Folders. + */ +typedef NS_ENUM(NSUInteger, FIRStorageMetadataType) { + FIRStorageMetadataTypeUnknown, + FIRStorageMetadataTypeFile, + FIRStorageMetadataTypeFolder, +}; + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageDeleteTask.h b/Firebase/Storage/Private/FIRStorageDeleteTask.h new file mode 100644 index 0000000..c97fd27 --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageDeleteTask.h @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRStorageTask.h" + +@class GTMSessionFetcherService; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Task which provides the ability to delete an object in Firebase Storage. + */ +@interface FIRStorageDeleteTask : FIRStorageTask<FIRStorageTaskManagement> + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + completion:(FIRStorageVoidError)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageDownloadTask_Private.h b/Firebase/Storage/Private/FIRStorageDownloadTask_Private.h new file mode 100644 index 0000000..293d1d5 --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageDownloadTask_Private.h @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +@class GTMSessionFetcherService; + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRStorageDownloadTask () + +/** + * Bytes which have been downloaded so far. + */ +@property(readonly, nonatomic) NSData *downloadData; + +/** + * The file on disk to write to. + */ +@property(copy, nonatomic) NSURL *fileURL; + +/** + * Initializes a download task with a base FIRStorageReference and GTMSessionFetcherService. + * @param reference The base FIRStorageReference which fetchers use for configuration. + * @param service The GTMSessionFetcherService which will create fetchers. + * @param fileURL The system URL to download to. If nil, download in memory as bytes. + * @return Returns an instance of FIRStorageDownloadTask + */ +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + file:(nullable NSURL *)fileURL; + +/** + * Cancels the download task and passes an appropriate error to the developer. + * @param error NSError to propegate to the developer. + */ +- (void)cancelWithError:(NSError *)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageErrors.h b/Firebase/Storage/Private/FIRStorageErrors.h new file mode 100644 index 0000000..7c236d9 --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageErrors.h @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRStorageConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRStorageReference; + +/** + * Adds wrappers for common Firebase Storage errors (including creating errors from GCS errors). + * For more information on unwrapping GCS errors, see the GCS errors docs: + * https://cloud.google.com/storage/docs/json_api/v1/status-codes + * This is never publicly exposed to end developers (as they will simply see an NSError). + */ +@interface FIRStorageErrors : NSObject + +/** + * Creates a Firebase Storage error from a specific FIRStorageErrorCode. + */ ++ (NSError *)errorWithCode:(FIRStorageErrorCode)code; + +/** + * Creates a Firebase Storage error from a specific FIRStorageErrorCode while adding + * custom info from an optionally provided info dictionary. + */ ++ (NSError *)errorWithCode:(FIRStorageErrorCode)code + infoDictionary:(nullable NSDictionary *)dictionary; + +/** + * Creates a Firebase Storage error from a specific GCS error and FIRStorageReference. + * @param error Server error to wrap and return as a Firebase Storage error. + * @param reference FIRStorageReference which provides context about the request being made. + * @return Returns an Firebase Storage error, or nil if no error is provided. + */ ++ (nullable NSError *)errorWithServerError:(nullable NSError *)error + reference:(nullable FIRStorageReference *)reference; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageGetMetadataTask.h b/Firebase/Storage/Private/FIRStorageGetMetadataTask.h new file mode 100644 index 0000000..5f1dc8f --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageGetMetadataTask.h @@ -0,0 +1,34 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRStorageTask.h" + +@class GTMSessionFetcherService; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Task which provides the ability to get metadata on an object in Firebase Storage. + */ +@interface FIRStorageGetMetadataTask : FIRStorageTask<FIRStorageTaskManagement> + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + completion:(FIRStorageVoidMetadataError)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageMetadata_Private.h b/Firebase/Storage/Private/FIRStorageMetadata_Private.h new file mode 100644 index 0000000..629c935 --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageMetadata_Private.h @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRStorageConstants_Private.h" + +@class FIRStorageReference; + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRStorageMetadata () + +@property(readwrite, nonatomic) NSString *name; + +@property(readwrite, nonatomic) NSString *path; + +@property(readwrite, nonatomic) FIRStorageReference *reference; + +/** + * The type of the object, either a "File" or a "Folder". + */ +@property(readwrite) FIRStorageMetadataType type; + +/** + * Returns an RFC3339 formatted date from a string. + * @param dateString An NSString of the form: yyyy-MM-ddTHH:mm:ss.SSSZ. + * @return An NSDate populated from the string or nil if conversion isn't possible. + */ +- (nullable NSDate *)dateFromRFC3339String:(NSString *)dateString; + +/** + * Returns an RFC3339 formatted string from an NSDate object. + * @param date The NSDate object to be converted to a string. + * @return An NSString of the form: yyyy-MM-ddTHH:mm:ss.SSSZ or nil if conversion isn't possible. + */ +- (nullable NSString *)RFC3339StringFromDate:(NSDate *)date; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageObservableTask_Private.h b/Firebase/Storage/Private/FIRStorageObservableTask_Private.h new file mode 100644 index 0000000..e37b63f --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageObservableTask_Private.h @@ -0,0 +1,45 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +NS_ASSUME_NONNULL_BEGIN + +@class FIRStorageTaskSnapshot; + +@class GTMSessionFetcherService; + +@interface FIRStorageObservableTask () + +/** + * Creates a new FIRStorageTask initialized with a FIRStorageReference and GTMSessionFetcherService. + * @param reference A FIRStorageReference the task will be performed on. + * @param service A GTMSessionFetcherService which provides the fetchers and configuration for + * requests. + * @return A new FIRStorageTask representing the current task. + */ +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service; + +/** + * Raise events for a given task status by passing along a snapshot of existing task state. + * @param status A FIRStorageTaskStatus to raise events for. + * @param snapshot A FIRStorageTaskSnapshot snapshot of task state to pass through the handler. + */ +- (void)fireHandlersForStatus:(FIRStorageTaskStatus)status + snapshot:(FIRStorageTaskSnapshot *)snapshot; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStoragePath.h b/Firebase/Storage/Private/FIRStoragePath.h new file mode 100644 index 0000000..53ff7ef --- /dev/null +++ b/Firebase/Storage/Private/FIRStoragePath.h @@ -0,0 +1,106 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +/** + * Represents a path in GCS, which can be represented as: gs://bucket/path/to/object + * or http[s]://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object?token=<12345> + * This class also includes helper methods to parse those URI/Ls, as well as to + * add and remove path segments. + */ +@interface FIRStoragePath : NSObject + +/** + * The GCS bucket in the path. + */ +@property(copy, nonatomic) NSString *bucket; + +/** + * The GCS object in the path. + */ +@property(copy, nonatomic, nullable) NSString *object; + +/** + * Parses a generic string (representing some URI or URL) and returns the appropriate path. + * @param string String which is parsed into a path. + * @return Returns an instance of FIRStoragePath or nil if one can't be created. + * @throws Throws an exception if the string is not a valid gs:// URI or http[s]:// URL. + */ ++ (nullable FIRStoragePath *)pathFromString:(NSString *)string; + +/** + * Parses a gs://bucket/path/to/object URI into a GCS path. + * @param aURIString gs:// URI which is parsed into a path. + * @return Returns an instance of FIRStoragePath or nil if one can't be created. + * @throws Throws an exception if the string is not a valid gs:// URI. + */ ++ (nullable FIRStoragePath *)pathFromGSURI:(NSString *)aURIString; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Constructs an FIRStoragePath object that represents the given bucket and object. + * @param bucket The name of the bucket. + * @param object The name of the object. + * @return An instance of FIRStoragePath representing the @a bucket and @a object. + */ +- (instancetype)initWithBucket:(NSString *)bucket + object:(nullable NSString *)object NS_DESIGNATED_INITIALIZER; + +/** + * Parses a http[s]://firebasestorage.googleapis.com/v0/b/bucket/o/path/to/object...?token=<12345> + * URL into a GCS path. + * @param aURLString http[s]:// URL which is parsed into a path. + * string which is parsed into a path. + * @return Returns an instance of FIRStoragePath or nil if one can't be created. + * @throws Throws an exception if the string is not a valid http[s]:// URL. + */ ++ (nullable FIRStoragePath *)pathFromHTTPURL:(NSString *)aURLString; + +/** + * Creates a new path based off of the current path and a string appended to it. + * Note that all slashes are compressed to a single slash, and leading and trailing slashes + * are removed. + * @param path String to append to the current path. + * @return Returns a new instance of FIRStoragePath with the new path appended. + */ +- (FIRStoragePath *)child:(NSString *)path; + +/** + * Creates a new path based off of the current path with the last path segment removed. + * @return Returns a new instance of FIRStoragePath pointing to the parent path, + * or nil if the current path points to the root. + */ +- (nullable FIRStoragePath *)parent; + +/** + * Creates a new path based off of the root of the bucket. + * @return Returns a new instance of FIRStoragePath pointing to the root of the bucket. + */ +- (FIRStoragePath *)root; + +/** + * Returns a GS URI representing the current path. + * @return Returns a gs://bucket/path/to/object URI representing the current path. + */ +- (NSString *)stringValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageReference_Private.h b/Firebase/Storage/Private/FIRStorageReference_Private.h new file mode 100644 index 0000000..825964d --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageReference_Private.h @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRStoragePath.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRStorageReference () + +@property(nonatomic, readwrite) FIRStorage *storage; + +/** + * The current path which points to an object in the Google Cloud Storage bucket. + */ +@property(strong, nonatomic) FIRStoragePath *path; + +- (instancetype)initWithStorage:(FIRStorage *)storage + path:(FIRStoragePath *)path NS_DESIGNATED_INITIALIZER; + +- (NSString *)stringValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.h b/Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.h new file mode 100644 index 0000000..1762a61 --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageTaskSnapshot_Private.h @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRStorageConstants_Private.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRStorageMetadata; +@class FIRStorageReference; +@class FIRStorageTask; + +@interface FIRStorageTaskSnapshot () + +@property(readwrite, copy, nonatomic) FIRStorageTask *task; +@property(readwrite, copy, nonatomic) FIRStorageMetadata *metadata; +@property(readwrite, copy, nonatomic) FIRStorageReference *reference; +@property(readwrite, strong, nonatomic) NSProgress *progress; +@property(readwrite, copy, nonatomic) NSError *error; + +/** + * Creates a new task snapshot from the given properties. + * @param task The task being represented in this snapshot. + * @param state The current state of the parent task. + * @param metadata The FIRStorageMetadata of a task. Before upload/update, contains the metadata + * to be updated; after, contains the returned metadata. May be nil if no metadata is provided + * or returned. + * @param reference The FIRStorageReference that spawned the task this snapshot is based on. + * @param progress An NSProgress object containing progress of the task this snapshot is based on, + * or nil if the task doesn't report progress. + * @param error An NSError object containing an error that occurred during the task, + * if one occurred. + * @return Returns the constructed snapshot. + */ +- (instancetype)initWithTask:(__kindof FIRStorageTask *)task + state:(FIRStorageTaskState)state + metadata:(nullable FIRStorageMetadata *)metadata + reference:(FIRStorageReference *)reference + progress:(nullable NSProgress *)progress + error:(nullable NSError *)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageTask_Private.h b/Firebase/Storage/Private/FIRStorageTask_Private.h new file mode 100644 index 0000000..598006b --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageTask_Private.h @@ -0,0 +1,77 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRStorageConstants_Private.h" +#import "FIRStorageErrors.h" +#import "FIRStorageReference.h" +#import "FIRStorageReference_Private.h" +#import "FIRStorageTaskSnapshot.h" +#import "FIRStorageTaskSnapshot_Private.h" +#import "FIRStorageUtils.h" + +#import <GTMSessionFetcher/GTMSessionFetcher.h> +#import <GTMSessionFetcher/GTMSessionFetcherService.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRStorageTask () + +/** + * State for the current task in progress. + */ +@property(atomic) FIRStorageTaskState state; + +/** + * FIRStorageMetadata for the task in progress, or nil if none present. + */ +@property(strong, nonatomic, nullable) FIRStorageMetadata *metadata; + +/** + * Error which occurred during task execution, or nil if no error occurred. + */ +@property(strong, nonatomic, nullable) NSError *error; + +/** + * NSProgress object which tracks the progess of an observable task. + */ +@property(strong, nonatomic) NSProgress *progress; + +/** + * Reference pointing to the location the task is being performed against. + */ +@property(strong, nonatomic) FIRStorageReference *reference; + +@property(strong, readwrite, nonatomic, nonnull) FIRStorageTaskSnapshot *snapshot; + +@property(readonly, copy, nonatomic) NSURLRequest *baseRequest; + +@property(strong, atomic) GTMSessionFetcher *fetcher; + +@property(readonly, nonatomic) GTMSessionFetcherService *fetcherService; + +/** + * Creates a new FIRStorageTask initialized with a FIRStorageReference and GTMSessionFetcherService. + * @param reference A FIRStorageReference the task will be performed on. + * @param service A GTMSessionFetcherService which provides the fetchers and configuration for + * requests. + * @return A new FIRStorageTask representing the current task. + */ +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageTokenAuthorizer.h b/Firebase/Storage/Private/FIRStorageTokenAuthorizer.h new file mode 100644 index 0000000..78a8218 --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageTokenAuthorizer.h @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Google + * + * 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 <GTMSessionFetcher/GTMSessionFetcherService.h> + +@class FIRApp; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Wrapper class for FIRApp that implements the GTMFetcherAuthorizationProtocol, + * so as to easily provide GTMSessionFetcher fetches a Firebase Authentication JWT + * for the current logged in user. Handles token expiration and other failure cases. + * If no authentication provider exists or no token is found, no token is added + * and the request is passed. + */ +@interface FIRStorageTokenAuthorizer : NSObject<GTMFetcherAuthorizationProtocol> + +/** + * Initializes the token authorizer with an instance of FIRApp. + * @param app An instance of FIRApp which provides auth tokens. + * @return Returns an instance of FIRStorageTokenAuthorizer which adds the appropriate + * "Authorization" header to all outbound requests. Note that a token may not be added + * if a getTokenImplementation doesn't exist on FIRApp. This allows for unauthenticated + * access, if Firebase Storage rules allow for it. + */ +- (instancetype)initWithApp:(FIRApp *)app fetcherService:(GTMSessionFetcherService *)service; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h b/Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h new file mode 100644 index 0000000..2fcefdd --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageUpdateMetadataTask.h @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRStorageTask.h" + +@class GTMSessionFetcherService; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Task which provides the ability update the metadata on an object in Firebase Storage. + */ +@interface FIRStorageUpdateMetadataTask : FIRStorageTask<FIRStorageTaskManagement> + +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + metadata:(FIRStorageMetadata *)metadata + completion:(FIRStorageVoidMetadataError)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageUploadTask_Private.h b/Firebase/Storage/Private/FIRStorageUploadTask_Private.h new file mode 100644 index 0000000..468d9d3 --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageUploadTask_Private.h @@ -0,0 +1,69 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +@class GTMSessionUploadFetcher; + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRStorageUploadTask () + +/** + * The data to be uploaded (if uploading bytes). + */ +@property(readonly, copy, nonatomic, nullable) NSData *uploadData; + +/** + * The name of a file on disk to be uploaded (if uploading from a file). + */ +@property(readonly, copy, nonatomic, nullable) NSURL *fileURL; + +/** + * The FIRStorageMetadata about the object being uploaded. + */ +@property(readonly, copy, nonatomic) FIRStorageMetadata *uploadMetadata; + +/** + * GTMSessionUploadFetcher used by all uploads. + */ +@property(strong, atomic) GTMSessionUploadFetcher *uploadFetcher; + +/** + * Initializes an upload task with a base FIRStorageReference and GTMSessionFetcherService. + * @param reference The base FIRStorageReference which fetchers use for configuration. + * @param service The GTMSessionFetcherService which will create fetchers. + * @param uploadData The NSData object to be uploaded. + * @return Returns an instance of FIRStorageUploadTask. + */ +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + data:(NSData *)uploadData + metadata:(FIRStorageMetadata *)metadata; + +/** + * Initializes an upload task with a base FIRStorageReference and GTMSessionFetcherService. + * @param reference The base FIRStorageReference which fetchers use for configuration. + * @param service The GTMSessionFetcherService which will create fetchers. + * @param fileURL The system file URL to upload from. + * @return Returns an instance of FIRStorageUploadTask. + */ +- (instancetype)initWithReference:(FIRStorageReference *)reference + fetcherService:(GTMSessionFetcherService *)service + file:(NSURL *)fileURL + metadata:(FIRStorageMetadata *)metadata; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorageUtils.h b/Firebase/Storage/Private/FIRStorageUtils.h new file mode 100644 index 0000000..e687c82 --- /dev/null +++ b/Firebase/Storage/Private/FIRStorageUtils.h @@ -0,0 +1,93 @@ +/* + * Copyright 2017 Google + * + * 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/Foundation.h> + +@class FIRStoragePath; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FIRStorageUtils provides a number of helper methods for commonly used operations + * in Firebase Storage, such as JSON parsing, escaping, and file extensions. + */ +@interface FIRStorageUtils : NSObject + +/** + * Returns a percent encoded string appropriate for GCS. + * See https://cloud.google.com/storage/docs/naming for more details. + * @param string A path to escape characters according to the GCS + * @return A percent encoded string appropriate for GCS operations or nil if string is nil + * or can't be escaped. + */ ++ (nullable NSString *)GCSEscapedString:(NSString *)string; + +/** + * Returns the MIME type for a file extension. + * Example of how to get MIME type here: http://ddeville.me/2011/12/mime-to-UTI-cocoa/ + * @param extension A file extension such as "txt", "png", etc. + * @return The MIME type for the input extension such as "text/plain", "image/png", etc. + * or nil if no type is found. + */ ++ (nullable NSString *)MIMETypeForExtension:(NSString *)extension; + +/** + * Returns a properly escaped query string from a given dictionary of query items to values. + * @param dictionary A dictionary containing query items and associated values. + * @return A properly escaped query string or the empty string for a nil or empty dictionary. + */ ++ (NSString *)queryStringForDictionary:(nullable NSDictionary *)dictionary; + +/** + * Returns a base NSURLRequest used by all tasks. + * @param path The FIRStoragePath to create a request for. + * @return Returns a properly formatted NSURLRequest of the form: + * scheme://host/version/b/<bucket>/o[/path/to/object] + */ ++ (NSURLRequest *)defaultRequestForPath:(FIRStoragePath *)path; + +/** + * Creates the appropriate GCS percent escaped path for a given FIRStoragePath. + * @param path The FIRStoragePath to encode. + * @return Returns the GCS encoded URL for a given FIRStoragePath. + */ ++ (NSString *)encodedURLForPath:(FIRStoragePath *)path; + +@end + +@interface NSDictionary (FIRStorageNSDictionaryJSONHelpers) + +/** + * Returns a dictionary representation of the data in @a data. + * @param data NSData containing JSON data. + * @return An NSDictionary representation of the JSON, or nil if serialization failed. + */ ++ (nullable instancetype)frs_dictionaryFromJSONData:(nullable NSData *)data; + +@end + +@interface NSData (FIRStorageNSDataJSONHelpers) + +/** + * Returns an NSData instance containing JSON serialized from @a dictionary. + * @param dictionary An NSDictionary containing only types serializable to JSON. + * @return An NSData object representing the binary JSON, or nil if serialization failed. + */ ++ (nullable instancetype)frs_dataFromJSONDictionary:(nullable NSDictionary *)dictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Storage/Private/FIRStorage_Private.h b/Firebase/Storage/Private/FIRStorage_Private.h new file mode 100644 index 0000000..aefe808 --- /dev/null +++ b/Firebase/Storage/Private/FIRStorage_Private.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +@class FIRApp; +@class GTMSessionFetcherService; + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRStorage () + +@property(strong, nonatomic, readwrite) FIRApp *app; + +@property(strong, nonatomic) GTMSessionFetcherService *fetcherServiceForApp; + +@property(strong, nonatomic) NSString *storageBucket; + +/** + * Enables/disables GTMSessionFetcher HTTP logging + * @param isLoggingEnabled Boolean passed through to enable/disable GTMSessionFetcher logging + */ ++ (void)setGTMSessionFetcherLoggingEnabled:(BOOL)isLoggingEnabled; + +@end + +NS_ASSUME_NONNULL_END |