985 lines
38 KiB
Objective-C
985 lines
38 KiB
Objective-C
// Copyright 2016-present 650 Industries. All rights reserved.
|
|
|
|
#import <UMCore/UMModuleRegistry.h>
|
|
|
|
#import <EXFileSystem/EXFileSystem.h>
|
|
|
|
#import <CommonCrypto/CommonDigest.h>
|
|
#import <MobileCoreServices/MobileCoreServices.h>
|
|
|
|
#import <EXFileSystem/EXFileSystemLocalFileHandler.h>
|
|
#import <EXFileSystem/EXFileSystemAssetLibraryHandler.h>
|
|
|
|
#import <UMFileSystemInterface/UMFileSystemInterface.h>
|
|
#import <UMFileSystemInterface/UMFilePermissionModuleInterface.h>
|
|
|
|
#import <UMCore/UMEventEmitterService.h>
|
|
|
|
#import <EXFileSystem/EXResumablesManager.h>
|
|
#import <EXFileSystem/EXSessionTaskDispatcher.h>
|
|
#import <EXFileSystem/EXSessionDownloadTaskDelegate.h>
|
|
#import <EXFileSystem/EXSessionResumableDownloadTaskDelegate.h>
|
|
#import <EXFileSystem/EXSessionUploadTaskDelegate.h>
|
|
|
|
NSString * const EXDownloadProgressEventName = @"expo-file-system.downloadProgress";
|
|
|
|
typedef NS_ENUM(NSInteger, EXFileSystemSessionType) {
|
|
EXFileSystemBackgroundSession = 0,
|
|
EXFileSystemForegroundSession = 1,
|
|
};
|
|
|
|
typedef NS_ENUM(NSInteger, EXFileSystemUploadType) {
|
|
EXFileSystemInvalidType = -1,
|
|
EXFileSystemBinaryContent = 0,
|
|
EXFileSystemMultipart = 1,
|
|
};
|
|
|
|
@interface EXFileSystem ()
|
|
|
|
@property (nonatomic, strong) NSURLSession *backgroundSession;
|
|
@property (nonatomic, strong) NSURLSession *foregroundSession;
|
|
@property (nonatomic, strong) EXSessionTaskDispatcher *sessionTaskDispatcher;
|
|
@property (nonatomic, strong) EXResumablesManager *resumableManager;
|
|
@property (nonatomic, weak) UMModuleRegistry *moduleRegistry;
|
|
@property (nonatomic, weak) id<UMEventEmitterService> eventEmitter;
|
|
@property (nonatomic, strong) NSString *documentDirectory;
|
|
@property (nonatomic, strong) NSString *cachesDirectory;
|
|
@property (nonatomic, strong) NSString *bundleDirectory;
|
|
|
|
@end
|
|
|
|
@implementation EXFileSystem
|
|
|
|
UM_REGISTER_MODULE();
|
|
|
|
+ (const NSString *)exportedModuleName
|
|
{
|
|
return @"ExponentFileSystem";
|
|
}
|
|
|
|
+ (const NSArray<Protocol *> *)exportedInterfaces
|
|
{
|
|
return @[@protocol(UMFileSystemInterface)];
|
|
}
|
|
|
|
- (instancetype)initWithDocumentDirectory:(NSString *)documentDirectory cachesDirectory:(NSString *)cachesDirectory bundleDirectory:(NSString *)bundleDirectory
|
|
{
|
|
if (self = [super init]) {
|
|
_documentDirectory = documentDirectory;
|
|
_cachesDirectory = cachesDirectory;
|
|
_bundleDirectory = bundleDirectory;
|
|
|
|
_resumableManager = [EXResumablesManager new];
|
|
|
|
[EXFileSystem ensureDirExistsWithPath:_documentDirectory];
|
|
[EXFileSystem ensureDirExistsWithPath:_cachesDirectory];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
NSArray<NSString *> *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
|
NSString *documentDirectory = [documentPaths objectAtIndex:0];
|
|
|
|
NSArray<NSString *> *cachesPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
|
NSString *cacheDirectory = [cachesPaths objectAtIndex:0];
|
|
|
|
return [self initWithDocumentDirectory:documentDirectory
|
|
cachesDirectory:cacheDirectory
|
|
bundleDirectory:[NSBundle mainBundle].bundlePath];
|
|
}
|
|
|
|
- (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry
|
|
{
|
|
_moduleRegistry = moduleRegistry;
|
|
_eventEmitter = [_moduleRegistry getModuleImplementingProtocol:@protocol(UMEventEmitterService)];
|
|
|
|
_sessionTaskDispatcher = [[EXSessionTaskDispatcher alloc] initWithSessionHandler:[moduleRegistry getSingletonModuleForName:@"SessionHandler"]];
|
|
_backgroundSession = [self _createSession:EXFileSystemBackgroundSession delegate:_sessionTaskDispatcher];
|
|
_foregroundSession = [self _createSession:EXFileSystemForegroundSession delegate:_sessionTaskDispatcher];
|
|
}
|
|
|
|
- (NSDictionary *)constantsToExport
|
|
{
|
|
return @{
|
|
@"documentDirectory": _documentDirectory ? [NSURL fileURLWithPath:_documentDirectory].absoluteString : [NSNull null],
|
|
@"cacheDirectory": _cachesDirectory ? [NSURL fileURLWithPath:_cachesDirectory].absoluteString : [NSNull null],
|
|
@"bundleDirectory": _bundleDirectory ? [NSURL fileURLWithPath:_bundleDirectory].absoluteString : [NSNull null]
|
|
};
|
|
}
|
|
|
|
- (NSArray<NSString *> *)supportedEvents
|
|
{
|
|
return @[EXDownloadProgressEventName];
|
|
}
|
|
|
|
- (void)startObserving {
|
|
|
|
}
|
|
|
|
|
|
- (void)stopObserving {
|
|
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[_sessionTaskDispatcher deactivate];
|
|
[_backgroundSession invalidateAndCancel];
|
|
[_foregroundSession invalidateAndCancel];
|
|
}
|
|
|
|
- (NSDictionary *)encodingMap
|
|
{
|
|
/*
|
|
TODO:Bacon: match node.js fs
|
|
https://github.com/nodejs/node/blob/master/lib/buffer.js
|
|
ascii
|
|
base64
|
|
binary
|
|
hex
|
|
ucs2/ucs-2
|
|
utf16le/utf-16le
|
|
utf8/utf-8
|
|
latin1 (ISO8859-1, only in node 6.4.0+)
|
|
*/
|
|
return @{
|
|
@"ascii": @(NSASCIIStringEncoding),
|
|
@"nextstep": @(NSNEXTSTEPStringEncoding),
|
|
@"japaneseeuc": @(NSJapaneseEUCStringEncoding),
|
|
@"utf8": @(NSUTF8StringEncoding),
|
|
@"isolatin1": @(NSISOLatin1StringEncoding),
|
|
@"symbol": @(NSSymbolStringEncoding),
|
|
@"nonlossyascii": @(NSNonLossyASCIIStringEncoding),
|
|
@"shiftjis": @(NSShiftJISStringEncoding),
|
|
@"isolatin2": @(NSISOLatin2StringEncoding),
|
|
@"unicode": @(NSUnicodeStringEncoding),
|
|
@"windowscp1251": @(NSWindowsCP1251StringEncoding),
|
|
@"windowscp1252": @(NSWindowsCP1252StringEncoding),
|
|
@"windowscp1253": @(NSWindowsCP1253StringEncoding),
|
|
@"windowscp1254": @(NSWindowsCP1254StringEncoding),
|
|
@"windowscp1250": @(NSWindowsCP1250StringEncoding),
|
|
@"iso2022jp": @(NSISO2022JPStringEncoding),
|
|
@"macosroman": @(NSMacOSRomanStringEncoding),
|
|
@"utf16": @(NSUTF16StringEncoding),
|
|
@"utf16bigendian": @(NSUTF16BigEndianStringEncoding),
|
|
@"utf16littleendian": @(NSUTF16LittleEndianStringEncoding),
|
|
@"utf32": @(NSUTF32StringEncoding),
|
|
@"utf32bigendian": @(NSUTF32BigEndianStringEncoding),
|
|
@"utf32littleendian": @(NSUTF32LittleEndianStringEncoding),
|
|
};
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(getInfoAsync,
|
|
getInfoAsyncWithURI:(NSString *)uriString
|
|
withOptions:(NSDictionary *)options
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURL *uri = [NSURL URLWithString:uriString];
|
|
// no scheme provided in uri, handle as a local path and add 'file://' scheme
|
|
if (!uri.scheme) {
|
|
uri = [NSURL fileURLWithPath:uriString isDirectory:false];
|
|
}
|
|
if (!([self permissionsForURI:uri] & UMFileSystemPermissionRead)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"File '%@' isn't readable.", uri],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
if ([uri.scheme isEqualToString:@"file"]) {
|
|
[EXFileSystemLocalFileHandler getInfoForFile:uri withOptions:options resolver:resolve rejecter:reject];
|
|
} else if ([uri.scheme isEqualToString:@"assets-library"] || [uri.scheme isEqualToString:@"ph"]) {
|
|
[EXFileSystemAssetLibraryHandler getInfoForFile:uri withOptions:options resolver:resolve rejecter:reject];
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_INVALID_URI",
|
|
[NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
|
|
nil);
|
|
}
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(readAsStringAsync,
|
|
readAsStringAsyncWithURI:(NSString *)uriString
|
|
withOptions:(NSDictionary *)options
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURL *uri = [NSURL URLWithString:uriString];
|
|
// no scheme provided in uri, handle as a local path and add 'file://' scheme
|
|
if (!uri.scheme) {
|
|
uri = [NSURL fileURLWithPath:uriString isDirectory:false];
|
|
}
|
|
if (!([self permissionsForURI:uri] & UMFileSystemPermissionRead)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"File '%@' isn't readable.", uri],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
if ([uri.scheme isEqualToString:@"file"]) {
|
|
NSString *encodingType = @"utf8";
|
|
if (options[@"encoding"] && [options[@"encoding"] isKindOfClass:[NSString class]]) {
|
|
encodingType = [options[@"encoding"] lowercaseString];
|
|
}
|
|
if ([encodingType isEqualToString:@"base64"]) {
|
|
NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:uri.path];
|
|
if (file == nil) {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_READ_FILE",
|
|
[NSString stringWithFormat:@"File '%@' could not be read.", uri.path],
|
|
nil);
|
|
return;
|
|
}
|
|
// position and length are used as a cursor/paging system.
|
|
if ([options[@"position"] isKindOfClass:[NSNumber class]]) {
|
|
[file seekToFileOffset:[options[@"position"] intValue]];
|
|
}
|
|
|
|
NSData *data;
|
|
if ([options[@"length"] isKindOfClass:[NSNumber class]]) {
|
|
data = [file readDataOfLength:[options[@"length"] intValue]];
|
|
} else {
|
|
data = [file readDataToEndOfFile];
|
|
}
|
|
resolve([data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]);
|
|
} else {
|
|
NSUInteger encoding = NSUTF8StringEncoding;
|
|
id possibleEncoding = [[self encodingMap] valueForKey:encodingType];
|
|
if (possibleEncoding != nil) {
|
|
encoding = [possibleEncoding integerValue];
|
|
}
|
|
NSError *error;
|
|
NSString *string = [NSString stringWithContentsOfFile:uri.path encoding:encoding error:&error];
|
|
if (string) {
|
|
resolve(string);
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_READ_FILE",
|
|
[NSString stringWithFormat:@"File '%@' could not be read.", uri],
|
|
error);
|
|
}
|
|
}
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_INVALID_URI",
|
|
[NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
|
|
nil);
|
|
}
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(writeAsStringAsync,
|
|
writeAsStringAsyncWithURI:(NSString *)uriString
|
|
withString:(NSString *)string
|
|
withOptions:(NSDictionary *)options
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURL *uri = [NSURL URLWithString:uriString];
|
|
if (!([self permissionsForURI:uri] & UMFileSystemPermissionWrite)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"File '%@' isn't writable.", uri],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
if ([uri.scheme isEqualToString:@"file"]) {
|
|
NSString *encodingType = @"utf8";
|
|
if ([options[@"encoding"] isKindOfClass:[NSString class]]) {
|
|
encodingType = [options[@"encoding"] lowercaseString];
|
|
}
|
|
if ([encodingType isEqualToString:@"base64"]) {
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
NSData *imageData = [[NSData alloc] initWithBase64EncodedString:string options:NSDataBase64DecodingIgnoreUnknownCharacters];
|
|
if (imageData) {
|
|
// TODO:Bacon: Should we surface `attributes`?
|
|
if ([[NSFileManager defaultManager] createFileAtPath:uri.path contents:imageData attributes:nil]) {
|
|
resolve([NSNull null]);
|
|
} else {
|
|
return reject(@"ERR_FILESYSTEM_UNKNOWN_FILE",
|
|
[NSString stringWithFormat:@"No such file or directory '%@'", uri.path],
|
|
nil);
|
|
}
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_INVALID_FORMAT",
|
|
@"Failed to parse base64 string.",
|
|
nil);
|
|
}
|
|
});
|
|
} else {
|
|
NSUInteger encoding = NSUTF8StringEncoding;
|
|
id possibleEncoding = [[self encodingMap] valueForKey:encodingType];
|
|
if (possibleEncoding != nil) {
|
|
encoding = [possibleEncoding integerValue];
|
|
}
|
|
|
|
NSError *error;
|
|
if ([string writeToFile:uri.path atomically:YES encoding:encoding error:&error]) {
|
|
resolve([NSNull null]);
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_WRITE_TO_FILE",
|
|
[NSString stringWithFormat:@"File '%@' could not be written.", uri],
|
|
error);
|
|
}
|
|
}
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_INVALID_URI",
|
|
[NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
|
|
nil);
|
|
}
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(deleteAsync,
|
|
deleteAsyncWithURI:(NSString *)uriString
|
|
withOptions:(NSDictionary *)options
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURL *uri = [NSURL URLWithString:uriString];
|
|
if (!([self permissionsForURI:[uri URLByAppendingPathComponent:@".."]] & UMFileSystemPermissionWrite)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"Location '%@' isn't deletable.", uri],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
if ([uri.scheme isEqualToString:@"file"]) {
|
|
NSString *path = uri.path;
|
|
if ([self _checkIfFileExists:path]) {
|
|
NSError *error;
|
|
if ([[NSFileManager defaultManager] removeItemAtPath:path error:&error]) {
|
|
resolve([NSNull null]);
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_DELETE_FILE",
|
|
[NSString stringWithFormat:@"File '%@' could not be deleted.", uri],
|
|
error);
|
|
}
|
|
} else {
|
|
if (options[@"idempotent"]) {
|
|
resolve([NSNull null]);
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_FIND_FILE",
|
|
[NSString stringWithFormat:@"File '%@' could not be deleted because it could not be found.", uri],
|
|
nil);
|
|
}
|
|
}
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_INVALID_URI",
|
|
[NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
|
|
nil);
|
|
}
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(moveAsync,
|
|
moveAsyncWithOptions:(NSDictionary *)options
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURL *from = [NSURL URLWithString:options[@"from"]];
|
|
if (!from) {
|
|
reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `from` location.", nil);
|
|
return;
|
|
}
|
|
if (!([self permissionsForURI:[from URLByAppendingPathComponent:@".."]] & UMFileSystemPermissionWrite)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"Location '%@' isn't movable.", from],
|
|
nil);
|
|
return;
|
|
}
|
|
NSURL *to = [NSURL URLWithString:options[@"to"]];
|
|
if (!to) {
|
|
reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `to` location.", nil);
|
|
return;
|
|
}
|
|
if (!([self permissionsForURI:to] & UMFileSystemPermissionWrite)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"File '%@' isn't writable.", to],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
// NOTE: The destination-delete and the move should happen atomically, but we hope for the best for now
|
|
if ([from.scheme isEqualToString:@"file"]) {
|
|
NSString *fromPath = [from.path stringByStandardizingPath];
|
|
NSString *toPath = [to.path stringByStandardizingPath];
|
|
NSError *error;
|
|
if ([self _checkIfFileExists:toPath]) {
|
|
if (![[NSFileManager defaultManager] removeItemAtPath:toPath error:&error]) {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_MOVE_FILE",
|
|
[NSString stringWithFormat:@"File '%@' could not be moved to '%@' because a file already exists at "
|
|
"the destination and could not be deleted.", from, to],
|
|
error);
|
|
return;
|
|
}
|
|
}
|
|
if ([[NSFileManager defaultManager] moveItemAtPath:fromPath toPath:toPath error:&error]) {
|
|
resolve([NSNull null]);
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_MOVE_FILE",
|
|
[NSString stringWithFormat:@"File '%@' could not be moved to '%@'.", from, to],
|
|
error);
|
|
}
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_INVALID_URI",
|
|
[NSString stringWithFormat:@"Unsupported URI scheme for '%@'", from],
|
|
nil);
|
|
}
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(copyAsync,
|
|
copyAsyncWithOptions:(NSDictionary *)options
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURL *from = [NSURL URLWithString:options[@"from"]];
|
|
if (!from) {
|
|
reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `from` location.", nil);
|
|
return;
|
|
}
|
|
if (!([self permissionsForURI:from] & UMFileSystemPermissionRead)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"File '%@' isn't readable.", from],
|
|
nil);
|
|
return;
|
|
}
|
|
NSURL *to = [NSURL URLWithString:options[@"to"]];
|
|
if (!to) {
|
|
reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `to` location.", nil);
|
|
return;
|
|
}
|
|
if (!([self permissionsForURI:to] & UMFileSystemPermissionWrite)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"File '%@' isn't writable.", to],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
if ([from.scheme isEqualToString:@"file"]) {
|
|
[EXFileSystemLocalFileHandler copyFrom:from to:to resolver:resolve rejecter:reject];
|
|
} else if ([from.scheme isEqualToString:@"assets-library"] || [from.scheme isEqualToString:@"ph"]) {
|
|
[EXFileSystemAssetLibraryHandler copyFrom:from to:to resolver:resolve rejecter:reject];
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_INVALID_URI",
|
|
[NSString stringWithFormat:@"Unsupported URI scheme for '%@'", from],
|
|
nil);
|
|
}
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(makeDirectoryAsync,
|
|
makeDirectoryAsyncWithURI:(NSString *)uriString
|
|
withOptions:(NSDictionary *)options
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
|
|
NSURL *uri = [NSURL URLWithString:uriString];
|
|
if (!([self permissionsForURI:uri] & UMFileSystemPermissionWrite)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"Directory '%@' could not be created because the location isn't writable.", uri],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
if ([uri.scheme isEqualToString:@"file"]) {
|
|
NSError *error;
|
|
if ([[NSFileManager defaultManager] createDirectoryAtPath:uri.path
|
|
withIntermediateDirectories:[options[@"intermediates"] boolValue]
|
|
attributes:nil
|
|
error:&error]) {
|
|
resolve([NSNull null]);
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_CREATE_DIRECTORY",
|
|
[NSString stringWithFormat:@"Directory '%@' could not be created.", uri],
|
|
error);
|
|
}
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_INVALID_URI",
|
|
[NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
|
|
nil);
|
|
}
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(readDirectoryAsync,
|
|
readDirectoryAsyncWithURI:(NSString *)uriString
|
|
withOptions:(NSDictionary *)options
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURL *uri = [NSURL URLWithString:uriString];
|
|
if (!([self permissionsForURI:uri] & UMFileSystemPermissionRead)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"Location '%@' isn't readable.", uri],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
if ([uri.scheme isEqualToString:@"file"]) {
|
|
NSError *error;
|
|
NSArray<NSString *> *children = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:uri.path error:&error];
|
|
if (children) {
|
|
resolve(children);
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_READ_DIRECTORY",
|
|
[NSString stringWithFormat:@"Directory '%@' could not be read.", uri],
|
|
error);
|
|
}
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_INVALID_URI",
|
|
[NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
|
|
nil);
|
|
}
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(downloadAsync,
|
|
downloadAsyncWithUrl:(NSString *)urlString
|
|
localURI:(NSString *)localUriString
|
|
options:(NSDictionary *)options
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURL *url = [NSURL URLWithString:urlString];
|
|
NSURL *localUri = [NSURL URLWithString:localUriString];
|
|
if (!([self checkIfFileDirExists:localUri.path])) {
|
|
reject(@"ERR_FILESYSTEM_WRONG_DESTINATION",
|
|
[NSString stringWithFormat:@"Directory for '%@' doesn't exist. Please make sure directory '%@' exists before calling downloadAsync.", localUriString, [localUri.path stringByDeletingLastPathComponent]],
|
|
nil);
|
|
return;
|
|
}
|
|
if (!([self permissionsForURI:localUri] & UMFileSystemPermissionWrite)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"File '%@' isn't writable.", localUri],
|
|
nil);
|
|
return;
|
|
}
|
|
if (![self _checkHeadersDictionary:options[@"headers"]]) {
|
|
reject(@"ERR_FILESYSTEM_INVALID_HEADERS",
|
|
@"Invalid headers dictionary. Keys and values should be strings.",
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]];
|
|
if (!session) {
|
|
reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE",
|
|
[NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
NSURLRequest *request = [self _createRequest:url headers:options[@"headers"]];
|
|
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request];
|
|
EXSessionTaskDelegate *taskDelegate = [[EXSessionDownloadTaskDelegate alloc] initWithResolve:resolve
|
|
reject:reject
|
|
localUrl:localUri
|
|
shouldCalculateMd5:[options[@"md5"] boolValue]];
|
|
[_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task];
|
|
[task resume];
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(uploadAsync,
|
|
uploadAsync:(NSString *)urlString
|
|
localURI:(NSString *)fileUriString
|
|
options:(NSDictionary *)options
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURL *fileUri = [NSURL URLWithString:fileUriString];
|
|
NSString *httpMethod = options[@"httpMethod"];
|
|
EXFileSystemUploadType type = [self _getUploadTypeFrom:options[@"uploadType"]];
|
|
if (![fileUri.scheme isEqualToString:@"file"]) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"Cannot upload file '%@'. Only 'file://' URI are supported.", fileUri],
|
|
nil);
|
|
return;
|
|
}
|
|
if (!([self _checkIfFileExists:fileUri.path])) {
|
|
reject(@"ERR_FILE_NOT_EXISTS",
|
|
[NSString stringWithFormat:@"File '%@' does not exist.", fileUri],
|
|
nil);
|
|
return;
|
|
}
|
|
if (![self _checkHeadersDictionary:options[@"headers"]]) {
|
|
reject(@"ERR_FILESYSTEM_INVALID_HEADERS_DICTIONARY",
|
|
@"Invalid headers dictionary. Keys and values should be strings.",
|
|
nil);
|
|
return;
|
|
}
|
|
if (!httpMethod) {
|
|
reject(@"ERR_FILESYSTEM_MISSING_HTTP_METHOD", @"Missing HTTP method.", nil);
|
|
return;
|
|
}
|
|
|
|
NSMutableURLRequest *request = [self _createRequest:[NSURL URLWithString:urlString] headers:options[@"headers"]];
|
|
[request setHTTPMethod:httpMethod];
|
|
NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]];
|
|
if (!session) {
|
|
reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE",
|
|
[NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
NSURLSessionUploadTask *task;
|
|
if (type == EXFileSystemBinaryContent) {
|
|
task = [session uploadTaskWithRequest:request fromFile:fileUri];
|
|
} else if (type == EXFileSystemMultipart) {
|
|
NSString *boundaryString = [[NSUUID UUID] UUIDString];
|
|
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundaryString] forHTTPHeaderField:@"Content-Type"];
|
|
NSData *httpBody = [self _createBodyWithBoundary:boundaryString
|
|
fileUri:fileUri
|
|
parameters:options[@"parameters"]
|
|
fieldName:options[@"fieldName"]
|
|
mimeType:options[@"mimeType"]];
|
|
[request setHTTPBody:httpBody];
|
|
task = [session uploadTaskWithStreamedRequest:request];
|
|
} else {
|
|
reject(@"ERR_FILESYSTEM_INVALID_UPLOAD_TYPE",
|
|
[NSString stringWithFormat:@"Invalid upload type: '%@'.", options[@"uploadType"]],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
EXSessionTaskDelegate *taskDelegate = [[EXSessionUploadTaskDelegate alloc] initWithResolve:resolve reject:reject];
|
|
[_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task];
|
|
[task resume];
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(downloadResumableStartAsync,
|
|
downloadResumableStartAsyncWithUrl:(NSString *)urlString
|
|
fileURI:(NSString *)fileUri
|
|
uuid:(NSString *)uuid
|
|
options:(NSDictionary *)options
|
|
resumeData:(NSString *)data
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURL *url = [NSURL URLWithString:urlString];
|
|
NSURL *localUrl = [NSURL URLWithString:fileUri];
|
|
if (!([self checkIfFileDirExists:localUrl.path])) {
|
|
reject(@"ERR_FILESYSTEM_WRONG_DESTINATION",
|
|
[NSString stringWithFormat:@"Directory for '%@' doesn't exist. Please make sure directory '%@' exists before calling downloadAsync.", fileUri, [localUrl.path stringByDeletingLastPathComponent]],
|
|
nil);
|
|
return;
|
|
}
|
|
if (![localUrl.scheme isEqualToString:@"file"]) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"Cannot download to '%@': only 'file://' URI destinations are supported.", fileUri],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
NSString *path = localUrl.path;
|
|
if (!([self _permissionsForPath:path] & UMFileSystemPermissionWrite)) {
|
|
reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
|
|
[NSString stringWithFormat:@"File '%@' isn't writable.", fileUri],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
if (![self _checkHeadersDictionary:options[@"headers"]]) {
|
|
reject(@"ERR_FILESYSTEM_INVALID_HEADERS_DICTIONARY",
|
|
@"Invalid headers dictionary. Keys and values should be strings.",
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
NSData *resumeData = data ? [[NSData alloc] initWithBase64EncodedString:data options:0] : nil;
|
|
[self _downloadResumableCreateSessionWithUrl:url
|
|
fileUrl:localUrl
|
|
uuid:uuid
|
|
optins:options
|
|
resumeData:resumeData
|
|
resolve:resolve
|
|
reject:reject];
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(downloadResumablePauseAsync,
|
|
downloadResumablePauseAsyncWithUUID:(NSString *)uuid
|
|
resolver:(UMPromiseResolveBlock)resolve
|
|
rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
NSURLSessionDownloadTask *task = [_resumableManager taskForId:uuid];
|
|
if (!task) {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_FIND_TASK",
|
|
[NSString stringWithFormat:@"There is no download object with UUID: %@", uuid],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
UM_WEAKIFY(self);
|
|
[task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
|
|
UM_ENSURE_STRONGIFY(self);
|
|
resolve(@{ @"resumeData": UMNullIfNil([resumeData base64EncodedStringWithOptions:0]) });
|
|
}];
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(getFreeDiskStorageAsync, getFreeDiskStorageAsyncWithResolver:(UMPromiseResolveBlock)resolve rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
if(![self freeDiskStorage]) {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_DETERMINE_DISK_CAPACITY", @"Unable to determine free disk storage capacity", nil);
|
|
} else {
|
|
resolve([self freeDiskStorage]);
|
|
}
|
|
}
|
|
|
|
UM_EXPORT_METHOD_AS(getTotalDiskCapacityAsync, getTotalDiskCapacityAsyncWithResolver:(UMPromiseResolveBlock)resolve rejecter:(UMPromiseRejectBlock)reject)
|
|
{
|
|
if(![self totalDiskCapacity]) {
|
|
reject(@"ERR_FILESYSTEM_CANNOT_DETERMINE_DISK_CAPACITY", @"Unable to determine total disk capacity", nil);
|
|
} else {
|
|
resolve([self totalDiskCapacity]);
|
|
}
|
|
}
|
|
|
|
#pragma mark - Internal methods
|
|
|
|
- (EXFileSystemUploadType)_getUploadTypeFrom:(NSNumber * _Nullable)type
|
|
{
|
|
switch ([type intValue]) {
|
|
case EXFileSystemBinaryContent:
|
|
case EXFileSystemMultipart:
|
|
return [type intValue];
|
|
}
|
|
|
|
return EXFileSystemInvalidType;
|
|
}
|
|
|
|
// Borrowed from http://stackoverflow.com/questions/2439020/wheres-the-iphone-mime-type-database
|
|
- (NSString *)_guessMIMETypeFromPath:(NSString *)path
|
|
{
|
|
CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[path pathExtension], NULL);
|
|
CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
|
|
CFRelease(UTI);
|
|
if (!MIMEType) {
|
|
return @"application/octet-stream";
|
|
}
|
|
return (__bridge NSString *)(MIMEType);
|
|
}
|
|
|
|
- (NSData *)_createBodyWithBoundary:(NSString *)boundary
|
|
fileUri:(NSURL *)fileUri
|
|
parameters:(NSDictionary * _Nullable)parameters
|
|
fieldName:(NSString * _Nullable)fieldName
|
|
mimeType:(NSString * _Nullable)mimetype
|
|
{
|
|
|
|
NSMutableData *body = [NSMutableData data];
|
|
NSData *data = [NSData dataWithContentsOfURL:fileUri];
|
|
NSString *filename = [[fileUri path] lastPathComponent];
|
|
|
|
if (!mimetype) {
|
|
mimetype = [self _guessMIMETypeFromPath:[fileUri path]];
|
|
}
|
|
|
|
[parameters enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) {
|
|
[body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", parameterKey] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
[body appendData:[[NSString stringWithFormat:@"%@\r\n", parameterValue] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
}];
|
|
|
|
[body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
[body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fieldName ?: filename, filename] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
[body appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimetype] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
[body appendData:data];
|
|
[body appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
|
[body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
|
|
|
|
return body;
|
|
}
|
|
|
|
- (NSMutableURLRequest *)_createRequest:(NSURL *)url headers:(NSDictionary * _Nullable)headers
|
|
{
|
|
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
|
|
if (headers != nil) {
|
|
for (NSString *headerKey in headers) {
|
|
[request setValue:[headers valueForKey:headerKey] forHTTPHeaderField:headerKey];
|
|
}
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
- (NSURLSession *)_sessionForType:(EXFileSystemSessionType)type
|
|
{
|
|
switch (type) {
|
|
case EXFileSystemBackgroundSession:
|
|
return _backgroundSession;
|
|
case EXFileSystemForegroundSession:
|
|
return _foregroundSession;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (BOOL)_checkHeadersDictionary:(NSDictionary * _Nullable)headers
|
|
{
|
|
for (id key in [headers allKeys]) {
|
|
if (![key isKindOfClass:[NSString class]] || ![headers[key] isKindOfClass:[NSString class]]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
- (NSURLSession *)_createSession:(EXFileSystemSessionType)type delegate:(id<NSURLSessionDelegate>)delegate
|
|
{
|
|
NSURLSessionConfiguration *sessionConfiguration;
|
|
if (type == EXFileSystemForegroundSession) {
|
|
sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
|
|
} else {
|
|
sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[[NSUUID UUID] UUIDString]];
|
|
}
|
|
sessionConfiguration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
|
sessionConfiguration.URLCache = nil;
|
|
return [NSURLSession sessionWithConfiguration:sessionConfiguration
|
|
delegate:delegate
|
|
delegateQueue:nil];
|
|
}
|
|
|
|
- (BOOL)_checkIfFileExists:(NSString *)path
|
|
{
|
|
return [[NSFileManager defaultManager] fileExistsAtPath:path];
|
|
}
|
|
|
|
- (void)_downloadResumableCreateSessionWithUrl:(NSURL *)url
|
|
fileUrl:(NSURL *)fileUrl
|
|
uuid:(NSString *)uuid
|
|
optins:(NSDictionary *)options
|
|
resumeData:(NSData * _Nullable)resumeData
|
|
resolve:(UMPromiseResolveBlock)resolve
|
|
reject:(UMPromiseRejectBlock)reject
|
|
{
|
|
UM_WEAKIFY(self);
|
|
EXDownloadDelegateOnWriteCallback onWrite = ^(NSURLSessionDownloadTask *task, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
|
|
UM_ENSURE_STRONGIFY(self);
|
|
[self sendEventWithName:EXDownloadProgressEventName
|
|
body:@{
|
|
@"uuid": uuid,
|
|
@"data": @{
|
|
@"totalBytesWritten": @(totalBytesWritten),
|
|
@"totalBytesExpectedToWrite": @(totalBytesExpectedToWrite),
|
|
},
|
|
}];
|
|
};
|
|
|
|
NSURLSessionDownloadTask *downloadTask;
|
|
NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]];
|
|
if (!session) {
|
|
reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE",
|
|
[NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]],
|
|
nil);
|
|
return;
|
|
}
|
|
|
|
if (resumeData) {
|
|
downloadTask = [session downloadTaskWithResumeData:resumeData];
|
|
} else {
|
|
NSURLRequest *request = [self _createRequest:url headers:options[@"headers"]];
|
|
downloadTask = [session downloadTaskWithRequest:request];
|
|
}
|
|
EXSessionTaskDelegate *taskDelegate = [[EXSessionResumableDownloadTaskDelegate alloc] initWithResolve:resolve
|
|
reject:reject
|
|
localUrl:fileUrl
|
|
shouldCalculateMd5:[options[@"md5"] boolValue]
|
|
onWriteCallback:onWrite
|
|
resumableManager:_resumableManager
|
|
uuid:uuid];
|
|
[_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:downloadTask];
|
|
[_resumableManager registerTask:downloadTask uuid:uuid];
|
|
[downloadTask resume];
|
|
}
|
|
|
|
- (UMFileSystemPermissionFlags)_permissionsForPath:(NSString *)path
|
|
{
|
|
return [[_moduleRegistry getModuleImplementingProtocol:@protocol(UMFilePermissionModuleInterface)] getPathPermissions:(NSString *)path];
|
|
}
|
|
|
|
- (void)sendEventWithName:(NSString *)eventName body:(id)body
|
|
{
|
|
if (_eventEmitter != nil) {
|
|
[_eventEmitter sendEventWithName:eventName body:body];
|
|
}
|
|
}
|
|
|
|
- (NSDictionary *)documentFileSystemAttributes {
|
|
return [[NSFileManager defaultManager] attributesOfFileSystemForPath:_documentDirectory error:nil];
|
|
}
|
|
|
|
#pragma mark - Public utils
|
|
|
|
- (UMFileSystemPermissionFlags)permissionsForURI:(NSURL *)uri
|
|
{
|
|
NSArray *validSchemas = @[
|
|
@"assets-library",
|
|
@"http",
|
|
@"https",
|
|
@"ph",
|
|
];
|
|
if ([validSchemas containsObject:uri.scheme]) {
|
|
return UMFileSystemPermissionRead;
|
|
}
|
|
if ([uri.scheme isEqualToString:@"file"]) {
|
|
return [self _permissionsForPath:uri.path];
|
|
}
|
|
return UMFileSystemPermissionNone;
|
|
}
|
|
|
|
- (BOOL)checkIfFileDirExists:(NSString *)path
|
|
{
|
|
NSString *dir = [path stringByDeletingLastPathComponent];
|
|
return [self _checkIfFileExists:dir];
|
|
}
|
|
|
|
#pragma mark - Class methods
|
|
|
|
- (BOOL)ensureDirExistsWithPath:(NSString *)path
|
|
{
|
|
return [EXFileSystem ensureDirExistsWithPath:path];
|
|
}
|
|
|
|
+ (BOOL)ensureDirExistsWithPath:(NSString *)path
|
|
{
|
|
BOOL isDir = NO;
|
|
NSError *error;
|
|
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir];
|
|
if (!(exists && isDir)) {
|
|
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error];
|
|
if (error) {
|
|
return NO;
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension
|
|
{
|
|
return [EXFileSystem generatePathInDirectory:directory withExtension:extension];
|
|
}
|
|
|
|
+ (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension
|
|
{
|
|
NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension];
|
|
[EXFileSystem ensureDirExistsWithPath:directory];
|
|
return [directory stringByAppendingPathComponent:fileName];
|
|
}
|
|
|
|
- (NSNumber *)totalDiskCapacity {
|
|
NSDictionary *storage = [self documentFileSystemAttributes];
|
|
|
|
if (storage) {
|
|
NSNumber *fileSystemSizeInBytes = storage[NSFileSystemSize];
|
|
return fileSystemSizeInBytes;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (NSNumber *)freeDiskStorage {
|
|
NSDictionary *storage = [self documentFileSystemAttributes];
|
|
|
|
if (storage) {
|
|
NSNumber *freeFileSystemSizeInBytes = storage[NSFileSystemFreeSize];
|
|
return freeFileSystemSizeInBytes;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
@end
|