1// Copyright 2016-present 650 Industries. All rights reserved.
2
3#import <ABI47_0_0ExpoModulesCore/ABI47_0_0EXModuleRegistry.h>
4
5#import <ABI47_0_0EXFileSystem/ABI47_0_0EXFileSystem.h>
6
7#import <CommonCrypto/CommonDigest.h>
8#import <MobileCoreServices/MobileCoreServices.h>
9
10#import <ABI47_0_0EXFileSystem/ABI47_0_0EXFileSystemLocalFileHandler.h>
11#import <ABI47_0_0EXFileSystem/ABI47_0_0EXFileSystemAssetLibraryHandler.h>
12
13#import <ABI47_0_0ExpoModulesCore/ABI47_0_0EXFileSystemInterface.h>
14#import <ABI47_0_0ExpoModulesCore/ABI47_0_0EXFilePermissionModuleInterface.h>
15
16#import <ABI47_0_0ExpoModulesCore/ABI47_0_0EXEventEmitterService.h>
17
18#import <ABI47_0_0EXFileSystem/ABI47_0_0EXTaskHandlersManager.h>
19#import <ABI47_0_0EXFileSystem/ABI47_0_0EXSessionTaskDispatcher.h>
20#import <ABI47_0_0EXFileSystem/ABI47_0_0EXSessionDownloadTaskDelegate.h>
21#import <ABI47_0_0EXFileSystem/ABI47_0_0EXSessionResumableDownloadTaskDelegate.h>
22#import <ABI47_0_0EXFileSystem/ABI47_0_0EXSessionUploadTaskDelegate.h>
23#import <ABI47_0_0EXFileSystem/ABI47_0_0EXSessionCancelableUploadTaskDelegate.h>
24
25NSString * const ABI47_0_0EXDownloadProgressEventName = @"expo-file-system.downloadProgress";
26NSString * const ABI47_0_0EXUploadProgressEventName = @"expo-file-system.uploadProgress";
27
28typedef NS_ENUM(NSInteger, ABI47_0_0EXFileSystemSessionType) {
29  ABI47_0_0EXFileSystemBackgroundSession = 0,
30  ABI47_0_0EXFileSystemForegroundSession = 1,
31};
32
33typedef NS_ENUM(NSInteger, ABI47_0_0EXFileSystemUploadType) {
34  ABI47_0_0EXFileSystemInvalidType = -1,
35  ABI47_0_0EXFileSystemBinaryContent = 0,
36  ABI47_0_0EXFileSystemMultipart = 1,
37};
38
39@interface ABI47_0_0EXFileSystem ()
40
41@property (nonatomic, strong) NSURLSession *backgroundSession;
42@property (nonatomic, strong) NSURLSession *foregroundSession;
43@property (nonatomic, strong) ABI47_0_0EXSessionTaskDispatcher *sessionTaskDispatcher;
44@property (nonatomic, strong) ABI47_0_0EXTaskHandlersManager *taskHandlersManager;
45@property (nonatomic, weak) ABI47_0_0EXModuleRegistry *moduleRegistry;
46@property (nonatomic, weak) id<ABI47_0_0EXEventEmitterService> 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 ABI47_0_0EXFileSystem
54
55ABI47_0_0EX_REGISTER_MODULE();
56
57+ (const NSString *)exportedModuleName
58{
59  return @"ExponentFileSystem";
60}
61
62+ (const NSArray<Protocol *> *)exportedInterfaces
63{
64  return @[@protocol(ABI47_0_0EXFileSystemInterface)];
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 = [ABI47_0_0EXTaskHandlersManager new];
75
76    [ABI47_0_0EXFileSystem ensureDirExistsWithPath:_documentDirectory];
77    [ABI47_0_0EXFileSystem 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:(ABI47_0_0EXModuleRegistry *)moduleRegistry
96{
97  _moduleRegistry = moduleRegistry;
98  _eventEmitter = [_moduleRegistry getModuleImplementingProtocol:@protocol(ABI47_0_0EXEventEmitterService)];
99
100  _sessionTaskDispatcher = [[ABI47_0_0EXSessionTaskDispatcher alloc] initWithSessionHandler:[moduleRegistry getSingletonModuleForName:@"SessionHandler"]];
101  _backgroundSession = [self _createSession:ABI47_0_0EXFileSystemBackgroundSession delegate:_sessionTaskDispatcher];
102  _foregroundSession = [self _createSession:ABI47_0_0EXFileSystemForegroundSession 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 @[ABI47_0_0EXDownloadProgressEventName, ABI47_0_0EXUploadProgressEventName];
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
176ABI47_0_0EX_EXPORT_METHOD_AS(getInfoAsync,
177                    getInfoAsyncWithURI:(NSString *)uriString
178                    withOptions:(NSDictionary *)options
179                    resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
180                    rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
181{
182  NSURL *uri = [NSURL URLWithString: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] & ABI47_0_0EXFileSystemPermissionRead)) {
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    [ABI47_0_0EXFileSystemLocalFileHandler getInfoForFile:uri withOptions:options resolver:resolve rejecter:reject];
196  } else if ([uri.scheme isEqualToString:@"assets-library"] || [uri.scheme isEqualToString:@"ph"]) {
197    [ABI47_0_0EXFileSystemAssetLibraryHandler 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
205ABI47_0_0EX_EXPORT_METHOD_AS(readAsStringAsync,
206                    readAsStringAsyncWithURI:(NSString *)uriString
207                    withOptions:(NSDictionary *)options
208                    resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
209                    rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
210{
211  NSURL *uri = [NSURL URLWithString: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] & ABI47_0_0EXFileSystemPermissionRead)) {
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
271ABI47_0_0EX_EXPORT_METHOD_AS(writeAsStringAsync,
272                    writeAsStringAsyncWithURI:(NSString *)uriString
273                    withString:(NSString *)string
274                    withOptions:(NSDictionary *)options
275                    resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
276                    rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
277{
278  NSURL *uri = [NSURL URLWithString:uriString];
279  if (!([self permissionsForURI:uri] & ABI47_0_0EXFileSystemPermissionWrite)) {
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
332ABI47_0_0EX_EXPORT_METHOD_AS(deleteAsync,
333                    deleteAsyncWithURI:(NSString *)uriString
334                    withOptions:(NSDictionary *)options
335                    resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
336                    rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
337{
338  NSURL *uri = [NSURL URLWithString:uriString];
339  if (!([self permissionsForURI:[uri URLByAppendingPathComponent:@".."]] & ABI47_0_0EXFileSystemPermissionWrite)) {
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
373ABI47_0_0EX_EXPORT_METHOD_AS(moveAsync,
374                    moveAsyncWithOptions:(NSDictionary *)options
375                    resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
376                    rejecter:(ABI47_0_0EXPromiseRejectBlock)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:@".."]] & ABI47_0_0EXFileSystemPermissionWrite)) {
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] & ABI47_0_0EXFileSystemPermissionWrite)) {
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
429ABI47_0_0EX_EXPORT_METHOD_AS(copyAsync,
430                    copyAsyncWithOptions:(NSDictionary *)options
431                    resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
432                    rejecter:(ABI47_0_0EXPromiseRejectBlock)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] & ABI47_0_0EXFileSystemPermissionRead)) {
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] & ABI47_0_0EXFileSystemPermissionWrite)) {
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    [ABI47_0_0EXFileSystemLocalFileHandler copyFrom:from to:to resolver:resolve rejecter:reject];
459  } else if ([from.scheme isEqualToString:@"assets-library"] || [from.scheme isEqualToString:@"ph"]) {
460    [ABI47_0_0EXFileSystemAssetLibraryHandler 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
468ABI47_0_0EX_EXPORT_METHOD_AS(makeDirectoryAsync,
469                    makeDirectoryAsyncWithURI:(NSString *)uriString
470                    withOptions:(NSDictionary *)options
471                    resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
472                    rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
473{
474
475  NSURL *uri = [NSURL URLWithString:uriString];
476  if (!([self permissionsForURI:uri] & ABI47_0_0EXFileSystemPermissionWrite)) {
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
502ABI47_0_0EX_EXPORT_METHOD_AS(readDirectoryAsync,
503                    readDirectoryAsyncWithURI:(NSString *)uriString
504                    withOptions:(NSDictionary *)options
505                    resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
506                    rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
507{
508  NSURL *uri = [NSURL URLWithString:uriString];
509  if (!([self permissionsForURI:uri] & ABI47_0_0EXFileSystemPermissionRead)) {
510    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
511           [NSString stringWithFormat:@"Location '%@' isn't readable.", uri],
512           nil);
513    return;
514  }
515
516  if ([uri.scheme isEqualToString:@"file"]) {
517    NSError *error;
518    NSArray<NSString *> *children = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:uri.path error:&error];
519    if (children) {
520      resolve(children);
521    } else {
522      reject(@"ERR_FILESYSTEM_CANNOT_READ_DIRECTORY",
523             [NSString stringWithFormat:@"Directory '%@' could not be read.", uri],
524             error);
525    }
526  } else {
527    reject(@"ERR_FILESYSTEM_INVALID_URI",
528           [NSString stringWithFormat:@"Unsupported URI scheme for '%@'", uri],
529           nil);
530  }
531}
532
533ABI47_0_0EX_EXPORT_METHOD_AS(downloadAsync,
534                    downloadAsyncWithUrl:(NSString *)urlString
535                                localURI:(NSString *)localUriString
536                                 options:(NSDictionary *)options
537                                resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
538                                rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
539{
540  NSURL *url = [NSURL URLWithString:urlString];
541  NSURL *localUri = [NSURL URLWithString:localUriString];
542  if (!([self checkIfFileDirExists:localUri.path])) {
543    reject(@"ERR_FILESYSTEM_WRONG_DESTINATION",
544           [NSString stringWithFormat:@"Directory for '%@' doesn't exist. Please make sure directory '%@' exists before calling downloadAsync.", localUriString, [localUri.path stringByDeletingLastPathComponent]],
545           nil);
546    return;
547  }
548  if (!([self permissionsForURI:localUri] & ABI47_0_0EXFileSystemPermissionWrite)) {
549    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
550           [NSString stringWithFormat:@"File '%@' isn't writable.", localUri],
551           nil);
552    return;
553  }
554  if (![self _checkHeadersDictionary:options[@"headers"]]) {
555    reject(@"ERR_FILESYSTEM_INVALID_HEADERS",
556           @"Invalid headers dictionary. Keys and values should be strings.",
557           nil);
558    return;
559  }
560
561  NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]];
562  if (!session) {
563    reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE",
564           [NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]],
565           nil);
566    return;
567  }
568
569  NSURLRequest *request = [self _createRequest:url headers:options[@"headers"]];
570  NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request];
571  ABI47_0_0EXSessionTaskDelegate *taskDelegate = [[ABI47_0_0EXSessionDownloadTaskDelegate alloc] initWithResolve:resolve
572                                                                                        reject:reject
573                                                                                      localUrl:localUri
574                                                                            shouldCalculateMd5:[options[@"md5"] boolValue]];
575  [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task];
576  [task resume];
577}
578
579ABI47_0_0EX_EXPORT_METHOD_AS(uploadAsync,
580                    uploadAsync:(NSString *)urlString
581                       localURI:(NSString *)fileUriString
582                        options:(NSDictionary *)options
583                       resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
584                       rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
585{
586  NSURLSessionUploadTask *task = [self createUploadTask:urlString localURI:fileUriString options:options rejecter:reject];
587  if (!task) {
588    return;
589  }
590
591  ABI47_0_0EXSessionTaskDelegate *taskDelegate = [[ABI47_0_0EXSessionUploadTaskDelegate alloc] initWithResolve:resolve reject:reject];
592  [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task];
593  [task resume];
594}
595
596ABI47_0_0EX_EXPORT_METHOD_AS(uploadTaskStartAsync,
597                    uploadTaskStartAsync:(NSString *)urlString
598                                localURI:(NSString *)fileUriString
599                                    uuid:(NSString *)uuid
600                                 options:(NSDictionary *)options
601                                resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
602                                rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
603{
604  NSURLSessionUploadTask *task = [self createUploadTask:urlString localURI:fileUriString options:options rejecter:reject];
605  if (!task) {
606    return;
607  }
608
609  ABI47_0_0EX_WEAKIFY(self);
610  ABI47_0_0EXUploadDelegateOnSendCallback onSend = ^(NSURLSessionUploadTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend) {
611    ABI47_0_0EX_ENSURE_STRONGIFY(self);
612    [self sendEventWithName:ABI47_0_0EXUploadProgressEventName
613                       body:@{
614                             @"uuid": uuid,
615                             @"data": @{
616                                 @"totalByteSent": @(totalBytesSent),
617                                 @"totalBytesExpectedToSend": @(totalBytesExpectedToSend),
618                             },
619                           }];
620  };
621
622  ABI47_0_0EXSessionTaskDelegate *taskDelegate = [[ABI47_0_0EXSessionCancelableUploadTaskDelegate alloc] initWithResolve:resolve
623                                                                                          reject:reject
624                                                                                  onSendCallback:onSend
625                                                                                resumableManager:_taskHandlersManager
626                                                                                            uuid:uuid];
627
628  [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:task];
629  [_taskHandlersManager registerTask:task uuid:uuid];
630  [task resume];
631}
632
633- (NSURLSessionUploadTask * _Nullable)createUploadTask:(NSString *)urlString
634                                              localURI:(NSString *)fileUriString
635                                               options:(NSDictionary *)options
636                                              rejecter:(ABI47_0_0EXPromiseRejectBlock)reject
637{
638  NSURL *fileUri = [NSURL URLWithString:fileUriString];
639  NSString *httpMethod = options[@"httpMethod"];
640  ABI47_0_0EXFileSystemUploadType type = [self _getUploadTypeFrom:options[@"uploadType"]];
641  if (![fileUri.scheme isEqualToString:@"file"]) {
642    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
643           [NSString stringWithFormat:@"Cannot upload file '%@'. Only 'file://' URI are supported.", fileUri],
644           nil);
645    return nil;
646  }
647  if (!([self _checkIfFileExists:fileUri.path])) {
648    reject(@"ERR_FILE_NOT_EXISTS",
649           [NSString stringWithFormat:@"File '%@' does not exist.", fileUri],
650           nil);
651    return nil;
652  }
653  if (![self _checkHeadersDictionary:options[@"headers"]]) {
654    reject(@"ERR_FILESYSTEM_INVALID_HEADERS_DICTIONARY",
655           @"Invalid headers dictionary. Keys and values should be strings.",
656           nil);
657    return nil;
658  }
659  if (!httpMethod) {
660    reject(@"ERR_FILESYSTEM_MISSING_HTTP_METHOD", @"Missing HTTP method.", nil);
661    return nil;
662  }
663
664  NSMutableURLRequest *request = [self _createRequest:[NSURL URLWithString:urlString] headers:options[@"headers"]];
665  [request setHTTPMethod:httpMethod];
666  NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]];
667  if (!session) {
668    reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE",
669           [NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]],
670           nil);
671    return nil;
672  }
673
674  NSURLSessionUploadTask *task;
675  if (type == ABI47_0_0EXFileSystemBinaryContent) {
676    task = [session uploadTaskWithRequest:request fromFile:fileUri];
677  } else if (type == ABI47_0_0EXFileSystemMultipart) {
678    NSString *boundaryString = [[NSUUID UUID] UUIDString];
679    [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundaryString] forHTTPHeaderField:@"Content-Type"];
680    NSData *httpBody = [self _createBodyWithBoundary:boundaryString
681                                             fileUri:fileUri
682                                          parameters:options[@"parameters"]
683                                           fieldName:options[@"fieldName"]
684                                            mimeType:options[@"mimeType"]];
685    [request setHTTPBody:httpBody];
686    task = [session uploadTaskWithStreamedRequest:request];
687  } else {
688    reject(@"ERR_FILESYSTEM_INVALID_UPLOAD_TYPE",
689           [NSString stringWithFormat:@"Invalid upload type: '%@'.", options[@"uploadType"]],
690           nil);
691  }
692  return task;
693}
694
695ABI47_0_0EX_EXPORT_METHOD_AS(downloadResumableStartAsync,
696                    downloadResumableStartAsyncWithUrl:(NSString *)urlString
697                                               fileURI:(NSString *)fileUri
698                                                  uuid:(NSString *)uuid
699                                               options:(NSDictionary *)options
700                                            resumeData:(NSString *)data
701                                              resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
702                                              rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
703{
704  NSURL *url = [NSURL URLWithString:urlString];
705  NSURL *localUrl = [NSURL URLWithString:fileUri];
706  if (!([self checkIfFileDirExists:localUrl.path])) {
707    reject(@"ERR_FILESYSTEM_WRONG_DESTINATION",
708           [NSString stringWithFormat:@"Directory for '%@' doesn't exist. Please make sure directory '%@' exists before calling downloadAsync.", fileUri, [localUrl.path stringByDeletingLastPathComponent]],
709           nil);
710    return;
711  }
712  if (![localUrl.scheme isEqualToString:@"file"]) {
713    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
714           [NSString stringWithFormat:@"Cannot download to '%@': only 'file://' URI destinations are supported.", fileUri],
715           nil);
716    return;
717  }
718
719  NSString *path = localUrl.path;
720  if (!([self _permissionsForPath:path] & ABI47_0_0EXFileSystemPermissionWrite)) {
721    reject(@"ERR_FILESYSTEM_NO_PERMISSIONS",
722           [NSString stringWithFormat:@"File '%@' isn't writable.", fileUri],
723           nil);
724    return;
725  }
726
727  if (![self _checkHeadersDictionary:options[@"headers"]]) {
728    reject(@"ERR_FILESYSTEM_INVALID_HEADERS_DICTIONARY",
729           @"Invalid headers dictionary. Keys and values should be strings.",
730           nil);
731    return;
732  }
733
734  NSData *resumeData = data ? [[NSData alloc] initWithBase64EncodedString:data options:0] : nil;
735  [self _downloadResumableCreateSessionWithUrl:url
736                                       fileUrl:localUrl
737                                          uuid:uuid
738                                        optins:options
739                                    resumeData:resumeData
740                                       resolve:resolve
741                                        reject:reject];
742}
743
744ABI47_0_0EX_EXPORT_METHOD_AS(downloadResumablePauseAsync,
745                    downloadResumablePauseAsyncWithUUID:(NSString *)uuid
746                    resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
747                    rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
748{
749  NSURLSessionDownloadTask *task = [_taskHandlersManager downloadTaskForId:uuid];
750  if (!task) {
751    reject(@"ERR_FILESYSTEM_CANNOT_FIND_TASK",
752           [NSString stringWithFormat:@"There is no download object with UUID: %@", uuid],
753           nil);
754    return;
755  }
756
757  ABI47_0_0EX_WEAKIFY(self);
758  [task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
759    ABI47_0_0EX_ENSURE_STRONGIFY(self);
760    resolve(@{ @"resumeData": ABI47_0_0EXNullIfNil([resumeData base64EncodedStringWithOptions:0]) });
761  }];
762}
763
764ABI47_0_0EX_EXPORT_METHOD_AS(networkTaskCancelAsync,
765                    networkTaskCancelAsyncWithUUID:(NSString *)uuid
766                    resolver:(ABI47_0_0EXPromiseResolveBlock)resolve
767                    rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
768{
769  NSURLSessionDownloadTask *task = [_taskHandlersManager taskForId:uuid];
770  if (task) {
771    [task cancel];
772  }
773  resolve([NSNull null]);
774}
775
776ABI47_0_0EX_EXPORT_METHOD_AS(getFreeDiskStorageAsync, getFreeDiskStorageAsyncWithResolver:(ABI47_0_0EXPromiseResolveBlock)resolve rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
777{
778  NSError *error = nil;
779  NSNumber *freeDiskStorage = [self freeDiskStorageWithError:&error];
780
781  if(!freeDiskStorage || error) {
782    reject(@"ERR_FILESYSTEM_CANNOT_DETERMINE_DISK_CAPACITY", @"Unable to determine free disk storage capacity", error);
783  } else {
784    resolve(freeDiskStorage);
785  }
786}
787
788ABI47_0_0EX_EXPORT_METHOD_AS(getTotalDiskCapacityAsync, getTotalDiskCapacityAsyncWithResolver:(ABI47_0_0EXPromiseResolveBlock)resolve rejecter:(ABI47_0_0EXPromiseRejectBlock)reject)
789{
790  NSError *error = nil;
791  NSNumber *diskCapacity = [self totalDiskCapacityWithError:&error];
792
793  if (!diskCapacity || error) {
794    reject(@"ERR_FILESYSTEM_CANNOT_DETERMINE_DISK_CAPACITY", @"Unable to determine total disk capacity", error);
795  } else {
796    resolve(diskCapacity);
797  }
798}
799
800#pragma mark - Internal methods
801
802- (ABI47_0_0EXFileSystemUploadType)_getUploadTypeFrom:(NSNumber * _Nullable)type
803{
804  switch ([type intValue]) {
805    case ABI47_0_0EXFileSystemBinaryContent:
806    case ABI47_0_0EXFileSystemMultipart:
807      return [type intValue];
808  }
809
810  return ABI47_0_0EXFileSystemInvalidType;
811}
812
813// Borrowed from http://stackoverflow.com/questions/2439020/wheres-the-iphone-mime-type-database
814- (NSString *)_guessMIMETypeFromPath:(NSString *)path
815{
816  CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[path pathExtension], NULL);
817  CFStringRef MIMEType = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType);
818  if (UTI) {
819    CFRelease(UTI);
820  }
821  if (!MIMEType) {
822    return @"application/octet-stream";
823  }
824  return (__bridge NSString *)(MIMEType);
825}
826
827- (NSData *)_createBodyWithBoundary:(NSString *)boundary
828                            fileUri:(NSURL *)fileUri
829                         parameters:(NSDictionary * _Nullable)parameters
830                          fieldName:(NSString * _Nullable)fieldName
831                           mimeType:(NSString * _Nullable)mimetype
832{
833
834  NSMutableData *body = [NSMutableData data];
835  NSData *data = [NSData dataWithContentsOfURL:fileUri];
836  NSString *filename  = [[fileUri path] lastPathComponent];
837
838  if (!mimetype) {
839    mimetype = [self _guessMIMETypeFromPath:[fileUri path]];
840  }
841
842  [parameters enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) {
843    [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
844    [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n", parameterKey] dataUsingEncoding:NSUTF8StringEncoding]];
845    [body appendData:[[NSString stringWithFormat:@"%@\r\n", parameterValue] dataUsingEncoding:NSUTF8StringEncoding]];
846  }];
847
848  [body appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
849  [body appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fieldName ?: filename, filename]    dataUsingEncoding:NSUTF8StringEncoding]];
850  [body appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimetype] dataUsingEncoding:NSUTF8StringEncoding]];
851  [body appendData:data];
852  [body appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
853  [body appendData:[[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:NSUTF8StringEncoding]];
854
855  return body;
856}
857
858- (NSMutableURLRequest *)_createRequest:(NSURL *)url headers:(NSDictionary * _Nullable)headers
859{
860  NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
861  if (headers != nil) {
862    for (NSString *headerKey in headers) {
863      [request setValue:[headers valueForKey:headerKey] forHTTPHeaderField:headerKey];
864    }
865  }
866
867  return request;
868}
869
870- (NSURLSession *)_sessionForType:(ABI47_0_0EXFileSystemSessionType)type
871{
872  switch (type) {
873    case ABI47_0_0EXFileSystemBackgroundSession:
874      return _backgroundSession;
875    case ABI47_0_0EXFileSystemForegroundSession:
876      return _foregroundSession;
877  }
878  return nil;
879}
880
881- (BOOL)_checkHeadersDictionary:(NSDictionary * _Nullable)headers
882{
883  for (id key in [headers allKeys]) {
884    if (![key isKindOfClass:[NSString class]] || ![headers[key] isKindOfClass:[NSString class]]) {
885      return false;
886    }
887  }
888
889  return true;
890}
891
892- (NSURLSession *)_createSession:(ABI47_0_0EXFileSystemSessionType)type delegate:(id<NSURLSessionDelegate>)delegate
893{
894  NSURLSessionConfiguration *sessionConfiguration;
895  if (type == ABI47_0_0EXFileSystemForegroundSession) {
896    sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
897  } else {
898    sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:[[NSUUID UUID] UUIDString]];
899  }
900  sessionConfiguration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
901  sessionConfiguration.URLCache = nil;
902  return [NSURLSession sessionWithConfiguration:sessionConfiguration
903                                       delegate:delegate
904                                  delegateQueue:nil];
905}
906
907- (BOOL)_checkIfFileExists:(NSString *)path
908{
909  return [[NSFileManager defaultManager] fileExistsAtPath:path];
910}
911
912- (void)_downloadResumableCreateSessionWithUrl:(NSURL *)url
913                                       fileUrl:(NSURL *)fileUrl
914                                          uuid:(NSString *)uuid
915                                        optins:(NSDictionary *)options
916                                    resumeData:(NSData * _Nullable)resumeData
917                                       resolve:(ABI47_0_0EXPromiseResolveBlock)resolve
918                                        reject:(ABI47_0_0EXPromiseRejectBlock)reject
919{
920  ABI47_0_0EX_WEAKIFY(self);
921  ABI47_0_0EXDownloadDelegateOnWriteCallback onWrite = ^(NSURLSessionDownloadTask *task, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
922    ABI47_0_0EX_ENSURE_STRONGIFY(self);
923    [self sendEventWithName:ABI47_0_0EXDownloadProgressEventName
924                       body:@{
925                             @"uuid": uuid,
926                             @"data": @{
927                                 @"totalBytesWritten": @(totalBytesWritten),
928                                 @"totalBytesExpectedToWrite": @(totalBytesExpectedToWrite),
929                             },
930                           }];
931  };
932
933  NSURLSessionDownloadTask *downloadTask;
934  NSURLSession *session = [self _sessionForType:[options[@"sessionType"] intValue]];
935  if (!session) {
936    reject(@"ERR_FILESYSTEM_INVALID_SESSION_TYPE",
937           [NSString stringWithFormat:@"Invalid session type: '%@'", options[@"sessionType"]],
938           nil);
939    return;
940  }
941
942  if (resumeData) {
943    downloadTask = [session downloadTaskWithResumeData:resumeData];
944  } else {
945    NSURLRequest *request = [self _createRequest:url headers:options[@"headers"]];
946    downloadTask = [session downloadTaskWithRequest:request];
947  }
948  ABI47_0_0EXSessionTaskDelegate *taskDelegate = [[ABI47_0_0EXSessionResumableDownloadTaskDelegate alloc] initWithResolve:resolve
949                                                                                                 reject:reject
950                                                                                               localUrl:fileUrl
951                                                                                     shouldCalculateMd5:[options[@"md5"] boolValue]
952                                                                                        onWriteCallback:onWrite
953                                                                                       resumableManager:_taskHandlersManager
954                                                                                                   uuid:uuid];
955  [_sessionTaskDispatcher registerTaskDelegate:taskDelegate forTask:downloadTask];
956  [_taskHandlersManager registerTask:downloadTask uuid:uuid];
957  [downloadTask resume];
958}
959
960- (ABI47_0_0EXFileSystemPermissionFlags)_permissionsForPath:(NSString *)path
961{
962  return [[_moduleRegistry getModuleImplementingProtocol:@protocol(ABI47_0_0EXFilePermissionModuleInterface)] getPathPermissions:(NSString *)path];
963}
964
965- (void)sendEventWithName:(NSString *)eventName body:(id)body
966{
967  if (_eventEmitter != nil) {
968    [_eventEmitter sendEventWithName:eventName body:body];
969  }
970}
971
972- (NSDictionary<NSURLResourceKey, id> *)documentFileResourcesForKeys:(NSArray<NSURLResourceKey> *)keys
973                                                               error:(out NSError * __autoreleasing *)error
974{
975  if (!keys.count) {
976    return @{};
977  }
978
979  NSURL *documentDirectoryUrl = [NSURL fileURLWithPath:_documentDirectory];
980  NSDictionary *results = [documentDirectoryUrl resourceValuesForKeys:keys
981                                                                error:error];
982
983  if (!results) {
984    return @{};
985  }
986
987  return results;
988}
989
990#pragma mark - Public utils
991
992- (ABI47_0_0EXFileSystemPermissionFlags)permissionsForURI:(NSURL *)uri
993{
994  NSArray *validSchemas = @[
995                            @"assets-library",
996                            @"http",
997                            @"https",
998                            @"ph",
999                            ];
1000  if ([validSchemas containsObject:uri.scheme]) {
1001    return ABI47_0_0EXFileSystemPermissionRead;
1002  }
1003  if ([uri.scheme isEqualToString:@"file"]) {
1004    return [self _permissionsForPath:uri.path];
1005  }
1006  return ABI47_0_0EXFileSystemPermissionNone;
1007}
1008
1009- (BOOL)checkIfFileDirExists:(NSString *)path
1010{
1011  NSString *dir = [path stringByDeletingLastPathComponent];
1012  return [self _checkIfFileExists:dir];
1013}
1014
1015#pragma mark - Class methods
1016
1017- (BOOL)ensureDirExistsWithPath:(NSString *)path
1018{
1019  return [ABI47_0_0EXFileSystem ensureDirExistsWithPath:path];
1020}
1021
1022+ (BOOL)ensureDirExistsWithPath:(NSString *)path
1023{
1024  BOOL isDir = NO;
1025  NSError *error;
1026  BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir];
1027  if (!(exists && isDir)) {
1028    [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error];
1029    if (error) {
1030      return NO;
1031    }
1032  }
1033  return YES;
1034}
1035
1036- (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension
1037{
1038  return [ABI47_0_0EXFileSystem generatePathInDirectory:directory withExtension:extension];
1039}
1040
1041+ (NSString *)generatePathInDirectory:(NSString *)directory withExtension:(NSString *)extension
1042{
1043  NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension];
1044  [ABI47_0_0EXFileSystem ensureDirExistsWithPath:directory];
1045  return [directory stringByAppendingPathComponent:fileName];
1046}
1047
1048// '<ARCType> *__autoreleasing*' problem solution: https://stackoverflow.com/a/8862061/4337317
1049- (NSNumber *)totalDiskCapacityWithError:(out NSError * __autoreleasing *)error
1050{
1051  NSDictionary *results = [self documentFileResourcesForKeys:@[NSURLVolumeTotalCapacityKey]
1052                                                       error:error];
1053
1054  return results[NSURLVolumeTotalCapacityKey];
1055}
1056
1057// '<ARCType> *__autoreleasing*' problem solution: https://stackoverflow.com/a/8862061/4337317
1058- (NSNumber *)freeDiskStorageWithError:(out NSError * __autoreleasing *)error
1059{
1060  NSDictionary *results = [self documentFileResourcesForKeys:@[NSURLVolumeAvailableCapacityForImportantUsageKey]
1061                                                       error:error];
1062
1063  return results[NSURLVolumeAvailableCapacityForImportantUsageKey];
1064}
1065
1066@end
1067