rn-file-toolkit 1.0.10 → 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 +227 -128
- package/app.plugin.js +34 -8
- package/ios/FileToolkit.mm +346 -155
- package/lib/commonjs/NativeFileToolkit.js.map +1 -1
- package/lib/commonjs/index.js +109 -30
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/NativeFileToolkit.js.map +1 -1
- package/lib/module/index.js +109 -31
- 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 +189 -35
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 {
|
|
@@ -102,22 +115,52 @@ RCT_EXPORT_MODULE()
|
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
- (NSString *)calculateChecksumForPath:(NSString *)path algorithm:(NSString *)algo {
|
|
105
|
-
|
|
106
|
-
if (!
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
|
|
109
125
|
if ([algo isEqualToString:@"MD5"]) {
|
|
110
|
-
|
|
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);
|
|
111
136
|
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
|
|
112
137
|
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) [output appendFormat:@"%02x", digest[i]];
|
|
113
138
|
return output;
|
|
114
139
|
} else if ([algo isEqualToString:@"SHA1"]) {
|
|
115
|
-
|
|
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);
|
|
116
150
|
NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];
|
|
117
151
|
for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) [output appendFormat:@"%02x", digest[i]];
|
|
118
152
|
return output;
|
|
119
153
|
} else {
|
|
120
|
-
|
|
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);
|
|
121
164
|
NSMutableString *output = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
|
|
122
165
|
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) [output appendFormat:@"%02x", digest[i]];
|
|
123
166
|
return output;
|
|
@@ -147,6 +190,10 @@ RCT_EXPORT_MODULE()
|
|
|
147
190
|
BOOL isBackground = [options[@"background"] boolValue];
|
|
148
191
|
NSString *downloadId = options[@"downloadId"] ?: [self generateDownloadId];
|
|
149
192
|
NSURL *url = [NSURL URLWithString:urlString];
|
|
193
|
+
if (!url) {
|
|
194
|
+
resolve(@{@"success": @NO, @"error": @"Invalid URL"});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
150
197
|
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
|
151
198
|
|
|
152
199
|
// Add custom headers
|
|
@@ -161,16 +208,20 @@ RCT_EXPORT_MODULE()
|
|
|
161
208
|
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request];
|
|
162
209
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
163
210
|
|
|
164
|
-
self.
|
|
165
|
-
|
|
166
|
-
|
|
211
|
+
dispatch_sync(self.syncQueue, ^{
|
|
212
|
+
self.taskIdMap[taskKey] = downloadId;
|
|
213
|
+
self.activeTasks[downloadId] = task;
|
|
214
|
+
self.downloadOptions[downloadId] = options;
|
|
215
|
+
});
|
|
167
216
|
task.taskDescription = downloadId;
|
|
168
217
|
|
|
169
218
|
if (isBackground) {
|
|
170
219
|
// Resolve immediately with the downloadId — result comes via event
|
|
171
220
|
resolve(@{@"success": @YES, @"downloadId": downloadId});
|
|
172
221
|
} else {
|
|
173
|
-
self.
|
|
222
|
+
dispatch_sync(self.syncQueue, ^{
|
|
223
|
+
self.activePromises[downloadId] = @{@"resolve": resolve, @"reject": reject};
|
|
224
|
+
});
|
|
174
225
|
}
|
|
175
226
|
|
|
176
227
|
[task resume];
|
|
@@ -179,17 +230,22 @@ RCT_EXPORT_MODULE()
|
|
|
179
230
|
// ─── pauseDownload ────────────────────────────────────────────────────────────
|
|
180
231
|
|
|
181
232
|
- (void)pauseDownload:(NSString *)downloadId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
182
|
-
NSURLSessionDownloadTask *task =
|
|
233
|
+
__block NSURLSessionDownloadTask *task = nil;
|
|
234
|
+
dispatch_sync(self.syncQueue, ^{
|
|
235
|
+
task = self.activeTasks[downloadId];
|
|
236
|
+
});
|
|
183
237
|
if (!task) {
|
|
184
238
|
resolve(@{@"success": @NO, @"error": @"Download not found"});
|
|
185
239
|
return;
|
|
186
240
|
}
|
|
187
241
|
|
|
188
242
|
[task cancelByProducingResumeData:^(NSData *resumeData) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
243
|
+
dispatch_sync(self.syncQueue, ^{
|
|
244
|
+
if (resumeData) {
|
|
245
|
+
self.resumeDataStore[downloadId] = resumeData;
|
|
246
|
+
}
|
|
247
|
+
[self.activeTasks removeObjectForKey:downloadId];
|
|
248
|
+
});
|
|
193
249
|
resolve(@{@"success": @YES});
|
|
194
250
|
}];
|
|
195
251
|
}
|
|
@@ -197,22 +253,28 @@ RCT_EXPORT_MODULE()
|
|
|
197
253
|
// ─── resumeDownload ───────────────────────────────────────────────────────────
|
|
198
254
|
|
|
199
255
|
- (void)resumeDownload:(NSString *)downloadId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
200
|
-
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
|
+
});
|
|
201
262
|
if (!resumeData) {
|
|
202
263
|
resolve(@{@"success": @NO, @"error": @"No resume data — download was not paused or was cancelled"});
|
|
203
264
|
return;
|
|
204
265
|
}
|
|
205
266
|
|
|
206
|
-
NSDictionary *options = self.downloadOptions[downloadId];
|
|
207
267
|
BOOL isBackground = [options[@"background"] boolValue];
|
|
208
268
|
NSURLSession *session = isBackground ? self.bgSession : self.fgSession;
|
|
209
269
|
|
|
210
270
|
NSURLSessionDownloadTask *task = [session downloadTaskWithResumeData:resumeData];
|
|
211
271
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
212
272
|
|
|
213
|
-
self.
|
|
214
|
-
|
|
215
|
-
|
|
273
|
+
dispatch_sync(self.syncQueue, ^{
|
|
274
|
+
self.taskIdMap[taskKey] = downloadId;
|
|
275
|
+
self.activeTasks[downloadId] = task;
|
|
276
|
+
[self.resumeDataStore removeObjectForKey:downloadId];
|
|
277
|
+
});
|
|
216
278
|
|
|
217
279
|
[task resume];
|
|
218
280
|
resolve(@{@"success": @YES});
|
|
@@ -221,15 +283,26 @@ RCT_EXPORT_MODULE()
|
|
|
221
283
|
// ─── cancelDownload ───────────────────────────────────────────────────────────
|
|
222
284
|
|
|
223
285
|
- (void)cancelDownload:(NSString *)downloadId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
224
|
-
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
|
+
}
|
|
225
303
|
if (task) {
|
|
226
304
|
[task cancel];
|
|
227
|
-
[self.activeTasks removeObjectForKey:downloadId];
|
|
228
305
|
}
|
|
229
|
-
[self.resumeDataStore removeObjectForKey:downloadId];
|
|
230
|
-
[self.activePromises removeObjectForKey:downloadId];
|
|
231
|
-
[self.downloadOptions removeObjectForKey:downloadId];
|
|
232
|
-
[self.retryAttempts removeObjectForKey:downloadId];
|
|
233
306
|
resolve(@{@"success": @YES});
|
|
234
307
|
}
|
|
235
308
|
|
|
@@ -581,17 +654,24 @@ totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
|
|
|
581
654
|
if (totalBytesExpectedToWrite > 0) {
|
|
582
655
|
int progress = (int)((totalBytesWritten * 100) / totalBytesExpectedToWrite);
|
|
583
656
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)downloadTask.taskIdentifier];
|
|
584
|
-
NSString *downloadId = self.taskIdMap[taskKey] ?: @"";
|
|
585
657
|
NSString *url = downloadTask.originalRequest.URL.absoluteString ?: @"";
|
|
586
658
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
+
});
|
|
595
675
|
}
|
|
596
676
|
}
|
|
597
677
|
|
|
@@ -599,10 +679,16 @@ totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
|
|
|
599
679
|
downloadTask:(NSURLSessionDownloadTask *)downloadTask
|
|
600
680
|
didFinishDownloadingToURL:(NSURL *)location {
|
|
601
681
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)downloadTask.taskIdentifier];
|
|
602
|
-
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
|
+
});
|
|
603
690
|
if (!downloadId) return;
|
|
604
691
|
|
|
605
|
-
NSDictionary *options = self.downloadOptions[downloadId];
|
|
606
692
|
NSString *fileName = [self fileNameFromOptions:options task:downloadTask];
|
|
607
693
|
NSString *destType = options[@"destination"] ?: @"downloads";
|
|
608
694
|
NSURL *destURL = [self destURLForFileName:fileName destination:destType];
|
|
@@ -639,24 +725,34 @@ didFinishDownloadingToURL:(NSURL *)location {
|
|
|
639
725
|
}
|
|
640
726
|
}
|
|
641
727
|
|
|
642
|
-
NSDictionary *funcs =
|
|
728
|
+
__block NSDictionary *funcs = nil;
|
|
643
729
|
BOOL isBackground = [options[@"background"] boolValue];
|
|
730
|
+
dispatch_sync(self.syncQueue, ^{
|
|
731
|
+
funcs = self.activePromises[downloadId];
|
|
732
|
+
});
|
|
644
733
|
|
|
645
734
|
if (funcs && !isBackground) {
|
|
646
735
|
// Foreground: resolve the promise
|
|
647
736
|
RCTPromiseResolveBlock resolve = funcs[@"resolve"];
|
|
648
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
|
+
}
|
|
649
743
|
} else {
|
|
650
|
-
// Background: fire the correct event based on isError flag
|
|
744
|
+
// Background: fire the correct event based on isError flag
|
|
651
745
|
NSString *event = isError ? @"onDownloadError" : @"onDownloadComplete";
|
|
652
746
|
[self sendEventWithName:event body:resultDict];
|
|
653
747
|
}
|
|
654
748
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
+
});
|
|
660
756
|
}
|
|
661
757
|
|
|
662
758
|
- (void)URLSession:(NSURLSession *)session
|
|
@@ -664,12 +760,20 @@ didFinishDownloadingToURL:(NSURL *)location {
|
|
|
664
760
|
didCompleteWithError:(NSError *)error {
|
|
665
761
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
666
762
|
|
|
667
|
-
// ── Upload task completion
|
|
668
|
-
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
|
+
});
|
|
669
774
|
if (uploadId) {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
NSString *tempFile = funcs[@"tempFile"];
|
|
775
|
+
RCTPromiseResolveBlock uploadResolve = uploadFuncs[@"resolve"];
|
|
776
|
+
NSString *tempFile = uploadFuncs[@"tempFile"];
|
|
673
777
|
|
|
674
778
|
if (tempFile) {
|
|
675
779
|
[[NSFileManager defaultManager] removeItemAtPath:tempFile error:nil];
|
|
@@ -679,7 +783,7 @@ didCompleteWithError:(NSError *)error {
|
|
|
679
783
|
if (uploadResolve) uploadResolve(@{@"success": @NO, @"error": error.localizedDescription, @"uploadId": uploadId});
|
|
680
784
|
} else {
|
|
681
785
|
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
|
|
682
|
-
NSData *responseData =
|
|
786
|
+
NSData *responseData = uploadRespData ?: [NSData data];
|
|
683
787
|
NSString *respString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] ?: @"";
|
|
684
788
|
|
|
685
789
|
if (uploadResolve) uploadResolve(@{
|
|
@@ -690,45 +794,66 @@ didCompleteWithError:(NSError *)error {
|
|
|
690
794
|
});
|
|
691
795
|
}
|
|
692
796
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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;
|
|
698
809
|
}
|
|
699
810
|
|
|
700
811
|
// ── Download task error handling ───────────────────────────────────────
|
|
701
812
|
if (!error) return;
|
|
702
|
-
// Ignore cancellation
|
|
703
|
-
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
|
+
}
|
|
704
820
|
|
|
705
|
-
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
|
+
});
|
|
706
831
|
if (!downloadId) return;
|
|
707
832
|
|
|
708
|
-
NSDictionary *options = self.downloadOptions[downloadId];
|
|
709
833
|
BOOL isBackground = [options[@"background"] boolValue];
|
|
710
834
|
|
|
711
|
-
// ── Retry logic
|
|
835
|
+
// ── Retry logic ────────────────────────────────────────────────────
|
|
712
836
|
NSDictionary *retryConfig = options[@"retry"];
|
|
713
837
|
NSInteger maxAttempts = retryConfig ? [retryConfig[@"attempts"] integerValue] : 0;
|
|
714
838
|
NSInteger baseDelay = retryConfig ? ([retryConfig[@"delay"] integerValue] ?: 1000) : 1000;
|
|
715
|
-
NSInteger currentAttempt = [self.retryAttempts[downloadId] integerValue];
|
|
716
839
|
|
|
717
840
|
// Remove old task mapping — will be replaced on retry
|
|
718
|
-
|
|
719
|
-
|
|
841
|
+
dispatch_sync(self.syncQueue, ^{
|
|
842
|
+
[self.taskIdMap removeObjectForKey:taskKey];
|
|
843
|
+
[self.activeTasks removeObjectForKey:downloadId];
|
|
844
|
+
});
|
|
720
845
|
|
|
721
846
|
if (currentAttempt < maxAttempts) {
|
|
722
847
|
// Schedule a retry
|
|
723
848
|
NSInteger nextAttempt = currentAttempt + 1;
|
|
724
|
-
self.
|
|
849
|
+
dispatch_sync(self.syncQueue, ^{
|
|
850
|
+
self.retryAttempts[downloadId] = @(nextAttempt);
|
|
851
|
+
});
|
|
725
852
|
|
|
726
|
-
// Bug #4 fix: avoid integer overflow — cap shift operand, then apply MIN
|
|
727
853
|
NSInteger shiftBits = currentAttempt < 15 ? currentAttempt : 15;
|
|
728
854
|
NSInteger delayMs = MIN(baseDelay * (1 << shiftBits), (NSInteger)30000);
|
|
729
855
|
|
|
730
856
|
// Emit retry event so JS onRetry callback is called
|
|
731
|
-
// Bug #3 fix: include the error description that triggered this retry
|
|
732
857
|
[self sendEventWithName:@"onDownloadRetry" body:@{
|
|
733
858
|
@"downloadId": downloadId,
|
|
734
859
|
@"url": options[@"url"] ?: @"",
|
|
@@ -736,16 +861,14 @@ didCompleteWithError:(NSError *)error {
|
|
|
736
861
|
@"error": error.localizedDescription ?: @""
|
|
737
862
|
}];
|
|
738
863
|
|
|
739
|
-
// Bug #2 fix: use __weak self to avoid retain cycle and crash on dealloc during delay
|
|
740
864
|
__weak typeof(self) weakSelf = self;
|
|
741
865
|
dispatch_after(
|
|
742
866
|
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayMs * NSEC_PER_MSEC)),
|
|
743
867
|
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
|
744
868
|
^{
|
|
745
869
|
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
746
|
-
if (!strongSelf) return;
|
|
870
|
+
if (!strongSelf) return;
|
|
747
871
|
|
|
748
|
-
// Recreate task with same options & downloadId
|
|
749
872
|
NSString *urlString = options[@"url"];
|
|
750
873
|
if (!urlString) return;
|
|
751
874
|
NSURL *url = [NSURL URLWithString:urlString];
|
|
@@ -761,8 +884,10 @@ didCompleteWithError:(NSError *)error {
|
|
|
761
884
|
NSURLSessionDownloadTask *newTask = [sess downloadTaskWithRequest:request];
|
|
762
885
|
NSString *newTaskKey = [NSString stringWithFormat:@"%lu",
|
|
763
886
|
(unsigned long)newTask.taskIdentifier];
|
|
764
|
-
strongSelf.
|
|
765
|
-
|
|
887
|
+
dispatch_sync(strongSelf.syncQueue, ^{
|
|
888
|
+
strongSelf.taskIdMap[newTaskKey] = downloadId;
|
|
889
|
+
strongSelf.activeTasks[downloadId] = newTask;
|
|
890
|
+
});
|
|
766
891
|
newTask.taskDescription = downloadId;
|
|
767
892
|
[newTask resume];
|
|
768
893
|
}
|
|
@@ -770,21 +895,25 @@ didCompleteWithError:(NSError *)error {
|
|
|
770
895
|
return; // Don't resolve promise yet — retry is in flight
|
|
771
896
|
}
|
|
772
897
|
|
|
773
|
-
// ── No more retries — normal error path
|
|
774
|
-
[self.retryAttempts removeObjectForKey:downloadId];
|
|
775
|
-
|
|
898
|
+
// ── No more retries — normal error path ──────────────────────────────
|
|
776
899
|
NSDictionary *errDict = @{@"success": @NO, @"downloadId": downloadId, @"error": error.localizedDescription};
|
|
777
900
|
|
|
778
|
-
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];
|
|
779
908
|
if (funcs && !isBackground) {
|
|
780
909
|
RCTPromiseResolveBlock resolve = funcs[@"resolve"];
|
|
781
910
|
resolve(errDict);
|
|
782
|
-
} else {
|
|
783
|
-
[self sendEventWithName:@"onDownloadError" body:errDict];
|
|
784
911
|
}
|
|
785
912
|
|
|
786
|
-
|
|
787
|
-
|
|
913
|
+
dispatch_sync(self.syncQueue, ^{
|
|
914
|
+
[self.activePromises removeObjectForKey:downloadId];
|
|
915
|
+
[self.downloadOptions removeObjectForKey:downloadId];
|
|
916
|
+
});
|
|
788
917
|
}
|
|
789
918
|
|
|
790
919
|
// ─── Upload progress delegate ────────────────────────────────────────────────
|
|
@@ -794,15 +923,22 @@ didCompleteWithError:(NSError *)error {
|
|
|
794
923
|
didSendBodyData:(int64_t)bytesSent
|
|
795
924
|
totalBytesSent:(int64_t)totalBytesSent
|
|
796
925
|
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
|
|
797
|
-
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
798
|
-
NSString *uploadId = self.uploadTaskIdMap[taskKey];
|
|
799
|
-
if (!uploadId) return;
|
|
800
|
-
|
|
801
|
-
NSString *url = self.uploadUrls[uploadId] ?: @"";
|
|
802
926
|
if (totalBytesExpectedToSend > 0) {
|
|
927
|
+
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
803
928
|
int progress = (int)((totalBytesSent * 100) / totalBytesExpectedToSend);
|
|
804
|
-
|
|
805
|
-
|
|
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
|
+
});
|
|
806
942
|
}
|
|
807
943
|
}
|
|
808
944
|
|
|
@@ -812,15 +948,20 @@ totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
|
|
|
812
948
|
dataTask:(NSURLSessionDataTask *)dataTask
|
|
813
949
|
didReceiveData:(NSData *)data {
|
|
814
950
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)dataTask.taskIdentifier];
|
|
815
|
-
NSString *uploadId =
|
|
951
|
+
__block NSString *uploadId = nil;
|
|
952
|
+
dispatch_sync(self.syncQueue, ^{
|
|
953
|
+
uploadId = self.uploadTaskIdMap[taskKey];
|
|
954
|
+
});
|
|
816
955
|
if (!uploadId) return;
|
|
817
956
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
responseData
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
+
});
|
|
824
965
|
}
|
|
825
966
|
|
|
826
967
|
// ─── TurboModule ──────────────────────────────────────────────────────────────
|
|
@@ -913,23 +1054,24 @@ totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
|
|
|
913
1054
|
NSURLSessionUploadTask *task = [self.fgSession uploadTaskWithRequest:request fromFile:tempFileURL];
|
|
914
1055
|
NSString *taskKey = [NSString stringWithFormat:@"%lu", (unsigned long)task.taskIdentifier];
|
|
915
1056
|
|
|
916
|
-
self.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
+
});
|
|
923
1066
|
|
|
924
1067
|
[task resume];
|
|
925
1068
|
}
|
|
926
1069
|
|
|
927
1070
|
// ─── saveBase64AsFile ─────────────────────────────────────────────────────────
|
|
928
1071
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
saveRejecter:(RCTPromiseRejectBlock)reject)
|
|
1072
|
+
RCT_EXPORT_METHOD(saveBase64AsFile:(NSDictionary *)options
|
|
1073
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1074
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
933
1075
|
{
|
|
934
1076
|
NSString *base64String = options[@"base64Data"];
|
|
935
1077
|
if (!base64String || base64String.length == 0) {
|
|
@@ -968,10 +1110,9 @@ RCT_REMAP_METHOD(saveBase64AsFile,
|
|
|
968
1110
|
|
|
969
1111
|
// ─── urlToBase64 ──────────────────────────────────────────────────────────────
|
|
970
1112
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
urlRejecter:(RCTPromiseRejectBlock)reject)
|
|
1113
|
+
RCT_EXPORT_METHOD(urlToBase64:(NSDictionary *)options
|
|
1114
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1115
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
975
1116
|
{
|
|
976
1117
|
NSString *urlString = options[@"url"];
|
|
977
1118
|
if (!urlString || urlString.length == 0) {
|
|
@@ -996,7 +1137,10 @@ RCT_REMAP_METHOD(urlToBase64,
|
|
|
996
1137
|
}
|
|
997
1138
|
}
|
|
998
1139
|
|
|
999
|
-
|
|
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) {
|
|
1000
1144
|
if (error) {
|
|
1001
1145
|
resolve(@{@"success": @NO, @"error": error.localizedDescription});
|
|
1002
1146
|
return;
|
|
@@ -1031,13 +1175,35 @@ RCT_REMAP_METHOD(urlToBase64,
|
|
|
1031
1175
|
[task resume];
|
|
1032
1176
|
}
|
|
1033
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
|
+
|
|
1034
1201
|
// ─── shareFile ────────────────────────────────────────────────────────────────
|
|
1035
1202
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
shareRejecter:(RCTPromiseRejectBlock)reject)
|
|
1203
|
+
RCT_EXPORT_METHOD(shareFile:(NSString *)filePath
|
|
1204
|
+
options:(NSDictionary *)options
|
|
1205
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1206
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
1041
1207
|
{
|
|
1042
1208
|
if (!filePath || filePath.length == 0) {
|
|
1043
1209
|
resolve(@{@"success": @NO, @"error": @"File path is required"});
|
|
@@ -1052,11 +1218,10 @@ RCT_REMAP_METHOD(shareFile,
|
|
|
1052
1218
|
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
|
|
1053
1219
|
|
|
1054
1220
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1055
|
-
UIViewController *rootViewController = [
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
rootViewController = rootViewController.presentedViewController;
|
|
1221
|
+
UIViewController *rootViewController = [self topMostViewController];
|
|
1222
|
+
if (!rootViewController) {
|
|
1223
|
+
resolve(@{@"success": @NO, @"error": @"No visible view controller found"});
|
|
1224
|
+
return;
|
|
1060
1225
|
}
|
|
1061
1226
|
|
|
1062
1227
|
NSArray *itemsToShare = @[fileURL];
|
|
@@ -1085,11 +1250,10 @@ RCT_REMAP_METHOD(shareFile,
|
|
|
1085
1250
|
|
|
1086
1251
|
// ─── openFile ─────────────────────────────────────────────────────────────────
|
|
1087
1252
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
openRejecter:(RCTPromiseRejectBlock)reject)
|
|
1253
|
+
RCT_EXPORT_METHOD(openFile:(NSString *)filePath
|
|
1254
|
+
mimeType:(NSString *)mimeType
|
|
1255
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1256
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
1093
1257
|
{
|
|
1094
1258
|
if (!filePath || filePath.length == 0) {
|
|
1095
1259
|
resolve(@{@"success": @NO, @"error": @"File path is required"});
|
|
@@ -1104,11 +1268,10 @@ RCT_REMAP_METHOD(openFile,
|
|
|
1104
1268
|
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
|
|
1105
1269
|
|
|
1106
1270
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1107
|
-
UIViewController *rootViewController = [
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
rootViewController = rootViewController.presentedViewController;
|
|
1271
|
+
UIViewController *rootViewController = [self topMostViewController];
|
|
1272
|
+
if (!rootViewController) {
|
|
1273
|
+
resolve(@{@"success": @NO, @"error": @"No visible view controller found"});
|
|
1274
|
+
return;
|
|
1112
1275
|
}
|
|
1113
1276
|
|
|
1114
1277
|
// Use UIDocumentInteractionController for opening files
|
|
@@ -1136,11 +1299,7 @@ RCT_REMAP_METHOD(openFile,
|
|
|
1136
1299
|
|
|
1137
1300
|
// UIDocumentInteractionControllerDelegate method
|
|
1138
1301
|
- (UIViewController *)documentInteractionControllerViewControllerForPreview:(UIDocumentInteractionController *)controller {
|
|
1139
|
-
|
|
1140
|
-
while (rootViewController.presentedViewController) {
|
|
1141
|
-
rootViewController = rootViewController.presentedViewController;
|
|
1142
|
-
}
|
|
1143
|
-
return rootViewController;
|
|
1302
|
+
return [self topMostViewController];
|
|
1144
1303
|
}
|
|
1145
1304
|
|
|
1146
1305
|
// ─── Unzip ────────────────────────────────────────────────────────────────────
|
|
@@ -1276,12 +1435,31 @@ RCT_EXPORT_METHOD(unzip:(NSString *)sourcePath
|
|
|
1276
1435
|
if (method == 0) {
|
|
1277
1436
|
// Store (no compression)
|
|
1278
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
|
+
}
|
|
1279
1446
|
if (![fileData writeToFile:destPath options:NSDataWritingAtomic error:error]) {
|
|
1280
1447
|
return NO;
|
|
1281
1448
|
}
|
|
1282
1449
|
} else if (method == 8) {
|
|
1283
1450
|
// Deflate — use zlib inflate with raw deflate stream
|
|
1284
|
-
|
|
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];
|
|
1285
1463
|
z_stream strm;
|
|
1286
1464
|
memset(&strm, 0, sizeof(strm));
|
|
1287
1465
|
strm.next_in = (Bytef *)compData;
|
|
@@ -1323,6 +1501,14 @@ RCT_EXPORT_METHOD(unzip:(NSString *)sourcePath
|
|
|
1323
1501
|
output = result;
|
|
1324
1502
|
}
|
|
1325
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
|
+
}
|
|
1326
1512
|
if (![output writeToFile:destPath options:NSDataWritingAtomic error:error]) {
|
|
1327
1513
|
return NO;
|
|
1328
1514
|
}
|
|
@@ -1746,10 +1932,9 @@ RCT_EXPORT_METHOD(zip:(NSString *)sourcePath
|
|
|
1746
1932
|
|
|
1747
1933
|
// ─── saveToMediaStore ─────────────────────────────────────────────────────
|
|
1748
1934
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
mediaStoreRejecter:(RCTPromiseRejectBlock)reject)
|
|
1935
|
+
RCT_EXPORT_METHOD(saveToMediaStore:(NSDictionary *)options
|
|
1936
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
1937
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
1753
1938
|
{
|
|
1754
1939
|
NSString *filePath = options[@"filePath"];
|
|
1755
1940
|
if (!filePath || filePath.length == 0) {
|
|
@@ -1766,21 +1951,27 @@ RCT_REMAP_METHOD(saveToMediaStore,
|
|
|
1766
1951
|
|
|
1767
1952
|
if ([mediaType isEqualToString:@"image"] || [mediaType isEqualToString:@"video"]) {
|
|
1768
1953
|
// Use Photos framework for images and videos
|
|
1769
|
-
PHPhotoLibrary
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
if ([mediaType isEqualToString:@"image"]) {
|
|
1774
|
-
[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:fileURL];
|
|
1775
|
-
} else {
|
|
1776
|
-
[PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:fileURL];
|
|
1777
|
-
}
|
|
1778
|
-
} completionHandler:^(BOOL success, NSError *error) {
|
|
1779
|
-
if (success) {
|
|
1780
|
-
resolve(@{@"success": @YES, @"uri": filePath});
|
|
1781
|
-
} else {
|
|
1782
|
-
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;
|
|
1783
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
|
+
}];
|
|
1784
1975
|
}];
|
|
1785
1976
|
} else {
|
|
1786
1977
|
// For audio/download types, copy to Documents directory (iOS has no shared media store for these)
|