1// Copyright 2016-present 650 Industries. All rights reserved.
2
3#import <ExpoModulesCore/EXModuleRegistry.h>
4
5#import <ExpoFileSystem/EXFileSystem.h>
6
7#import <CommonCrypto/CommonDigest.h>
8#import <MobileCoreServices/MobileCoreServices.h>
9
10#import <ExpoFileSystem/EXFileSystemLocalFileHandler.h>
11#import <ExpoFileSystem/EXFileSystemAssetLibraryHandler.h>
12
13#import <ExpoModulesCore/EXFileSystemInterface.h>
14#import <ExpoModulesCore/EXFilePermissionModuleInterface.h>
15
16#import <ExpoModulesCore/EXEventEmitterService.h>
17
18#import <ExpoFileSystem/EXTaskHandlersManager.h>
19#import <ExpoFileSystem/EXSessionTaskDispatcher.h>
20#import <ExpoFileSystem/EXSessionDownloadTaskDelegate.h>
21#import <ExpoFileSystem/EXSessionResumableDownloadTaskDelegate.h>
22#import <ExpoFileSystem/EXSessionUploadTaskDelegate.h>
23#import <ExpoFileSystem/EXSessionCancelableUploadTaskDelegate.h>
24
25NSString * const EXDownloadProgressEventName = @"expo-file-system.downloadProgress";
26NSString * const EXUploadProgressEventName = @"expo-file-system.uploadProgress";
27
28typedef NS_ENUM(NSInteger, EXFileSystemSessionType) {
29  EXFileSystemBackgroundSession = 0,
30  EXFileSystemForegroundSession = 1,
31};
32
33typedef NS_ENUM(NSInteger, EXFileSystemUploadType) {
34  EXFileSystemInvalidType = -1,
35  EXFileSystemBinaryContent = 0,
36  EXFileSystemMultipart = 1,
37};
38
39@interface EXFileSystem ()
40
41@property (nonatomic, strong) NSURLSession *backgroundSession;
42@property (nonatomic, strong) NSURLSession *foregroundSession;
43@property (nonatomic, strong) EXSessionTaskDispatcher *sessionTaskDispatcher;
44@property (nonatomic, strong) EXTaskHandlersManager *taskHandlersManager;
45@property (nonatomic, weak) EXModuleRegistry *moduleRegistry;
46@property (nonatomic, weak) id<EXEventEmitterService> eventEmitter;
47@property (nonatomic, strong) NSString *documentDirectory;
48@property (nonatomic, strong) NSString *cachesDirectory;
49@property (nonatomic, strong) NSString *bundleDirectory;
50
51@end
52
53@implementation EXFileSystem
54
55EX_REGISTER_MODULE();
56
57+ (const NSString *)exportedModuleName
58{
59  return @"ExponentFileSystem";
60}
61
62+ (const NSArray<Protocol *> *)exportedInterfaces
63{
64  return @[@protocol(EXFileSystemInterface)];
65}
66
67- (instancetype)initWithDocumentDirectory:(NSString *)documentDirectory cachesDirectory:(NSString *)cachesDirectory bundleDirectory:(NSString *)bundleDirectory
68{
69  if (self = [super init]) {
70    _documentDirectory = documentDirectory;
71    _cachesDirectory = cachesDirectory;
72    _bundleDirectory = bundleDirectory;
73
74    _taskHandlersManager = [EXTaskHandlersManager new];
75
76    [EXFileSystem ensureDirExistsWithPath:_documentDirectory];
77    [EXFileSystem ensureDirExistsWithPath:_cachesDirectory];
78  }
79  return self;
80}
81
82- (instancetype)init
83{
84  NSArray<NSString *> *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
85  NSString *documentDirectory = [documentPaths objectAtIndex:0];
86
87  NSArray<NSString *> *cachesPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
88  NSString *cacheDirectory = [cachesPaths objectAtIndex:0];
89
90  return [self initWithDocumentDirectory:documentDirectory
91                         cachesDirectory:cacheDirectory
92                         bundleDirectory:[NSBundle mainBundle].bundlePath];
93}
94
95- (void)setModuleRegistry:(EXModuleRegistry *)moduleRegistry
96{
97  _moduleRegistry = moduleRegistry;
98  _eventEmitter = [_moduleRegistry getModuleImplementingProtocol:@protocol(EXEventEmitterService)];
99
100  _sessionTaskDispatcher = [[EXSessionTaskDispatcher alloc] initWithSessionHandler:[moduleRegistry getSingletonModuleForName:@"SessionHandler"]];
101  _backgroundSession = [self _createSession:EXFileSystemBackgroundSession delegate:_sessionTaskDispatcher];
102  _foregroundSession = [self _createSession:EXFileSystemForegroundSession delegate:_sessionTaskDispatcher];
103}
104
105- (NSDictionary *)constantsToExport
106{
107  return @{
108           @"documentDirectory": _documentDirectory ? [NSURL fileURLWithPath:_documentDirectory].absoluteString : [NSNull null],
109           @"cacheDirectory": _cachesDirectory ? [NSURL fileURLWithPath:_cachesDirectory].absoluteString : [NSNull null],
110           @"bundleDirectory": _bundleDirectory ? [NSURL fileURLWithPath:_bundleDirectory].absoluteString : [NSNull null]
111           };
112}
113
114- (NSArray<NSString *> *)supportedEvents
115{
116  return @[EXDownloadProgressEventName, EXUploadProgressEventName];
117}
118
119- (void)startObserving {
120
121}
122
123
124- (void)stopObserving {
125
126}
127
128- (void)dealloc
129{
130  [_sessionTaskDispatcher deactivate];
131  [_backgroundSession invalidateAndCancel];
132  [_foregroundSession invalidateAndCancel];
133}
134
135- (NSDictionary *)encodingMap
136{
137  /*
138   TODO:Bacon: match node.js fs
139   https://github.com/nodejs/node/blob/master/lib/buffer.js
140   ascii
141   base64
142   binary
143   hex
144   ucs2/ucs-2
145   utf16le/utf-16le
146   utf8/utf-8
147   latin1 (ISO8859-1, only in node 6.4.0+)
148   */
149  return @{
150           @"ascii": @(NSASCIIStringEncoding),
151           @"nextstep": @(NSNEXTSTEPStringEncoding),
152           @"japaneseeuc": @(NSJapaneseEUCStringEncoding),
153           @"utf8": @(NSUTF8StringEncoding),
154           @"isolatin1": @(NSISOLatin1StringEncoding),
155           @"symbol": @(NSSymbolStringEncoding),
156           @"nonlossyascii": @(NSNonLossyASCIIStringEncoding),
157           @"shiftjis": @(NSShiftJISStringEncoding),
158           @"isolatin2": @(NSISOLatin2StringEncoding),
159           @"unicode": @(NSUnicodeStringEncoding),
160           @"windowscp1251": @(NSWindowsCP1251StringEncoding),
161           @"windowscp1252": @(NSWindowsCP1252StringEncoding),
162           @"windowscp1253": @(NSWindowsCP1253StringEncoding),
163           @"windowscp1254": @(NSWindowsCP1254StringEncoding),
164           @"windowscp1250": @(NSWindowsCP1250StringEncoding),
165           @"iso2022jp": @(NSISO2022JPStringEncoding),
166           @"macosroman": @(NSMacOSRomanStringEncoding),
167           @"utf16": @(NSUTF16StringEncoding),
168           @"utf16bigendian": @(NSUTF16BigEndianStringEncoding),
169           @"utf16littleendian": @(NSUTF16LittleEndianStringEncoding),
170           @"utf32": @(NSUTF32StringEncoding),
171           @"utf32bigendian": @(NSUTF32BigEndianStringEncoding),
172           @"utf32littleendian": @(NSUTF32LittleEndianStringEncoding),
173           };
174}
175
176EX_EXPORT_METHOD_AS(getInfoAsync,
177                    getInfoAsyncWithURI:(NSString *)uriString
178                    withOptions:(NSDictionary *)options
179                    resolver:(EXPromiseResolveBlock)resolve
180                    rejecter:(EXPromiseRejectBlock)reject)
181{
182  NSURL *uri = [self percentEncodedURLFromURIString:uriString];
183  // no scheme provided in uri, handle as a local path and add 'file://' scheme
184  if (!uri.scheme) {
185    uri = [NSURL fileURLWithPath:uriString isDirectory:false];
186  }
187  if (!([self permissionsForURI:uri] & EXFileSystemPermissionRead)) {
188    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
189           [NSString stringWithFormat:@"File '%@' isn't readable.", uri],
190           nil);
191    return;
192  }
193
194  if ([uri.scheme isEqualToString:@"file"]) {
195    [EXFileSystemLocalFileHandler getInfoForFile:uri withOptions:options resolver:resolve rejecter:reject];
196  } else if ([uri.scheme isEqualToString:@"assets-library"] || [uri.scheme isEqualToString:@"ph"]) {
197    [EXFileSystemAssetLibraryHandler getInfoForFile:uri withOptions:options resolver:resolve rejecter:reject];
198  } else {
199    reject(@"ERR_FILESYSTEM_INVALID_URI",
200           [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
201           nil);
202  }
203}
204
205EX_EXPORT_METHOD_AS(readAsStringAsync,
206                    readAsStringAsyncWithURI:(NSString *)uriString
207                    withOptions:(NSDictionary *)options
208                    resolver:(EXPromiseResolveBlock)resolve
209                    rejecter:(EXPromiseRejectBlock)reject)
210{
211  NSURL *uri = [self percentEncodedURLFromURIString:uriString];
212  // no scheme provided in uri, handle as a local path and add 'file://' scheme
213  if (!uri.scheme) {
214    uri = [NSURL fileURLWithPath:uriString isDirectory:false];
215  }
216  if (!([self permissionsForURI:uri] & EXFileSystemPermissionRead)) {
217    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
218           [NSString stringWithFormat:@"File '%@' isn't readable.", uri],
219           nil);
220    return;
221  }
222
223  if ([uri.scheme isEqualToString:@"file"]) {
224    NSString *encodingType = @"utf8";
225    if (options[@"encoding"] && [options[@"encoding"] isKindOfClass:[NSString class]]) {
226      encodingType = [options[@"encoding"] lowercaseString];
227    }
228    if ([encodingType isEqualToString:@"base64"]) {
229      NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:uri.path];
230      if (file == nil) {
231        reject(@"ERR_FILESYSTEM_CANNOT_READ_FILE",
232               [NSString stringWithFormat:@"File '%@' could not be read.", uri.path],
233               nil);
234        return;
235      }
236      // position and length are used as a cursor/paging system.
237      if ([options[@"position"] isKindOfClass:[NSNumber class]]) {
238        [file seekToFileOffset:[options[@"position"] intValue]];
239      }
240
241      NSData *data;
242      if ([options[@"length"] isKindOfClass:[NSNumber class]]) {
243        data = [file readDataOfLength:[options[@"length"] intValue]];
244      } else {
245        data = [file readDataToEndOfFile];
246      }
247      resolve([data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]);
248    } else {
249      NSUInteger encoding = NSUTF8StringEncoding;
250      id possibleEncoding = [[self encodingMap] valueForKey:encodingType];
251      if (possibleEncoding != nil) {
252        encoding = [possibleEncoding integerValue];
253      }
254      NSError *error;
255      NSString *string = [NSString stringWithContentsOfFile:uri.path encoding:encoding error:&error];
256      if (string) {
257        resolve(string);
258      } else {
259        reject(@"ERR_FILESYSTEM_CANNOT_READ_FILE",
260               [NSString stringWithFormat:@"File '%@' could not be read.", uri],
261               error);
262      }
263    }
264  } else {
265    reject(@"ERR_FILESYSTEM_INVALID_URI",
266           [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
267           nil);
268  }
269}
270
271EX_EXPORT_METHOD_AS(writeAsStringAsync,
272                    writeAsStringAsyncWithURI:(NSString *)uriString
273                    withString:(NSString *)string
274                    withOptions:(NSDictionary *)options
275                    resolver:(EXPromiseResolveBlock)resolve
276                    rejecter:(EXPromiseRejectBlock)reject)
277{
278  NSURL *uri = [self percentEncodedURLFromURIString:uriString];
279  if (!([self permissionsForURI:uri] & EXFileSystemPermissionWrite)) {
280    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
281           [NSString stringWithFormat:@"File '%@' isn't writable.", uri],
282           nil);
283    return;
284  }
285
286  if ([uri.scheme isEqualToString:@"file"]) {
287    NSString *encodingType = @"utf8";
288    if ([options[@"encoding"] isKindOfClass:[NSString class]]) {
289      encodingType = [options[@"encoding"] lowercaseString];
290    }
291    if ([encodingType isEqualToString:@"base64"]) {
292      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
293        NSData *imageData = [[NSData alloc] initWithBase64EncodedString:string options:NSDataBase64DecodingIgnoreUnknownCharacters];
294        if (imageData) {
295          // TODO:Bacon: Should we surface `attributes`?
296          if ([[NSFileManager defaultManager] createFileAtPath:uri.path contents:imageData attributes:nil]) {
297            resolve([NSNull null]);
298          } else {
299            return reject(@"ERR_FILESYSTEM_UNKNOWN_FILE",
300                          [NSString stringWithFormat:@"No such file or directory '%@'", uri.path],
301                          nil);
302          }
303        } else {
304          reject(@"ERR_FILESYSTEM_INVALID_FORMAT",
305                 @"Failed to parse base64 string.",
306                 nil);
307        }
308      });
309    } else {
310      NSUInteger encoding = NSUTF8StringEncoding;
311      id possibleEncoding = [[self encodingMap] valueForKey:encodingType];
312      if (possibleEncoding != nil) {
313        encoding = [possibleEncoding integerValue];
314      }
315
316      NSError *error;
317      if ([string writeToFile:uri.path atomically:YES encoding:encoding error:&error]) {
318        resolve([NSNull null]);
319      } else {
320        reject(@"ERR_FILESYSTEM_CANNOT_WRITE_TO_FILE",
321               [NSString stringWithFormat:@"File '%@' could not be written.", uri],
322               error);
323      }
324    }
325  } else {
326    reject(@"ERR_FILESYSTEM_INVALID_URI",
327           [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
328           nil);
329  }
330}
331
332EX_EXPORT_METHOD_AS(deleteAsync,
333                    deleteAsyncWithURI:(NSString *)uriString
334                    withOptions:(NSDictionary *)options
335                    resolver:(EXPromiseResolveBlock)resolve
336                    rejecter:(EXPromiseRejectBlock)reject)
337{
338  NSURL *uri = [self percentEncodedURLFromURIString:uriString];
339  if (!([self permissionsForURI:[uri URLByAppendingPathComponent:@".."]] & EXFileSystemPermissionWrite)) {
340    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
341           [NSString stringWithFormat:@"Location '%@' isn't deletable.", uri],
342           nil);
343    return;
344  }
345
346  if ([uri.scheme isEqualToString:@"file"]) {
347    NSString *path = uri.path;
348    if ([self _checkIfFileExists:path]) {
349      NSError *error;
350      if ([[NSFileManager defaultManager] removeItemAtPath:path error:&error]) {
351        resolve([NSNull null]);
352      } else {
353        reject(@"ERR_FILESYSTEM_CANNOT_DELETE_FILE",
354               [NSString stringWithFormat:@"File '%@' could not be deleted.", uri],
355               error);
356      }
357    } else {
358      if (options[@"idempotent"]) {
359        resolve([NSNull null]);
360      } else {
361        reject(@"ERR_FILESYSTEM_CANNOT_FIND_FILE",
362               [NSString stringWithFormat:@"File '%@' could not be deleted because it could not be found.", uri],
363               nil);
364      }
365    }
366  } else {
367    reject(@"ERR_FILESYSTEM_INVALID_URI",
368           [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
369           nil);
370  }
371}
372
373EX_EXPORT_METHOD_AS(moveAsync,
374                    moveAsyncWithOptions:(NSDictionary *)options
375                    resolver:(EXPromiseResolveBlock)resolve
376                    rejecter:(EXPromiseRejectBlock)reject)
377{
378  NSURL *from = [NSURL URLWithString:options[@"from"]];
379  if (!from) {
380    reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `from` location.", nil);
381    return;
382  }
383  if (!([self permissionsForURI:[from URLByAppendingPathComponent:@".."]] & EXFileSystemPermissionWrite)) {
384    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
385           [NSString stringWithFormat:@"Location '%@' isn't movable.", from],
386           nil);
387    return;
388  }
389  NSURL *to = [NSURL URLWithString:options[@"to"]];
390  if (!to) {
391    reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `to` location.", nil);
392    return;
393  }
394  if (!([self permissionsForURI:to] & EXFileSystemPermissionWrite)) {
395    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
396           [NSString stringWithFormat:@"File '%@' isn't writable.", to],
397           nil);
398    return;
399  }
400
401  // NOTE: The destination-delete and the move should happen atomically, but we hope for the best for now
402  if ([from.scheme isEqualToString:@"file"]) {
403    NSString *fromPath = [from.path stringByStandardizingPath];
404    NSString *toPath = [to.path stringByStandardizingPath];
405    NSError *error;
406    if ([self _checkIfFileExists:toPath]) {
407      if (![[NSFileManager defaultManager] removeItemAtPath:toPath error:&error]) {
408        reject(@"ERR_FILESYSTEM_CANNOT_MOVE_FILE",
409               [NSString stringWithFormat:@"File '%@' could not be moved to '%@' because a file already exists at "
410                "the destination and could not be deleted.", from, to],
411               error);
412        return;
413      }
414    }
415    if ([[NSFileManager defaultManager] moveItemAtPath:fromPath toPath:toPath error:&error]) {
416      resolve([NSNull null]);
417    } else {
418      reject(@"ERR_FILESYSTEM_CANNOT_MOVE_FILE",
419             [NSString stringWithFormat:@"File '%@' could not be moved to '%@'.", from, to],
420             error);
421    }
422  } else {
423    reject(@"ERR_FILESYSTEM_INVALID_URI",
424           [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", from],
425           nil);
426  }
427}
428
429EX_EXPORT_METHOD_AS(copyAsync,
430                    copyAsyncWithOptions:(NSDictionary *)options
431                    resolver:(EXPromiseResolveBlock)resolve
432                    rejecter:(EXPromiseRejectBlock)reject)
433{
434  NSURL *from = [NSURL URLWithString:options[@"from"]];
435  if (!from) {
436    reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `from` location.", nil);
437    return;
438  }
439  if (!([self permissionsForURI:from] & EXFileSystemPermissionRead)) {
440    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
441           [NSString stringWithFormat:@"File '%@' isn't readable.", from],
442           nil);
443    return;
444  }
445  NSURL *to = [NSURL URLWithString:options[@"to"]];
446  if (!to) {
447    reject(@"ERR_FILESYSTEM_MISSING_PARAMETER", @"Need a `to` location.", nil);
448    return;
449  }
450  if (!([self permissionsForURI:to] & EXFileSystemPermissionWrite)) {
451    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
452           [NSString stringWithFormat:@"File '%@' isn't writable.", to],
453           nil);
454    return;
455  }
456
457  if ([from.scheme isEqualToString:@"file"]) {
458    [EXFileSystemLocalFileHandler copyFrom:from to:to resolver:resolve rejecter:reject];
459  } else if ([from.scheme isEqualToString:@"assets-library"] || [from.scheme isEqualToString:@"ph"]) {
460    [EXFileSystemAssetLibraryHandler copyFrom:from to:to resolver:resolve rejecter:reject];
461  } else {
462    reject(@"ERR_FILESYSTEM_INVALID_URI",
463           [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", from],
464           nil);
465  }
466}
467
468EX_EXPORT_METHOD_AS(makeDirectoryAsync,
469                    makeDirectoryAsyncWithURI:(NSString *)uriString
470                    withOptions:(NSDictionary *)options
471                    resolver:(EXPromiseResolveBlock)resolve
472                    rejecter:(EXPromiseRejectBlock)reject)
473{
474
475  NSURL *uri = [self percentEncodedURLFromURIString:uriString];
476 if (!([self permissionsForURI:uri] & EXFileSystemPermissionWrite)) {
477    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
478           [NSString stringWithFormat:@"Directory '%@' could not be created because the location isn't writable.", uri],
479           nil);
480    return;
481  }
482
483  if ([uri.scheme isEqualToString:@"file"]) {
484    NSError *error;
485    if ([[NSFileManager defaultManager] createDirectoryAtPath:uri.path
486                                  withIntermediateDirectories:[options[@"intermediates"] boolValue]
487                                                   attributes:nil
488                                                        error:&error]) {
489      resolve([NSNull null]);
490    } else {
491      reject(@"ERR_FILESYSTEM_CANNOT_CREATE_DIRECTORY",
492             [NSString stringWithFormat:@"Directory '%@' could not be created.", uri],
493             error);
494    }
495  } else {
496    reject(@"ERR_FILESYSTEM_INVALID_URI",
497           [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
498           nil);
499  }
500}
501
502EX_EXPORT_METHOD_AS(readDirectoryAsync,
503                    readDirectoryAsyncWithURI:(NSString *)uriString
504                    resolver:(EXPromiseResolveBlock)resolve
505                    rejecter:(EXPromiseRejectBlock)reject)
506{
507  NSURL *uri = [self percentEncodedURLFromURIString:uriString];
508  if (!([self permissionsForURI:uri] & EXFileSystemPermissionRead)) {
509    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
510           [NSString stringWithFormat:@"Location '%@' isn't readable.", uri],
511           nil);
512    return;
513  }
514
515  if ([uri.scheme isEqualToString:@"file"]) {
516    NSError *error;
517    NSArray<NSString *> *children = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:uri.path error:&error];
518    if (children) {
519      resolve(children);
520    } else {
521      reject(@"ERR_FILESYSTEM_CANNOT_READ_DIRECTORY",
522             [NSString stringWithFormat:@"Directory '%@' could not be read.", uri],
523             error);
524    }
525  } else {
526    reject(@"ERR_FILESYSTEM_INVALID_URI",
527           [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
528           nil);
529  }
530}
531
532EX_EXPORT_METHOD_AS(downloadAsync,
533                    downloadAsyncWithUrl:(NSString *)urlString
534                                localURI:(NSString *)localUriString
535                                 options:(NSDictionary *)options
536                                resolver:(EXPromiseResolveBlock)resolve
537                                rejecter:(EXPromiseRejectBlock)reject)
538{
539  NSURL *url = [NSURL URLWithString:urlString];
540  NSURL *localUri = [self percentEncodedURLFromURIString:localUriString];
541  if (!([self checkIfFileDirExists:localUri.path])) {
542    reject(@"ERR_FILESYSTEM_WRONG_DESTINATION",
543           [NSString stringWithFormat:@"Directory for '%@' doesn't exist. Please make sure directory '%@' exists before calling downloadAsync.", localUriString, [localUri.path stringByDeletingLastPathComponent]],
544           nil);
545    return;
546  }
547  if (!([self permissionsForURI:localUri] & EXFileSystemPermissionWrite)) {
548    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
549           [NSString stringWithFormat:@"File '%@' isn't writable.", localUri],
550           nil);
551    return;
552  }
553  if (![self _checkHeadersDictionary:options[@"headers"]]) {
554    reject(@"ERR_FILESYSTEM_INVALID_HEADERS",
555           @"Invalid headers dictionary. Keys and values should be strings.",
556           nil);
557    return;
558  }
559
560  NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]];
561  if (!session) {
562    reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE",
563           [NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]],
564           nil);
565    return;
566  }
567
568  NSURLRequest *request = [self _createRequest:url headers:options[@"headers"]];
569  NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request];
570  EXSessionTaskDelegate *taskDelegate = [[EXSessionDownloadTaskDelegate alloc] initWithResolve:resolve
571                                                                                        reject:reject
572                                                                                      localUrl:localUri
573                                                                            shouldCalculateMd5:[options[@"md5"] boolValue]];
574  [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task];
575  [task resume];
576}
577
578EX_EXPORT_METHOD_AS(uploadAsync,
579                    uploadAsync:(NSString *)urlString
580                       localURI:(NSString *)fileUriString
581                        options:(NSDictionary *)options
582                       resolver:(EXPromiseResolveBlock)resolve
583                       rejecter:(EXPromiseRejectBlock)reject)
584{
585  NSURLSessionUploadTask *task = [self createUploadTask:urlString localURI:fileUriString options:options rejecter:reject];
586  if (!task) {
587    return;
588  }
589
590  EXSessionTaskDelegate *taskDelegate = [[EXSessionUploadTaskDelegate alloc] initWithResolve:resolve reject:reject];
591  [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task];
592  [task resume];
593}
594
595EX_EXPORT_METHOD_AS(uploadTaskStartAsync,
596                    uploadTaskStartAsync:(NSString *)urlString
597                                localURI:(NSString *)fileUriString
598                                    uuid:(NSString *)uuid
599                                 options:(NSDictionary *)options
600                                resolver:(EXPromiseResolveBlock)resolve
601                                rejecter:(EXPromiseRejectBlock)reject)
602{
603  NSURLSessionUploadTask *task = [self createUploadTask:urlString localURI:fileUriString options:options rejecter:reject];
604  if (!task) {
605    return;
606  }
607
608  EX_WEAKIFY(self);
609  EXUploadDelegateOnSendCallback onSend = ^(NSURLSessionUploadTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
610    EX_ENSURE_STRONGIFY(self);
611    [self sendEventWithName:EXUploadProgressEventName
612                       body:@{
613                             @"uuid": uuid,
614                             @"data": @{
615                                 @"totalBytesSent": @(totalBytesSent),
616                                 @"totalBytesExpectedToSend": @(totalBytesExpectedToSend),
617                             },
618                           }];
619  };
620
621  EXSessionTaskDelegate *taskDelegate = [[EXSessionCancelableUploadTaskDelegate alloc] initWithResolve:resolve
622                                                                                          reject:reject
623                                                                                  onSendCallback:onSend
624                                                                                resumableManager:_taskHandlersManager
625                                                                                            uuid:uuid];
626
627  [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task];
628  [_taskHandlersManager registerTask:task uuid:uuid];
629  [task resume];
630}
631
632- (NSURLSessionUploadTask * _Nullable)createUploadTask:(NSString *)urlString
633                                              localURI:(NSString *)fileUriString
634                                               options:(NSDictionary *)options
635                                              rejecter:(EXPromiseRejectBlock)reject
636{
637  NSURL *fileUri = [self percentEncodedURLFromURIString:fileUriString];
638  NSString *httpMethod = options[@"httpMethod"];
639  EXFileSystemUploadType type = [self _getUploadTypeFrom:options[@"uploadType"]];
640  if (![fileUri.scheme isEqualToString:@"file"]) {
641    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
642           [NSString stringWithFormat:@"Cannot upload file '%@'. Only 'file://' URI are supported.", fileUri],
643           nil);
644    return nil;
645  }
646  if (!([self _checkIfFileExists:fileUri.path])) {
647    reject(@"ERR_FILE_NOT_EXISTS",
648           [NSString stringWithFormat:@"File '%@' does not exist.", fileUri],
649           nil);
650    return nil;
651  }
652  if (![self _checkHeadersDictionary:options[@"headers"]]) {
653    reject(@"ERR_FILESYSTEM_INVALID_HEADERS_DICTIONARY",
654           @"Invalid headers dictionary. Keys and values should be strings.",
655           nil);
656    return nil;
657  }
658  if (!httpMethod) {
659    reject(@"ERR_FILESYSTEM_MISSING_HTTP_METHOD", @"Missing HTTP method.", nil);
660    return nil;
661  }
662
663  NSMutableURLRequest *request = [self _createRequest:[NSURL URLWithString:urlString] headers:options[@"headers"]];
664  [request setHTTPMethod:httpMethod];
665  NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]];
666  if (!session) {
667    reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE",
668           [NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]],
669           nil);
670    return nil;
671  }
672
673  NSURLSessionUploadTask *task;
674  if (type == EXFileSystemBinaryContent) {
675    task = [session uploadTaskWithRequest:request fromFile:fileUri];
676  } else if (type == EXFileSystemMultipart) {
677    NSString *boundaryString = [[NSUUID UUID] UUIDString];
678    [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundaryString] forHTTPHeaderField:@"Content-Type"];
679    NSData *httpBody = [self _createBodyWithBoundary:boundaryString
680                                             fileUri:fileUri
681                                          parameters:options[@"parameters"]
682                                           fieldName:options[@"fieldName"]
683                                            mimeType:options[@"mimeType"]];
684    [request setHTTPBody:httpBody];
685    task = [session uploadTaskWithStreamedRequest:request];
686  } else {
687    reject(@"ERR_FILESYSTEM_INVALID_UPLOAD_TYPE",
688           [NSString stringWithFormat:@"Invalid upload type: '%@'.", options[@"uploadType"]],
689           nil);
690  }
691  return task;
692}
693
694EX_EXPORT_METHOD_AS(downloadResumableStartAsync,
695                    downloadResumableStartAsyncWithUrl:(NSString *)urlString
696                                               fileURI:(NSString *)fileUri
697                                                  uuid:(NSString *)uuid
698                                               options:(NSDictionary *)options
699                                            resumeData:(NSString *)data
700                                              resolver:(EXPromiseResolveBlock)resolve
701                                              rejecter:(EXPromiseRejectBlock)reject)
702{
703  NSURL *url = [NSURL URLWithString:urlString];
704  NSURL *localUrl = [self percentEncodedURLFromURIString:fileUri];
705  if (!([self checkIfFileDirExists:localUrl.path])) {
706    reject(@"ERR_FILESYSTEM_WRONG_DESTINATION",
707           [NSString stringWithFormat:@"Directory for '%@' doesn't exist. Please make sure directory '%@' exists before calling downloadAsync.", fileUri, [localUrl.path stringByDeletingLastPathComponent]],
708           nil);
709    return;
710  }
711  if (![localUrl.scheme isEqualToString:@"file"]) {
712    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
713           [NSString stringWithFormat:@"Cannot download to '%@': only 'file://' URI destinations are supported.", fileUri],
714           nil);
715    return;
716  }
717
718  NSString *path = localUrl.path;
719  if (!([self _permissionsForPath:path] & EXFileSystemPermissionWrite)) {
720    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
721           [NSString stringWithFormat:@"File '%@' isn't writable.", fileUri],
722           nil);
723    return;
724  }
725
726  if (![self _checkHeadersDictionary:options[@"headers"]]) {
727    reject(@"ERR_FILESYSTEM_INVALID_HEADERS_DICTIONARY",
728           @"Invalid headers dictionary. Keys and values should be strings.",
729           nil);
730    return;
731  }
732
733  NSData *resumeData = data ? [[NSData alloc] initWithBase64EncodedString:data options:0] : nil;
734  [self _downloadResumableCreateSessionWithUrl:url
735                                       fileUrl:localUrl
736                                          uuid:uuid
737                                        optins:options
738                                    resumeData:resumeData
739                                       resolve:resolve
740                                        reject:reject];
741}
742
743EX_EXPORT_METHOD_AS(downloadResumablePauseAsync,
744                    downloadResumablePauseAsyncWithUUID:(NSString *)uuid
745                    resolver:(EXPromiseResolveBlock)resolve
746                    rejecter:(EXPromiseRejectBlock)reject)
747{
748  NSURLSessionDownloadTask *task = [_taskHandlersManager downloadTaskForId:uuid];
749  if (!task) {
750    reject(@"ERR_FILESYSTEM_CANNOT_FIND_TASK",
751           [NSString stringWithFormat:@"There is no download object with UUID: %@", uuid],
752           nil);
753    return;
754  }
755
756  EX_WEAKIFY(self);
757  [task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
758    EX_ENSURE_STRONGIFY(self);
759    resolve(@{ @"resumeData": EXNullIfNil([resumeData base64EncodedStringWithOptions:0]) });
760  }];
761}
762
763EX_EXPORT_METHOD_AS(networkTaskCancelAsync,
764                    networkTaskCancelAsyncWithUUID:(NSString *)uuid
765                    resolver:(EXPromiseResolveBlock)resolve
766                    rejecter:(EXPromiseRejectBlock)reject)
767{
768  NSURLSessionTask *task = [_taskHandlersManager taskForId:uuid];
769  if (task) {
770    [task cancel];
771  }
772  resolve([NSNull null]);
773}
774
775EX_EXPORT_METHOD_AS(getFreeDiskStorageAsync, getFreeDiskStorageAsyncWithResolver:(EXPromiseResolveBlock)resolve rejecter:(EXPromiseRejectBlock)reject)
776{
777  NSError *error = nil;
778  NSNumber *freeDiskStorage = [self freeDiskStorageWithError:&error];
779
780  if(!freeDiskStorage || error) {
781    reject(@"ERR_FILESYSTEM_CANNOT_DETERMINE_DISK_CAPACITY", @"Unable to determine free disk storage capacity", error);
782  } else {
783    resolve(freeDiskStorage);
784  }
785}
786
787EX_EXPORT_METHOD_AS(getTotalDiskCapacityAsync, getTotalDiskCapacityAsyncWithResolver:(EXPromiseResolveBlock)resolve rejecter:(EXPromiseRejectBlock)reject)
788{
789  NSError *error = nil;
790  NSNumber *diskCapacity = [self totalDiskCapacityWithError:&error];
791
792  if (!diskCapacity || error) {
793    reject(@"ERR_FILESYSTEM_CANNOT_DETERMINE_DISK_CAPACITY", @"Unable to determine total disk capacity", error);
794  } else {
795    resolve(diskCapacity);
796  }
797}
798
799#pragma mark - Internal methods
800
801- (EXFileSystemUploadType)_getUploadTypeFrom:(NSNumber * _Nullable)type
802{
803  switch ([type intValue]) {
804    case EXFileSystemBinaryContent:
805    case EXFileSystemMultipart:
806      return [type intValue];
807  }
808
809  return EXFileSystemInvalidType;
810}
811
812// Borrowed from http://stackoverflow.com/questions/2439020/wheres-the-iphone-mime-type-database
813- (NSString *)_guessMIMETypeFromPath:(NSString *)path
814{
815  CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[path pathExtension], NULL);
816  CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
817  if (UTI) {
818    CFRelease(UTI);
819  }
820  if (!MIMEType) {
821    return @"application/octet-stream";
822  }
823  return (__bridge NSString *)(MIMEType);
824}
825
826- (NSData *)_createBodyWithBoundary:(NSString *)boundary
827                            fileUri:(NSURL *)fileUri
828                         parameters:(NSDictionary * _Nullable)parameters
829                          fieldName:(NSString * _Nullable)fieldName
830                           mimeType:(NSString * _Nullable)mimetype
831{
832
833  NSMutableData *body = [NSMutableData data];
834  NSData *data = [NSData dataWithContentsOfURL:fileUri];
835  NSString *filename  = [[fileUri path] lastPathComponent];
836
837  if (!mimetype) {
838    mimetype = [self _guessMIMETypeFromPath:[fileUri path]];
839  }
840
841  [parameters enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) {
842    [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
843    [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", parameterKey] dataUsingEncoding:NSUTF8StringEncoding]];
844    [body appendData:[[NSString stringWithFormat:@"%@\r\n", parameterValue] dataUsingEncoding:NSUTF8StringEncoding]];
845  }];
846
847  [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
848  [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fieldName ?: filename, filename]    dataUsingEncoding:NSUTF8StringEncoding]];
849  [body appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimetype] dataUsingEncoding:NSUTF8StringEncoding]];
850  [body appendData:data];
851  [body appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
852  [body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
853
854  return body;
855}
856
857- (NSMutableURLRequest *)_createRequest:(NSURL *)url headers:(NSDictionary * _Nullable)headers
858{
859  NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
860  if (headers != nil) {
861    for (NSString *headerKey in headers) {
862      [request setValue:[headers valueForKey:headerKey] forHTTPHeaderField:headerKey];
863    }
864  }
865
866  return request;
867}
868
869- (NSURLSession *)_sessionForType:(EXFileSystemSessionType)type
870{
871  switch (type) {
872    case EXFileSystemBackgroundSession:
873      return _backgroundSession;
874    case EXFileSystemForegroundSession:
875      return _foregroundSession;
876  }
877  return nil;
878}
879
880- (BOOL)_checkHeadersDictionary:(NSDictionary * _Nullable)headers
881{
882  for (id key in [headers allKeys]) {
883    if (![key isKindOfClass:[NSString class]] || ![headers[key] isKindOfClass:[NSString class]]) {
884      return false;
885    }
886  }
887
888  return true;
889}
890
891- (NSURLSession *)_createSession:(EXFileSystemSessionType)type delegate:(id<NSURLSessionDelegate>)delegate
892{
893  NSURLSessionConfiguration *sessionConfiguration;
894  if (type == EXFileSystemForegroundSession) {
895    sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
896  } else {
897    sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[[NSUUID UUID] UUIDString]];
898  }
899  sessionConfiguration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
900  sessionConfiguration.URLCache = nil;
901  return [NSURLSession sessionWithConfiguration:sessionConfiguration
902                                       delegate:delegate
903                                  delegateQueue:nil];
904}
905
906- (BOOL)_checkIfFileExists:(NSString *)path
907{
908  return [[NSFileManager defaultManager] fileExistsAtPath:path];
909}
910
911- (void)_downloadResumableCreateSessionWithUrl:(NSURL *)url
912                                       fileUrl:(NSURL *)fileUrl
913                                          uuid:(NSString *)uuid
914                                        optins:(NSDictionary *)options
915                                    resumeData:(NSData * _Nullable)resumeData
916                                       resolve:(EXPromiseResolveBlock)resolve
917                                        reject:(EXPromiseRejectBlock)reject
918{
919  EX_WEAKIFY(self);
920  EXDownloadDelegateOnWriteCallback onWrite = ^(NSURLSessionDownloadTask *task, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
921    EX_ENSURE_STRONGIFY(self);
922    [self sendEventWithName:EXDownloadProgressEventName
923                       body:@{
924                             @"uuid": uuid,
925                             @"data": @{
926                                 @"totalBytesWritten": @(totalBytesWritten),
927                                 @"totalBytesExpectedToWrite": @(totalBytesExpectedToWrite),
928                             },
929                           }];
930  };
931
932  NSURLSessionDownloadTask *downloadTask;
933  NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]];
934  if (!session) {
935    reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE",
936           [NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]],
937           nil);
938    return;
939  }
940
941  if (resumeData) {
942    downloadTask = [session downloadTaskWithResumeData:resumeData];
943  } else {
944    NSURLRequest *request = [self _createRequest:url headers:options[@"headers"]];
945    downloadTask = [session downloadTaskWithRequest:request];
946  }
947  EXSessionTaskDelegate *taskDelegate = [[EXSessionResumableDownloadTaskDelegate alloc] initWithResolve:resolve
948                                                                                                 reject:reject
949                                                                                               localUrl:fileUrl
950                                                                                     shouldCalculateMd5:[options[@"md5"] boolValue]
951                                                                                        onWriteCallback:onWrite
952                                                                                       resumableManager:_taskHandlersManager
953                                                                                                   uuid:uuid];
954  [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:downloadTask];
955  [_taskHandlersManager registerTask:downloadTask uuid:uuid];
956  [downloadTask resume];
957}
958
959- (EXFileSystemPermissionFlags)_permissionsForPath:(NSString *)path
960{
961  return [[_moduleRegistry getModuleImplementingProtocol:@protocol(EXFilePermissionModuleInterface)] getPathPermissions:(NSString *)path];
962}
963
964- (void)sendEventWithName:(NSString *)eventName body:(id)body
965{
966  if (_eventEmitter != nil) {
967    [_eventEmitter sendEventWithName:eventName body:body];
968  }
969}
970
971- (NSDictionary<NSURLResourceKey, id> *)documentFileResourcesForKeys:(NSArray<NSURLResourceKey> *)keys
972                                                               error:(out NSError * __autoreleasing *)error
973{
974  if (!keys.count) {
975    return @{};
976  }
977
978  NSURL *documentDirectoryUrl = [NSURL fileURLWithPath:_documentDirectory];
979  NSDictionary *results = [documentDirectoryUrl resourceValuesForKeys:keys
980                                                                error:error];
981
982  if (!results) {
983    return @{};
984  }
985
986  return results;
987}
988
989#pragma mark - Public utils
990
991- (EXFileSystemPermissionFlags)permissionsForURI:(NSURL *)uri
992{
993  NSArray *validSchemas = @[
994                            @"assets-library",
995                            @"http",
996                            @"https",
997                            @"ph",
998                            ];
999  if ([validSchemas containsObject:uri.scheme]) {
1000    return EXFileSystemPermissionRead;
1001  }
1002  if ([uri.scheme isEqualToString:@"file"]) {
1003    return [self _permissionsForPath:uri.path];
1004  }
1005  return EXFileSystemPermissionNone;
1006}
1007
1008- (BOOL)checkIfFileDirExists:(NSString *)path
1009{
1010  NSString *dir = [path stringByDeletingLastPathComponent];
1011  return [self _checkIfFileExists:dir];
1012}
1013
1014/**
1015  Given an URI string, returns a percent-encoded URL as an NSURL object.
1016  Only encodes characters that are outside the set of allowed characters defined by RFC3986.
1017*/
1018- (nullable NSURL *)percentEncodedURLFromURIString:(nonnull NSString *)uri
1019{
1020  NSMutableCharacterSet *allowedCharacterSet = [NSMutableCharacterSet alphanumericCharacterSet];
1021  [allowedCharacterSet addCharactersInString:@"-._~:/?#[]@!$&'()*+,;="];
1022  NSString *encodedUriString = [uri stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
1023
1024  return [NSURL URLWithString:[encodedUriString stringByReplacingOccurrencesOfString:@"%25" withString:@"%"]];
1025}
1026
1027#pragma mark - Class methods
1028
1029- (BOOL)ensureDirExistsWithPath:(NSString *)path
1030{
1031  return [EXFileSystem ensureDirExistsWithPath:path];
1032}
1033
1034+ (BOOL)ensureDirExistsWithPath:(NSString *)path
1035{
1036  BOOL isDir = NO;
1037  NSError *error;
1038  BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir];
1039  if (!(exists && isDir)) {
1040    [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error];
1041    if (error) {
1042      return NO;
1043    }
1044  }
1045  return YES;
1046}
1047
1048- (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension
1049{
1050  return [EXFileSystem generatePathInDirectory:directory withExtension:extension];
1051}
1052
1053+ (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension
1054{
1055  NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension];
1056  [EXFileSystem ensureDirExistsWithPath:directory];
1057  return [directory stringByAppendingPathComponent:fileName];
1058}
1059
1060// '<ARCType> *__autoreleasing*' problem solution: https://stackoverflow.com/a/8862061/4337317
1061- (NSNumber *)totalDiskCapacityWithError:(out NSError * __autoreleasing *)error
1062{
1063  NSDictionary *results = [self documentFileResourcesForKeys:@[NSURLVolumeTotalCapacityKey]
1064                                                       error:error];
1065
1066  return results[NSURLVolumeTotalCapacityKey];
1067}
1068
1069// '<ARCType> *__autoreleasing*' problem solution: https://stackoverflow.com/a/8862061/4337317
1070- (NSNumber *)freeDiskStorageWithError:(out NSError * __autoreleasing *)error
1071{
1072#if TARGET_OS_TV
1073  NSURLResourceKey key = NSURLVolumeAvailableCapacityKey;
1074#else
1075  NSURLResourceKey key = NSURLVolumeAvailableCapacityForImportantUsageKey;
1076#endif
1077
1078  NSDictionary *results = [self documentFileResourcesForKeys:@[key]
1079                                                       error:error];
1080
1081  return results[key];
1082}
1083
1084@end
1085