react-native-firebase/ios/RNFirebase/storage/RNFirebaseStorage.m

490 lines
22 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#import "RNFirebaseStorage.h"
#if __has_include(<FirebaseStorage/FIRStorage.h>)
#import "RNFirebaseEvents.h"
#import <MobileCoreServices/MobileCoreServices.h>
#import <Photos/Photos.h>
#import <Firebase.h>
@implementation RNFirebaseStorage
RCT_EXPORT_MODULE(RNFirebaseStorage);
// Run on a different thread
- (dispatch_queue_t)methodQueue {
return dispatch_queue_create("com.invertase.firebase.storage", DISPATCH_QUEUE_SERIAL);
}
/**
delete
@url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#delete
@param NSString path
*/
RCT_EXPORT_METHOD(delete:(NSString *) appName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
FIRStorageReference *fileRef = [self getReference:path appName:appName];
[fileRef deleteWithCompletion:^(NSError *_Nullable error) {
if (error != nil) {
[self promiseRejectStorageException:reject error:error];
} else {
resolve([NSNull null]);
}
}];
}
/**
getDownloadURL
@url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#getDownloadURL
@param NSString path
*/
RCT_EXPORT_METHOD(getDownloadURL:(NSString *) appName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
FIRStorageReference *fileRef = [self getReference:path appName:appName];
[fileRef downloadURLWithCompletion:^(NSURL *_Nullable URL, NSError *_Nullable error) {
if (error != nil) {
[self promiseRejectStorageException:reject error:error];
} else {
resolve([URL absoluteString]);
}
}];
}
/**
getMetadata
@url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#getMetadata
@param NSString path
*/
RCT_EXPORT_METHOD(getMetadata:(NSString *) appName
path:(NSString *) path
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
FIRStorageReference *fileRef = [self getReference:path appName:appName];
[fileRef metadataWithCompletion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) {
if (error != nil) {
[self promiseRejectStorageException:reject error:error];
} else {
resolve([metadata dictionaryRepresentation]);
}
}];
}
/**
updateMetadata
@url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#updateMetadata
@param NSString path
@param NSDictionary metadata
*/
RCT_EXPORT_METHOD(updateMetadata:(NSString *) appName
path:(NSString *) path
metadata:(NSDictionary *) metadata
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
FIRStorageReference *fileRef = [self getReference:path appName:appName];
FIRStorageMetadata *firmetadata = [self buildMetadataFromMap:metadata];
[fileRef updateMetadata:firmetadata completion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) {
if (error != nil) {
[self promiseRejectStorageException:reject error:error];
} else {
resolve([metadata dictionaryRepresentation]);
}
}];
}
/**
downloadFile
@url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#downloadFile
@param NSString path
@param NSString localPath
*/
RCT_EXPORT_METHOD(downloadFile:(NSString *) appName
path:(NSString *) path
localPath:(NSString *) localPath
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
FIRStorageReference *fileRef = [self getReference:path appName:appName];
NSURL *localFile = [NSURL fileURLWithPath:localPath];
FIRStorageDownloadTask *downloadTask = [fileRef writeToFile:localFile];
// listen for state changes, errors, and completion of the download.
[downloadTask observeStatus:FIRStorageTaskStatusResume handler:^(FIRStorageTaskSnapshot *snapshot) {
// download resumed, also fires when the upload starts
NSDictionary *event = [self getDownloadTaskAsDictionary:snapshot];
[self sendJSEvent:appName type:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event];
}];
[downloadTask observeStatus:FIRStorageTaskStatusPause handler:^(FIRStorageTaskSnapshot *snapshot) {
// download paused
NSDictionary *event = [self getDownloadTaskAsDictionary:snapshot];
[self sendJSEvent:appName type:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event];
}];
[downloadTask observeStatus:FIRStorageTaskStatusProgress handler:^(FIRStorageTaskSnapshot *snapshot) {
// download reported progress
NSDictionary *event = [self getDownloadTaskAsDictionary:snapshot];
[self sendJSEvent:appName type:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event];
}];
[downloadTask observeStatus:FIRStorageTaskStatusSuccess handler:^(FIRStorageTaskSnapshot *snapshot) {
// download completed successfully
NSDictionary *resp = [self getDownloadTaskAsDictionary:snapshot];
[self sendJSEvent:appName type:STORAGE_EVENT path:path title:STORAGE_DOWNLOAD_SUCCESS props:resp];
resolve(resp);
}];
[downloadTask observeStatus:FIRStorageTaskStatusFailure handler:^(FIRStorageTaskSnapshot *snapshot) {
// download task failed
// TODO sendJSError event
if (snapshot.error != nil) {
[self promiseRejectStorageException:reject error:snapshot.error];
}
}];
}
/**
setMaxDownloadRetryTime
@url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxDownloadRetryTime
@param NSNumber milliseconds
*/
RCT_EXPORT_METHOD(setMaxDownloadRetryTime:(NSString *) appName
milliseconds:(NSNumber *) milliseconds) {
[[FIRStorage storageForApp:[FIRApp appNamed:appName]] setMaxDownloadRetryTime:[milliseconds doubleValue]];
}
/**
setMaxOperationRetryTime
@url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxOperationRetryTime
@param NSNumber milliseconds
*/
RCT_EXPORT_METHOD(setMaxOperationRetryTime:(NSString *) appName
milliseconds:(NSNumber *) milliseconds) {
[[FIRStorage storageForApp:[FIRApp appNamed:appName]] setMaxOperationRetryTime:[milliseconds doubleValue]];
}
/**
setMaxUploadRetryTime
@url https://firebase.google.com/docs/reference/js/firebase.storage.Storage#setMaxUploadRetryTime
*/
RCT_EXPORT_METHOD(setMaxUploadRetryTime:(NSString *) appName
milliseconds:(NSNumber *) milliseconds) {
[[FIRStorage storageForApp:[FIRApp appNamed:appName]] setMaxUploadRetryTime:[milliseconds doubleValue]];
}
/**
putFile
@url https://firebase.google.com/docs/reference/js/firebase.storage.Reference#putFile
@param NSString path
@param NSString localPath
@param NSDictionary metadata
*/
RCT_EXPORT_METHOD(putFile:(NSString *) appName
path:(NSString *) path
localPath:(NSString *) localPath
metadata:(NSDictionary *) metadata
resolver:(RCTPromiseResolveBlock) resolve
rejecter:(RCTPromiseRejectBlock) reject) {
FIRStorageMetadata *firmetadata = [self buildMetadataFromMap:metadata];
if ([localPath hasPrefix:@"assets-library://"] || [localPath hasPrefix:@"ph://"]) {
PHFetchResult *assets;
if ([localPath hasPrefix:@"assets-library://"]) {
NSURL *localFile = [[NSURL alloc] initWithString:localPath];
assets = [PHAsset fetchAssetsWithALAssetURLs:@[localFile] options:nil];
} else {
NSString *assetId = [localPath substringFromIndex:@"ph://".length];
assets = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetId] options:nil];
}
PHAsset *asset = [assets firstObject];
// this is based on http://stackoverflow.com/questions/35241449
if (asset.mediaType == PHAssetMediaTypeImage) {
// images
PHImageRequestOptions *options = [PHImageRequestOptions new];
options.networkAccessAllowed = true;
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
if (info[PHImageErrorKey] == nil) {
if (UTTypeConformsTo((__bridge CFStringRef)dataUTI, kUTTypeJPEG)) {
firmetadata.contentType = [self utiToMimeType:dataUTI];
[self uploadData:appName data:imageData firmetadata:firmetadata path:path resolver:resolve rejecter:reject];
} else {
// if the image UTI is not JPEG then convert to JPEG, e.g. HEI
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
NSDictionary *imageInfo = (__bridge NSDictionary*)CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
NSDictionary *imageMetadata = [imageInfo copy];
NSMutableData *imageDataJPEG = [NSMutableData data];
CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageDataJPEG, kUTTypeJPEG, 1, NULL);
CGImageDestinationAddImageFromSource(destination, source, 0, (__bridge CFDictionaryRef)imageMetadata);
CGImageDestinationFinalize(destination);
// Manually set mimetype to JPEG
firmetadata.contentType = @"image/jpeg";
[self uploadData:appName data:[NSData dataWithData:imageDataJPEG] firmetadata:firmetadata path:path resolver:resolve rejecter:reject];
}
} else {
reject(@"storage/request-image-data-failed", @"Could not obtain image data for the specified file.", nil);
}
}];
} else if (asset.mediaType == PHAssetMediaTypeVideo) {
// video
PHVideoRequestOptions *options = [PHVideoRequestOptions new];
options.networkAccessAllowed = true;
[[PHImageManager defaultManager] requestExportSessionForVideo:asset options:options exportPreset:AVAssetExportPresetHighestQuality resultHandler:^(AVAssetExportSession *_Nullable exportSession, NSDictionary *_Nullable info) {
if (info[PHImageErrorKey] == nil) {
NSURL *tempUrl = [self temporaryFileUrl];
exportSession.outputURL = tempUrl;
NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
for (PHAssetResource *resource in resources) {
exportSession.outputFileType = resource.uniformTypeIdentifier;
if (exportSession.outputFileType != nil) break;
}
[exportSession exportAsynchronouslyWithCompletionHandler:^{
if (exportSession.status == AVAssetExportSessionStatusCompleted) {
firmetadata.contentType = [self utiToMimeType:exportSession.outputFileType];
[self uploadFile:appName url:tempUrl firmetadata:firmetadata path:path resolver:resolve rejecter:reject];
// we're not cleaning up the temporary file at the moment, just relying on the OS to do that in it's own time - todo?
} else {
reject(@"storage/temporary-file-failure", @"Unable to create temporary file for upload.", nil);
}
}];
} else {
reject(@"storage/export-session-failure", @"Unable to create export session for asset.", nil);
}
}];
}
} else {
// TODO: Content type for file?
NSData *data = [[NSFileManager defaultManager] contentsAtPath:localPath];
[self uploadData:appName data:data firmetadata:firmetadata path:path resolver:resolve rejecter:reject];
}
}
- (NSURL *)temporaryFileUrl {
NSString *filename = [NSString stringWithFormat:@"%@.tmp", [[NSProcessInfo processInfo] globallyUniqueString]];
return [[NSURL fileURLWithPath:NSTemporaryDirectory()] URLByAppendingPathComponent:filename];
}
- (NSString *)utiToMimeType:(NSString *) dataUTI {
return (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)dataUTI, kUTTagClassMIMEType);
}
- (void)uploadFile:(NSString *)appName url:(NSURL *)url firmetadata:(FIRStorageMetadata *)firmetadata path:(NSString *)path resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
FIRStorageReference *fileRef = [self getReference:path appName:appName];
FIRStorageUploadTask *uploadTask = [fileRef putFile:url metadata:firmetadata];
[self addUploadObservers:appName uploadTask:uploadTask path:path resolver:resolve rejecter:reject];
}
- (void)uploadData:(NSString *)appName data:(NSData *)data firmetadata:(FIRStorageMetadata *)firmetadata path:(NSString *)path resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
FIRStorageReference *fileRef = [self getReference:path appName:appName];
FIRStorageUploadTask *uploadTask = [fileRef putData:data metadata:firmetadata];
[self addUploadObservers:appName uploadTask:uploadTask path:path resolver:resolve rejecter:reject];
}
- (void)addUploadObservers:(NSString *)appName uploadTask:(FIRStorageUploadTask *)uploadTask path:(NSString *)path resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
// listen for state changes, errors, and completion of the upload.
[uploadTask observeStatus:FIRStorageTaskStatusResume handler:^(FIRStorageTaskSnapshot *snapshot) {
// upload resumed, also fires when the upload starts
NSDictionary *event = [self getUploadTaskAsDictionary:snapshot];
[self sendJSEvent:appName type:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event];
}];
[uploadTask observeStatus:FIRStorageTaskStatusPause handler:^(FIRStorageTaskSnapshot *snapshot) {
// upload paused
NSDictionary *event = [self getUploadTaskAsDictionary:snapshot];
[self sendJSEvent:appName type:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event];
}];
[uploadTask observeStatus:FIRStorageTaskStatusProgress handler:^(FIRStorageTaskSnapshot *snapshot) {
// upload reported progress
NSDictionary *event = [self getUploadTaskAsDictionary:snapshot];
[self sendJSEvent:appName type:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:event];
}];
[uploadTask observeStatus:FIRStorageTaskStatusSuccess handler:^(FIRStorageTaskSnapshot *snapshot) {
// upload completed successfully
NSDictionary *resp = [self getUploadTaskAsDictionary:snapshot];
[self sendJSEvent:appName type:STORAGE_EVENT path:path title:STORAGE_STATE_CHANGED props:resp];
[self sendJSEvent:appName type:STORAGE_EVENT path:path title:STORAGE_UPLOAD_SUCCESS props:resp];
resolve(resp);
}];
[uploadTask observeStatus:FIRStorageTaskStatusFailure handler:^(FIRStorageTaskSnapshot *snapshot) {
if (snapshot.error != nil) {
[self promiseRejectStorageException:reject error:snapshot.error];
}
}];
}
- (FIRStorageReference *)getReference:(NSString *)path
appName:(NSString *)appName {
if ([path hasPrefix:@"url::"]) {
NSString *url = [path substringFromIndex:5];
return [[FIRStorage storageForApp:[FIRApp appNamed:appName]] referenceForURL:url];
} else {
return [[FIRStorage storageForApp:[FIRApp appNamed:appName]] referenceWithPath:path];
}
}
- (NSDictionary *)getDownloadTaskAsDictionary:(FIRStorageTaskSnapshot *)task {
return @{@"bytesTransferred": @(task.progress.completedUnitCount), @"ref": task.reference.fullPath, @"state": [self getTaskStatus:task.status], @"totalBytes": @(task.progress.totalUnitCount)};
}
- (NSDictionary *)getUploadTaskAsDictionary:(FIRStorageTaskSnapshot *)task {
NSString *downloadUrl = [task.metadata.downloadURL absoluteString];
NSDictionary *metadata = [task.metadata dictionaryRepresentation];
return @{@"bytesTransferred": @(task.progress.completedUnitCount), @"downloadURL": downloadUrl != nil ? downloadUrl : [NSNull null], @"metadata": metadata != nil ? metadata : [NSNull null], @"ref": task.reference.fullPath, @"state": [self getTaskStatus:task.status], @"totalBytes": @(task.progress.totalUnitCount)};
}
- (FIRStorageMetadata *)buildMetadataFromMap:(NSDictionary *)metadata {
FIRStorageMetadata *storageMetadata = [[FIRStorageMetadata alloc] initWithDictionary:metadata];
storageMetadata.customMetadata = [metadata[@"customMetadata"] mutableCopy];
return storageMetadata;
}
- (NSString *)getTaskStatus:(FIRStorageTaskStatus)status {
if (status == FIRStorageTaskStatusResume || status == FIRStorageTaskStatusProgress) {
return @"running";
} else if (status == FIRStorageTaskStatusPause) {
return @"paused";
} else if (status == FIRStorageTaskStatusSuccess) {
return @"success";
} else if (status == FIRStorageTaskStatusFailure) {
return @"error";
} else {
return @"unknown";
}
}
- (NSString *)getPathForDirectory:(int)directory {
NSArray *paths = NSSearchPathForDirectoriesInDomains(directory, NSUserDomainMask, YES);
return [paths firstObject];
}
- (NSDictionary *)constantsToExport {
return @{@"MAIN_BUNDLE_PATH": [[NSBundle mainBundle] bundlePath], @"CACHES_DIRECTORY_PATH": [self getPathForDirectory:NSCachesDirectory], @"DOCUMENT_DIRECTORY_PATH": [self getPathForDirectory:NSDocumentDirectory], @"EXTERNAL_DIRECTORY_PATH": [NSNull null], @"EXTERNAL_STORAGE_DIRECTORY_PATH": [NSNull null], @"TEMP_DIRECTORY_PATH": NSTemporaryDirectory(), @"LIBRARY_DIRECTORY_PATH": [self getPathForDirectory:NSLibraryDirectory], @"FILETYPE_REGULAR": NSFileTypeRegular, @"FILETYPE_DIRECTORY": NSFileTypeDirectory};
}
- (NSArray<NSString *> *)supportedEvents {
return @[STORAGE_EVENT, STORAGE_ERROR];
}
- (void)sendJSError:(NSString *)appName error:(NSError *)error path:(NSString *)path {
NSDictionary *evt = @{@"path": path, @"message": [error debugDescription]};
[self sendJSEvent:appName type:STORAGE_ERROR path:path title:STORAGE_ERROR props:evt];
}
- (void)sendJSEvent:(NSString *)appName type:(NSString *)type path:(NSString *)path title:(NSString *)title props:(NSDictionary *)props {
@try {
[self sendEventWithName:type body:@{@"eventName": title, @"appName": appName, @"path": path, @"body": props}];
} @catch (NSException *err) {
NSLog(@"An error occurred in sendJSEvent: %@", [err debugDescription]);
NSLog(@"Tried to send: %@ with %@", title, props);
}
}
/**
Reject a promise with a storage exception
@param reject RCTPromiseRejectBlock
@param error NSError
*/
- (void)promiseRejectStorageException:(RCTPromiseRejectBlock)reject error:(NSError *)error {
NSString *code = @"storage/unknown";
NSString *message = [error localizedDescription];
NSDictionary *userInfo = [error userInfo];
NSError *underlyingError = userInfo[NSUnderlyingErrorKey];
NSString *underlyingErrorDescription = [underlyingError localizedDescription];
switch (error.code) {
case FIRStorageErrorCodeUnknown:
if ([underlyingErrorDescription isEqualToString:@"The operation couldnt be completed. Permission denied"]) {
code = @"storage/invalid-device-file-path";
message = @"The specified device file path is invalid or is restricted.";
} else {
code = @"storage/unknown";
message = @"An unknown error has occurred.";
}
break;
case FIRStorageErrorCodeObjectNotFound:
code = @"storage/object-not-found";
message = @"No object exists at the desired reference.";
break;
case FIRStorageErrorCodeBucketNotFound:
code = @"storage/bucket-not-found";
message = @"No bucket is configured for Firebase Storage.";
break;
case FIRStorageErrorCodeProjectNotFound:
code = @"storage/project-not-found";
message = @"No project is configured for Firebase Storage.";
break;
case FIRStorageErrorCodeQuotaExceeded:
code = @"storage/quota-exceeded";
message = @"Quota on your Firebase Storage bucket has been exceeded.";
break;
case FIRStorageErrorCodeUnauthenticated:
code = @"storage/unauthenticated";
message = @"User is unauthenticated. Authenticate and try again.";
break;
case FIRStorageErrorCodeUnauthorized:
code = @"storage/unauthorized";
message = @"User is not authorized to perform the desired action.";
break;
case FIRStorageErrorCodeRetryLimitExceeded:
code = @"storage/retry-limit-exceeded";
message = @"The maximum time limit on an operation (upload, download, delete, etc.) has been exceeded.";
break;
case FIRStorageErrorCodeNonMatchingChecksum:
code = @"storage/non-matching-checksum";
message = @"File on the client does not match the checksum of the file received by the server.";
break;
case FIRStorageErrorCodeDownloadSizeExceeded:
code = @"storage/download-size-exceeded";
message = @"Size of the downloaded file exceeds the amount of memory allocated for the download.";
break;
case FIRStorageErrorCodeCancelled:
code = @"storage/cancelled";
message = @"User cancelled the operation.";
break;
default:
break;
}
if (userInfo != nil && userInfo[@"data"]) {
// errors with 'data' are unserializable - it breaks react so we send nil instead
reject(code, message, nil);
} else {
reject(code, message, error);
}
}
+ (BOOL)requiresMainQueueSetup
{
return YES;
}
@end
#else
@implementation RNFirebaseStorage
@end
#endif