@kesha-antonov/react-native-background-downloader 4.5.4 → 4.5.5
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 +45 -0
- package/android/src/main/java/com/eko/DownloadConstants.kt +3 -0
- package/android/src/main/java/com/eko/Downloader.kt +92 -0
- package/android/src/main/java/com/eko/RNBackgroundDownloaderModuleImpl.kt +79 -5
- package/android/src/main/java/com/eko/ResumableDownloader.kt +7 -0
- package/android/src/main/java/com/eko/utils/StorageManager.kt +95 -0
- package/ios/RNBGDTaskConfig.h +3 -0
- package/ios/RNBGDTaskConfig.mm +3 -0
- package/ios/RNBackgroundDownloader.mm +325 -42
- package/lib/DownloadTask.d.ts +39 -0
- package/lib/DownloadTask.js +176 -0
- package/lib/NativeRNBackgroundDownloader.d.ts +127 -0
- package/lib/NativeRNBackgroundDownloader.js +4 -0
- package/lib/UploadTask.d.ts +30 -0
- package/lib/UploadTask.js +174 -0
- package/lib/config.d.ts +33 -0
- package/lib/config.js +79 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +498 -0
- package/lib/types.d.ts +249 -0
- package/lib/types.js +2 -0
- package/package.json +7 -4
- package/plugin/package.json +6 -0
- package/react-native-background-downloader.podspec +8 -2
- package/src/DownloadTask.ts +2 -0
- package/src/NativeRNBackgroundDownloader.ts +2 -0
- package/src/config.ts +6 -1
- package/src/index.ts +4 -0
- package/src/types.ts +27 -0
- package/CHANGELOG.md +0 -294
- package/example/android/app/debug.keystore +0 -0
- package/ios/.DS_Store +0 -0
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
#define ID_TO_UPLOAD_CONFIG_MAP_KEY @"com.eko.bguploadidmap"
|
|
14
14
|
#define PROGRESS_INTERVAL_KEY @"progressInterval"
|
|
15
15
|
#define PROGRESS_MIN_BYTES_KEY @"progressMinBytes"
|
|
16
|
+
// Persisted map of downloads whose finished file could not be moved to its destination
|
|
17
|
+
// yet because the device was locked (Data Protection). The move is retried when protected
|
|
18
|
+
// data becomes available / on next launch. See issue #101.
|
|
19
|
+
#define PENDING_MOVES_MAP_KEY @"com.eko.bgdpendingmoves"
|
|
16
20
|
|
|
17
21
|
// Session configuration constants
|
|
18
22
|
static const NSInteger kMaxConnectionsPerHost = 4;
|
|
@@ -66,6 +70,11 @@ static CompletionHandler storedCompletionHandler;
|
|
|
66
70
|
// Controls whether debug logs are sent to JS
|
|
67
71
|
BOOL isLogsEnabled;
|
|
68
72
|
|
|
73
|
+
// Downloads that finished but whose file couldn't be moved to its destination because
|
|
74
|
+
// the device was locked. Keyed by configId -> staging/destination info. Retried on
|
|
75
|
+
// UIApplicationProtectedDataDidBecomeAvailable, app foreground, and launch. See #101.
|
|
76
|
+
NSMutableDictionary<NSString *, NSDictionary *> *pendingMoves;
|
|
77
|
+
|
|
69
78
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
70
79
|
// Queue of events that arrived before the TurboModule event emitter callback was set.
|
|
71
80
|
// This prevents crashes (std::bad_function_call / SIGABRT) when NSURLSession delegate
|
|
@@ -240,6 +249,11 @@ static const int kMaxEventRetries = 50; // 50 retries * 100ms = 5 seconds max w
|
|
|
240
249
|
isSessionActivated = NO;
|
|
241
250
|
pendingDownloads = [[NSMutableArray alloc] init];
|
|
242
251
|
|
|
252
|
+
// Restore any downloads whose file move was deferred because the device was locked
|
|
253
|
+
NSData *pendingMovesData = [mmkv getDataForKey:PENDING_MOVES_MAP_KEY];
|
|
254
|
+
NSMutableDictionary *pendingMovesDecoded = pendingMovesData != nil ? [self deserializePendingMoves:pendingMovesData] : nil;
|
|
255
|
+
pendingMoves = pendingMovesDecoded != nil ? pendingMovesDecoded : [[NSMutableDictionary alloc] init];
|
|
256
|
+
|
|
243
257
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
244
258
|
pendingEmitEvents = [[NSMutableArray alloc] init];
|
|
245
259
|
#endif
|
|
@@ -260,6 +274,10 @@ static const int kMaxEventRetries = 50; // 50 retries * 100ms = 5 seconds max w
|
|
|
260
274
|
|
|
261
275
|
// Initialize session early to receive background events on app relaunch
|
|
262
276
|
[self lazyRegisterSession];
|
|
277
|
+
|
|
278
|
+
// Retry any moves that were deferred while the device was locked (e.g. a download
|
|
279
|
+
// that finished in the background before the first unlock). See #101.
|
|
280
|
+
[self processPendingMoves];
|
|
263
281
|
}
|
|
264
282
|
|
|
265
283
|
return self;
|
|
@@ -341,10 +359,23 @@ static const int kMaxEventRetries = 50; // 50 retries * 100ms = 5 seconds max w
|
|
|
341
359
|
selector:@selector(handleBridgeHotReload:)
|
|
342
360
|
name:RCTJavaScriptWillStartLoadingNotification
|
|
343
361
|
object:nil];
|
|
362
|
+
|
|
363
|
+
// Finish any moves deferred while the device was locked, as soon as
|
|
364
|
+
// protected data becomes available again (device unlocked). See #101.
|
|
365
|
+
// Note: the ObjC constant has no "Notification" suffix (unlike most UIApplication ones).
|
|
366
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
367
|
+
selector:@selector(handleProtectedDataAvailable:)
|
|
368
|
+
name:UIApplicationProtectedDataDidBecomeAvailable
|
|
369
|
+
object:nil];
|
|
344
370
|
}
|
|
345
371
|
}
|
|
346
372
|
}
|
|
347
373
|
|
|
374
|
+
- (void)handleProtectedDataAvailable:(NSNotification *) note {
|
|
375
|
+
DLog(nil, @"[RNBackgroundDownloader] - [handleProtectedDataAvailable]");
|
|
376
|
+
[self processPendingMoves];
|
|
377
|
+
}
|
|
378
|
+
|
|
348
379
|
- (void)unregisterBridgeListener {
|
|
349
380
|
DLog(nil, @"[RNBackgroundDownloader] - [unregisterBridgeListener]");
|
|
350
381
|
if (isBridgeListenerInited == YES) {
|
|
@@ -356,6 +387,8 @@ static const int kMaxEventRetries = 50; // 50 retries * 100ms = 5 seconds max w
|
|
|
356
387
|
- (void)handleBridgeAppEnterForeground:(NSNotification *) note {
|
|
357
388
|
DLog(nil, @"[RNBackgroundDownloader] - [handleBridgeAppEnterForeground]");
|
|
358
389
|
[self resumeTasks];
|
|
390
|
+
// Device is unlocked when foregrounded, so finish any locked-deferred file moves.
|
|
391
|
+
[self processPendingMoves];
|
|
359
392
|
}
|
|
360
393
|
|
|
361
394
|
- (void)resumeTasks {
|
|
@@ -414,15 +447,24 @@ RCT_EXPORT_METHOD(setLogsEnabled:(BOOL)enabled) {
|
|
|
414
447
|
|
|
415
448
|
- (void)_setMaxParallelDownloadsInternal:(NSInteger)max {
|
|
416
449
|
DLog(nil, @"[RNBackgroundDownloader] - [setMaxParallelDownloads:%ld]", (long)max);
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
450
|
+
// Never let an NSException escape this void method. methodQueue is a custom
|
|
451
|
+
// background queue, and on the new architecture the TurboModule bridge would
|
|
452
|
+
// convert an escaping exception into a JS error via Hermes JSI on this thread
|
|
453
|
+
// (not the JS thread). Hermes is not thread-safe, so that crashes with SIGSEGV.
|
|
454
|
+
// See https://github.com/kesha-antonov/react-native-background-downloader/issues/161
|
|
455
|
+
@try {
|
|
456
|
+
@synchronized (sharedLock) {
|
|
457
|
+
if (max >= 1) {
|
|
458
|
+
sessionConfig.HTTPMaximumConnectionsPerHost = max;
|
|
459
|
+
// Recreate session with new config if it's already initialized
|
|
460
|
+
if (urlSession != nil) {
|
|
461
|
+
[self unregisterSession];
|
|
462
|
+
[self lazyRegisterSession];
|
|
463
|
+
}
|
|
424
464
|
}
|
|
425
465
|
}
|
|
466
|
+
} @catch (NSException *exception) {
|
|
467
|
+
DLog(nil, @"[RNBackgroundDownloader] - [setMaxParallelDownloads] error: %@", exception.reason);
|
|
426
468
|
}
|
|
427
469
|
}
|
|
428
470
|
|
|
@@ -438,13 +480,20 @@ RCT_EXPORT_METHOD(setMaxParallelDownloads:(NSInteger)max) {
|
|
|
438
480
|
|
|
439
481
|
- (void)_setAllowsCellularAccessInternal:(BOOL)allows {
|
|
440
482
|
DLog(nil, @"[RNBackgroundDownloader] - [setAllowsCellularAccess:%@]", allows ? @"YES" : @"NO");
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
483
|
+
// Never let an NSException escape this void method - see the note on
|
|
484
|
+
// -_setMaxParallelDownloadsInternal: and issue #161. Recreating the session
|
|
485
|
+
// here can throw, and an escaping exception crashes Hermes off the JS thread.
|
|
486
|
+
@try {
|
|
487
|
+
@synchronized (sharedLock) {
|
|
488
|
+
sessionConfig.allowsCellularAccess = allows;
|
|
489
|
+
// Recreate session with new config if it's already initialized
|
|
490
|
+
if (urlSession != nil) {
|
|
491
|
+
[self unregisterSession];
|
|
492
|
+
[self lazyRegisterSession];
|
|
493
|
+
}
|
|
447
494
|
}
|
|
495
|
+
} @catch (NSException *exception) {
|
|
496
|
+
DLog(nil, @"[RNBackgroundDownloader] - [setAllowsCellularAccess] error: %@", exception.reason);
|
|
448
497
|
}
|
|
449
498
|
}
|
|
450
499
|
|
|
@@ -466,6 +515,7 @@ RCT_EXPORT_METHOD(setAllowsCellularAccess:(BOOL)allows) {
|
|
|
466
515
|
NSString *destination = options.destination();
|
|
467
516
|
NSString *metadata = options.metadata() ? options.metadata() : @"";
|
|
468
517
|
NSDictionary *headers = options.headers() ? (NSDictionary *)options.headers() : nil;
|
|
518
|
+
NSString *dataProtection = options.iosDataProtection() ? options.iosDataProtection() : nil;
|
|
469
519
|
|
|
470
520
|
if (options.progressInterval().has_value()) {
|
|
471
521
|
progressInterval = options.progressInterval().value() / 1000.0;
|
|
@@ -486,6 +536,7 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
|
|
|
486
536
|
NSString *destination = options[@"destination"];
|
|
487
537
|
NSString *metadata = options[@"metadata"] ?: @"";
|
|
488
538
|
NSDictionary *headers = options[@"headers"];
|
|
539
|
+
NSString *dataProtection = options[@"iosDataProtection"];
|
|
489
540
|
|
|
490
541
|
NSNumber *progressIntervalScope = options[@"progressInterval"];
|
|
491
542
|
if (progressIntervalScope) {
|
|
@@ -522,38 +573,47 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
|
|
|
522
573
|
}
|
|
523
574
|
}
|
|
524
575
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
576
|
+
// download is a void method dispatched on the custom background methodQueue.
|
|
577
|
+
// Any NSException that escapes would be converted to a JS error via Hermes JSI
|
|
578
|
+
// on this thread (not the JS thread) and crash with SIGSEGV (issue #161), so we
|
|
579
|
+
// never let one propagate out of this method.
|
|
580
|
+
@try {
|
|
581
|
+
@synchronized (sharedLock) {
|
|
582
|
+
[self sendDebugLog:@"download: calling lazyRegisterSession" taskId:identifier];
|
|
583
|
+
[self lazyRegisterSession];
|
|
528
584
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
585
|
+
// If session is not yet activated, queue the download to be executed after activation
|
|
586
|
+
// This fixes the issue where downloads don't start on fresh app installs
|
|
587
|
+
if (!isSessionActivated) {
|
|
588
|
+
DLog(identifier, @"[RNBackgroundDownloader] - [download] session not activated, queueing download");
|
|
589
|
+
[self sendDebugLog:@"download: session not activated, queueing download" taskId:identifier];
|
|
590
|
+
__weak RNBackgroundDownloader *weakSelf = self;
|
|
591
|
+
dispatch_block_t downloadBlock = ^{
|
|
592
|
+
RNBackgroundDownloader *strongSelf = weakSelf;
|
|
593
|
+
if (strongSelf) {
|
|
594
|
+
[strongSelf executeDownloadWithRequest:request identifier:identifier url:url destination:destination metadata:metadata dataProtection:dataProtection];
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
[pendingDownloads addObject:downloadBlock];
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
544
600
|
|
|
545
|
-
|
|
546
|
-
|
|
601
|
+
[self sendDebugLog:@"download: session activated, executing download" taskId:identifier];
|
|
602
|
+
[self executeDownloadWithRequest:request identifier:identifier url:url destination:destination metadata:metadata dataProtection:dataProtection];
|
|
603
|
+
}
|
|
604
|
+
} @catch (NSException *exception) {
|
|
605
|
+
DLog(identifier, @"[RNBackgroundDownloader] - [download] error: %@", exception.reason);
|
|
606
|
+
[self sendDebugLog:[NSString stringWithFormat:@"download: ERROR - %@", exception.reason] taskId:identifier];
|
|
547
607
|
}
|
|
548
608
|
}
|
|
549
609
|
|
|
550
610
|
// Internal method to execute the download after session is activated
|
|
551
|
-
- (void)executeDownloadWithRequest:(NSMutableURLRequest *)request identifier:(NSString *)identifier url:(NSString *)url destination:(NSString *)destination metadata:(NSString *)metadata {
|
|
611
|
+
- (void)executeDownloadWithRequest:(NSMutableURLRequest *)request identifier:(NSString *)identifier url:(NSString *)url destination:(NSString *)destination metadata:(NSString *)metadata dataProtection:(NSString *)dataProtection {
|
|
552
612
|
@synchronized (sharedLock) {
|
|
553
613
|
DLog(identifier, @"[RNBackgroundDownloader] - [executeDownloadWithRequest]");
|
|
554
614
|
[self sendDebugLog:@"executeDownloadWithRequest: creating download task" taskId:identifier];
|
|
555
615
|
|
|
556
|
-
NSURLSessionDownloadTask __strong *task = [
|
|
616
|
+
NSURLSessionDownloadTask __strong *task = [self createDownloadTaskWithRequest:request identifier:identifier];
|
|
557
617
|
if (task == nil) {
|
|
558
618
|
DLog(identifier, @"[RNBackgroundDownloader] - [Error] failed to create download task");
|
|
559
619
|
[self sendDebugLog:@"executeDownloadWithRequest: ERROR - failed to create download task" taskId:identifier];
|
|
@@ -568,6 +628,8 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
|
|
|
568
628
|
@"destination": destination,
|
|
569
629
|
@"metadata": metadata
|
|
570
630
|
}];
|
|
631
|
+
// nil means "use the library default" when saving the file (see -nsFileProtectionFromKey:)
|
|
632
|
+
taskConfig.dataProtection = dataProtection;
|
|
571
633
|
|
|
572
634
|
taskToConfigMap[@(task.taskIdentifier)] = taskConfig;
|
|
573
635
|
[mmkv setData:[self serialize: taskToConfigMap] forKey:ID_TO_CONFIG_MAP_KEY];
|
|
@@ -582,6 +644,36 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
|
|
|
582
644
|
}
|
|
583
645
|
}
|
|
584
646
|
|
|
647
|
+
// Creates a download task on the current session. If the session has been
|
|
648
|
+
// invalidated (e.g. after a hot reload or a prior invalidateAndCancel),
|
|
649
|
+
// -downloadTaskWithRequest: raises:
|
|
650
|
+
// "attempted to create a NSURLSessionDownloadTask in a session that has
|
|
651
|
+
// been invalidated"
|
|
652
|
+
// In that case we recreate the background session and retry once.
|
|
653
|
+
// Must be called while holding sharedLock.
|
|
654
|
+
// See https://github.com/kesha-antonov/react-native-background-downloader/issues/157
|
|
655
|
+
- (NSURLSessionDownloadTask *)createDownloadTaskWithRequest:(NSMutableURLRequest *)request identifier:(NSString *)identifier {
|
|
656
|
+
@try {
|
|
657
|
+
return [urlSession downloadTaskWithRequest:request];
|
|
658
|
+
} @catch (NSException *exception) {
|
|
659
|
+
DLog(identifier, @"[RNBackgroundDownloader] - [createDownloadTask] session invalidated, recreating: %@", exception.reason);
|
|
660
|
+
[self sendDebugLog:[NSString stringWithFormat:@"createDownloadTask: session invalidated, recreating (%@)", exception.reason] taskId:identifier];
|
|
661
|
+
|
|
662
|
+
// Recreate the background session directly (do not call unregisterSession,
|
|
663
|
+
// which would invalidateAndCancel and drop queued downloads).
|
|
664
|
+
urlSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
|
|
665
|
+
isSessionActivated = YES;
|
|
666
|
+
|
|
667
|
+
@try {
|
|
668
|
+
return [urlSession downloadTaskWithRequest:request];
|
|
669
|
+
} @catch (NSException *retryException) {
|
|
670
|
+
DLog(identifier, @"[RNBackgroundDownloader] - [createDownloadTask] retry failed: %@", retryException.reason);
|
|
671
|
+
[self sendDebugLog:[NSString stringWithFormat:@"createDownloadTask: retry failed (%@)", retryException.reason] taskId:identifier];
|
|
672
|
+
return nil;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
585
677
|
- (void)pauseTaskInternal:(NSString *)identifier resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
586
678
|
DLog(identifier, @"[RNBackgroundDownloader] - [pauseTask]");
|
|
587
679
|
@try {
|
|
@@ -776,6 +868,15 @@ RCT_EXPORT_METHOD(resumeTask:(NSString *)id resolver:(RCTPromiseResolveBlock)res
|
|
|
776
868
|
[idsToPauseSet removeObject:identifier];
|
|
777
869
|
// Clean up any updated headers
|
|
778
870
|
[idToUpdatedHeadersMap removeObjectForKey:identifier];
|
|
871
|
+
// Drop any locked-deferred move staged for this task (#101)
|
|
872
|
+
NSDictionary *pendingMove = pendingMoves[identifier];
|
|
873
|
+
if (pendingMove != nil) {
|
|
874
|
+
NSString *stagingPath = pendingMove[@"staging"];
|
|
875
|
+
if (stagingPath != nil)
|
|
876
|
+
[[NSFileManager defaultManager] removeItemAtPath:stagingPath error:nil];
|
|
877
|
+
[pendingMoves removeObjectForKey:identifier];
|
|
878
|
+
[self persistPendingMoves];
|
|
879
|
+
}
|
|
779
880
|
}
|
|
780
881
|
resolve(nil);
|
|
781
882
|
} @catch (NSException *exception) {
|
|
@@ -1194,17 +1295,27 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
|
|
|
1194
1295
|
[self sendDebugLog:@"didFinishDownloadingToURL: download finished" taskId:taskConfig.id];
|
|
1195
1296
|
|
|
1196
1297
|
NSError *error = [self getServerError:downloadTask];
|
|
1298
|
+
BOOL deferred = NO;
|
|
1197
1299
|
if (!error) {
|
|
1198
|
-
[self saveFile:taskConfig downloadURL:location error:&error];
|
|
1300
|
+
[self saveFile:taskConfig downloadURL:location bytesDownloaded:downloadTask.countOfBytesReceived bytesTotal:downloadTask.countOfBytesExpectedToReceive error:&error deferred:&deferred];
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Drop any buffered progress for this task so it cannot arrive in JS after downloadComplete
|
|
1304
|
+
[progressReports removeObjectForKey:taskConfig.id];
|
|
1305
|
+
|
|
1306
|
+
if (deferred) {
|
|
1307
|
+
// The device was locked and the file couldn't be moved to its destination yet.
|
|
1308
|
+
// The bytes are staged and the move (and the downloadComplete event) will happen
|
|
1309
|
+
// once protected data becomes available. Do not emit complete/failed now. See #101.
|
|
1310
|
+
[self sendDebugLog:@"didFinishDownloadingToURL: move deferred until device unlock" taskId:taskConfig.id];
|
|
1311
|
+
[self removeTaskFromMap:downloadTask];
|
|
1312
|
+
return;
|
|
1199
1313
|
}
|
|
1200
1314
|
|
|
1201
1315
|
if (error) {
|
|
1202
1316
|
[self sendDebugLog:[NSString stringWithFormat:@"didFinishDownloadingToURL: error - %@", error.localizedDescription] taskId:taskConfig.id];
|
|
1203
1317
|
}
|
|
1204
1318
|
|
|
1205
|
-
// Drop any buffered progress for this task so it cannot arrive in JS after downloadComplete
|
|
1206
|
-
[progressReports removeObjectForKey:taskConfig.id];
|
|
1207
|
-
|
|
1208
1319
|
[self sendDownloadCompletionEvent:taskConfig task:downloadTask error:error];
|
|
1209
1320
|
|
|
1210
1321
|
[self removeTaskFromMap:downloadTask];
|
|
@@ -1981,8 +2092,20 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
1981
2092
|
userInfo:@{NSLocalizedDescriptionKey: [NSHTTPURLResponse localizedStringForStatusCode:statusCode]}];
|
|
1982
2093
|
}
|
|
1983
2094
|
|
|
1984
|
-
|
|
2095
|
+
// Maps the JS data-protection key to an NSFileProtection* constant.
|
|
2096
|
+
// nil / unknown -> NSFileProtectionCompleteUntilFirstUserAuthentication, which lets a
|
|
2097
|
+
// background download save its file even while the device is locked (after first unlock).
|
|
2098
|
+
// See https://github.com/kesha-antonov/react-native-background-downloader/issues/101
|
|
2099
|
+
- (NSString *)nsFileProtectionFromKey:(NSString *)key {
|
|
2100
|
+
if ([key isEqualToString:@"complete"]) return NSFileProtectionComplete;
|
|
2101
|
+
if ([key isEqualToString:@"completeUnlessOpen"]) return NSFileProtectionCompleteUnlessOpen;
|
|
2102
|
+
if ([key isEqualToString:@"none"]) return NSFileProtectionNone;
|
|
2103
|
+
return NSFileProtectionCompleteUntilFirstUserAuthentication;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
- (BOOL)saveFile: (nonnull RNBGDTaskConfig *) taskConfig downloadURL:(nonnull NSURL *)location bytesDownloaded:(int64_t)bytesDownloaded bytesTotal:(int64_t)bytesTotal error:(NSError **)saveError deferred:(BOOL *)deferred {
|
|
1985
2107
|
DLog(taskConfig.id, @"[RNBackgroundDownloader] - [saveFile]");
|
|
2108
|
+
if (deferred) *deferred = NO;
|
|
1986
2109
|
// taskConfig.destination is absolute path.
|
|
1987
2110
|
// The absolute path may change when the application is restarted.
|
|
1988
2111
|
// But the relative path remains the same.
|
|
@@ -2004,10 +2127,170 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
|
|
|
2004
2127
|
NSURL *destinationURL = [NSURL fileURLWithPath:fileAbsolutePath];
|
|
2005
2128
|
|
|
2006
2129
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
2007
|
-
|
|
2130
|
+
// Create the destination directory with a protection class that allows writing while the
|
|
2131
|
+
// device is locked, so a background download can save when it finishes on a locked device.
|
|
2132
|
+
[fileManager createDirectoryAtURL:[destinationURL URLByDeletingLastPathComponent]
|
|
2133
|
+
withIntermediateDirectories:YES
|
|
2134
|
+
attributes:@{NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication}
|
|
2135
|
+
error:nil];
|
|
2008
2136
|
[fileManager removeItemAtURL:destinationURL error:nil];
|
|
2009
2137
|
|
|
2010
|
-
|
|
2138
|
+
NSError *moveError = nil;
|
|
2139
|
+
BOOL moved = [fileManager moveItemAtURL:location toURL:destinationURL error:&moveError];
|
|
2140
|
+
if (moved) {
|
|
2141
|
+
// Apply the requested protection level to the final file (best-effort).
|
|
2142
|
+
NSString *protection = [self nsFileProtectionFromKey:taskConfig.dataProtection];
|
|
2143
|
+
[fileManager setAttributes:@{NSFileProtectionKey: protection} ofItemAtPath:destinationURL.path error:nil];
|
|
2144
|
+
return YES;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// The move failed - most commonly because the device is locked and the destination is
|
|
2148
|
+
// protected (Data Protection). Stage the downloaded bytes to an unprotected location and
|
|
2149
|
+
// finish the move once the device is unlocked, instead of losing the download. See #101.
|
|
2150
|
+
if ([self stageAndDeferMoveForConfig:taskConfig location:location destination:destinationURL bytesDownloaded:bytesDownloaded bytesTotal:bytesTotal]) {
|
|
2151
|
+
if (deferred) *deferred = YES;
|
|
2152
|
+
return NO;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// Couldn't even stage the file - report the original move error.
|
|
2156
|
+
if (saveError) *saveError = moveError;
|
|
2157
|
+
return NO;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
#pragma mark - Locked-device deferred move (issue #101)
|
|
2161
|
+
|
|
2162
|
+
// Directory (with NSFileProtectionNone) used to hold finished downloads whose move to the
|
|
2163
|
+
// final destination is deferred because the device is locked.
|
|
2164
|
+
- (NSURL *)pendingMovesDirectory {
|
|
2165
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
2166
|
+
NSURL *cachesDir = [[fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] firstObject];
|
|
2167
|
+
NSURL *dir = [cachesDir URLByAppendingPathComponent:@"RNBGDPendingMoves" isDirectory:YES];
|
|
2168
|
+
if (![fileManager fileExistsAtPath:dir.path]) {
|
|
2169
|
+
// NSFileProtectionNone so we can write here even while the device is locked.
|
|
2170
|
+
[fileManager createDirectoryAtURL:dir
|
|
2171
|
+
withIntermediateDirectories:YES
|
|
2172
|
+
attributes:@{NSFileProtectionKey: NSFileProtectionNone}
|
|
2173
|
+
error:nil];
|
|
2174
|
+
}
|
|
2175
|
+
return dir;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// Moves the just-finished download out of the system temp into an unprotected staging file and
|
|
2179
|
+
// records the pending move. Returns YES if the bytes were safely staged. Must hold sharedLock.
|
|
2180
|
+
- (BOOL)stageAndDeferMoveForConfig:(RNBGDTaskConfig *)taskConfig location:(NSURL *)location destination:(NSURL *)destinationURL bytesDownloaded:(int64_t)bytesDownloaded bytesTotal:(int64_t)bytesTotal {
|
|
2181
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
2182
|
+
NSURL *stagingURL = [[self pendingMovesDirectory] URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.staged", taskConfig.id]];
|
|
2183
|
+
|
|
2184
|
+
[fileManager removeItemAtURL:stagingURL error:nil];
|
|
2185
|
+
NSError *stageError = nil;
|
|
2186
|
+
if (![fileManager moveItemAtURL:location toURL:stagingURL error:&stageError]) {
|
|
2187
|
+
DLog(taskConfig.id, @"[RNBackgroundDownloader] - [stageAndDeferMove] staging failed: %@", stageError.localizedDescription);
|
|
2188
|
+
return NO;
|
|
2189
|
+
}
|
|
2190
|
+
// Make sure the staged file is readable while locked.
|
|
2191
|
+
[fileManager setAttributes:@{NSFileProtectionKey: NSFileProtectionNone} ofItemAtPath:stagingURL.path error:nil];
|
|
2192
|
+
|
|
2193
|
+
pendingMoves[taskConfig.id] = @{
|
|
2194
|
+
@"id": taskConfig.id,
|
|
2195
|
+
@"staging": stagingURL.path,
|
|
2196
|
+
@"destination": destinationURL.path,
|
|
2197
|
+
@"dataProtection": taskConfig.dataProtection ?: @"",
|
|
2198
|
+
@"bytesDownloaded": @(bytesDownloaded),
|
|
2199
|
+
@"bytesTotal": @(bytesTotal)
|
|
2200
|
+
};
|
|
2201
|
+
[self persistPendingMoves];
|
|
2202
|
+
DLog(taskConfig.id, @"[RNBackgroundDownloader] - [stageAndDeferMove] staged %lld bytes, move deferred until unlock", (long long)bytesDownloaded);
|
|
2203
|
+
return YES;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// Attempts to complete any deferred moves. Safe to call repeatedly; only succeeds for items
|
|
2207
|
+
// whose destination is currently writable (i.e. the device is unlocked enough). Emits
|
|
2208
|
+
// downloadComplete for each move that finishes.
|
|
2209
|
+
- (void)processPendingMoves {
|
|
2210
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
|
2211
|
+
@synchronized (self->sharedLock) {
|
|
2212
|
+
if (self->pendingMoves.count == 0) return;
|
|
2213
|
+
|
|
2214
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
2215
|
+
NSArray<NSString *> *ids = [self->pendingMoves allKeys];
|
|
2216
|
+
BOOL changed = NO;
|
|
2217
|
+
|
|
2218
|
+
for (NSString *taskId in ids) {
|
|
2219
|
+
NSDictionary *info = self->pendingMoves[taskId];
|
|
2220
|
+
NSString *stagingPath = info[@"staging"];
|
|
2221
|
+
NSString *destinationPath = info[@"destination"];
|
|
2222
|
+
if (stagingPath == nil || destinationPath == nil) {
|
|
2223
|
+
[self->pendingMoves removeObjectForKey:taskId];
|
|
2224
|
+
changed = YES;
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
if (![fileManager fileExistsAtPath:stagingPath]) {
|
|
2229
|
+
// Staged file is gone - nothing we can do, drop the record.
|
|
2230
|
+
[self->pendingMoves removeObjectForKey:taskId];
|
|
2231
|
+
changed = YES;
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
NSURL *destinationURL = [NSURL fileURLWithPath:destinationPath];
|
|
2236
|
+
[fileManager createDirectoryAtURL:[destinationURL URLByDeletingLastPathComponent]
|
|
2237
|
+
withIntermediateDirectories:YES
|
|
2238
|
+
attributes:@{NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication}
|
|
2239
|
+
error:nil];
|
|
2240
|
+
[fileManager removeItemAtURL:destinationURL error:nil];
|
|
2241
|
+
|
|
2242
|
+
NSError *moveError = nil;
|
|
2243
|
+
BOOL moved = [fileManager moveItemAtURL:[NSURL fileURLWithPath:stagingPath] toURL:destinationURL error:&moveError];
|
|
2244
|
+
if (!moved) {
|
|
2245
|
+
// Still can't write (device likely still locked) - keep it for the next attempt.
|
|
2246
|
+
DLog(taskId, @"[RNBackgroundDownloader] - [processPendingMoves] still deferred: %@", moveError.localizedDescription);
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
NSString *protection = [self nsFileProtectionFromKey:(info[@"dataProtection"] != nil && [info[@"dataProtection"] length] > 0 ? info[@"dataProtection"] : nil)];
|
|
2251
|
+
[fileManager setAttributes:@{NSFileProtectionKey: protection} ofItemAtPath:destinationURL.path error:nil];
|
|
2252
|
+
|
|
2253
|
+
[self->pendingMoves removeObjectForKey:taskId];
|
|
2254
|
+
changed = YES;
|
|
2255
|
+
|
|
2256
|
+
DLog(taskId, @"[RNBackgroundDownloader] - [processPendingMoves] completed deferred move");
|
|
2257
|
+
NSDictionary *body = @{
|
|
2258
|
+
@"id": taskId,
|
|
2259
|
+
@"location": destinationPath,
|
|
2260
|
+
@"bytesDownloaded": info[@"bytesDownloaded"] ?: @0,
|
|
2261
|
+
@"bytesTotal": info[@"bytesTotal"] ?: @0
|
|
2262
|
+
};
|
|
2263
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
2264
|
+
[self safeEmitEvent:@"onDownloadComplete" value:body];
|
|
2265
|
+
#else
|
|
2266
|
+
[self sendEventWithName:@"downloadComplete" body:body];
|
|
2267
|
+
#endif
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
if (changed) [self persistPendingMoves];
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
- (void)persistPendingMoves {
|
|
2276
|
+
NSError *error = nil;
|
|
2277
|
+
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:pendingMoves requiringSecureCoding:YES error:&error];
|
|
2278
|
+
if (error) {
|
|
2279
|
+
DLog(nil, @"[RNBackgroundDownloader] - [persistPendingMoves] serialization error: %@", error);
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
[mmkv setData:data forKey:PENDING_MOVES_MAP_KEY];
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
- (NSMutableDictionary *)deserializePendingMoves:(NSData *)data {
|
|
2286
|
+
NSError *error = nil;
|
|
2287
|
+
NSSet *classes = [NSSet setWithObjects:[NSMutableDictionary class], [NSDictionary class], [NSString class], [NSNumber class], nil];
|
|
2288
|
+
NSDictionary *decoded = [NSKeyedUnarchiver unarchivedObjectOfClasses:classes fromData:data error:&error];
|
|
2289
|
+
if (error || decoded == nil) {
|
|
2290
|
+
DLog(nil, @"[RNBackgroundDownloader] - [deserializePendingMoves] error: %@", error);
|
|
2291
|
+
return nil;
|
|
2292
|
+
}
|
|
2293
|
+
return [decoded mutableCopy];
|
|
2011
2294
|
}
|
|
2012
2295
|
|
|
2013
2296
|
#pragma mark - serialization
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TaskInfo, DownloadTask as DownloadTaskType, BeginHandler, ProgressHandler, DoneHandler, ErrorHandler, BeginHandlerParams, ProgressHandlerParams, DoneHandlerParams, ErrorHandlerParams, TaskInfoNative, DownloadParams, DownloadTaskState, Metadata } from './types';
|
|
2
|
+
export declare class DownloadTask {
|
|
3
|
+
id: string;
|
|
4
|
+
metadata: Metadata;
|
|
5
|
+
destination?: string;
|
|
6
|
+
state: DownloadTaskState;
|
|
7
|
+
errorCode: number;
|
|
8
|
+
bytesDownloaded: number;
|
|
9
|
+
bytesTotal: number;
|
|
10
|
+
downloadParams?: DownloadParams;
|
|
11
|
+
beginHandler?: BeginHandler;
|
|
12
|
+
progressHandler?: ProgressHandler;
|
|
13
|
+
doneHandler?: DoneHandler;
|
|
14
|
+
errorHandler?: ErrorHandler;
|
|
15
|
+
constructor(taskParams: TaskInfo | TaskInfoNative, originalTask?: DownloadTaskType);
|
|
16
|
+
begin(handler: BeginHandler): this;
|
|
17
|
+
progress(handler: ProgressHandler): this;
|
|
18
|
+
done(handler: DoneHandler): this;
|
|
19
|
+
error(handler: ErrorHandler): this;
|
|
20
|
+
onBegin(params: BeginHandlerParams): void;
|
|
21
|
+
onProgress(params: ProgressHandlerParams): void;
|
|
22
|
+
onDone(params: DoneHandlerParams): void;
|
|
23
|
+
onError(params: ErrorHandlerParams): void;
|
|
24
|
+
/**
|
|
25
|
+
* Update download parameters.
|
|
26
|
+
* If the task is paused, this will also update headers in the native layer.
|
|
27
|
+
* If the task is in-progress or completed, only the local JS object is updated.
|
|
28
|
+
*
|
|
29
|
+
* @param downloadParams - The new download parameters
|
|
30
|
+
* @returns Promise<boolean> - true if native headers were updated, false otherwise
|
|
31
|
+
*/
|
|
32
|
+
setDownloadParams(downloadParams: DownloadParams): Promise<boolean>;
|
|
33
|
+
pause(): Promise<void>;
|
|
34
|
+
resume(): Promise<void>;
|
|
35
|
+
start(): void;
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
tryParseJson(metadata?: string | Metadata | object): Metadata | null;
|
|
38
|
+
private headersToUnsafeObject;
|
|
39
|
+
}
|