rn-file-toolkit 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android/src/main/AndroidManifest.xml +4 -2
- package/android/src/main/java/com/filetoolkit/FileToolkitModule.kt +247 -136
- package/app.plugin.js +34 -8
- package/ios/FileToolkit.mm +362 -162
- package/lib/commonjs/NativeFileToolkit.js.map +1 -1
- package/lib/commonjs/index.js +126 -33
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/NativeFileToolkit.js.map +1 -1
- package/lib/module/index.js +126 -34
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/commonjs/src/NativeFileToolkit.d.ts +2 -0
- package/lib/typescript/commonjs/src/NativeFileToolkit.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +89 -8
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/NativeFileToolkit.d.ts +2 -0
- package/lib/typescript/module/src/NativeFileToolkit.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +89 -8
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/package.json +1 -2
- package/react-native.config.js +8 -0
- package/src/NativeFileToolkit.ts +4 -0
- package/src/index.tsx +206 -38
package/ios/FileToolkit.mm
CHANGED
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
@property (nonatomic, strong) NSMutableDictionary *uploadTaskIdMap; // taskIdentifier → uploadId
|
|
30
30
|
// Strong ref to prevent ARC deallocation during preview
|
|
31
31
|
@property (nonatomic, strong) UIDocumentInteractionController *documentController;
|
|
32
|
+
// Serial queue for thread-safe dictionary access
|
|
33
|
+
@property (nonatomic, strong) dispatch_queue_t syncQueue;
|
|
34
|
+
@property (nonatomic, assign) BOOL hasListeners;
|
|
32
35
|
@end
|
|
33
36
|
|
|
34
37
|
@implementation FileToolkit
|
|
@@ -37,6 +40,7 @@ RCT_EXPORT_MODULE()
|
|
|
37
40
|
|
|
38
41
|
- (instancetype)init {
|
|
39
42
|
if (self = [super init]) {
|
|
43
|
+
self.syncQueue = dispatch_queue_create("com.filetoolkit.syncQueue", DISPATCH_QUEUE_SERIAL);
|
|
40
44
|
self.activePromises = [NSMutableDictionary new];
|
|
41
45
|
self.downloadOptions = [NSMutableDictionary new];
|
|
42
46
|
self.activeTasks = [NSMutableDictionary new];
|
|
@@ -53,8 +57,9 @@ RCT_EXPORT_MODULE()
|
|
|
53
57
|
self.fgSession = [NSURLSession sessionWithConfiguration:fgConfig delegate:self delegateQueue:nil];
|
|
54
58
|
|
|
55
59
|
// Background session (survives app suspension)
|
|
60
|
+
NSString *bgId = [NSString stringWithFormat:@"%@.filetoolkit.background", NSBundle.mainBundle.bundleIdentifier];
|
|
56
61
|
NSURLSessionConfiguration *bgConfig =
|
|
57
|
-
[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier
|
|
62
|
+
[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:bgId];
|
|
58
63
|
bgConfig.discretionary = NO;
|
|
59
64
|
bgConfig.sessionSendsLaunchEvents = YES;
|
|
60
65
|
self.bgSession = [NSURLSession sessionWithConfiguration:bgConfig delegate:self delegateQueue:nil];
|
|
@@ -66,6 +71,14 @@ RCT_EXPORT_MODULE()
|
|
|
66
71
|
return @[@"onDownloadProgress", @"onDownloadComplete", @"onDownloadError", @"onUploadProgress", @"onDownloadRetry"];
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
- (void)startObserving {
|
|
75
|
+
self.hasListeners = YES;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
- (void)stopObserving {
|
|
79
|
+
self.hasListeners = NO;
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
70
83
|
|
|
71
84
|
- (NSString *)generateDownloadId {
|
|
@@ -91,26 +104,63 @@ RCT_EXPORT_MODULE()
|
|
|
91
104
|
[[NSFileManager defaultManager] createDirectoryAtURL:dirURL withIntermediateDirectories:YES attributes:nil error:nil];
|
|
92
105
|
}
|
|
93
106
|
|
|
107
|
+
// Use a toolkit-owned subdirectory for cache and documents to avoid
|
|
108
|
+
// conflicts with the app's own files (clearCache only removes this folder)
|
|
109
|
+
if ([destType isEqualToString:@"cache"] || [destType isEqualToString:@"documents"]) {
|
|
110
|
+
dirURL = [dirURL URLByAppendingPathComponent:@"RNFileToolkit"];
|
|
111
|
+
[[NSFileManager defaultManager] createDirectoryAtURL:dirURL withIntermediateDirectories:YES attributes:nil error:nil];
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
return [dirURL URLByAppendingPathComponent:fileName];
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
- (NSString *)calculateChecksumForPath:(NSString *)path algorithm:(NSString *)algo {
|
|
98
|
-
|
|
99
|
-
if (!
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
NSInputStream *inputStream = [NSInputStream inputStreamWithFileAtPath:path];
|
|
119
|
+
if (!inputStream) return nil;
|
|
120
|
+
[inputStream open];
|
|
121
|
+
|
|
122
|
+
const NSUInteger bufferSize = 65536; // 64KB
|
|
123
|
+
uint8_t buffer[bufferSize];
|
|
124
|
+
|
|
102
125
|
if ([algo isEqualToString:@"MD5"]) {
|
|
103
|
-
|
|
126
|
+
CC_MD5_CTX ctx;
|
|
127
|
+
CC_MD5_Init(&ctx);
|
|
128
|
+
while ([inputStream hasBytesAvailable]) {
|
|
129
|
+
NSInteger bytesRead = [inputStream read:buffer maxLength:bufferSize];
|
|
130
|
+
if (bytesRead > 0) CC_MD5_Update(&ctx, buffer, (CC_LONG)bytesRead);
|
|
131
|
+
else break;
|
|
132
|
+
}
|
|
133
|
+
[inputStream close];
|
|
134
|
+
unsigned char digest[CC_MD5_DIGEST_LENGTH];
|
|
135
|
+
CC_MD5_Final(digest, &ctx);
|
|
104
136
|
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
|
|
105
137
|
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) [output appendFormat:@"%02x", digest[i]];
|
|
106
138
|
return output;
|
|
107
139
|
} else if ([algo isEqualToString:@"SHA1"]) {
|
|
108
|
-
|
|
140
|
+
CC_SHA1_CTX ctx;
|
|
141
|
+
CC_SHA1_Init(&ctx);
|
|
142
|
+
while ([inputStream hasBytesAvailable]) {
|
|
143
|
+
NSInteger bytesRead = [inputStream read:buffer maxLength:bufferSize];
|
|
144
|
+
if (bytesRead > 0) CC_SHA1_Update(&ctx, buffer, (CC_LONG)bytesRead);
|
|
145
|
+
else break;
|
|
146
|
+
}
|
|
147
|
+
[inputStream close];
|
|
148
|
+
unsigned char digest[CC_SHA1_DIGEST_LENGTH];
|
|
149
|
+
CC_SHA1_Final(digest, &ctx);
|
|
109
150
|
NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];
|
|
110
151
|
for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) [output appendFormat:@"%02x", digest[i]];
|
|
111
152
|
return output;
|
|
112
153
|
} else {
|
|
113
|
-
|
|
154
|
+
CC_SHA256_CTX ctx;
|
|
155
|
+
CC_SHA256_Init(&ctx);
|
|
156
|
+
while ([inputStream hasBytesAvailable]) {
|
|
157
|
+
NSInteger bytesRead = [inputStream read:buffer maxLength:bufferSize];
|
|
158
|
+
if (bytesRead > 0) CC_SHA256_Update(&ctx, buffer, (CC_LONG)bytesRead);
|
|
159
|
+
else break;
|
|
160
|
+
}
|
|
161
|
+
[inputStream close];
|
|
162
|
+
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
|
|
163
|
+
CC_SHA256_Final(digest, &ctx);
|
|
114
164
|
NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
|
|
115
165
|
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) [output appendFormat:@"%02x", digest[i]];
|
|
116
166
|
return output;
|
|
@@ -140,6 +190,10 @@ RCT_EXPORT_MODULE()
|
|
|
140
190
|
BOOL isBackground = [options[@"background"] boolValue];
|
|
141
191
|
NSString *downloadId = options[@"downloadId"] ?: [self generateDownloadId];
|
|
142
192
|
NSURL *url = [NSURL URLWithString:urlString];
|
|
193
|
+
if (!url) {
|
|
194
|
+
resolve(@{@"success": @NO, @"error": @"Invalid URL"});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
143
197
|
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
|
144
198
|
|
|
145
199
|
// Add custom headers
|
|
@@ -154,16 +208,20 @@ RCT_EXPORT_MODULE()
|
|
|
154
208
|
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request];
|
|
155
209
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
156
210
|
|
|
157
|
-
self.
|
|
158
|
-
|
|
159
|
-
|
|
211
|
+
dispatch_sync(self.syncQueue, ^{
|
|
212
|
+
self.taskIdMap[taskKey] = downloadId;
|
|
213
|
+
self.activeTasks[downloadId] = task;
|
|
214
|
+
self.downloadOptions[downloadId] = options;
|
|
215
|
+
});
|
|
160
216
|
task.taskDescription = downloadId;
|
|
161
217
|
|
|
162
218
|
if (isBackground) {
|
|
163
219
|
// Resolve immediately with the downloadId — result comes via event
|
|
164
220
|
resolve(@{@"success": @YES, @"downloadId": downloadId});
|
|
165
221
|
} else {
|
|
166
|
-
self.
|
|
222
|
+
dispatch_sync(self.syncQueue, ^{
|
|
223
|
+
self.activePromises[downloadId] = @{@"resolve": resolve, @"reject": reject};
|
|
224
|
+
});
|
|
167
225
|
}
|
|
168
226
|
|
|
169
227
|
[task resume];
|
|
@@ -172,17 +230,22 @@ RCT_EXPORT_MODULE()
|
|
|
172
230
|
// ─── pauseDownload ────────────────────────────────────────────────────────────
|
|
173
231
|
|
|
174
232
|
- (void)pauseDownload:(NSString *)downloadId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
175
|
-
NSURLSessionDownloadTask *task =
|
|
233
|
+
__block NSURLSessionDownloadTask *task = nil;
|
|
234
|
+
dispatch_sync(self.syncQueue, ^{
|
|
235
|
+
task = self.activeTasks[downloadId];
|
|
236
|
+
});
|
|
176
237
|
if (!task) {
|
|
177
238
|
resolve(@{@"success": @NO, @"error": @"Download not found"});
|
|
178
239
|
return;
|
|
179
240
|
}
|
|
180
241
|
|
|
181
242
|
[task cancelByProducingResumeData:^(NSData *resumeData) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
243
|
+
dispatch_sync(self.syncQueue, ^{
|
|
244
|
+
if (resumeData) {
|
|
245
|
+
self.resumeDataStore[downloadId] = resumeData;
|
|
246
|
+
}
|
|
247
|
+
[self.activeTasks removeObjectForKey:downloadId];
|
|
248
|
+
});
|
|
186
249
|
resolve(@{@"success": @YES});
|
|
187
250
|
}];
|
|
188
251
|
}
|
|
@@ -190,22 +253,28 @@ RCT_EXPORT_MODULE()
|
|
|
190
253
|
// ─── resumeDownload ───────────────────────────────────────────────────────────
|
|
191
254
|
|
|
192
255
|
- (void)resumeDownload:(NSString *)downloadId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
193
|
-
NSData *resumeData =
|
|
256
|
+
__block NSData *resumeData = nil;
|
|
257
|
+
__block NSDictionary *options = nil;
|
|
258
|
+
dispatch_sync(self.syncQueue, ^{
|
|
259
|
+
resumeData = self.resumeDataStore[downloadId];
|
|
260
|
+
options = self.downloadOptions[downloadId];
|
|
261
|
+
});
|
|
194
262
|
if (!resumeData) {
|
|
195
263
|
resolve(@{@"success": @NO, @"error": @"No resume data — download was not paused or was cancelled"});
|
|
196
264
|
return;
|
|
197
265
|
}
|
|
198
266
|
|
|
199
|
-
NSDictionary *options = self.downloadOptions[downloadId];
|
|
200
267
|
BOOL isBackground = [options[@"background"] boolValue];
|
|
201
268
|
NSURLSession *session = isBackground ? self.bgSession : self.fgSession;
|
|
202
269
|
|
|
203
270
|
NSURLSessionDownloadTask *task = [session downloadTaskWithResumeData:resumeData];
|
|
204
271
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
205
272
|
|
|
206
|
-
self.
|
|
207
|
-
|
|
208
|
-
|
|
273
|
+
dispatch_sync(self.syncQueue, ^{
|
|
274
|
+
self.taskIdMap[taskKey] = downloadId;
|
|
275
|
+
self.activeTasks[downloadId] = task;
|
|
276
|
+
[self.resumeDataStore removeObjectForKey:downloadId];
|
|
277
|
+
});
|
|
209
278
|
|
|
210
279
|
[task resume];
|
|
211
280
|
resolve(@{@"success": @YES});
|
|
@@ -214,15 +283,26 @@ RCT_EXPORT_MODULE()
|
|
|
214
283
|
// ─── cancelDownload ───────────────────────────────────────────────────────────
|
|
215
284
|
|
|
216
285
|
- (void)cancelDownload:(NSString *)downloadId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
217
|
-
NSURLSessionDownloadTask *task =
|
|
286
|
+
__block NSURLSessionDownloadTask *task = nil;
|
|
287
|
+
__block NSDictionary *funcs = nil;
|
|
288
|
+
dispatch_sync(self.syncQueue, ^{
|
|
289
|
+
task = self.activeTasks[downloadId];
|
|
290
|
+
funcs = self.activePromises[downloadId];
|
|
291
|
+
if (task) {
|
|
292
|
+
[self.activeTasks removeObjectForKey:downloadId];
|
|
293
|
+
}
|
|
294
|
+
[self.resumeDataStore removeObjectForKey:downloadId];
|
|
295
|
+
[self.activePromises removeObjectForKey:downloadId];
|
|
296
|
+
[self.downloadOptions removeObjectForKey:downloadId];
|
|
297
|
+
[self.retryAttempts removeObjectForKey:downloadId];
|
|
298
|
+
});
|
|
299
|
+
if (funcs) {
|
|
300
|
+
RCTPromiseResolveBlock dlResolve = funcs[@"resolve"];
|
|
301
|
+
if (dlResolve) dlResolve(@{@"success": @NO, @"error": @"Cancelled"});
|
|
302
|
+
}
|
|
218
303
|
if (task) {
|
|
219
304
|
[task cancel];
|
|
220
|
-
[self.activeTasks removeObjectForKey:downloadId];
|
|
221
305
|
}
|
|
222
|
-
[self.resumeDataStore removeObjectForKey:downloadId];
|
|
223
|
-
[self.activePromises removeObjectForKey:downloadId];
|
|
224
|
-
[self.downloadOptions removeObjectForKey:downloadId];
|
|
225
|
-
[self.retryAttempts removeObjectForKey:downloadId];
|
|
226
306
|
resolve(@{@"success": @YES});
|
|
227
307
|
}
|
|
228
308
|
|
|
@@ -239,10 +319,12 @@ RCT_EXPORT_MODULE()
|
|
|
239
319
|
URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
|
|
240
320
|
downloadsDir = [fallbackDocs URLByAppendingPathComponent:@"Downloads"];
|
|
241
321
|
}
|
|
242
|
-
NSURL *cacheDir = [[NSFileManager defaultManager]
|
|
243
|
-
URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject
|
|
244
|
-
|
|
245
|
-
|
|
322
|
+
NSURL *cacheDir = [[[NSFileManager defaultManager]
|
|
323
|
+
URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject
|
|
324
|
+
URLByAppendingPathComponent:@"RNFileToolkit"];
|
|
325
|
+
NSURL *docsDir = [[[NSFileManager defaultManager]
|
|
326
|
+
URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject
|
|
327
|
+
URLByAppendingPathComponent:@"RNFileToolkit"];
|
|
246
328
|
|
|
247
329
|
NSMutableArray<NSURL *> *dirs = [NSMutableArray new];
|
|
248
330
|
if (downloadsDir) [dirs addObject:downloadsDir];
|
|
@@ -292,15 +374,15 @@ RCT_EXPORT_MODULE()
|
|
|
292
374
|
// ─── clearCache ───────────────────────────────────────────────────────────────
|
|
293
375
|
|
|
294
376
|
- (void)clearCache:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
295
|
-
// Only clear
|
|
377
|
+
// Only clear the toolkit-owned subdirectory — never touch the app's own files or Downloads
|
|
296
378
|
NSURL *cacheDir = [[NSFileManager defaultManager]
|
|
297
379
|
URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject;
|
|
298
380
|
NSURL *docsDir = [[NSFileManager defaultManager]
|
|
299
381
|
URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
|
|
300
382
|
|
|
301
383
|
NSMutableArray<NSURL *> *dirs = [NSMutableArray new];
|
|
302
|
-
if (cacheDir) [dirs addObject:cacheDir];
|
|
303
|
-
if (docsDir) [dirs addObject:docsDir];
|
|
384
|
+
if (cacheDir) [dirs addObject:[cacheDir URLByAppendingPathComponent:@"RNFileToolkit"]];
|
|
385
|
+
if (docsDir) [dirs addObject:[docsDir URLByAppendingPathComponent:@"RNFileToolkit"]];
|
|
304
386
|
|
|
305
387
|
for (NSURL *dirURL in dirs) {
|
|
306
388
|
NSArray<NSURL *> *files = [[NSFileManager defaultManager]
|
|
@@ -572,17 +654,24 @@ totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
|
|
|
572
654
|
if (totalBytesExpectedToWrite > 0) {
|
|
573
655
|
int progress = (int)((totalBytesWritten * 100) / totalBytesExpectedToWrite);
|
|
574
656
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)downloadTask.taskIdentifier];
|
|
575
|
-
NSString *downloadId = self.taskIdMap[taskKey] ?: @"";
|
|
576
657
|
NSString *url = downloadTask.originalRequest.URL.absoluteString ?: @"";
|
|
577
658
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
659
|
+
__weak typeof(self) weakSelf = self;
|
|
660
|
+
dispatch_async(self.syncQueue, ^{
|
|
661
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
662
|
+
if (!strongSelf) return;
|
|
663
|
+
NSString *downloadId = strongSelf.taskIdMap[taskKey];
|
|
664
|
+
if (!downloadId) return;
|
|
665
|
+
|
|
666
|
+
[strongSelf sendEventWithName:@"onDownloadProgress"
|
|
667
|
+
body:@{
|
|
668
|
+
@"url": url,
|
|
669
|
+
@"downloadId": downloadId,
|
|
670
|
+
@"progress": @(progress),
|
|
671
|
+
@"bytesDownloaded": @(totalBytesWritten),
|
|
672
|
+
@"totalBytes": @(totalBytesExpectedToWrite)
|
|
673
|
+
}];
|
|
674
|
+
});
|
|
586
675
|
}
|
|
587
676
|
}
|
|
588
677
|
|
|
@@ -590,10 +679,16 @@ totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
|
|
|
590
679
|
downloadTask:(NSURLSessionDownloadTask *)downloadTask
|
|
591
680
|
didFinishDownloadingToURL:(NSURL *)location {
|
|
592
681
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)downloadTask.taskIdentifier];
|
|
593
|
-
NSString *downloadId =
|
|
682
|
+
__block NSString *downloadId = nil;
|
|
683
|
+
__block NSDictionary *options = nil;
|
|
684
|
+
dispatch_sync(self.syncQueue, ^{
|
|
685
|
+
downloadId = self.taskIdMap[taskKey];
|
|
686
|
+
if (downloadId) {
|
|
687
|
+
options = self.downloadOptions[downloadId];
|
|
688
|
+
}
|
|
689
|
+
});
|
|
594
690
|
if (!downloadId) return;
|
|
595
691
|
|
|
596
|
-
NSDictionary *options = self.downloadOptions[downloadId];
|
|
597
692
|
NSString *fileName = [self fileNameFromOptions:options task:downloadTask];
|
|
598
693
|
NSString *destType = options[@"destination"] ?: @"downloads";
|
|
599
694
|
NSURL *destURL = [self destURLForFileName:fileName destination:destType];
|
|
@@ -630,24 +725,34 @@ didFinishDownloadingToURL:(NSURL *)location {
|
|
|
630
725
|
}
|
|
631
726
|
}
|
|
632
727
|
|
|
633
|
-
NSDictionary *funcs =
|
|
728
|
+
__block NSDictionary *funcs = nil;
|
|
634
729
|
BOOL isBackground = [options[@"background"] boolValue];
|
|
730
|
+
dispatch_sync(self.syncQueue, ^{
|
|
731
|
+
funcs = self.activePromises[downloadId];
|
|
732
|
+
});
|
|
635
733
|
|
|
636
734
|
if (funcs && !isBackground) {
|
|
637
735
|
// Foreground: resolve the promise
|
|
638
736
|
RCTPromiseResolveBlock resolve = funcs[@"resolve"];
|
|
639
737
|
resolve(resultDict);
|
|
738
|
+
// Also emit the error event for foreground failures so global listeners fire
|
|
739
|
+
// consistently across platforms (mirrors Android behaviour).
|
|
740
|
+
if (isError) {
|
|
741
|
+
[self sendEventWithName:@"onDownloadError" body:resultDict];
|
|
742
|
+
}
|
|
640
743
|
} else {
|
|
641
|
-
// Background: fire the correct event based on isError flag
|
|
744
|
+
// Background: fire the correct event based on isError flag
|
|
642
745
|
NSString *event = isError ? @"onDownloadError" : @"onDownloadComplete";
|
|
643
746
|
[self sendEventWithName:event body:resultDict];
|
|
644
747
|
}
|
|
645
748
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
749
|
+
dispatch_sync(self.syncQueue, ^{
|
|
750
|
+
[self.activePromises removeObjectForKey:downloadId];
|
|
751
|
+
[self.downloadOptions removeObjectForKey:downloadId];
|
|
752
|
+
[self.activeTasks removeObjectForKey:downloadId];
|
|
753
|
+
[self.taskIdMap removeObjectForKey:taskKey];
|
|
754
|
+
[self.retryAttempts removeObjectForKey:downloadId];
|
|
755
|
+
});
|
|
651
756
|
}
|
|
652
757
|
|
|
653
758
|
- (void)URLSession:(NSURLSession *)session
|
|
@@ -655,12 +760,20 @@ didFinishDownloadingToURL:(NSURL *)location {
|
|
|
655
760
|
didCompleteWithError:(NSError *)error {
|
|
656
761
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
657
762
|
|
|
658
|
-
// ── Upload task completion
|
|
659
|
-
NSString *uploadId =
|
|
763
|
+
// ── Upload task completion ─────────────────────────────────────────────────
|
|
764
|
+
__block NSString *uploadId = nil;
|
|
765
|
+
__block NSDictionary *uploadFuncs = nil;
|
|
766
|
+
__block NSData *uploadRespData = nil;
|
|
767
|
+
dispatch_sync(self.syncQueue, ^{
|
|
768
|
+
uploadId = self.uploadTaskIdMap[taskKey];
|
|
769
|
+
if (uploadId) {
|
|
770
|
+
uploadFuncs = self.uploadPromises[uploadId];
|
|
771
|
+
uploadRespData = self.uploadResponseData[uploadId];
|
|
772
|
+
}
|
|
773
|
+
});
|
|
660
774
|
if (uploadId) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
NSString *tempFile = funcs[@"tempFile"];
|
|
775
|
+
RCTPromiseResolveBlock uploadResolve = uploadFuncs[@"resolve"];
|
|
776
|
+
NSString *tempFile = uploadFuncs[@"tempFile"];
|
|
664
777
|
|
|
665
778
|
if (tempFile) {
|
|
666
779
|
[[NSFileManager defaultManager] removeItemAtPath:tempFile error:nil];
|
|
@@ -670,7 +783,7 @@ didCompleteWithError:(NSError *)error {
|
|
|
670
783
|
if (uploadResolve) uploadResolve(@{@"success": @NO, @"error": error.localizedDescription, @"uploadId": uploadId});
|
|
671
784
|
} else {
|
|
672
785
|
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
|
|
673
|
-
NSData *responseData =
|
|
786
|
+
NSData *responseData = uploadRespData ?: [NSData data];
|
|
674
787
|
NSString *respString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] ?: @"";
|
|
675
788
|
|
|
676
789
|
if (uploadResolve) uploadResolve(@{
|
|
@@ -681,45 +794,66 @@ didCompleteWithError:(NSError *)error {
|
|
|
681
794
|
});
|
|
682
795
|
}
|
|
683
796
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
797
|
+
dispatch_sync(self.syncQueue, ^{
|
|
798
|
+
// Guard against double-resolution in upload
|
|
799
|
+
if (self.uploadPromises[uploadId]) {
|
|
800
|
+
[self.uploadPromises removeObjectForKey:uploadId];
|
|
801
|
+
[self.uploadUrls removeObjectForKey:uploadId];
|
|
802
|
+
[self.uploadResponseData removeObjectForKey:uploadId];
|
|
803
|
+
[self.uploadTaskIdMap removeObjectForKey:taskKey];
|
|
804
|
+
} else {
|
|
805
|
+
uploadId = nil; // Already resolved
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
if (!uploadId) return;
|
|
689
809
|
}
|
|
690
810
|
|
|
691
811
|
// ── Download task error handling ───────────────────────────────────────
|
|
692
812
|
if (!error) return;
|
|
693
|
-
// Ignore cancellation
|
|
694
|
-
if (error.code == NSURLErrorCancelled)
|
|
813
|
+
// Ignore cancellation — but still clean up taskIdMap to prevent memory leak
|
|
814
|
+
if (error.code == NSURLErrorCancelled) {
|
|
815
|
+
dispatch_sync(self.syncQueue, ^{
|
|
816
|
+
[self.taskIdMap removeObjectForKey:taskKey];
|
|
817
|
+
});
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
695
820
|
|
|
696
|
-
NSString *downloadId =
|
|
821
|
+
__block NSString *downloadId = nil;
|
|
822
|
+
__block NSDictionary *options = nil;
|
|
823
|
+
__block NSInteger currentAttempt = 0;
|
|
824
|
+
dispatch_sync(self.syncQueue, ^{
|
|
825
|
+
downloadId = self.taskIdMap[taskKey];
|
|
826
|
+
if (downloadId) {
|
|
827
|
+
options = self.downloadOptions[downloadId];
|
|
828
|
+
currentAttempt = [self.retryAttempts[downloadId] integerValue];
|
|
829
|
+
}
|
|
830
|
+
});
|
|
697
831
|
if (!downloadId) return;
|
|
698
832
|
|
|
699
|
-
NSDictionary *options = self.downloadOptions[downloadId];
|
|
700
833
|
BOOL isBackground = [options[@"background"] boolValue];
|
|
701
834
|
|
|
702
|
-
// ── Retry logic
|
|
835
|
+
// ── Retry logic ────────────────────────────────────────────────────
|
|
703
836
|
NSDictionary *retryConfig = options[@"retry"];
|
|
704
837
|
NSInteger maxAttempts = retryConfig ? [retryConfig[@"attempts"] integerValue] : 0;
|
|
705
838
|
NSInteger baseDelay = retryConfig ? ([retryConfig[@"delay"] integerValue] ?: 1000) : 1000;
|
|
706
|
-
NSInteger currentAttempt = [self.retryAttempts[downloadId] integerValue];
|
|
707
839
|
|
|
708
840
|
// Remove old task mapping — will be replaced on retry
|
|
709
|
-
|
|
710
|
-
|
|
841
|
+
dispatch_sync(self.syncQueue, ^{
|
|
842
|
+
[self.taskIdMap removeObjectForKey:taskKey];
|
|
843
|
+
[self.activeTasks removeObjectForKey:downloadId];
|
|
844
|
+
});
|
|
711
845
|
|
|
712
846
|
if (currentAttempt < maxAttempts) {
|
|
713
847
|
// Schedule a retry
|
|
714
848
|
NSInteger nextAttempt = currentAttempt + 1;
|
|
715
|
-
self.
|
|
849
|
+
dispatch_sync(self.syncQueue, ^{
|
|
850
|
+
self.retryAttempts[downloadId] = @(nextAttempt);
|
|
851
|
+
});
|
|
716
852
|
|
|
717
|
-
// Bug #4 fix: avoid integer overflow — cap shift operand, then apply MIN
|
|
718
853
|
NSInteger shiftBits = currentAttempt < 15 ? currentAttempt : 15;
|
|
719
854
|
NSInteger delayMs = MIN(baseDelay * (1 << shiftBits), (NSInteger)30000);
|
|
720
855
|
|
|
721
856
|
// Emit retry event so JS onRetry callback is called
|
|
722
|
-
// Bug #3 fix: include the error description that triggered this retry
|
|
723
857
|
[self sendEventWithName:@"onDownloadRetry" body:@{
|
|
724
858
|
@"downloadId": downloadId,
|
|
725
859
|
@"url": options[@"url"] ?: @"",
|
|
@@ -727,16 +861,14 @@ didCompleteWithError:(NSError *)error {
|
|
|
727
861
|
@"error": error.localizedDescription ?: @""
|
|
728
862
|
}];
|
|
729
863
|
|
|
730
|
-
// Bug #2 fix: use __weak self to avoid retain cycle and crash on dealloc during delay
|
|
731
864
|
__weak typeof(self) weakSelf = self;
|
|
732
865
|
dispatch_after(
|
|
733
866
|
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayMs * NSEC_PER_MSEC)),
|
|
734
867
|
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
|
735
868
|
^{
|
|
736
869
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
737
|
-
if (!strongSelf) return;
|
|
870
|
+
if (!strongSelf) return;
|
|
738
871
|
|
|
739
|
-
// Recreate task with same options & downloadId
|
|
740
872
|
NSString *urlString = options[@"url"];
|
|
741
873
|
if (!urlString) return;
|
|
742
874
|
NSURL *url = [NSURL URLWithString:urlString];
|
|
@@ -752,8 +884,10 @@ didCompleteWithError:(NSError *)error {
|
|
|
752
884
|
NSURLSessionDownloadTask *newTask = [sess downloadTaskWithRequest:request];
|
|
753
885
|
NSString *newTaskKey = [NSString stringWithFormat:@"%lu",
|
|
754
886
|
(unsigned long)newTask.taskIdentifier];
|
|
755
|
-
strongSelf.
|
|
756
|
-
|
|
887
|
+
dispatch_sync(strongSelf.syncQueue, ^{
|
|
888
|
+
strongSelf.taskIdMap[newTaskKey] = downloadId;
|
|
889
|
+
strongSelf.activeTasks[downloadId] = newTask;
|
|
890
|
+
});
|
|
757
891
|
newTask.taskDescription = downloadId;
|
|
758
892
|
[newTask resume];
|
|
759
893
|
}
|
|
@@ -761,21 +895,25 @@ didCompleteWithError:(NSError *)error {
|
|
|
761
895
|
return; // Don't resolve promise yet — retry is in flight
|
|
762
896
|
}
|
|
763
897
|
|
|
764
|
-
// ── No more retries — normal error path
|
|
765
|
-
[self.retryAttempts removeObjectForKey:downloadId];
|
|
766
|
-
|
|
898
|
+
// ── No more retries — normal error path ──────────────────────────────
|
|
767
899
|
NSDictionary *errDict = @{@"success": @NO, @"downloadId": downloadId, @"error": error.localizedDescription};
|
|
768
900
|
|
|
769
|
-
NSDictionary *funcs =
|
|
901
|
+
__block NSDictionary *funcs = nil;
|
|
902
|
+
dispatch_sync(self.syncQueue, ^{
|
|
903
|
+
[self.retryAttempts removeObjectForKey:downloadId];
|
|
904
|
+
funcs = self.activePromises[downloadId];
|
|
905
|
+
});
|
|
906
|
+
// Always emit the event so global onDownloadError listeners fire on both platforms.
|
|
907
|
+
[self sendEventWithName:@"onDownloadError" body:errDict];
|
|
770
908
|
if (funcs && !isBackground) {
|
|
771
909
|
RCTPromiseResolveBlock resolve = funcs[@"resolve"];
|
|
772
910
|
resolve(errDict);
|
|
773
|
-
} else {
|
|
774
|
-
[self sendEventWithName:@"onDownloadError" body:errDict];
|
|
775
911
|
}
|
|
776
912
|
|
|
777
|
-
|
|
778
|
-
|
|
913
|
+
dispatch_sync(self.syncQueue, ^{
|
|
914
|
+
[self.activePromises removeObjectForKey:downloadId];
|
|
915
|
+
[self.downloadOptions removeObjectForKey:downloadId];
|
|
916
|
+
});
|
|
779
917
|
}
|
|
780
918
|
|
|
781
919
|
// ─── Upload progress delegate ────────────────────────────────────────────────
|
|
@@ -785,15 +923,22 @@ didCompleteWithError:(NSError *)error {
|
|
|
785
923
|
didSendBodyData:(int64_t)bytesSent
|
|
786
924
|
totalBytesSent:(int64_t)totalBytesSent
|
|
787
925
|
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
|
|
788
|
-
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
789
|
-
NSString *uploadId = self.uploadTaskIdMap[taskKey];
|
|
790
|
-
if (!uploadId) return;
|
|
791
|
-
|
|
792
|
-
NSString *url = self.uploadUrls[uploadId] ?: @"";
|
|
793
926
|
if (totalBytesExpectedToSend > 0) {
|
|
927
|
+
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
794
928
|
int progress = (int)((totalBytesSent * 100) / totalBytesExpectedToSend);
|
|
795
|
-
|
|
796
|
-
|
|
929
|
+
|
|
930
|
+
__weak typeof(self) weakSelf = self;
|
|
931
|
+
dispatch_async(self.syncQueue, ^{
|
|
932
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
933
|
+
if (!strongSelf) return;
|
|
934
|
+
|
|
935
|
+
NSString *uploadId = strongSelf.uploadTaskIdMap[taskKey];
|
|
936
|
+
if (uploadId) {
|
|
937
|
+
NSString *url = strongSelf.uploadUrls[uploadId] ?: @"";
|
|
938
|
+
[strongSelf sendEventWithName:@"onUploadProgress"
|
|
939
|
+
body:@{@"url": url, @"uploadId": uploadId, @"progress": @(progress)}];
|
|
940
|
+
}
|
|
941
|
+
});
|
|
797
942
|
}
|
|
798
943
|
}
|
|
799
944
|
|
|
@@ -803,15 +948,20 @@ totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
|
|
|
803
948
|
dataTask:(NSURLSessionDataTask *)dataTask
|
|
804
949
|
didReceiveData:(NSData *)data {
|
|
805
950
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)dataTask.taskIdentifier];
|
|
806
|
-
NSString *uploadId =
|
|
951
|
+
__block NSString *uploadId = nil;
|
|
952
|
+
dispatch_sync(self.syncQueue, ^{
|
|
953
|
+
uploadId = self.uploadTaskIdMap[taskKey];
|
|
954
|
+
});
|
|
807
955
|
if (!uploadId) return;
|
|
808
956
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
responseData
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
957
|
+
dispatch_sync(self.syncQueue, ^{
|
|
958
|
+
NSMutableData *responseData = self.uploadResponseData[uploadId];
|
|
959
|
+
if (!responseData) {
|
|
960
|
+
responseData = [NSMutableData new];
|
|
961
|
+
self.uploadResponseData[uploadId] = responseData;
|
|
962
|
+
}
|
|
963
|
+
[responseData appendData:data];
|
|
964
|
+
});
|
|
815
965
|
}
|
|
816
966
|
|
|
817
967
|
// ─── TurboModule ──────────────────────────────────────────────────────────────
|
|
@@ -904,23 +1054,24 @@ totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
|
|
|
904
1054
|
NSURLSessionUploadTask *task = [self.fgSession uploadTaskWithRequest:request fromFile:tempFileURL];
|
|
905
1055
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
906
1056
|
|
|
907
|
-
self.
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1057
|
+
dispatch_sync(self.syncQueue, ^{
|
|
1058
|
+
self.uploadTaskIdMap[taskKey] = uploadId;
|
|
1059
|
+
self.uploadPromises[uploadId] = @{
|
|
1060
|
+
@"resolve": resolve,
|
|
1061
|
+
@"reject": reject,
|
|
1062
|
+
@"tempFile": tempFilePath
|
|
1063
|
+
};
|
|
1064
|
+
self.uploadUrls[uploadId] = urlString;
|
|
1065
|
+
});
|
|
914
1066
|
|
|
915
1067
|
[task resume];
|
|
916
1068
|
}
|
|
917
1069
|
|
|
918
1070
|
// ─── saveBase64AsFile ─────────────────────────────────────────────────────────
|
|
919
1071
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
saveRejecter:(RCTPromiseRejectBlock)reject)
|
|
1072
|
+
RCT_EXPORT_METHOD(saveBase64AsFile:(NSDictionary *)options
|
|
1073
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1074
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
924
1075
|
{
|
|
925
1076
|
NSString *base64String = options[@"base64Data"];
|
|
926
1077
|
if (!base64String || base64String.length == 0) {
|
|
@@ -959,10 +1110,9 @@ RCT_REMAP_METHOD(saveBase64AsFile,
|
|
|
959
1110
|
|
|
960
1111
|
// ─── urlToBase64 ──────────────────────────────────────────────────────────────
|
|
961
1112
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
urlRejecter:(RCTPromiseRejectBlock)reject)
|
|
1113
|
+
RCT_EXPORT_METHOD(urlToBase64:(NSDictionary *)options
|
|
1114
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1115
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
966
1116
|
{
|
|
967
1117
|
NSString *urlString = options[@"url"];
|
|
968
1118
|
if (!urlString || urlString.length == 0) {
|
|
@@ -987,7 +1137,10 @@ RCT_REMAP_METHOD(urlToBase64,
|
|
|
987
1137
|
}
|
|
988
1138
|
}
|
|
989
1139
|
|
|
990
|
-
|
|
1140
|
+
// Create ephemeral session instead of using sharedSession
|
|
1141
|
+
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
|
1142
|
+
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
|
|
1143
|
+
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
991
1144
|
if (error) {
|
|
992
1145
|
resolve(@{@"success": @NO, @"error": error.localizedDescription});
|
|
993
1146
|
return;
|
|
@@ -1022,13 +1175,35 @@ RCT_REMAP_METHOD(urlToBase64,
|
|
|
1022
1175
|
[task resume];
|
|
1023
1176
|
}
|
|
1024
1177
|
|
|
1178
|
+
// ─── topMostViewController (works with both AppDelegate-window and SceneDelegate) ─────
|
|
1179
|
+
|
|
1180
|
+
- (UIViewController *)topMostViewController {
|
|
1181
|
+
UIWindow *window = nil;
|
|
1182
|
+
if (@available(iOS 13.0, *)) {
|
|
1183
|
+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
|
1184
|
+
if (scene.activationState == UISceneActivationStateForegroundActive &&
|
|
1185
|
+
[scene isKindOfClass:[UIWindowScene class]]) {
|
|
1186
|
+
window = ((UIWindowScene *)scene).windows.firstObject;
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (!window) {
|
|
1192
|
+
window = [UIApplication sharedApplication].delegate.window;
|
|
1193
|
+
}
|
|
1194
|
+
UIViewController *rootVC = window.rootViewController;
|
|
1195
|
+
while (rootVC.presentedViewController) {
|
|
1196
|
+
rootVC = rootVC.presentedViewController;
|
|
1197
|
+
}
|
|
1198
|
+
return rootVC;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1025
1201
|
// ─── shareFile ────────────────────────────────────────────────────────────────
|
|
1026
1202
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
shareRejecter:(RCTPromiseRejectBlock)reject)
|
|
1203
|
+
RCT_EXPORT_METHOD(shareFile:(NSString *)filePath
|
|
1204
|
+
options:(NSDictionary *)options
|
|
1205
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1206
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
1032
1207
|
{
|
|
1033
1208
|
if (!filePath || filePath.length == 0) {
|
|
1034
1209
|
resolve(@{@"success": @NO, @"error": @"File path is required"});
|
|
@@ -1043,11 +1218,10 @@ RCT_REMAP_METHOD(shareFile,
|
|
|
1043
1218
|
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
|
|
1044
1219
|
|
|
1045
1220
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1046
|
-
UIViewController *rootViewController = [
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
rootViewController = rootViewController.presentedViewController;
|
|
1221
|
+
UIViewController *rootViewController = [self topMostViewController];
|
|
1222
|
+
if (!rootViewController) {
|
|
1223
|
+
resolve(@{@"success": @NO, @"error": @"No visible view controller found"});
|
|
1224
|
+
return;
|
|
1051
1225
|
}
|
|
1052
1226
|
|
|
1053
1227
|
NSArray *itemsToShare = @[fileURL];
|
|
@@ -1076,11 +1250,10 @@ RCT_REMAP_METHOD(shareFile,
|
|
|
1076
1250
|
|
|
1077
1251
|
// ─── openFile ─────────────────────────────────────────────────────────────────
|
|
1078
1252
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
openRejecter:(RCTPromiseRejectBlock)reject)
|
|
1253
|
+
RCT_EXPORT_METHOD(openFile:(NSString *)filePath
|
|
1254
|
+
mimeType:(NSString *)mimeType
|
|
1255
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1256
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
1084
1257
|
{
|
|
1085
1258
|
if (!filePath || filePath.length == 0) {
|
|
1086
1259
|
resolve(@{@"success": @NO, @"error": @"File path is required"});
|
|
@@ -1095,11 +1268,10 @@ RCT_REMAP_METHOD(openFile,
|
|
|
1095
1268
|
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
|
|
1096
1269
|
|
|
1097
1270
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1098
|
-
UIViewController *rootViewController = [
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
rootViewController = rootViewController.presentedViewController;
|
|
1271
|
+
UIViewController *rootViewController = [self topMostViewController];
|
|
1272
|
+
if (!rootViewController) {
|
|
1273
|
+
resolve(@{@"success": @NO, @"error": @"No visible view controller found"});
|
|
1274
|
+
return;
|
|
1103
1275
|
}
|
|
1104
1276
|
|
|
1105
1277
|
// Use UIDocumentInteractionController for opening files
|
|
@@ -1127,11 +1299,7 @@ RCT_REMAP_METHOD(openFile,
|
|
|
1127
1299
|
|
|
1128
1300
|
// UIDocumentInteractionControllerDelegate method
|
|
1129
1301
|
- (UIViewController *)documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *)controller {
|
|
1130
|
-
|
|
1131
|
-
while (rootViewController.presentedViewController) {
|
|
1132
|
-
rootViewController = rootViewController.presentedViewController;
|
|
1133
|
-
}
|
|
1134
|
-
return rootViewController;
|
|
1302
|
+
return [self topMostViewController];
|
|
1135
1303
|
}
|
|
1136
1304
|
|
|
1137
1305
|
// ─── Unzip ────────────────────────────────────────────────────────────────────
|
|
@@ -1267,12 +1435,31 @@ RCT_EXPORT_METHOD(unzip:(NSString *)sourcePath
|
|
|
1267
1435
|
if (method == 0) {
|
|
1268
1436
|
// Store (no compression)
|
|
1269
1437
|
NSData *fileData = [NSData dataWithBytes:compData length:compSize];
|
|
1438
|
+
// Verify CRC32 before writing
|
|
1439
|
+
uint32_t expectedCRC = CFSwapInt32LittleToHost(crc32val);
|
|
1440
|
+
uLong actualCRC = crc32(0L, (const Bytef *)fileData.bytes, (uInt)fileData.length);
|
|
1441
|
+
if ((uint32_t)actualCRC != expectedCRC) {
|
|
1442
|
+
if (error) *error = [NSError errorWithDomain:@"RNFileToolkit" code:-3
|
|
1443
|
+
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"CRC32 mismatch for entry: %@", fileName]}];
|
|
1444
|
+
return NO;
|
|
1445
|
+
}
|
|
1270
1446
|
if (![fileData writeToFile:destPath options:NSDataWritingAtomic error:error]) {
|
|
1271
1447
|
return NO;
|
|
1272
1448
|
}
|
|
1273
1449
|
} else if (method == 8) {
|
|
1274
1450
|
// Deflate — use zlib inflate with raw deflate stream
|
|
1275
|
-
|
|
1451
|
+
// Guard against maliciously crafted entries claiming huge uncompressed sizes.
|
|
1452
|
+
// Cap at 512 MB — large enough for legitimate files, small enough to be safe.
|
|
1453
|
+
const uint32_t kMaxUncompSize = 512u * 1024u * 1024u;
|
|
1454
|
+
if (uncompSize > kMaxUncompSize) {
|
|
1455
|
+
if (error) {
|
|
1456
|
+
*error = [NSError errorWithDomain:@"RNFileToolkit" code:-4
|
|
1457
|
+
userInfo:@{NSLocalizedDescriptionKey:
|
|
1458
|
+
[NSString stringWithFormat:@"ZIP entry uncompressed size (%u bytes) exceeds limit", uncompSize]}];
|
|
1459
|
+
}
|
|
1460
|
+
return NO;
|
|
1461
|
+
}
|
|
1462
|
+
NSMutableData *output = [NSMutableData dataWithLength:uncompSize > 0 ? uncompSize : 65536];
|
|
1276
1463
|
z_stream strm;
|
|
1277
1464
|
memset(&strm, 0, sizeof(strm));
|
|
1278
1465
|
strm.next_in = (Bytef *)compData;
|
|
@@ -1314,6 +1501,14 @@ RCT_EXPORT_METHOD(unzip:(NSString *)sourcePath
|
|
|
1314
1501
|
output = result;
|
|
1315
1502
|
}
|
|
1316
1503
|
|
|
1504
|
+
// Verify CRC32 of decompressed data before writing
|
|
1505
|
+
uint32_t expectedCRC = CFSwapInt32LittleToHost(crc32val);
|
|
1506
|
+
uLong actualCRC = crc32(0L, (const Bytef *)output.bytes, (uInt)output.length);
|
|
1507
|
+
if ((uint32_t)actualCRC != expectedCRC) {
|
|
1508
|
+
if (error) *error = [NSError errorWithDomain:@"RNFileToolkit" code:-3
|
|
1509
|
+
userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"CRC32 mismatch for entry: %@", fileName]}];
|
|
1510
|
+
return NO;
|
|
1511
|
+
}
|
|
1317
1512
|
if (![output writeToFile:destPath options:NSDataWritingAtomic error:error]) {
|
|
1318
1513
|
return NO;
|
|
1319
1514
|
}
|
|
@@ -1737,10 +1932,9 @@ RCT_EXPORT_METHOD(zip:(NSString *)sourcePath
|
|
|
1737
1932
|
|
|
1738
1933
|
// ─── saveToMediaStore ─────────────────────────────────────────────────────
|
|
1739
1934
|
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
mediaStoreRejecter:(RCTPromiseRejectBlock)reject)
|
|
1935
|
+
RCT_EXPORT_METHOD(saveToMediaStore:(NSDictionary *)options
|
|
1936
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1937
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
1744
1938
|
{
|
|
1745
1939
|
NSString *filePath = options[@"filePath"];
|
|
1746
1940
|
if (!filePath || filePath.length == 0) {
|
|
@@ -1757,21 +1951,27 @@ RCT_REMAP_METHOD(saveToMediaStore,
|
|
|
1757
1951
|
|
|
1758
1952
|
if ([mediaType isEqualToString:@"image"] || [mediaType isEqualToString:@"video"]) {
|
|
1759
1953
|
// Use Photos framework for images and videos
|
|
1760
|
-
PHPhotoLibrary
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
if ([mediaType isEqualToString:@"image"]) {
|
|
1765
|
-
[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:fileURL];
|
|
1766
|
-
} else {
|
|
1767
|
-
[PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:fileURL];
|
|
1768
|
-
}
|
|
1769
|
-
} completionHandler:^(BOOL success, NSError *error) {
|
|
1770
|
-
if (success) {
|
|
1771
|
-
resolve(@{@"success": @YES, @"uri": filePath});
|
|
1772
|
-
} else {
|
|
1773
|
-
resolve(@{@"success": @NO, @"error": error.localizedDescription ?: @"Failed to save to Photos"});
|
|
1954
|
+
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
|
|
1955
|
+
if (status != PHAuthorizationStatusAuthorized && status != PHAuthorizationStatusLimited) {
|
|
1956
|
+
resolve(@{@"success": @NO, @"error": @"Photo library access denied"});
|
|
1957
|
+
return;
|
|
1774
1958
|
}
|
|
1959
|
+
|
|
1960
|
+
PHPhotoLibrary *photoLibrary = [PHPhotoLibrary sharedPhotoLibrary];
|
|
1961
|
+
[photoLibrary performChanges:^{
|
|
1962
|
+
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
|
|
1963
|
+
if ([mediaType isEqualToString:@"image"]) {
|
|
1964
|
+
[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:fileURL];
|
|
1965
|
+
} else {
|
|
1966
|
+
[PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:fileURL];
|
|
1967
|
+
}
|
|
1968
|
+
} completionHandler:^(BOOL success, NSError *error) {
|
|
1969
|
+
if (success) {
|
|
1970
|
+
resolve(@{@"success": @YES, @"uri": filePath});
|
|
1971
|
+
} else {
|
|
1972
|
+
resolve(@{@"success": @NO, @"error": error.localizedDescription ?: @"Failed to save to Photos"});
|
|
1973
|
+
}
|
|
1974
|
+
}];
|
|
1775
1975
|
}];
|
|
1776
1976
|
} else {
|
|
1777
1977
|
// For audio/download types, copy to Documents directory (iOS has no shared media store for these)
|