@hot-updater/react-native 0.29.1 → 0.29.3
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 +0 -4
- package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +178 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +82 -47
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +359 -0
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +386 -0
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +7 -213
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +8 -126
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -2
- package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +462 -0
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +4 -113
- package/package.json +6 -6
package/HotUpdater.podspec
CHANGED
|
@@ -23,10 +23,6 @@ Pod::Spec.new do |s|
|
|
|
23
23
|
"OTHER_SWIFT_FLAGS" => "-enable-experimental-feature AccessLevelOnImport"
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
# SWCompression dependency for ZIP/TAR/GZIP/Brotli extraction support
|
|
27
|
-
# Native Compression framework is used for GZIP and Brotli decompression
|
|
28
|
-
s.dependency "SWCompression", "~> 4.8.0"
|
|
29
|
-
|
|
30
26
|
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
|
|
31
27
|
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
|
|
32
28
|
if respond_to?(:install_modules_dependencies, true)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum ArchiveExtractionUtilities {
|
|
4
|
+
static let bufferSize = 64 * 1024
|
|
5
|
+
|
|
6
|
+
static func readUpToCount(from handle: FileHandle, count: Int) throws -> Data? {
|
|
7
|
+
guard count >= 0 else {
|
|
8
|
+
throw NSError(
|
|
9
|
+
domain: "ArchiveExtractionUtilities",
|
|
10
|
+
code: 5,
|
|
11
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid read size: \(count)"]
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if count == 0 {
|
|
16
|
+
return Data()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
|
|
20
|
+
return try handle.read(upToCount: count)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return handle.readData(ofLength: count)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static func readExactly(from handle: FileHandle, count: Int) throws -> Data {
|
|
27
|
+
guard count >= 0 else {
|
|
28
|
+
throw NSError(
|
|
29
|
+
domain: "ArchiveExtractionUtilities",
|
|
30
|
+
code: 1,
|
|
31
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid read size: \(count)"]
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if count == 0 {
|
|
36
|
+
return Data()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
guard let data = try readUpToCount(from: handle, count: count), data.count == count else {
|
|
40
|
+
throw NSError(
|
|
41
|
+
domain: "ArchiveExtractionUtilities",
|
|
42
|
+
code: 2,
|
|
43
|
+
userInfo: [NSLocalizedDescriptionKey: "Unexpected end of archive while reading \(count) bytes"]
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return data
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static func currentOffset(for handle: FileHandle) -> UInt64 {
|
|
51
|
+
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
|
|
52
|
+
return (try? handle.offset()) ?? 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return handle.offsetInFile
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static func seek(_ handle: FileHandle, to offset: UInt64) throws {
|
|
59
|
+
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
|
|
60
|
+
try handle.seek(toOffset: offset)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
handle.seek(toFileOffset: offset)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static func skipBytes(_ byteCount: UInt64, in handle: FileHandle) throws {
|
|
68
|
+
guard byteCount > 0 else {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let offset = currentOffset(for: handle)
|
|
73
|
+
let (targetOffset, overflowed) = offset.addingReportingOverflow(byteCount)
|
|
74
|
+
|
|
75
|
+
guard !overflowed else {
|
|
76
|
+
throw NSError(
|
|
77
|
+
domain: "ArchiveExtractionUtilities",
|
|
78
|
+
code: 6,
|
|
79
|
+
userInfo: [NSLocalizedDescriptionKey: "Archive offset overflow while skipping \(byteCount) bytes from offset \(offset)"]
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try seek(handle, to: targetOffset)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static func normalizedRelativePath(from rawPath: String) -> String? {
|
|
87
|
+
var candidate = rawPath
|
|
88
|
+
.replacingOccurrences(of: "\\", with: "/")
|
|
89
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
90
|
+
|
|
91
|
+
while candidate.hasPrefix("/") {
|
|
92
|
+
candidate.removeFirst()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
while candidate.hasPrefix("./") {
|
|
96
|
+
candidate.removeFirst(2)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let components = candidate
|
|
100
|
+
.split(separator: "/")
|
|
101
|
+
.map(String.init)
|
|
102
|
+
|
|
103
|
+
guard !components.isEmpty else {
|
|
104
|
+
return nil
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
guard !components.contains(".."), !components.contains(".") else {
|
|
108
|
+
return nil
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return components.joined(separator: "/")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static func extractionURL(for relativePath: String, destinationRoot: String) throws -> URL {
|
|
115
|
+
let rootURL = URL(fileURLWithPath: destinationRoot, isDirectory: true)
|
|
116
|
+
let targetURL = rootURL.appendingPathComponent(relativePath)
|
|
117
|
+
let standardizedRoot = rootURL.standardizedFileURL.path
|
|
118
|
+
let standardizedTarget = targetURL.standardizedFileURL.path
|
|
119
|
+
|
|
120
|
+
guard standardizedTarget == standardizedRoot ||
|
|
121
|
+
standardizedTarget.hasPrefix(standardizedRoot + "/") else {
|
|
122
|
+
throw NSError(
|
|
123
|
+
domain: "ArchiveExtractionUtilities",
|
|
124
|
+
code: 3,
|
|
125
|
+
userInfo: [NSLocalizedDescriptionKey: "Path traversal attempt detected: \(relativePath)"]
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return targetURL
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static func ensureDirectory(at url: URL, fileManager: FileManager = .default) throws {
|
|
133
|
+
var isDirectory = ObjCBool(false)
|
|
134
|
+
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
|
|
135
|
+
if isDirectory.boolValue {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try fileManager.removeItem(at: url)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static func createOutputFile(at url: URL, fileManager: FileManager = .default) throws -> FileHandle {
|
|
146
|
+
try ensureDirectory(at: url.deletingLastPathComponent(), fileManager: fileManager)
|
|
147
|
+
|
|
148
|
+
if fileManager.fileExists(atPath: url.path) {
|
|
149
|
+
try fileManager.removeItem(at: url)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
guard fileManager.createFile(atPath: url.path, contents: nil) else {
|
|
153
|
+
throw NSError(
|
|
154
|
+
domain: "ArchiveExtractionUtilities",
|
|
155
|
+
code: 4,
|
|
156
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to create output file at \(url.path)"]
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return try FileHandle(forWritingTo: url)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
extension Data {
|
|
165
|
+
func archiveUInt16LE(at offset: Int) -> UInt16 {
|
|
166
|
+
let byte0 = UInt16(self[offset])
|
|
167
|
+
let byte1 = UInt16(self[offset + 1]) << 8
|
|
168
|
+
return byte0 | byte1
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
func archiveUInt32LE(at offset: Int) -> UInt32 {
|
|
172
|
+
let byte0 = UInt32(self[offset])
|
|
173
|
+
let byte1 = UInt32(self[offset + 1]) << 8
|
|
174
|
+
let byte2 = UInt32(self[offset + 2]) << 16
|
|
175
|
+
let byte3 = UInt32(self[offset + 3]) << 24
|
|
176
|
+
return byte0 | byte1 | byte2 | byte3
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -147,6 +147,17 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
147
147
|
let manifest: ManifestAssets
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
private enum UpdateProgress {
|
|
151
|
+
static let downloadEnd = 0.7
|
|
152
|
+
static let verificationStart = 0.72
|
|
153
|
+
static let verificationEnd = 0.82
|
|
154
|
+
static let extractionStart = 0.82
|
|
155
|
+
static let extractionEnd = 0.97
|
|
156
|
+
static let bundleValidation = 0.98
|
|
157
|
+
static let activationReady = 0.99
|
|
158
|
+
static let complete = 1.0
|
|
159
|
+
}
|
|
160
|
+
|
|
150
161
|
private let fileSystem: FileSystemService
|
|
151
162
|
private let downloadService: DownloadService
|
|
152
163
|
private let decompressService: DecompressService
|
|
@@ -178,7 +189,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
178
189
|
|
|
179
190
|
// Create queue for file operations
|
|
180
191
|
self.fileOperationQueue = DispatchQueue(label: "com.hotupdater.fileoperations",
|
|
181
|
-
qos: .
|
|
192
|
+
qos: .userInitiated,
|
|
182
193
|
attributes: .concurrent)
|
|
183
194
|
|
|
184
195
|
// Ensure bundle store directory exists
|
|
@@ -623,26 +634,23 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
623
634
|
*/
|
|
624
635
|
func findBundleFile(in directoryPath: String) -> Result<String?, Error> {
|
|
625
636
|
NSLog("[BundleStorage] Searching for bundle file in directory: \(directoryPath)")
|
|
626
|
-
|
|
627
|
-
|
|
637
|
+
|
|
638
|
+
let iosBundlePath = (directoryPath as NSString).appendingPathComponent("index.ios.bundle")
|
|
639
|
+
if self.fileSystem.fileExists(atPath: iosBundlePath) {
|
|
640
|
+
NSLog("[BundleStorage] Found iOS bundle atPath: \(iosBundlePath)")
|
|
641
|
+
return .success(iosBundlePath)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
let mainBundlePath = (directoryPath as NSString).appendingPathComponent("main.jsbundle")
|
|
645
|
+
if self.fileSystem.fileExists(atPath: mainBundlePath) {
|
|
646
|
+
NSLog("[BundleStorage] Found main bundle atPath: \(mainBundlePath)")
|
|
647
|
+
return .success(mainBundlePath)
|
|
648
|
+
}
|
|
649
|
+
|
|
628
650
|
do {
|
|
629
651
|
let contents = try self.fileSystem.contentsOfDirectory(atPath: directoryPath)
|
|
630
652
|
NSLog("[BundleStorage] Directory contents: \(contents)")
|
|
631
|
-
|
|
632
|
-
// Check for iOS bundle file directly
|
|
633
|
-
let iosBundlePath = (directoryPath as NSString).appendingPathComponent("index.ios.bundle")
|
|
634
|
-
if self.fileSystem.fileExists(atPath: iosBundlePath) {
|
|
635
|
-
NSLog("[BundleStorage] Found iOS bundle atPath: \(iosBundlePath)")
|
|
636
|
-
return .success(iosBundlePath)
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Check for main bundle file
|
|
640
|
-
let mainBundlePath = (directoryPath as NSString).appendingPathComponent("main.jsbundle")
|
|
641
|
-
if self.fileSystem.fileExists(atPath: mainBundlePath) {
|
|
642
|
-
NSLog("[BundleStorage] Found main bundle atPath: \(mainBundlePath)")
|
|
643
|
-
return .success(mainBundlePath)
|
|
644
|
-
}
|
|
645
|
-
|
|
653
|
+
|
|
646
654
|
// Additional search: check all .bundle files
|
|
647
655
|
for file in contents {
|
|
648
656
|
if file.hasSuffix(".bundle") {
|
|
@@ -662,19 +670,23 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
662
670
|
}
|
|
663
671
|
|
|
664
672
|
/**
|
|
665
|
-
* Cleans up old bundles, keeping only the
|
|
673
|
+
* Cleans up old bundles, keeping only the requested bundle IDs.
|
|
666
674
|
* Executes synchronously on the calling thread.
|
|
667
675
|
* @param currentBundleId ID of the current active bundle (optional)
|
|
668
676
|
* @param bundleId ID of the new bundle to keep (optional)
|
|
669
677
|
* @return Result of operation
|
|
670
678
|
*/
|
|
671
679
|
func cleanupOldBundles(currentBundleId: String?, bundleId: String?) -> Result<Void, Error> {
|
|
680
|
+
cleanupOldBundles(bundleIdsToKeep: [currentBundleId, bundleId].compactMap { $0 })
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private func cleanupOldBundles(bundleIdsToKeep: [String]) -> Result<Void, Error> {
|
|
672
684
|
let storeDirResult = bundleStoreDir()
|
|
673
|
-
|
|
685
|
+
|
|
674
686
|
guard case .success(let storeDir) = storeDirResult else {
|
|
675
687
|
return .failure(storeDirResult.failureError ?? BundleStorageError.unknown(nil))
|
|
676
688
|
}
|
|
677
|
-
|
|
689
|
+
|
|
678
690
|
// List only directories that are not .tmp
|
|
679
691
|
let contents: [String]
|
|
680
692
|
do {
|
|
@@ -694,14 +706,12 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
694
706
|
|
|
695
707
|
return (!item.hasSuffix(".tmp") && self.fileSystem.fileExists(atPath: fullPath)) ? fullPath : nil
|
|
696
708
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
let bundleIdsToKeep = Set([currentBundleId, bundleId].compactMap { $0 })
|
|
700
|
-
|
|
709
|
+
let bundleIdsToKeepSet = Set(bundleIdsToKeep)
|
|
710
|
+
|
|
701
711
|
bundles.forEach { bundlePath in
|
|
702
712
|
let bundleName = (bundlePath as NSString).lastPathComponent
|
|
703
|
-
|
|
704
|
-
if !
|
|
713
|
+
|
|
714
|
+
if !bundleIdsToKeepSet.contains(bundleName) {
|
|
705
715
|
do {
|
|
706
716
|
try self.fileSystem.removeItem(atPath: bundlePath)
|
|
707
717
|
NSLog("[BundleStorage] Removing old bundle: \(bundleName)")
|
|
@@ -712,7 +722,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
712
722
|
NSLog("[BundleStorage] Keeping bundle: \(bundleName)")
|
|
713
723
|
}
|
|
714
724
|
}
|
|
715
|
-
|
|
725
|
+
|
|
716
726
|
// Remove any leftover .tmp directories
|
|
717
727
|
contents.forEach { item in
|
|
718
728
|
if item.hasSuffix(".tmp") {
|
|
@@ -725,9 +735,23 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
725
735
|
}
|
|
726
736
|
}
|
|
727
737
|
}
|
|
728
|
-
|
|
738
|
+
|
|
729
739
|
return .success(())
|
|
730
740
|
}
|
|
741
|
+
|
|
742
|
+
private func scheduleCleanupOldBundles(bundleIdsToKeep: [String]) {
|
|
743
|
+
let uniqueBundleIdsToKeep = Array(Set(bundleIdsToKeep.filter { !$0.isEmpty }))
|
|
744
|
+
guard !uniqueBundleIdsToKeep.isEmpty else {
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
self.fileOperationQueue.async(flags: .barrier) {
|
|
749
|
+
let cleanupResult = self.cleanupOldBundles(bundleIdsToKeep: uniqueBundleIdsToKeep)
|
|
750
|
+
if case .failure(let error) = cleanupResult {
|
|
751
|
+
NSLog("[BundleStorage] Error during deferred cleanup: \(error)")
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
731
755
|
|
|
732
756
|
/**
|
|
733
757
|
* Sets the current bundle URL in preferences.
|
|
@@ -895,12 +919,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
895
919
|
let _ = self.saveMetadata(updatedMetadata)
|
|
896
920
|
NSLog("[BundleStorage] Set staging bundle (cached): \(bundleId), verificationPending: true")
|
|
897
921
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
|
|
922
|
+
progressHandler(UpdateProgress.complete)
|
|
923
|
+
self.scheduleCleanupOldBundles(
|
|
924
|
+
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
925
|
+
)
|
|
904
926
|
completion(.success(true))
|
|
905
927
|
case .failure(let error):
|
|
906
928
|
completion(.failure(error))
|
|
@@ -997,8 +1019,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
997
1019
|
}
|
|
998
1020
|
},
|
|
999
1021
|
progressHandler: { downloadProgress in
|
|
1000
|
-
|
|
1001
|
-
progressHandler(downloadProgress * 0.8)
|
|
1022
|
+
progressHandler(Self.mapProgress(downloadProgress, start: 0, end: UpdateProgress.downloadEnd))
|
|
1002
1023
|
},
|
|
1003
1024
|
completion: { [weak self] result in
|
|
1004
1025
|
guard let self = self else {
|
|
@@ -1084,7 +1105,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1084
1105
|
* @param storeDir Path to the bundle-store directory
|
|
1085
1106
|
* @param bundleId ID of the bundle being processed
|
|
1086
1107
|
* @param tempDirectory Temporary directory for processing
|
|
1087
|
-
* @param progressHandler Callback for
|
|
1108
|
+
* @param progressHandler Callback for download/apply progress (0.0 to 1.0)
|
|
1088
1109
|
* @param completion Callback with result of the operation
|
|
1089
1110
|
*/
|
|
1090
1111
|
private func processDownloadedFileWithTmp(
|
|
@@ -1124,17 +1145,21 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1124
1145
|
}
|
|
1125
1146
|
|
|
1126
1147
|
// 4) Create tmpDir
|
|
1127
|
-
|
|
1148
|
+
guard self.fileSystem.createDirectory(atPath: tmpDir) else {
|
|
1149
|
+
throw BundleStorageError.directoryCreationFailed
|
|
1150
|
+
}
|
|
1128
1151
|
NSLog("[BundleStorage] Created tmpDir: \(tmpDir)")
|
|
1129
1152
|
logFileSystemDiagnostics(path: tmpDir, context: "TmpDir Created")
|
|
1130
1153
|
|
|
1131
1154
|
// 5) Verify bundle integrity (hash or signature based on fileHash format)
|
|
1132
1155
|
NSLog("[BundleStorage] Verifying bundle integrity...")
|
|
1156
|
+
progressHandler(UpdateProgress.verificationStart)
|
|
1133
1157
|
let tempBundleURL = URL(fileURLWithPath: tempBundleFile)
|
|
1134
1158
|
let verificationResult = SignatureVerifier.verifyBundle(fileURL: tempBundleURL, fileHash: fileHash)
|
|
1135
1159
|
switch verificationResult {
|
|
1136
1160
|
case .success:
|
|
1137
1161
|
NSLog("[BundleStorage] Bundle verification completed successfully")
|
|
1162
|
+
progressHandler(UpdateProgress.verificationEnd)
|
|
1138
1163
|
case .failure(let error):
|
|
1139
1164
|
NSLog("[BundleStorage] Bundle verification failed: \(error)")
|
|
1140
1165
|
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
@@ -1148,8 +1173,12 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1148
1173
|
logFileSystemDiagnostics(path: tempBundleFile, context: "Before Extraction")
|
|
1149
1174
|
do {
|
|
1150
1175
|
try self.decompressService.unzip(file: tempBundleFile, to: tmpDir, progressHandler: { unzipProgress in
|
|
1151
|
-
|
|
1152
|
-
|
|
1176
|
+
let progress = Self.mapProgress(
|
|
1177
|
+
unzipProgress,
|
|
1178
|
+
start: UpdateProgress.extractionStart,
|
|
1179
|
+
end: UpdateProgress.extractionEnd
|
|
1180
|
+
)
|
|
1181
|
+
progressHandler(progress)
|
|
1153
1182
|
})
|
|
1154
1183
|
NSLog("[BundleStorage] Extraction complete at \(tmpDir)")
|
|
1155
1184
|
logFileSystemDiagnostics(path: tmpDir, context: "After Extraction")
|
|
@@ -1167,6 +1196,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1167
1196
|
try? self.fileSystem.removeItem(atPath: tempBundleFile)
|
|
1168
1197
|
|
|
1169
1198
|
// 8) Verify that a valid bundle file exists inside tmpDir
|
|
1199
|
+
progressHandler(UpdateProgress.bundleValidation)
|
|
1170
1200
|
switch self.findBundleFile(in: tmpDir) {
|
|
1171
1201
|
case .success(let maybeBundlePath):
|
|
1172
1202
|
if let bundlePathInTmp = maybeBundlePath {
|
|
@@ -1210,13 +1240,13 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1210
1240
|
// 14) Clean up the temporary directory
|
|
1211
1241
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
1212
1242
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1243
|
+
progressHandler(UpdateProgress.activationReady)
|
|
1244
|
+
self.scheduleCleanupOldBundles(
|
|
1245
|
+
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
1246
|
+
)
|
|
1218
1247
|
|
|
1219
|
-
//
|
|
1248
|
+
// 15) Complete with success
|
|
1249
|
+
progressHandler(UpdateProgress.complete)
|
|
1220
1250
|
completion(.success(true))
|
|
1221
1251
|
case .failure(let err):
|
|
1222
1252
|
let nsError = err as NSError
|
|
@@ -1259,6 +1289,11 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1259
1289
|
}
|
|
1260
1290
|
}
|
|
1261
1291
|
|
|
1292
|
+
private static func mapProgress(_ value: Double, start: Double, end: Double) -> Double {
|
|
1293
|
+
let clampedValue = min(max(value, 0), 1)
|
|
1294
|
+
return start + (clampedValue * (end - start))
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1262
1297
|
// MARK: - Rollback Support
|
|
1263
1298
|
|
|
1264
1299
|
/**
|