@hot-updater/react-native 0.20.15 → 0.21.1
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/HotUpdater.podspec +7 -4
- package/android/build.gradle +3 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +78 -21
- package/android/src/main/java/com/hotupdater/DecompressService.kt +83 -0
- package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +26 -0
- package/android/src/main/java/com/hotupdater/HashUtils.kt +47 -0
- package/android/src/main/java/com/hotupdater/HotUpdater.kt +3 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +3 -3
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +3 -1
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +293 -0
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +105 -0
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +117 -0
- package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +175 -0
- package/android/src/newarch/HotUpdaterModule.kt +2 -0
- package/android/src/oldarch/HotUpdaterModule.kt +2 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +133 -60
- package/ios/HotUpdater/Internal/DecompressService.swift +89 -0
- package/ios/HotUpdater/Internal/DecompressionStrategy.swift +22 -0
- package/ios/HotUpdater/Internal/HashUtils.swift +63 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +5 -2
- package/ios/HotUpdater/Internal/HotUpdaterFactory.swift +4 -4
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +23 -10
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +229 -0
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +177 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +73 -7
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +165 -0
- package/lib/commonjs/checkForUpdate.js +1 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/fetchUpdateInfo.js +1 -1
- package/lib/commonjs/fetchUpdateInfo.js.map +1 -1
- package/lib/commonjs/native.js +3 -1
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +1 -0
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/fetchUpdateInfo.js +1 -1
- package/lib/module/fetchUpdateInfo.js.map +1 -1
- package/lib/module/native.js +3 -1
- package/lib/module/native.js.map +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +5 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +5 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/checkForUpdate.ts +1 -0
- package/src/fetchUpdateInfo.ts +1 -1
- package/src/native.ts +6 -0
- package/src/specs/NativeHotUpdater.ts +5 -0
- package/android/src/main/java/com/hotupdater/HttpDownloadService.kt +0 -98
- package/android/src/main/java/com/hotupdater/ZipFileUnzipService.kt +0 -74
- package/ios/HotUpdater/Internal/SSZipArchiveUnzipService.swift +0 -25
|
@@ -42,12 +42,14 @@ class HotUpdaterModule internal constructor(
|
|
|
42
42
|
try {
|
|
43
43
|
val bundleId = params.getString("bundleId")!!
|
|
44
44
|
val fileUrl = params.getString("fileUrl")
|
|
45
|
+
val fileHash = params.getString("fileHash")
|
|
45
46
|
|
|
46
47
|
val isSuccess =
|
|
47
48
|
HotUpdater.updateBundle(
|
|
48
49
|
mReactApplicationContext,
|
|
49
50
|
bundleId,
|
|
50
51
|
fileUrl,
|
|
52
|
+
fileHash,
|
|
51
53
|
) { progress ->
|
|
52
54
|
val progressParams =
|
|
53
55
|
WritableNativeMap().apply {
|
|
@@ -6,6 +6,9 @@ public enum BundleStorageError: Error {
|
|
|
6
6
|
case downloadFailed(Error)
|
|
7
7
|
case extractionFailed(Error)
|
|
8
8
|
case invalidBundle
|
|
9
|
+
case invalidZipFile
|
|
10
|
+
case insufficientDiskSpace
|
|
11
|
+
case hashMismatch
|
|
9
12
|
case moveOperationFailed(Error)
|
|
10
13
|
case copyOperationFailed(Error)
|
|
11
14
|
case fileSystemError(Error)
|
|
@@ -26,30 +29,30 @@ public protocol BundleStorageService {
|
|
|
26
29
|
func getBundleURL() -> URL?
|
|
27
30
|
|
|
28
31
|
// Bundle update
|
|
29
|
-
func updateBundle(bundleId: String, fileUrl: URL?, completion: @escaping (Result<Bool, Error>) -> Void)
|
|
32
|
+
func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, progressHandler: @escaping (Double) -> Void, completion: @escaping (Result<Bool, Error>) -> Void)
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
class BundleFileStorageService: BundleStorageService {
|
|
33
36
|
private let fileSystem: FileSystemService
|
|
34
37
|
private let downloadService: DownloadService
|
|
35
|
-
private let
|
|
38
|
+
private let decompressService: DecompressService
|
|
36
39
|
private let preferences: PreferencesService
|
|
37
|
-
|
|
40
|
+
|
|
38
41
|
// Queue for potentially long-running sequences within updateBundle or for explicit background tasks.
|
|
39
42
|
private let fileOperationQueue: DispatchQueue
|
|
40
|
-
|
|
43
|
+
|
|
41
44
|
private var activeTasks: [URLSessionTask] = []
|
|
42
|
-
|
|
45
|
+
|
|
43
46
|
public init(fileSystem: FileSystemService,
|
|
44
47
|
downloadService: DownloadService,
|
|
45
|
-
|
|
48
|
+
decompressService: DecompressService,
|
|
46
49
|
preferences: PreferencesService) {
|
|
47
|
-
|
|
50
|
+
|
|
48
51
|
self.fileSystem = fileSystem
|
|
49
52
|
self.downloadService = downloadService
|
|
50
|
-
self.
|
|
53
|
+
self.decompressService = decompressService
|
|
51
54
|
self.preferences = preferences
|
|
52
|
-
|
|
55
|
+
|
|
53
56
|
self.fileOperationQueue = DispatchQueue(label: "com.hotupdater.fileoperations",
|
|
54
57
|
qos: .utility,
|
|
55
58
|
attributes: .concurrent)
|
|
@@ -273,9 +276,11 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
273
276
|
* Updates the bundle from the specified URL. This operation is asynchronous.
|
|
274
277
|
* @param bundleId ID of the bundle to update
|
|
275
278
|
* @param fileUrl URL of the bundle file to download (or nil to reset)
|
|
279
|
+
* @param fileHash SHA256 hash of the bundle file for verification (nullable)
|
|
280
|
+
* @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
|
|
276
281
|
* @param completion Callback with result of the operation
|
|
277
282
|
*/
|
|
278
|
-
func updateBundle(bundleId: String, fileUrl: URL?, completion: @escaping (Result<Bool, Error>) -> Void) {
|
|
283
|
+
func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, progressHandler: @escaping (Double) -> Void, completion: @escaping (Result<Bool, Error>) -> Void) {
|
|
279
284
|
// Get the current bundle ID from the cached bundle URL (exclude fallback bundles)
|
|
280
285
|
let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
|
|
281
286
|
|
|
@@ -344,7 +349,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
344
349
|
do {
|
|
345
350
|
try self.fileSystem.removeItem(atPath: finalBundleDir)
|
|
346
351
|
// Continue with download process on success
|
|
347
|
-
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, storeDir: storeDir, completion: completion)
|
|
352
|
+
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
|
|
348
353
|
} catch let error {
|
|
349
354
|
NSLog("[BundleStorage] Failed to remove invalid bundle dir: \(error.localizedDescription)")
|
|
350
355
|
completion(.failure(BundleStorageError.fileSystemError(error)))
|
|
@@ -354,7 +359,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
354
359
|
completion(.failure(error))
|
|
355
360
|
}
|
|
356
361
|
} else {
|
|
357
|
-
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, storeDir: storeDir, completion: completion)
|
|
362
|
+
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
|
|
358
363
|
}
|
|
359
364
|
}
|
|
360
365
|
}
|
|
@@ -364,13 +369,17 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
364
369
|
* This method is part of the asynchronous `updateBundle` flow.
|
|
365
370
|
* @param bundleId ID of the bundle to update
|
|
366
371
|
* @param fileUrl URL of the bundle file to download
|
|
372
|
+
* @param fileHash SHA256 hash of the bundle file for verification (nullable)
|
|
367
373
|
* @param storeDir Path to the bundle-store directory
|
|
374
|
+
* @param progressHandler Callback for download and extraction progress
|
|
368
375
|
* @param completion Callback with result of the operation
|
|
369
376
|
*/
|
|
370
377
|
private func prepareAndDownloadBundle(
|
|
371
378
|
bundleId: String,
|
|
372
379
|
fileUrl: URL,
|
|
380
|
+
fileHash: String?,
|
|
373
381
|
storeDir: String,
|
|
382
|
+
progressHandler: @escaping (Double) -> Void,
|
|
374
383
|
completion: @escaping (Result<Bool, Error>) -> Void
|
|
375
384
|
) {
|
|
376
385
|
// 1) Prepare temp directory for download
|
|
@@ -388,36 +397,70 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
388
397
|
completion(.failure(BundleStorageError.directoryCreationFailed))
|
|
389
398
|
return
|
|
390
399
|
}
|
|
391
|
-
|
|
392
|
-
// 4)
|
|
393
|
-
let
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
400
|
+
|
|
401
|
+
// 4) Determine bundle filename from URL
|
|
402
|
+
let bundleFileName = fileUrl.lastPathComponent.isEmpty ? "bundle.zip" : fileUrl.lastPathComponent
|
|
403
|
+
let tempBundleFile = (tempDirectory as NSString).appendingPathComponent(bundleFileName)
|
|
404
|
+
|
|
405
|
+
NSLog("[BundleStorage] Checking file size and disk space...")
|
|
406
|
+
|
|
407
|
+
// 5) Check file size and disk space before download
|
|
408
|
+
self.downloadService.getFileSize(from: fileUrl) { [weak self] sizeResult in
|
|
409
|
+
guard let self = self else { return }
|
|
410
|
+
|
|
411
|
+
if case .success(let fileSize) = sizeResult {
|
|
412
|
+
// Check available disk space
|
|
413
|
+
do {
|
|
414
|
+
let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
|
|
415
|
+
if let freeSize = attributes[.systemFreeSize] as? Int64 {
|
|
416
|
+
let requiredSpace = fileSize * 2 // ZIP + extracted files
|
|
417
|
+
|
|
418
|
+
NSLog("[BundleStorage] File size: \(fileSize) bytes, Available: \(freeSize) bytes, Required: \(requiredSpace) bytes")
|
|
419
|
+
|
|
420
|
+
if freeSize < requiredSpace {
|
|
421
|
+
NSLog("[BundleStorage] Insufficient disk space")
|
|
422
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
423
|
+
completion(.failure(BundleStorageError.insufficientDiskSpace))
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
NSLog("[BundleStorage] Failed to check disk space: \(error.localizedDescription)")
|
|
429
|
+
// Continue with download despite disk check failure
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
NSLog("[BundleStorage] Unable to determine file size, proceeding with download")
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
NSLog("[BundleStorage] Starting download from \(fileUrl)")
|
|
436
|
+
|
|
437
|
+
// 6) DownloadService handles its own threading for the download task.
|
|
438
|
+
// The completion handler for downloadService.downloadFile is then dispatched to fileOperationQueue.
|
|
439
|
+
let task = self.downloadService.downloadFile(from: fileUrl,
|
|
440
|
+
to: tempBundleFile,
|
|
441
|
+
progressHandler: { downloadProgress in
|
|
442
|
+
// Map download progress to 0.0 - 0.8
|
|
443
|
+
progressHandler(downloadProgress * 0.8)
|
|
444
|
+
},
|
|
445
|
+
completion: { [weak self] result in
|
|
405
446
|
guard let self = self else {
|
|
406
447
|
let error = NSError(domain: "HotUpdaterError", code: 998,
|
|
407
448
|
userInfo: [NSLocalizedDescriptionKey: "Self deallocated during download"])
|
|
408
449
|
completion(.failure(error))
|
|
409
450
|
return
|
|
410
451
|
}
|
|
411
|
-
|
|
452
|
+
|
|
412
453
|
// Dispatch the processing of the downloaded file to the file operation queue
|
|
413
454
|
let workItem = DispatchWorkItem {
|
|
414
455
|
switch result {
|
|
415
456
|
case .success(let location):
|
|
416
457
|
self.processDownloadedFileWithTmp(location: location,
|
|
417
|
-
|
|
458
|
+
tempBundleFile: tempBundleFile,
|
|
459
|
+
fileHash: fileHash,
|
|
418
460
|
storeDir: storeDir,
|
|
419
461
|
bundleId: bundleId,
|
|
420
462
|
tempDirectory: tempDirectory,
|
|
463
|
+
progressHandler: progressHandler,
|
|
421
464
|
completion: completion)
|
|
422
465
|
case .failure(let error):
|
|
423
466
|
NSLog("[BundleStorage] Download failed: \(error.localizedDescription)")
|
|
@@ -427,9 +470,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
427
470
|
}
|
|
428
471
|
self.fileOperationQueue.async(execute: workItem)
|
|
429
472
|
})
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
473
|
+
|
|
474
|
+
if let task = task {
|
|
475
|
+
self.activeTasks.append(task) // Manage active tasks
|
|
476
|
+
}
|
|
433
477
|
}
|
|
434
478
|
}
|
|
435
479
|
|
|
@@ -437,24 +481,28 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
437
481
|
* Processes a downloaded bundle file using the "tmp" rename approach.
|
|
438
482
|
* This method is part of the asynchronous `updateBundle` flow and is expected to run on a background thread.
|
|
439
483
|
* @param location URL of the downloaded file
|
|
440
|
-
* @param
|
|
484
|
+
* @param tempBundleFile Path to store the downloaded bundle file
|
|
485
|
+
* @param fileHash SHA256 hash of the bundle file for verification (nullable)
|
|
441
486
|
* @param storeDir Path to the bundle-store directory
|
|
442
487
|
* @param bundleId ID of the bundle being processed
|
|
443
488
|
* @param tempDirectory Temporary directory for processing
|
|
489
|
+
* @param progressHandler Callback for extraction progress (0.8 to 1.0)
|
|
444
490
|
* @param completion Callback with result of the operation
|
|
445
491
|
*/
|
|
446
492
|
private func processDownloadedFileWithTmp(
|
|
447
493
|
location: URL,
|
|
448
|
-
|
|
494
|
+
tempBundleFile: String,
|
|
495
|
+
fileHash: String?,
|
|
449
496
|
storeDir: String,
|
|
450
497
|
bundleId: String,
|
|
451
498
|
tempDirectory: String,
|
|
499
|
+
progressHandler: @escaping (Double) -> Void,
|
|
452
500
|
completion: @escaping (Result<Bool, Error>) -> Void
|
|
453
501
|
) {
|
|
454
502
|
let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
|
|
455
503
|
NSLog("[BundleStorage] Processing downloaded file atPath: \(location.path)")
|
|
456
|
-
|
|
457
|
-
// 1) Ensure the
|
|
504
|
+
|
|
505
|
+
// 1) Ensure the bundle file exists
|
|
458
506
|
guard self.fileSystem.fileExists(atPath: location.path) else {
|
|
459
507
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
460
508
|
completion(.failure(BundleStorageError.fileSystemError(NSError(
|
|
@@ -464,58 +512,83 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
464
512
|
))))
|
|
465
513
|
return
|
|
466
514
|
}
|
|
467
|
-
|
|
515
|
+
|
|
468
516
|
// 2) Define tmpDir and realDir
|
|
469
517
|
let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
|
|
470
518
|
let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
471
|
-
|
|
519
|
+
|
|
472
520
|
do {
|
|
473
521
|
// 3) Remove any existing tmpDir
|
|
474
522
|
if self.fileSystem.fileExists(atPath: tmpDir) {
|
|
475
523
|
try self.fileSystem.removeItem(atPath: tmpDir)
|
|
476
524
|
NSLog("[BundleStorage] Removed existing tmpDir: \(tmpDir)")
|
|
477
525
|
}
|
|
478
|
-
|
|
526
|
+
|
|
479
527
|
// 4) Create tmpDir
|
|
480
528
|
try self.fileSystem.createDirectory(atPath: tmpDir)
|
|
481
529
|
NSLog("[BundleStorage] Created tmpDir: \(tmpDir)")
|
|
482
|
-
|
|
483
|
-
// 5)
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
530
|
+
|
|
531
|
+
// 5) Verify file hash if provided
|
|
532
|
+
if let expectedHash = fileHash {
|
|
533
|
+
NSLog("[BundleStorage] Verifying file hash...")
|
|
534
|
+
let tempBundleURL = URL(fileURLWithPath: tempBundleFile)
|
|
535
|
+
guard HashUtils.verifyHash(fileURL: tempBundleURL, expectedHash: expectedHash) else {
|
|
536
|
+
NSLog("[BundleStorage] Hash mismatch!")
|
|
537
|
+
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
538
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
539
|
+
completion(.failure(BundleStorageError.hashMismatch))
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
NSLog("[BundleStorage] Hash verification passed")
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 6) Unzip directly into tmpDir with progress tracking (0.8 - 1.0)
|
|
546
|
+
NSLog("[BundleStorage] Extracting \(tempBundleFile) → \(tmpDir)")
|
|
547
|
+
do {
|
|
548
|
+
try self.decompressService.unzip(file: tempBundleFile, to: tmpDir, progressHandler: { unzipProgress in
|
|
549
|
+
// Map unzip progress (0.0 - 1.0) to overall progress (0.8 - 1.0)
|
|
550
|
+
progressHandler(0.8 + (unzipProgress * 0.2))
|
|
551
|
+
})
|
|
552
|
+
NSLog("[BundleStorage] Extraction complete at \(tmpDir)")
|
|
553
|
+
} catch {
|
|
554
|
+
NSLog("[BundleStorage] Extraction failed: \(error.localizedDescription)")
|
|
555
|
+
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
556
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
557
|
+
completion(.failure(BundleStorageError.extractionFailed(error)))
|
|
558
|
+
return
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 7) Remove the downloaded bundle file
|
|
562
|
+
try? self.fileSystem.removeItem(atPath: tempBundleFile)
|
|
563
|
+
|
|
564
|
+
// 8) Verify that a valid bundle file exists inside tmpDir
|
|
492
565
|
switch self.findBundleFile(in: tmpDir) {
|
|
493
566
|
case .success(let maybeBundlePath):
|
|
494
567
|
if let bundlePathInTmp = maybeBundlePath {
|
|
495
|
-
//
|
|
568
|
+
// 9) Remove any existing realDir
|
|
496
569
|
if self.fileSystem.fileExists(atPath: realDir) {
|
|
497
570
|
try self.fileSystem.removeItem(atPath: realDir)
|
|
498
571
|
NSLog("[BundleStorage] Removed existing realDir: \(realDir)")
|
|
499
572
|
}
|
|
500
|
-
|
|
501
|
-
//
|
|
573
|
+
|
|
574
|
+
// 10) Rename (move) tmpDir → realDir
|
|
502
575
|
try self.fileSystem.moveItem(atPath: tmpDir, toPath: realDir)
|
|
503
576
|
NSLog("[BundleStorage] Renamed tmpDir to realDir: \(realDir)")
|
|
504
|
-
|
|
505
|
-
//
|
|
577
|
+
|
|
578
|
+
// 11) Construct final bundlePath for preferences
|
|
506
579
|
let finalBundlePath = (realDir as NSString).appendingPathComponent((bundlePathInTmp as NSString).lastPathComponent)
|
|
507
|
-
|
|
508
|
-
//
|
|
580
|
+
|
|
581
|
+
// 12) Set the bundle URL in preferences
|
|
509
582
|
let setResult = self.setBundleURL(localPath: finalBundlePath)
|
|
510
583
|
switch setResult {
|
|
511
584
|
case .success:
|
|
512
|
-
//
|
|
585
|
+
// 13) Clean up the temporary directory
|
|
513
586
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
514
|
-
|
|
515
|
-
//
|
|
587
|
+
|
|
588
|
+
// 14) Clean up old bundles, preserving current and latest
|
|
516
589
|
let _ = self.cleanupOldBundles(currentBundleId: currentBundleId, bundleId: bundleId)
|
|
517
|
-
|
|
518
|
-
//
|
|
590
|
+
|
|
591
|
+
// 15) Complete with success
|
|
519
592
|
completion(.success(true))
|
|
520
593
|
case .failure(let err):
|
|
521
594
|
// Preferences save failed → remove realDir and clean up
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unified decompression service that uses Strategy pattern to handle multiple compression formats.
|
|
5
|
+
* Automatically detects format by trying each strategy's validation and delegates to appropriate decompression strategy.
|
|
6
|
+
*/
|
|
7
|
+
class DecompressService {
|
|
8
|
+
/// Array of available strategies in order of detection priority
|
|
9
|
+
private let strategies: [DecompressionStrategy]
|
|
10
|
+
|
|
11
|
+
init() {
|
|
12
|
+
// Order matters: Try ZIP first (clear magic bytes), then TAR.GZ (GZIP magic bytes), then TAR.BR (fallback)
|
|
13
|
+
self.strategies = [
|
|
14
|
+
ZipDecompressionStrategy(),
|
|
15
|
+
TarGzDecompressionStrategy(),
|
|
16
|
+
TarBrDecompressionStrategy()
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extracts a compressed file to the destination directory.
|
|
22
|
+
* Automatically detects compression format by trying each strategy's validation.
|
|
23
|
+
* @param file Path to the compressed file
|
|
24
|
+
* @param destination Path to the destination directory
|
|
25
|
+
* @param progressHandler Callback for progress updates (0.0 - 1.0)
|
|
26
|
+
* @throws Error if decompression fails or no valid strategy found
|
|
27
|
+
*/
|
|
28
|
+
func unzip(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws {
|
|
29
|
+
// Collect file information for better error messages
|
|
30
|
+
let fileURL = URL(fileURLWithPath: file)
|
|
31
|
+
let fileName = fileURL.lastPathComponent
|
|
32
|
+
let fileSize = (try? FileManager.default.attributesOfItem(atPath: file)[.size] as? UInt64) ?? 0
|
|
33
|
+
|
|
34
|
+
// Try each strategy's validation
|
|
35
|
+
for strategy in strategies {
|
|
36
|
+
if strategy.isValid(file: file) {
|
|
37
|
+
NSLog("[DecompressService] Using strategy for \(fileName)")
|
|
38
|
+
try strategy.decompress(file: file, to: destination, progressHandler: progressHandler)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// No valid strategy found - provide detailed error message
|
|
44
|
+
let errorMessage = """
|
|
45
|
+
Failed to decompress file: \(fileName) (\(fileSize) bytes)
|
|
46
|
+
|
|
47
|
+
Tried strategies: ZIP (magic bytes 0x504B0304), TAR.GZ (magic bytes 0x1F8B), TAR.BR (file extension)
|
|
48
|
+
|
|
49
|
+
Supported formats:
|
|
50
|
+
- ZIP archives (.zip)
|
|
51
|
+
- GZIP compressed TAR archives (.tar.gz)
|
|
52
|
+
- Brotli compressed TAR archives (.tar.br)
|
|
53
|
+
|
|
54
|
+
Please verify the file is not corrupted and matches one of the supported formats.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
NSLog("[DecompressService] \(errorMessage)")
|
|
58
|
+
throw NSError(
|
|
59
|
+
domain: "DecompressService",
|
|
60
|
+
code: 1,
|
|
61
|
+
userInfo: [NSLocalizedDescriptionKey: errorMessage]
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extracts a compressed file to the destination directory (without progress tracking).
|
|
67
|
+
* @param file Path to the compressed file
|
|
68
|
+
* @param destination Path to the destination directory
|
|
69
|
+
* @throws Error if decompression fails or no valid strategy found
|
|
70
|
+
*/
|
|
71
|
+
func unzip(file: String, to destination: String) throws {
|
|
72
|
+
try unzip(file: file, to: destination, progressHandler: { _ in })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validates if a file is a valid compressed archive.
|
|
77
|
+
* @param file Path to the file to validate
|
|
78
|
+
* @return true if the file is valid for any strategy
|
|
79
|
+
*/
|
|
80
|
+
func isValid(file: String) -> Bool {
|
|
81
|
+
for strategy in strategies {
|
|
82
|
+
if strategy.isValid(file: file) {
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
NSLog("[DecompressService] No valid strategy found for file: \(file)")
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Protocol for decompression strategies
|
|
5
|
+
*/
|
|
6
|
+
protocol DecompressionStrategy {
|
|
7
|
+
/**
|
|
8
|
+
* Validates if a file can be decompressed by this strategy
|
|
9
|
+
* @param file Path to the file to validate
|
|
10
|
+
* @return true if the file is valid for this strategy
|
|
11
|
+
*/
|
|
12
|
+
func isValid(file: String) -> Bool
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Decompresses a file to the destination directory
|
|
16
|
+
* @param file Path to the compressed file
|
|
17
|
+
* @param destination Path to the destination directory
|
|
18
|
+
* @param progressHandler Callback for progress updates (0.0 - 1.0)
|
|
19
|
+
* @throws Error if decompression fails
|
|
20
|
+
*/
|
|
21
|
+
func decompress(file: String, to destination: String, progressHandler: @escaping (Double) -> Void) throws
|
|
22
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CryptoKit
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Utility class for file hash operations
|
|
6
|
+
*/
|
|
7
|
+
class HashUtils {
|
|
8
|
+
/// Buffer size for file reading operations (64KB for optimal I/O performance)
|
|
9
|
+
private static let BUFFER_SIZE = 65536
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Calculates SHA256 hash of a file
|
|
13
|
+
* @param fileURL URL of the file to hash
|
|
14
|
+
* @return Hex string of the hash (lowercase), or nil if error occurs
|
|
15
|
+
*/
|
|
16
|
+
static func calculateSHA256(fileURL: URL) -> String? {
|
|
17
|
+
guard let fileHandle = try? FileHandle(forReadingFrom: fileURL) else {
|
|
18
|
+
NSLog("[HashUtils] Failed to open file: \(fileURL.path)")
|
|
19
|
+
return nil
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
defer {
|
|
23
|
+
try? fileHandle.close()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var hasher = SHA256()
|
|
27
|
+
|
|
28
|
+
// Read file in chunks with autoreleasepool for memory efficiency
|
|
29
|
+
while autoreleasepool(invoking: {
|
|
30
|
+
let data = fileHandle.readData(ofLength: BUFFER_SIZE)
|
|
31
|
+
if data.count > 0 {
|
|
32
|
+
hasher.update(data: data)
|
|
33
|
+
return true
|
|
34
|
+
} else {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
}) { }
|
|
38
|
+
|
|
39
|
+
let digest = hasher.finalize()
|
|
40
|
+
return digest.map { String(format: "%02x", $0) }.joined()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Verifies file hash
|
|
45
|
+
* @param fileURL URL of the file to verify
|
|
46
|
+
* @param expectedHash Expected SHA256 hash (hex string, case-insensitive)
|
|
47
|
+
* @return true if hash matches, false otherwise
|
|
48
|
+
*/
|
|
49
|
+
static func verifyHash(fileURL: URL, expectedHash: String) -> Bool {
|
|
50
|
+
guard let actualHash = calculateSHA256(fileURL: fileURL) else {
|
|
51
|
+
NSLog("[HashUtils] Failed to calculate hash")
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let matches = actualHash.caseInsensitiveCompare(expectedHash) == .orderedSame
|
|
56
|
+
|
|
57
|
+
if !matches {
|
|
58
|
+
NSLog("[HashUtils] Hash mismatch - Expected: \(expectedHash), Actual: \(actualHash)")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return matches
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -31,7 +31,7 @@ static HotUpdaterImpl *_hotUpdaterImpl = [HotUpdaterFactory.shared create];
|
|
|
31
31
|
self = [super init];
|
|
32
32
|
if (self) {
|
|
33
33
|
observedTasks = [NSMutableSet set];
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
// Start observing notifications needed for cleanup/events
|
|
36
36
|
// Using self as observer
|
|
37
37
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
@@ -217,7 +217,10 @@ RCT_EXPORT_METHOD(updateBundle:(JS::NativeHotUpdater::UpdateBundleParams &)param
|
|
|
217
217
|
if (params.fileUrl()) {
|
|
218
218
|
paramDict[@"fileUrl"] = params.fileUrl();
|
|
219
219
|
}
|
|
220
|
-
|
|
220
|
+
if (params.fileHash()) {
|
|
221
|
+
paramDict[@"fileHash"] = params.fileHash();
|
|
222
|
+
}
|
|
223
|
+
|
|
221
224
|
[_hotUpdaterImpl updateBundle:paramDict resolver:resolve rejecter:reject];
|
|
222
225
|
}
|
|
223
226
|
#else
|
|
@@ -10,15 +10,15 @@ public class HotUpdaterFactory: NSObject {
|
|
|
10
10
|
let fileSystem = FileManagerService()
|
|
11
11
|
let preferences = VersionedPreferencesService()
|
|
12
12
|
let downloadService = URLSessionDownloadService()
|
|
13
|
-
let
|
|
14
|
-
|
|
13
|
+
let decompressService = DecompressService()
|
|
14
|
+
|
|
15
15
|
let bundleStorage = BundleFileStorageService(
|
|
16
16
|
fileSystem: fileSystem,
|
|
17
17
|
downloadService: downloadService,
|
|
18
|
-
|
|
18
|
+
decompressService: decompressService,
|
|
19
19
|
preferences: preferences
|
|
20
20
|
)
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
return HotUpdaterImpl(bundleStorage: bundleStorage, preferences: preferences)
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -16,15 +16,15 @@ import React
|
|
|
16
16
|
let fileSystem = FileManagerService()
|
|
17
17
|
let preferences = VersionedPreferencesService()
|
|
18
18
|
let downloadService = URLSessionDownloadService()
|
|
19
|
-
let
|
|
20
|
-
|
|
19
|
+
let decompressService = DecompressService()
|
|
20
|
+
|
|
21
21
|
let bundleStorage = BundleFileStorageService(
|
|
22
22
|
fileSystem: fileSystem,
|
|
23
23
|
downloadService: downloadService,
|
|
24
|
-
|
|
24
|
+
decompressService: decompressService,
|
|
25
25
|
preferences: preferences
|
|
26
26
|
)
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
self.init(bundleStorage: bundleStorage, preferences: preferences)
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -132,20 +132,33 @@ import React
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
let fileUrlString = data["fileUrl"] as? String ?? ""
|
|
135
|
-
|
|
135
|
+
|
|
136
136
|
var fileUrl: URL? = nil
|
|
137
137
|
if !fileUrlString.isEmpty {
|
|
138
138
|
guard let url = URL(string: fileUrlString) else {
|
|
139
|
-
throw NSError(domain: "HotUpdaterError", code: 103,
|
|
139
|
+
throw NSError(domain: "HotUpdaterError", code: 103,
|
|
140
140
|
userInfo: [NSLocalizedDescriptionKey: "Invalid 'fileUrl' provided: \(fileUrlString)"])
|
|
141
141
|
}
|
|
142
142
|
fileUrl = url
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
|
|
145
|
+
// Extract fileHash if provided
|
|
146
|
+
let fileHash = data["fileHash"] as? String
|
|
147
|
+
|
|
148
|
+
// Extract progress callback if provided
|
|
149
|
+
let progressCallback = data["progressCallback"] as? RCTResponseSenderBlock
|
|
150
|
+
|
|
151
|
+
NSLog("[HotUpdaterImpl] updateBundle called with bundleId: \(bundleId), fileUrl: \(fileUrl?.absoluteString ?? "nil"), fileHash: \(fileHash ?? "nil")")
|
|
152
|
+
|
|
147
153
|
// Heavy work is delegated to bundle storage service with safe error handling
|
|
148
|
-
bundleStorage.updateBundle(bundleId: bundleId, fileUrl: fileUrl
|
|
154
|
+
bundleStorage.updateBundle(bundleId: bundleId, fileUrl: fileUrl, fileHash: fileHash, progressHandler: { progress in
|
|
155
|
+
// Call JS progress callback if provided
|
|
156
|
+
if let callback = progressCallback {
|
|
157
|
+
DispatchQueue.main.async {
|
|
158
|
+
callback([progress])
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}) { [weak self] result in
|
|
149
162
|
guard self != nil else {
|
|
150
163
|
let error = NSError(domain: "HotUpdaterError", code: 998,
|
|
151
164
|
userInfo: [NSLocalizedDescriptionKey: "Self deallocated during update"])
|