1
2#import <ExpoFileSystem/EXFileSystemAssetLibraryHandler.h>
3#import <ExpoFileSystem/NSData+EXFileSystem.h>
4
5#import <Photos/Photos.h>
6
7@implementation EXFileSystemAssetLibraryHandler
8
9+ (void)getInfoForFile:(NSURL *)fileUri
10           withOptions:(NSDictionary *)options
11              resolver:(EXPromiseResolveBlock)resolve
12              rejecter:(EXPromiseRejectBlock)reject
13{
14  NSError *error;
15  PHFetchResult<PHAsset *> *fetchResult = [self fetchResultForUri:fileUri error:&error];
16  if (error) {
17    reject(@"E_UNSUPPORTED_ARG", error.description, error);
18    return;
19  }
20  if (fetchResult.count > 0) {
21    PHAsset *asset = fetchResult[0];
22    NSMutableDictionary *result = [NSMutableDictionary dictionary];
23    result[@"exists"] = @(YES);
24    result[@"isDirectory"] = @(NO);
25    result[@"uri"] = fileUri;
26    result[@"modificationTime"] = @(asset.modificationDate.timeIntervalSince1970);
27    if (options[@"md5"] || options[@"size"]) {
28      [[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
29        result[@"size"] = @(imageData.length);
30        if (options[@"md5"]) {
31          result[@"md5"] = [imageData md5String];
32        }
33        resolve(result);
34      }];
35    } else {
36      resolve(result);
37    }
38  } else {
39    resolve(@{@"exists": @(NO), @"isDirectory": @(NO)});
40  }
41}
42
43+ (void)copyFrom:(NSURL *)from
44              to:(NSURL *)to
45        resolver:(EXPromiseResolveBlock)resolve
46        rejecter:(EXPromiseRejectBlock)reject
47{
48  NSString *toPath = [to.path stringByStandardizingPath];
49
50  // NOTE: The destination-delete and the copy should happen atomically, but we hope for the best for now
51  NSError *error;
52  if ([[NSFileManager defaultManager] fileExistsAtPath:toPath]) {
53    if (![[NSFileManager defaultManager] removeItemAtPath:toPath error:&error]) {
54      reject(@"E_FILE_NOT_COPIED",
55             [NSString stringWithFormat:@"File '%@' could not be copied to '%@' because a file already exists at "
56              "the destination and could not be deleted.", from, to],
57             error);
58      return;
59    }
60  }
61
62  PHFetchResult<PHAsset *> *fetchResult = [self fetchResultForUri:from error:&error];
63  if (error) {
64    reject(@"E_UNSUPPORTED_ARG", error.description, error);
65    return;
66  }
67
68  if (fetchResult.count > 0) {
69    PHAsset *asset = fetchResult[0];
70    if (asset.mediaType == PHAssetMediaTypeVideo) {
71      [[PHImageManager defaultManager] requestAVAssetForVideo:asset options:nil resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
72        if (![asset isKindOfClass:[AVURLAsset class]]) {
73          reject(@"ERR_INCORRECT_ASSET_TYPE",
74                 [NSString stringWithFormat:@"File '%@' has incorrect asset type.", from],
75                 nil);
76          return;
77        }
78
79        AVURLAsset* urlAsset = (AVURLAsset*)asset;
80        NSNumber *size;
81        [urlAsset.URL getResourceValue:&size forKey:NSURLFileSizeKey error:nil];
82        NSData *data = [NSData dataWithContentsOfURL:urlAsset.URL];
83
84        [EXFileSystemAssetLibraryHandler copyData:data toPath:toPath resolver:resolve rejecter:reject];
85      }];
86    } else {
87      [[PHImageManager defaultManager] requestImageDataForAsset:asset options:nil resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
88        [EXFileSystemAssetLibraryHandler copyData:imageData toPath:toPath resolver:resolve rejecter:reject];
89      }];
90    }
91  } else {
92    reject(@"E_FILE_NOT_COPIED",
93           [NSString stringWithFormat:@"File '%@' could not be found.", from],
94           error);
95  }
96}
97
98// adapted from RCTImageLoader.m
99+ (PHFetchResult<PHAsset *> *)fetchResultForUri:(NSURL *)url error:(NSError **)error
100{
101  if ([url.scheme caseInsensitiveCompare:@"ph"] == NSOrderedSame) {
102    // Fetch assets using PHAsset localIdentifier (recommended)
103    NSString *const localIdentifier = [url.absoluteString substringFromIndex:@"ph://".length];
104    return [PHAsset fetchAssetsWithLocalIdentifiers:@[localIdentifier] options:nil];
105  } else if ([url.scheme caseInsensitiveCompare:@"assets-library"] == NSOrderedSame) {
106#if TARGET_OS_MACCATALYST
107    static BOOL hasWarned = NO;
108    if (!hasWarned) {
109      NSLog(@"assets-library:// URLs have been deprecated and cannot be accessed in macOS Catalyst. Returning nil (future warnings will be suppressed).");
110      hasWarned = YES;
111    }
112    return nil;
113#else
114    // This is the older, deprecated way of fetching assets from assets-library
115    // using the "assets-library://" protocol
116    return [PHAsset fetchAssetsWithALAssetURLs:@[url] options:nil];
117#endif
118  }
119
120  NSString *description = [NSString stringWithFormat:@"Invalid URL provided, expected scheme to be either 'ph' or 'assets-library', was '%@'.", url.scheme];
121  *error = [[NSError alloc] initWithDomain:NSURLErrorDomain
122                                      code:NSURLErrorUnsupportedURL
123                                  userInfo:@{NSLocalizedDescriptionKey: description}];
124  return nil;
125}
126
127
128+ (void)copyData:(NSData *)data
129          toPath:(NSString *)path
130        resolver:(EXPromiseResolveBlock)resolve
131        rejecter:(EXPromiseRejectBlock)reject
132{
133  if ([data writeToFile:path atomically:YES]) {
134    resolve(nil);
135  } else {
136    reject(@"E_FILE_NOT_COPIED",
137           [NSString stringWithFormat:@"File could not be copied to '%@'.", path],
138           nil);
139  }
140}
141
142@end
143