@hot-updater/react-native 1.0.0-rc.14 → 1.0.0-rc.16
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/android/proguard-rules.pro +2 -2
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +650 -636
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +3 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +0 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +10 -14
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +19 -0
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
- package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
- package/android/src/newarch/HotUpdater.kt +7 -9
- package/android/src/newarch/HotUpdaterModule.kt +40 -20
- package/android/src/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +40 -20
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +1126 -74
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -5
- package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +206 -0
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +773 -1016
- package/ios/HotUpdater/Internal/BundleMetadata.swift +5 -0
- package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +5 -27
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +47 -31
- package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -72
- package/lib/commonjs/checkForUpdate.js +7 -10
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +9 -4
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/commonjs/error.js +4 -13
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchJSON.js +9 -2
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/httpClient.js +151 -39
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +247 -5
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +15 -8
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +101 -0
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +7 -8
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +21 -16
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +7 -0
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.js +4 -1
- package/lib/commonjs/notifyAppReadyInsights.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +59 -0
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +55 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +3 -10
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.notification.spec.js +12 -4
- package/lib/commonjs/store.notification.spec.js.map +1 -1
- package/lib/commonjs/store.spec.js +2 -136
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/uuidv7.js +41 -0
- package/lib/commonjs/uuidv7.js.map +1 -0
- package/lib/commonjs/uuidv7.spec.js +30 -0
- package/lib/commonjs/uuidv7.spec.js.map +1 -0
- package/lib/commonjs/wrap.js +1 -3
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +7 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +7 -10
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +9 -4
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/module/error.js +4 -13
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchJSON.js +7 -1
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/httpClient.js +153 -41
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +248 -6
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +15 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +102 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +7 -8
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +21 -16
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +7 -0
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.js +5 -2
- package/lib/module/notifyAppReadyInsights.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.spec.js +59 -0
- package/lib/module/notifyAppReadyInsights.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +55 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +3 -10
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.notification.spec.js +12 -4
- package/lib/module/store.notification.spec.js.map +1 -1
- package/lib/module/store.spec.js +2 -136
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/uuidv7.js +36 -0
- package/lib/module/uuidv7.js.map +1 -0
- package/lib/module/uuidv7.spec.js +30 -0
- package/lib/module/uuidv7.spec.js.map +1 -0
- package/lib/module/wrap.js +1 -3
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +7 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +1 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +4 -13
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchJSON.d.ts +4 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +6 -0
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +0 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +1 -6
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +1 -3
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/uuidv7.d.ts +10 -0
- package/lib/typescript/commonjs/uuidv7.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +7 -4
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +1 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +4 -13
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchJSON.d.ts +4 -0
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +6 -0
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +0 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +1 -6
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +1 -3
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/uuidv7.d.ts +10 -0
- package/lib/typescript/module/uuidv7.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +7 -4
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/checkForUpdate.releaseCatalog.spec.ts +10 -4
- package/src/checkForUpdate.ts +5 -11
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +12 -1
- package/src/httpClient.spec.ts +289 -8
- package/src/httpClient.ts +224 -67
- package/src/index.spec.ts +70 -1
- package/src/index.ts +18 -7
- package/src/native.spec.ts +23 -16
- package/src/native.ts +12 -24
- package/src/notifyAppReadyInsights.boundary.spec.ts +8 -0
- package/src/notifyAppReadyInsights.spec.ts +60 -0
- package/src/notifyAppReadyInsights.transport.spec.ts +66 -0
- package/src/notifyAppReadyInsights.ts +13 -1
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +8 -36
- package/src/store.notification.spec.ts +13 -3
- package/src/store.spec.ts +0 -157
- package/src/store.ts +2 -23
- package/src/uuidv7.spec.ts +25 -0
- package/src/uuidv7.ts +38 -0
- package/src/wrap.spec.ts +8 -0
- package/src/wrap.tsx +0 -2
- package/src/wrap.types.ts +9 -4
- package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
- package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
- package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
- package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
- package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
- package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
- package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
|
@@ -52,27 +52,28 @@ public struct BsdiffPatchDescriptor {
|
|
|
52
52
|
public let baseFileHash: String
|
|
53
53
|
public let patchFileHash: String
|
|
54
54
|
public let patchUrl: URL
|
|
55
|
+
public let byteSize: Int64?
|
|
55
56
|
|
|
56
57
|
public init(
|
|
57
58
|
algorithm: String,
|
|
58
59
|
baseBundleId: String,
|
|
59
60
|
baseFileHash: String,
|
|
60
61
|
patchFileHash: String,
|
|
61
|
-
patchUrl: URL
|
|
62
|
+
patchUrl: URL,
|
|
63
|
+
byteSize: Int64? = nil
|
|
62
64
|
) {
|
|
63
65
|
self.algorithm = algorithm
|
|
64
66
|
self.baseBundleId = baseBundleId
|
|
65
67
|
self.baseFileHash = baseFileHash
|
|
66
68
|
self.patchFileHash = patchFileHash
|
|
67
69
|
self.patchUrl = patchUrl
|
|
70
|
+
self.byteSize = byteSize
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
public struct UpdateProgressPayload {
|
|
72
75
|
public let progress: Double
|
|
73
76
|
public let artifactType: String
|
|
74
|
-
public let downloadedBytes: Int64?
|
|
75
|
-
public let totalBytes: Int64?
|
|
76
77
|
public let details: DiffProgressDetails?
|
|
77
78
|
|
|
78
79
|
public struct DiffProgressFileSnapshot {
|
|
@@ -147,30 +148,19 @@ public struct UpdateProgressPayload {
|
|
|
147
148
|
public init(
|
|
148
149
|
progress: Double,
|
|
149
150
|
artifactType: String,
|
|
150
|
-
downloadedBytes: Int64? = nil,
|
|
151
|
-
totalBytes: Int64? = nil,
|
|
152
151
|
details: DiffProgressDetails? = nil
|
|
153
152
|
) {
|
|
154
153
|
self.progress = progress
|
|
155
154
|
self.artifactType = artifactType
|
|
156
|
-
self.downloadedBytes = downloadedBytes
|
|
157
|
-
self.totalBytes = totalBytes
|
|
158
155
|
self.details = details
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
public var userInfo: [String: Any] {
|
|
162
|
-
|
|
159
|
+
[
|
|
163
160
|
"artifactType": artifactType,
|
|
164
161
|
"progress": progress,
|
|
165
162
|
"details": details?.userInfo ?? NSNull()
|
|
166
163
|
]
|
|
167
|
-
if let downloadedBytes {
|
|
168
|
-
info["downloadedBytes"] = downloadedBytes
|
|
169
|
-
}
|
|
170
|
-
if let totalBytes {
|
|
171
|
-
info["totalBytes"] = totalBytes
|
|
172
|
-
}
|
|
173
|
-
return info
|
|
174
164
|
}
|
|
175
165
|
}
|
|
176
166
|
|
|
@@ -178,9 +168,7 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
178
168
|
case directoryCreationFailed
|
|
179
169
|
case downloadFailed(Error)
|
|
180
170
|
case incompleteDownload(expected: Int64, actual: Int64)
|
|
181
|
-
case extractionFormatError(Error)
|
|
182
171
|
case invalidBundle
|
|
183
|
-
case insufficientDiskSpace
|
|
184
172
|
case signatureVerificationFailed(SignatureVerificationError)
|
|
185
173
|
case moveOperationFailed(Error)
|
|
186
174
|
case bundleInCrashedHistory(String)
|
|
@@ -200,9 +188,7 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
200
188
|
case .directoryCreationFailed: return "DIRECTORY_CREATION_FAILED"
|
|
201
189
|
case .downloadFailed: return "DOWNLOAD_FAILED"
|
|
202
190
|
case .incompleteDownload: return "INCOMPLETE_DOWNLOAD"
|
|
203
|
-
case .extractionFormatError: return "EXTRACTION_FORMAT_ERROR"
|
|
204
191
|
case .invalidBundle: return "INVALID_BUNDLE"
|
|
205
|
-
case .insufficientDiskSpace: return "INSUFFICIENT_DISK_SPACE"
|
|
206
192
|
case .signatureVerificationFailed: return "SIGNATURE_VERIFICATION_FAILED"
|
|
207
193
|
case .moveOperationFailed: return "MOVE_OPERATION_FAILED"
|
|
208
194
|
case .bundleInCrashedHistory: return "BUNDLE_IN_CRASHED_HISTORY"
|
|
@@ -227,19 +213,10 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
227
213
|
userInfo[NSLocalizedDescriptionKey] = "Download incomplete: received \(actual) bytes, expected \(expected) bytes"
|
|
228
214
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The download was interrupted. Check network connection and try again"
|
|
229
215
|
|
|
230
|
-
case .extractionFormatError(let underlyingError):
|
|
231
|
-
userInfo[NSLocalizedDescriptionKey] = "The downloaded bundle file is not a valid compressed archive"
|
|
232
|
-
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
233
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The downloaded file is not a supported bundle archive. Try downloading again"
|
|
234
|
-
|
|
235
216
|
case .invalidBundle:
|
|
236
217
|
userInfo[NSLocalizedDescriptionKey] = "Bundle missing required platform files (index.ios.bundle or main.jsbundle)"
|
|
237
218
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Verify the bundle was built correctly with metro bundler"
|
|
238
219
|
|
|
239
|
-
case .insufficientDiskSpace:
|
|
240
|
-
userInfo[NSLocalizedDescriptionKey] = "Insufficient disk space to download and extract bundle"
|
|
241
|
-
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Free up device storage and try again"
|
|
242
|
-
|
|
243
220
|
case .signatureVerificationFailed(let underlyingError):
|
|
244
221
|
userInfo[NSLocalizedDescriptionKey] = "Bundle signature verification failed"
|
|
245
222
|
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
@@ -280,7 +257,7 @@ public protocol BundleStorageService {
|
|
|
280
257
|
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection
|
|
281
258
|
|
|
282
259
|
// Bundle update
|
|
283
|
-
func updateBundle(bundleId: String,
|
|
260
|
+
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL?, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<Bool, Error>) -> Void)
|
|
284
261
|
func stageReleaseSelection(_ selection: PersistedSelection) -> Bool
|
|
285
262
|
func acceptReleaseCatalog(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Bool
|
|
286
263
|
func getActiveUpdateState() -> [String: Any]
|
|
@@ -306,7 +283,7 @@ public protocol BundleStorageService {
|
|
|
306
283
|
|
|
307
284
|
/**
|
|
308
285
|
* Gets the current active bundle ID from bundle storage.
|
|
309
|
-
* Reads
|
|
286
|
+
* Reads the required OTA manifest.json.
|
|
310
287
|
*/
|
|
311
288
|
func getBundleId() -> String?
|
|
312
289
|
|
|
@@ -332,6 +309,8 @@ public extension BundleStorageService {
|
|
|
332
309
|
}
|
|
333
310
|
|
|
334
311
|
class BundleFileStorageService: BundleStorageService {
|
|
312
|
+
private static let maximumSafeByteSize: Int64 = 9_007_199_254_740_991
|
|
313
|
+
|
|
335
314
|
private struct ActiveBundleMetadataSnapshot {
|
|
336
315
|
let activeBundleId: String
|
|
337
316
|
let bundleId: String?
|
|
@@ -341,41 +320,36 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
341
320
|
private struct ParsedBundleManifest {
|
|
342
321
|
let bundleId: String
|
|
343
322
|
let assets: [String: ParsedManifestAsset]
|
|
323
|
+
let archive: ParsedArchive?
|
|
344
324
|
}
|
|
345
325
|
|
|
346
326
|
private struct ParsedManifestAsset {
|
|
347
327
|
let fileHash: String
|
|
348
328
|
let signature: String?
|
|
329
|
+
let byteSize: Int64?
|
|
330
|
+
let downloadByteSize: Int64?
|
|
349
331
|
}
|
|
350
332
|
|
|
351
|
-
private
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
static let extractionStart = 0.82
|
|
356
|
-
static let extractionEnd = 0.97
|
|
357
|
-
static let bundleValidation = 0.98
|
|
358
|
-
static let activationReady = 0.99
|
|
359
|
-
static let complete = 1.0
|
|
333
|
+
private struct ParsedArchive {
|
|
334
|
+
let downloadFileHash: String
|
|
335
|
+
let downloadByteSize: Int64
|
|
336
|
+
let tarByteSize: Int64
|
|
360
337
|
}
|
|
361
338
|
|
|
362
339
|
private let fileSystem: FileSystemService
|
|
363
340
|
private let downloadService: DownloadService
|
|
364
|
-
private let decompressService: DecompressService
|
|
365
341
|
private let preferences: PreferencesService
|
|
366
342
|
private let isolationKey: String
|
|
367
343
|
|
|
368
|
-
private let id = Int.random(in: 1..<100)
|
|
369
|
-
|
|
370
344
|
// Queue for potentially long-running sequences within updateBundle or for explicit background tasks.
|
|
371
345
|
private let fileOperationQueue: DispatchQueue
|
|
372
346
|
|
|
373
|
-
private var
|
|
374
|
-
|
|
347
|
+
private var hasPreparedLaunch = false
|
|
375
348
|
private var currentLaunchReport: LaunchReport?
|
|
376
349
|
private let activeBundleMetadataLock = NSLock()
|
|
377
350
|
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot?
|
|
378
351
|
private let builtInBundleIdProvider: () -> String
|
|
352
|
+
private let builtInAssetResolver: BuiltInAssetResolver
|
|
379
353
|
private let updateStrategyProvider: () -> UpdateStrategy
|
|
380
354
|
private let releaseStateLock = NSRecursiveLock()
|
|
381
355
|
private var pendingInstallSelections: [String: PersistedSelection] = [:]
|
|
@@ -389,6 +363,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
389
363
|
LaunchReport.launchReportFilename,
|
|
390
364
|
InstallIdentity.installIdentityFilename,
|
|
391
365
|
UserIdentity.userIdentityFilename,
|
|
366
|
+
"builtin-index-v1.json",
|
|
392
367
|
]
|
|
393
368
|
|
|
394
369
|
private func normalizeIdentityValue(_ value: String?) -> String? {
|
|
@@ -399,22 +374,6 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
399
374
|
return trimmed
|
|
400
375
|
}
|
|
401
376
|
|
|
402
|
-
private func emitArchiveProgress(
|
|
403
|
-
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
404
|
-
progress: Double,
|
|
405
|
-
downloadedBytes: Int64? = nil,
|
|
406
|
-
totalBytes: Int64? = nil
|
|
407
|
-
) {
|
|
408
|
-
progressHandler(
|
|
409
|
-
UpdateProgressPayload(
|
|
410
|
-
progress: max(0, min(progress, 1)),
|
|
411
|
-
artifactType: "archive",
|
|
412
|
-
downloadedBytes: downloadedBytes,
|
|
413
|
-
totalBytes: totalBytes
|
|
414
|
-
)
|
|
415
|
-
)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
377
|
private func createDiffProgressFiles(
|
|
419
378
|
changedAssets: [String: ChangedAssetDescriptor]
|
|
420
379
|
) -> [UpdateProgressPayload.DiffProgressFileSnapshot] {
|
|
@@ -576,10 +535,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
576
535
|
return
|
|
577
536
|
}
|
|
578
537
|
|
|
579
|
-
try
|
|
580
|
-
from: downloadedPath,
|
|
581
|
-
to: destinationPath
|
|
582
|
-
)
|
|
538
|
+
try BrotliFileDecompressor.decompress(from: downloadedPath, to: destinationPath)
|
|
583
539
|
try? fileSystem.removeItem(atPath: downloadedPath)
|
|
584
540
|
}
|
|
585
541
|
|
|
@@ -604,7 +560,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
604
560
|
|
|
605
561
|
let sourcePath: String
|
|
606
562
|
do {
|
|
607
|
-
sourcePath = try
|
|
563
|
+
sourcePath = try FileUtilities.fileURL(
|
|
608
564
|
for: assetPath,
|
|
609
565
|
destinationRoot: currentBundleDir
|
|
610
566
|
).path
|
|
@@ -722,6 +678,14 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
722
678
|
atPath path: String,
|
|
723
679
|
asset: ParsedManifestAsset
|
|
724
680
|
) throws {
|
|
681
|
+
if let expectedByteSize = asset.byteSize {
|
|
682
|
+
let attributes = try fileSystem.attributesOfItem(atPath: path)
|
|
683
|
+
guard let size = attributes[.size] as? NSNumber,
|
|
684
|
+
size.int64Value == expectedByteSize else {
|
|
685
|
+
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
725
689
|
guard let actualHash = HashUtils.calculateSHA256(fileURL: URL(fileURLWithPath: path)) else {
|
|
726
690
|
throw BundleStorageError.signatureVerificationFailed(.fileReadFailed)
|
|
727
691
|
}
|
|
@@ -751,10 +715,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
751
715
|
|
|
752
716
|
public init(fileSystem: FileSystemService,
|
|
753
717
|
downloadService: DownloadService,
|
|
754
|
-
decompressService: DecompressService,
|
|
755
718
|
preferences: PreferencesService,
|
|
756
719
|
isolationKey: String,
|
|
757
720
|
builtInBundleIdProvider: @escaping () -> String = { "" },
|
|
721
|
+
builtInAssetResolver: BuiltInAssetResolver? = nil,
|
|
758
722
|
updateStrategyProvider: @escaping () -> UpdateStrategy = {
|
|
759
723
|
let fingerprintHash = HotUpdaterConfig.shared.fingerprintHash
|
|
760
724
|
?? Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_FINGERPRINT_HASH") as? String
|
|
@@ -763,10 +727,14 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
763
727
|
|
|
764
728
|
self.fileSystem = fileSystem
|
|
765
729
|
self.downloadService = downloadService
|
|
766
|
-
self.decompressService = decompressService
|
|
767
730
|
self.preferences = preferences
|
|
768
731
|
self.isolationKey = isolationKey
|
|
769
732
|
self.builtInBundleIdProvider = builtInBundleIdProvider
|
|
733
|
+
self.builtInAssetResolver = builtInAssetResolver ?? IOSBuiltInAssetResolver(
|
|
734
|
+
cacheURL: URL(fileURLWithPath: fileSystem.documentsPath())
|
|
735
|
+
.appendingPathComponent("bundle-store", isDirectory: true)
|
|
736
|
+
.appendingPathComponent("builtin-index-v1.json")
|
|
737
|
+
)
|
|
770
738
|
self.updateStrategyProvider = updateStrategyProvider
|
|
771
739
|
|
|
772
740
|
// Create queue for file operations
|
|
@@ -779,6 +747,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
779
747
|
|
|
780
748
|
// Clean up old bundles if isolationKey format changed
|
|
781
749
|
checkAndCleanupIfIsolationKeyChanged()
|
|
750
|
+
recoverInterruptedPromotions()
|
|
782
751
|
}
|
|
783
752
|
|
|
784
753
|
// MARK: - Metadata File Paths
|
|
@@ -938,11 +907,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
938
907
|
}
|
|
939
908
|
|
|
940
909
|
private func createInitialMetadata() -> BundleMetadata {
|
|
941
|
-
let currentBundleId = getCachedBundleId()
|
|
942
910
|
return BundleMetadata(
|
|
943
911
|
isolationKey: isolationKey,
|
|
944
912
|
stableBundleId: nil,
|
|
945
|
-
stagingBundleId:
|
|
913
|
+
stagingBundleId: nil,
|
|
946
914
|
verificationPending: false,
|
|
947
915
|
pendingTransition: nil
|
|
948
916
|
)
|
|
@@ -956,18 +924,18 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
956
924
|
}
|
|
957
925
|
|
|
958
926
|
private func getActiveBundleId() -> String? {
|
|
959
|
-
|
|
927
|
+
guard let metadata = loadMetadataOrNull() else { return nil }
|
|
928
|
+
if let cachedBundleId = getCachedBundleId(),
|
|
929
|
+
cachedBundleId == metadata.stagingBundleId || cachedBundleId == metadata.stableBundleId {
|
|
960
930
|
return cachedBundleId
|
|
961
931
|
}
|
|
962
932
|
|
|
963
|
-
let
|
|
964
|
-
|
|
965
|
-
if let stagingBundleId = metadata?.stagingBundleId,
|
|
966
|
-
metadata?.verificationPending == false {
|
|
933
|
+
if let stagingBundleId = metadata.stagingBundleId,
|
|
934
|
+
metadata.verificationPending == false {
|
|
967
935
|
return stagingBundleId
|
|
968
936
|
}
|
|
969
937
|
|
|
970
|
-
if let stableBundleId = metadata
|
|
938
|
+
if let stableBundleId = metadata.stableBundleId {
|
|
971
939
|
return stableBundleId
|
|
972
940
|
}
|
|
973
941
|
|
|
@@ -1017,22 +985,6 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1017
985
|
}
|
|
1018
986
|
}
|
|
1019
987
|
|
|
1020
|
-
func canUseManifestDrivenInstall() -> Bool {
|
|
1021
|
-
guard let currentBundleId = getCachedBundleId(),
|
|
1022
|
-
case .success(let storeDir) = bundleStoreDir() else {
|
|
1023
|
-
return false
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
let currentBundleDir = (storeDir as NSString).appendingPathComponent(currentBundleId)
|
|
1027
|
-
guard fileSystem.fileExists(atPath: currentBundleDir),
|
|
1028
|
-
let snapshot = getActiveBundleMetadataSnapshot(),
|
|
1029
|
-
let currentManifest = parseBundleManifest(from: snapshot.manifest) else {
|
|
1030
|
-
return false
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
return currentManifest.assets.isEmpty == false
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
988
|
private func resolveActiveBundleMetadataSnapshot(
|
|
1037
989
|
activeBundleId: String,
|
|
1038
990
|
bundleDirectory: String
|
|
@@ -1041,9 +993,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1041
993
|
let manifestBundleId =
|
|
1042
994
|
(manifest["bundleId"] as? String)?
|
|
1043
995
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1044
|
-
let resolvedBundleId =
|
|
1045
|
-
(manifestBundleId?.isEmpty == false ? manifestBundleId : nil) ??
|
|
1046
|
-
readCompatibilityBundleId(in: bundleDirectory)
|
|
996
|
+
let resolvedBundleId = manifestBundleId?.isEmpty == false ? manifestBundleId : nil
|
|
1047
997
|
|
|
1048
998
|
return ActiveBundleMetadataSnapshot(
|
|
1049
999
|
activeBundleId: activeBundleId,
|
|
@@ -1070,34 +1020,6 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1070
1020
|
)
|
|
1071
1021
|
}
|
|
1072
1022
|
|
|
1073
|
-
private func readCompatibilityBundleId(in bundleDirectory: String) -> String? {
|
|
1074
|
-
let compatibilityBundleIdPath = (bundleDirectory as NSString)
|
|
1075
|
-
.appendingPathComponent(compatibilityBundleIdFilename())
|
|
1076
|
-
if fileSystem.fileExists(atPath: compatibilityBundleIdPath) {
|
|
1077
|
-
do {
|
|
1078
|
-
let compatibilityBundleId = try String(
|
|
1079
|
-
contentsOfFile: compatibilityBundleIdPath,
|
|
1080
|
-
encoding: .utf8
|
|
1081
|
-
)
|
|
1082
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1083
|
-
|
|
1084
|
-
if !compatibilityBundleId.isEmpty {
|
|
1085
|
-
return compatibilityBundleId
|
|
1086
|
-
}
|
|
1087
|
-
} catch {
|
|
1088
|
-
NSLog(
|
|
1089
|
-
"[BundleStorage] Failed to read compatibility bundle metadata at \(compatibilityBundleIdPath): \(error.localizedDescription)"
|
|
1090
|
-
)
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
return nil
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
private func compatibilityBundleIdFilename() -> String {
|
|
1098
|
-
"BUNDLE_ID"
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
1023
|
private func readManifest(in bundleDirectory: String) -> [String: Any]? {
|
|
1102
1024
|
let manifestPath = (bundleDirectory as NSString).appendingPathComponent("manifest.json")
|
|
1103
1025
|
guard fileSystem.fileExists(atPath: manifestPath) else {
|
|
@@ -1124,7 +1046,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1124
1046
|
|
|
1125
1047
|
var assets: [String: ParsedManifestAsset] = [:]
|
|
1126
1048
|
for (assetPath, assetValue) in rawAssets {
|
|
1127
|
-
guard
|
|
1049
|
+
guard FileUtilities.normalizedRelativePath(from: assetPath) == assetPath else {
|
|
1128
1050
|
return nil
|
|
1129
1051
|
}
|
|
1130
1052
|
guard let asset = assetValue as? [String: Any],
|
|
@@ -1137,42 +1059,136 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1137
1059
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1138
1060
|
assets[assetPath] = ParsedManifestAsset(
|
|
1139
1061
|
fileHash: fileHash,
|
|
1140
|
-
signature: signature?.isEmpty == false ? signature : nil
|
|
1062
|
+
signature: signature?.isEmpty == false ? signature : nil,
|
|
1063
|
+
byteSize: Self.parseSafeByteSize(asset["byteSize"]),
|
|
1064
|
+
downloadByteSize: Self.parseSafeByteSize(asset["downloadByteSize"])
|
|
1065
|
+
)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
let archive: ParsedArchive?
|
|
1069
|
+
if let payload = manifest["archive"] as? [String: Any],
|
|
1070
|
+
let downloadFileHash = payload["downloadFileHash"] as? String,
|
|
1071
|
+
Self.isPlainSHA256(downloadFileHash),
|
|
1072
|
+
let downloadByteSize = Self.parseSafeByteSize(payload["downloadByteSize"]),
|
|
1073
|
+
downloadByteSize > 0,
|
|
1074
|
+
let tarByteSize = Self.parseSafeByteSize(payload["tarByteSize"]),
|
|
1075
|
+
tarByteSize >= 1024 {
|
|
1076
|
+
archive = ParsedArchive(
|
|
1077
|
+
downloadFileHash: downloadFileHash,
|
|
1078
|
+
downloadByteSize: downloadByteSize,
|
|
1079
|
+
tarByteSize: tarByteSize
|
|
1141
1080
|
)
|
|
1081
|
+
} else {
|
|
1082
|
+
archive = nil
|
|
1142
1083
|
}
|
|
1143
1084
|
|
|
1144
|
-
return ParsedBundleManifest(
|
|
1085
|
+
return ParsedBundleManifest(
|
|
1086
|
+
bundleId: manifestBundleId,
|
|
1087
|
+
assets: assets,
|
|
1088
|
+
archive: archive
|
|
1089
|
+
)
|
|
1145
1090
|
}
|
|
1146
1091
|
|
|
1147
|
-
private func
|
|
1148
|
-
guard let
|
|
1092
|
+
private static func parseSafeByteSize(_ value: Any?) -> Int64? {
|
|
1093
|
+
guard let number = value as? NSNumber,
|
|
1094
|
+
String(cString: number.objCType) != "c" else {
|
|
1095
|
+
return nil
|
|
1096
|
+
}
|
|
1097
|
+
let doubleValue = number.doubleValue
|
|
1098
|
+
guard doubleValue.isFinite,
|
|
1099
|
+
doubleValue >= 0,
|
|
1100
|
+
doubleValue <= Double(maximumSafeByteSize),
|
|
1101
|
+
doubleValue.rounded(.towardZero) == doubleValue else {
|
|
1149
1102
|
return nil
|
|
1150
1103
|
}
|
|
1104
|
+
return Int64(doubleValue)
|
|
1105
|
+
}
|
|
1151
1106
|
|
|
1152
|
-
|
|
1107
|
+
private static func isPlainSHA256(_ value: String) -> Bool {
|
|
1108
|
+
value.count == 64 && value.allSatisfy {
|
|
1109
|
+
("0"..."9").contains($0) || ("a"..."f").contains($0)
|
|
1110
|
+
}
|
|
1153
1111
|
}
|
|
1154
1112
|
|
|
1155
|
-
private func
|
|
1113
|
+
private func copyVerifiedManifest(from source: String, to destination: String) throws {
|
|
1156
1114
|
let manifestDirectory = (destination as NSString).deletingLastPathComponent
|
|
1157
1115
|
guard fileSystem.createDirectory(atPath: manifestDirectory) else {
|
|
1158
1116
|
throw BundleStorageError.directoryCreationFailed
|
|
1159
1117
|
}
|
|
1118
|
+
if fileSystem.fileExists(atPath: destination) {
|
|
1119
|
+
try fileSystem.removeItem(atPath: destination)
|
|
1120
|
+
}
|
|
1121
|
+
do {
|
|
1122
|
+
try fileSystem.copyItem(atPath: source, toPath: destination)
|
|
1123
|
+
} catch {
|
|
1124
|
+
throw BundleStorageError.moveOperationFailed(error)
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
private func shouldUseArchive(
|
|
1129
|
+
archive: ParsedArchive?,
|
|
1130
|
+
archiveUrl: URL?,
|
|
1131
|
+
downloads: [String: ChangedAssetDescriptor],
|
|
1132
|
+
assets: [String: ParsedManifestAsset]
|
|
1133
|
+
) -> Bool {
|
|
1134
|
+
guard downloads.count >= 2,
|
|
1135
|
+
archive != nil,
|
|
1136
|
+
archiveUrl != nil,
|
|
1137
|
+
assets.values.allSatisfy({ $0.byteSize != nil }) else {
|
|
1138
|
+
return false
|
|
1139
|
+
}
|
|
1160
1140
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1141
|
+
var individualLowerBound: Int64 = 0
|
|
1142
|
+
var hasOfferedPatch = false
|
|
1143
|
+
for (path, descriptor) in downloads {
|
|
1144
|
+
guard let originalByteSize = assets[path]?.downloadByteSize else {
|
|
1145
|
+
return false
|
|
1146
|
+
}
|
|
1147
|
+
let plannedByteSize: Int64
|
|
1148
|
+
if let patch = descriptor.patch {
|
|
1149
|
+
hasOfferedPatch = true
|
|
1150
|
+
guard let patchByteSize = patch.byteSize,
|
|
1151
|
+
patchByteSize >= 0,
|
|
1152
|
+
patchByteSize <= Self.maximumSafeByteSize else {
|
|
1153
|
+
return false
|
|
1167
1154
|
}
|
|
1168
|
-
|
|
1155
|
+
plannedByteSize = min(originalByteSize, patchByteSize)
|
|
1156
|
+
} else {
|
|
1157
|
+
plannedByteSize = originalByteSize
|
|
1169
1158
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1159
|
+
let (sum, overflowed) = individualLowerBound.addingReportingOverflow(plannedByteSize)
|
|
1160
|
+
guard !overflowed,
|
|
1161
|
+
sum <= Self.maximumSafeByteSize else { return false }
|
|
1162
|
+
individualLowerBound = sum
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
guard let archive else { return false }
|
|
1166
|
+
var logicalByteSize: Int64 = 0
|
|
1167
|
+
for asset in assets.values {
|
|
1168
|
+
guard let byteSize = asset.byteSize else { return false }
|
|
1169
|
+
let (sum, overflowed) = logicalByteSize.addingReportingOverflow(byteSize)
|
|
1170
|
+
guard !overflowed,
|
|
1171
|
+
sum <= Self.maximumSafeByteSize else { return false }
|
|
1172
|
+
logicalByteSize = sum
|
|
1173
|
+
}
|
|
1174
|
+
let (tarOverhead, overheadUnderflowed) = archive.tarByteSize
|
|
1175
|
+
.subtractingReportingOverflow(logicalByteSize)
|
|
1176
|
+
guard !overheadUnderflowed,
|
|
1177
|
+
tarOverhead >= 0 else { return false }
|
|
1178
|
+
|
|
1179
|
+
if archive.downloadByteSize <= individualLowerBound {
|
|
1180
|
+
return true
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
guard downloads.count == assets.count,
|
|
1184
|
+
!hasOfferedPatch else {
|
|
1185
|
+
return false
|
|
1186
|
+
}
|
|
1187
|
+
let (archiveBound, boundOverflowed) = individualLowerBound
|
|
1188
|
+
.addingReportingOverflow(tarOverhead)
|
|
1189
|
+
guard !boundOverflowed,
|
|
1190
|
+
archiveBound <= Self.maximumSafeByteSize else { return false }
|
|
1191
|
+
return archive.downloadByteSize <= archiveBound
|
|
1176
1192
|
}
|
|
1177
1193
|
|
|
1178
1194
|
/**
|
|
@@ -1539,6 +1555,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1539
1555
|
metadata.stagingBundleId = nil
|
|
1540
1556
|
metadata.stagingSelection = selection
|
|
1541
1557
|
metadata.verificationPending = false
|
|
1558
|
+
metadata.launchInProgress = false
|
|
1542
1559
|
metadata.pendingTransition = nil
|
|
1543
1560
|
metadata.pendingSelectionTransition = nil
|
|
1544
1561
|
}
|
|
@@ -1666,7 +1683,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1666
1683
|
do {
|
|
1667
1684
|
var bundlePaths: [String] = []
|
|
1668
1685
|
for assetPath in parsedManifest.assets.keys {
|
|
1669
|
-
let assetURL = try
|
|
1686
|
+
let assetURL = try FileUtilities.fileURL(
|
|
1670
1687
|
for: assetPath,
|
|
1671
1688
|
destinationRoot: directoryPath
|
|
1672
1689
|
)
|
|
@@ -1688,38 +1705,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1688
1705
|
}
|
|
1689
1706
|
}
|
|
1690
1707
|
|
|
1691
|
-
|
|
1692
|
-
if self.fileSystem.fileExists(atPath: iosBundlePath) {
|
|
1693
|
-
NSLog("[BundleStorage] Found iOS bundle atPath: \(iosBundlePath)")
|
|
1694
|
-
return .success(iosBundlePath)
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
let mainBundlePath = (directoryPath as NSString).appendingPathComponent("main.jsbundle")
|
|
1698
|
-
if self.fileSystem.fileExists(atPath: mainBundlePath) {
|
|
1699
|
-
NSLog("[BundleStorage] Found main bundle atPath: \(mainBundlePath)")
|
|
1700
|
-
return .success(mainBundlePath)
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
do {
|
|
1704
|
-
let contents = try self.fileSystem.contentsOfDirectory(atPath: directoryPath)
|
|
1705
|
-
NSLog("[BundleStorage] Directory contents: \(contents)")
|
|
1706
|
-
|
|
1707
|
-
// Additional search: check all .bundle files
|
|
1708
|
-
for file in contents {
|
|
1709
|
-
if file.hasSuffix(".bundle") {
|
|
1710
|
-
let bundlePath = (directoryPath as NSString).appendingPathComponent(file)
|
|
1711
|
-
NSLog("[BundleStorage] Found alternative bundle atPath: \(bundlePath)")
|
|
1712
|
-
return .success(bundlePath)
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
NSLog("[BundleStorage] No bundle file found in directory: \(directoryPath)")
|
|
1717
|
-
NSLog("[BundleStorage] Available files: \(contents)")
|
|
1718
|
-
return .success(nil)
|
|
1719
|
-
} catch let error {
|
|
1720
|
-
NSLog("[BundleStorage] Error reading directory contents: \(error.localizedDescription)")
|
|
1721
|
-
return .failure(error)
|
|
1722
|
-
}
|
|
1708
|
+
return .success(nil)
|
|
1723
1709
|
}
|
|
1724
1710
|
|
|
1725
1711
|
/**
|
|
@@ -1752,7 +1738,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1752
1738
|
let bundles = contents.compactMap { item -> String? in
|
|
1753
1739
|
let fullPath = (storeDir as NSString).appendingPathComponent(item)
|
|
1754
1740
|
|
|
1755
|
-
if protectedBundleStoreEntries.contains(item) {
|
|
1741
|
+
if protectedBundleStoreEntries.contains(item) || item.hasSuffix(".install-backup") {
|
|
1756
1742
|
return nil
|
|
1757
1743
|
}
|
|
1758
1744
|
|
|
@@ -1864,11 +1850,11 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1864
1850
|
}
|
|
1865
1851
|
|
|
1866
1852
|
let relativePath = String(bundleURL.path.dropFirst(sourcePrefix.count))
|
|
1867
|
-
guard
|
|
1853
|
+
guard FileUtilities.normalizedRelativePath(from: relativePath) == relativePath else {
|
|
1868
1854
|
throw BundleStorageError.invalidBundle
|
|
1869
1855
|
}
|
|
1870
1856
|
|
|
1871
|
-
return try
|
|
1857
|
+
return try FileUtilities.fileURL(
|
|
1872
1858
|
for: relativePath,
|
|
1873
1859
|
destinationRoot: destinationDirectory
|
|
1874
1860
|
).path
|
|
@@ -1924,8 +1910,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1924
1910
|
private func selectLaunch(bundle: Bundle) -> LaunchSelection {
|
|
1925
1911
|
guard let metadata = loadMetadataOrNull() else {
|
|
1926
1912
|
return LaunchSelection(
|
|
1927
|
-
bundleURL:
|
|
1928
|
-
launchedBundleId:
|
|
1913
|
+
bundleURL: getFallbackBundleURL(bundle: bundle),
|
|
1914
|
+
launchedBundleId: nil,
|
|
1929
1915
|
shouldRollbackOnCrash: false
|
|
1930
1916
|
)
|
|
1931
1917
|
}
|
|
@@ -1972,13 +1958,29 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1972
1958
|
}
|
|
1973
1959
|
|
|
1974
1960
|
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection {
|
|
1961
|
+
builtInAssetResolver.use(bundle: bundle)
|
|
1975
1962
|
saveLaunchReport(nil)
|
|
1976
1963
|
if loadInstallIdentity() == nil {
|
|
1977
1964
|
let identity = InstallIdentity()
|
|
1978
1965
|
_ = saveInstallIdentity(identity)
|
|
1979
1966
|
}
|
|
1980
1967
|
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
1981
|
-
|
|
1968
|
+
// Only a new storage instance consumes an unfinished launch. Repeated bundle
|
|
1969
|
+
// lookups or bridge reloads in this process must not reject its own launch.
|
|
1970
|
+
if !hasPreparedLaunch,
|
|
1971
|
+
let metadata = loadMetadataOrNull(),
|
|
1972
|
+
metadata.verificationPending,
|
|
1973
|
+
metadata.launchInProgress,
|
|
1974
|
+
let stagingBundleId = metadata.stagingBundleId {
|
|
1975
|
+
rollbackPendingBundle(stagingBundleId)
|
|
1976
|
+
}
|
|
1977
|
+
hasPreparedLaunch = true
|
|
1978
|
+
let selection = selectLaunch(bundle: bundle)
|
|
1979
|
+
if selection.shouldRollbackOnCrash, var metadata = loadMetadataOrNull() {
|
|
1980
|
+
metadata.launchInProgress = true
|
|
1981
|
+
_ = saveMetadata(metadata)
|
|
1982
|
+
}
|
|
1983
|
+
return selection
|
|
1982
1984
|
}
|
|
1983
1985
|
|
|
1984
1986
|
// MARK: - Bundle Update
|
|
@@ -1986,12 +1988,12 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1986
1988
|
/**
|
|
1987
1989
|
* Updates the bundle from the specified URL. This operation is asynchronous.
|
|
1988
1990
|
* @param bundleId ID of the bundle to update
|
|
1989
|
-
* @param
|
|
1990
|
-
* @param
|
|
1991
|
+
* @param manifestUrl URL of the complete target manifest
|
|
1992
|
+
* @param manifestFileHash Hash or signature of the target manifest
|
|
1991
1993
|
* @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
|
|
1992
1994
|
* @param completion Callback with result of the operation
|
|
1993
1995
|
*/
|
|
1994
|
-
func updateBundle(bundleId: String,
|
|
1996
|
+
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<Bool, Error>) -> Void) {
|
|
1995
1997
|
// Check if bundle is in crashed history
|
|
1996
1998
|
let crashedHistory = loadCrashedHistory()
|
|
1997
1999
|
if crashedHistory.contains(bundleId) {
|
|
@@ -2000,928 +2002,682 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2000
2002
|
return
|
|
2001
2003
|
}
|
|
2002
2004
|
|
|
2003
|
-
|
|
2004
|
-
let currentBundleId = self.getCachedBundleId()
|
|
2005
|
-
|
|
2006
|
-
guard let validFileUrl = fileUrl else {
|
|
2007
|
-
NSLog("[BundleStorage] fileUrl is nil, resetting bundle URL.")
|
|
2008
|
-
// Dispatch the sequence to the file operation queue to ensure completion is called asynchronously
|
|
2009
|
-
// and to keep file operations off the calling thread if it's the main thread.
|
|
2010
|
-
fileOperationQueue.async {
|
|
2011
|
-
let setResult = self.setBundleURL(localPath: nil)
|
|
2012
|
-
switch setResult {
|
|
2013
|
-
case .success:
|
|
2014
|
-
let previousMetadata = self.loadMetadataOrNull()
|
|
2015
|
-
var resetMetadata = self.createInitialMetadata()
|
|
2016
|
-
resetMetadata.highestSeenCatalogs = previousMetadata?.highestSeenCatalogs ?? [:]
|
|
2017
|
-
resetMetadata.currentSelectionContexts = previousMetadata?.currentSelectionContexts ?? [:]
|
|
2018
|
-
let _ = self.saveMetadata(resetMetadata)
|
|
2019
|
-
self.saveLaunchReport(nil)
|
|
2020
|
-
let cleanupResult = self.cleanupOldBundles(currentBundleId: currentBundleId, bundleId: bundleId)
|
|
2021
|
-
switch cleanupResult {
|
|
2022
|
-
case .success:
|
|
2023
|
-
completion(.success(true))
|
|
2024
|
-
case .failure(let error):
|
|
2025
|
-
NSLog("[BundleStorage] Error during cleanup after reset: \(error)")
|
|
2026
|
-
completion(.failure(error))
|
|
2027
|
-
}
|
|
2028
|
-
case .failure(let error):
|
|
2029
|
-
NSLog("[BundleStorage] Error resetting bundle URL: \(error)")
|
|
2030
|
-
completion(.failure(error))
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
return
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
// Start the bundle update process on a background queue
|
|
2037
|
-
fileOperationQueue.async {
|
|
2038
|
-
|
|
2005
|
+
fileOperationQueue.async(flags: .barrier) {
|
|
2039
2006
|
let storeDirResult = self.bundleStoreDir()
|
|
2040
2007
|
guard case .success(let storeDir) = storeDirResult else {
|
|
2041
2008
|
completion(.failure(storeDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
2042
2009
|
return
|
|
2043
2010
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
if let bundlePath = existingBundlePath {
|
|
2055
|
-
NSLog("[BundleStorage] Using cached bundle at path: \(bundlePath)")
|
|
2056
|
-
let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
|
|
2057
|
-
guard let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
|
|
2058
|
-
completion(.failure(BundleStorageError.unknown(
|
|
2059
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2060
|
-
NSLocalizedDescriptionKey: "Release catalog selection is stale"
|
|
2061
|
-
])
|
|
2062
|
-
)))
|
|
2063
|
-
return
|
|
2064
|
-
}
|
|
2065
|
-
let setResult = self.setBundleURL(localPath: bundlePath)
|
|
2066
|
-
switch setResult {
|
|
2067
|
-
case .success:
|
|
2068
|
-
let _ = self.saveMetadata(updatedMetadata)
|
|
2069
|
-
NSLog("[BundleStorage] Set staging bundle (cached): \(bundleId), verificationPending: true")
|
|
2070
|
-
|
|
2071
|
-
self.emitArchiveProgress(
|
|
2072
|
-
progressHandler: progressHandler,
|
|
2073
|
-
progress: UpdateProgress.complete
|
|
2074
|
-
)
|
|
2075
|
-
self.scheduleCleanupOldBundles(
|
|
2076
|
-
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
2077
|
-
)
|
|
2078
|
-
completion(.success(true))
|
|
2079
|
-
case .failure(let error):
|
|
2080
|
-
completion(.failure(error))
|
|
2081
|
-
}
|
|
2082
|
-
return
|
|
2083
|
-
} else {
|
|
2084
|
-
NSLog("[BundleStorage] Cached directory exists but invalid, removing: \(finalBundleDir)")
|
|
2085
|
-
do {
|
|
2086
|
-
try self.fileSystem.removeItem(atPath: finalBundleDir)
|
|
2087
|
-
// Continue with download process on success
|
|
2088
|
-
if let manifestUrl,
|
|
2089
|
-
let manifestFileHash,
|
|
2090
|
-
let changedAssets,
|
|
2091
|
-
self.canUseManifestDrivenInstall() {
|
|
2092
|
-
self.updateBundleFromManifest(
|
|
2093
|
-
bundleId: bundleId,
|
|
2094
|
-
fileUrl: validFileUrl,
|
|
2095
|
-
fileHash: fileHash,
|
|
2096
|
-
manifestUrl: manifestUrl,
|
|
2097
|
-
manifestFileHash: manifestFileHash,
|
|
2098
|
-
changedAssets: changedAssets,
|
|
2099
|
-
storeDir: storeDir,
|
|
2100
|
-
progressHandler: progressHandler,
|
|
2101
|
-
completion: completion
|
|
2102
|
-
)
|
|
2103
|
-
} else {
|
|
2104
|
-
if manifestUrl != nil && manifestFileHash != nil && changedAssets != nil {
|
|
2105
|
-
NSLog("[BundleStorage] Skipping manifest-driven install for \(bundleId) because no active OTA manifest is available. Using archive.")
|
|
2106
|
-
}
|
|
2107
|
-
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
|
|
2108
|
-
}
|
|
2109
|
-
} catch let error {
|
|
2110
|
-
NSLog("[BundleStorage] Failed to remove invalid bundle dir: \(error.localizedDescription)")
|
|
2111
|
-
completion(.failure(BundleStorageError.unknown(error)))
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
case .failure(let error):
|
|
2115
|
-
completion(.failure(error))
|
|
2116
|
-
}
|
|
2117
|
-
} else {
|
|
2118
|
-
if let manifestUrl,
|
|
2119
|
-
let manifestFileHash,
|
|
2120
|
-
let changedAssets,
|
|
2121
|
-
self.canUseManifestDrivenInstall() {
|
|
2122
|
-
self.updateBundleFromManifest(
|
|
2123
|
-
bundleId: bundleId,
|
|
2124
|
-
fileUrl: validFileUrl,
|
|
2125
|
-
fileHash: fileHash,
|
|
2126
|
-
manifestUrl: manifestUrl,
|
|
2127
|
-
manifestFileHash: manifestFileHash,
|
|
2128
|
-
changedAssets: changedAssets,
|
|
2129
|
-
storeDir: storeDir,
|
|
2130
|
-
progressHandler: progressHandler,
|
|
2131
|
-
completion: completion
|
|
2132
|
-
)
|
|
2133
|
-
} else {
|
|
2134
|
-
if manifestUrl != nil && manifestFileHash != nil && changedAssets != nil {
|
|
2135
|
-
NSLog("[BundleStorage] Skipping manifest-driven install for \(bundleId) because no active OTA manifest is available. Using archive.")
|
|
2136
|
-
}
|
|
2137
|
-
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2011
|
+
self.updateBundleFromManifest(
|
|
2012
|
+
bundleId: bundleId,
|
|
2013
|
+
manifestUrl: manifestUrl,
|
|
2014
|
+
manifestFileHash: manifestFileHash,
|
|
2015
|
+
archiveUrl: archiveUrl,
|
|
2016
|
+
changedAssets: assets,
|
|
2017
|
+
storeDir: storeDir,
|
|
2018
|
+
progressHandler: progressHandler,
|
|
2019
|
+
completion: completion
|
|
2020
|
+
)
|
|
2140
2021
|
}
|
|
2141
2022
|
}
|
|
2142
|
-
|
|
2143
|
-
/**
|
|
2144
|
-
* Prepares directories and starts the download process.
|
|
2145
|
-
* This method is part of the asynchronous `updateBundle` flow.
|
|
2146
|
-
* @param bundleId ID of the bundle to update
|
|
2147
|
-
* @param fileUrl URL of the bundle file to download
|
|
2148
|
-
* @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
|
|
2149
|
-
* @param storeDir Path to the bundle-store directory
|
|
2150
|
-
* @param progressHandler Callback for download and extraction progress
|
|
2151
|
-
* @param completion Callback with result of the operation
|
|
2152
|
-
*/
|
|
2153
|
-
private func updateBundleFromManifest(
|
|
2154
|
-
bundleId: String,
|
|
2155
|
-
fileUrl: URL,
|
|
2156
|
-
fileHash: String?,
|
|
2157
|
-
manifestUrl: URL,
|
|
2158
|
-
manifestFileHash: String,
|
|
2159
|
-
changedAssets: [String: ChangedAssetDescriptor],
|
|
2160
|
-
storeDir: String,
|
|
2161
|
-
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
2162
|
-
completion: @escaping (Result<Bool, Error>) -> Void
|
|
2163
|
-
) {
|
|
2164
|
-
let tempDirResult = tempDir()
|
|
2165
|
-
guard case .success(let tempDirectory) = tempDirResult else {
|
|
2166
|
-
completion(.failure(tempDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
2167
|
-
return
|
|
2168
|
-
}
|
|
2169
2023
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2024
|
+
private func downloadManifestAsset(
|
|
2025
|
+
assetPath: String,
|
|
2026
|
+
expectedAsset: ParsedManifestAsset,
|
|
2027
|
+
changedAsset: ChangedAssetDescriptor,
|
|
2028
|
+
currentBundleId: String?,
|
|
2029
|
+
currentBundleDir: String?,
|
|
2030
|
+
destinationPath: String,
|
|
2031
|
+
tempDirectory: String,
|
|
2032
|
+
progressFile: UpdateProgressPayload.DiffProgressFileSnapshot,
|
|
2033
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void
|
|
2034
|
+
) throws {
|
|
2035
|
+
let expectedHash = expectedAsset.fileHash
|
|
2036
|
+
var diffFiles = [progressFile]
|
|
2037
|
+
let patched = applyPatchAssetIfPossible(
|
|
2038
|
+
assetPath: assetPath,
|
|
2039
|
+
changedAsset: changedAsset,
|
|
2040
|
+
currentBundleId: currentBundleId,
|
|
2041
|
+
currentBundleDir: currentBundleDir,
|
|
2042
|
+
destinationPath: destinationPath,
|
|
2043
|
+
expectedHash: expectedHash,
|
|
2044
|
+
tempDirectory: tempDirectory,
|
|
2045
|
+
files: &diffFiles,
|
|
2046
|
+
progressHandler: progressHandler
|
|
2047
|
+
)
|
|
2048
|
+
if patched {
|
|
2049
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2050
|
+
updateDiffProgressFile(
|
|
2051
|
+
files: &diffFiles,
|
|
2052
|
+
assetPath: assetPath,
|
|
2053
|
+
status: "downloaded",
|
|
2054
|
+
progress: 1
|
|
2055
|
+
)
|
|
2056
|
+
self.emitDiffProgress(
|
|
2057
|
+
progressHandler: progressHandler,
|
|
2058
|
+
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
2059
|
+
? "finalizing"
|
|
2060
|
+
: "downloading",
|
|
2061
|
+
files: diffFiles
|
|
2062
|
+
)
|
|
2173
2063
|
return
|
|
2174
2064
|
}
|
|
2175
2065
|
|
|
2176
|
-
let
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
var diffFiles = createDiffProgressFiles(changedAssets: changedAssets)
|
|
2184
|
-
|
|
2185
|
-
do {
|
|
2066
|
+
guard let changedAssetFileUrl = changedAsset.fileUrl else {
|
|
2067
|
+
updateDiffProgressFile(
|
|
2068
|
+
files: &diffFiles,
|
|
2069
|
+
assetPath: assetPath,
|
|
2070
|
+
status: "failed",
|
|
2071
|
+
progress: 0
|
|
2072
|
+
)
|
|
2186
2073
|
self.emitDiffProgress(
|
|
2187
2074
|
progressHandler: progressHandler,
|
|
2188
|
-
phase: "
|
|
2189
|
-
files: diffFiles
|
|
2190
|
-
manifestProgress: 0
|
|
2075
|
+
phase: "downloading",
|
|
2076
|
+
files: diffFiles
|
|
2191
2077
|
)
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
phase: "manifest",
|
|
2199
|
-
files: diffFiles,
|
|
2200
|
-
manifestProgress: progress.progress
|
|
2201
|
-
)
|
|
2202
|
-
}
|
|
2203
|
-
) {
|
|
2204
|
-
case .success:
|
|
2205
|
-
break
|
|
2206
|
-
case .failure(let error):
|
|
2207
|
-
if let downloadError = error as? DownloadError,
|
|
2208
|
-
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2209
|
-
throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
2210
|
-
}
|
|
2211
|
-
throw BundleStorageError.downloadFailed(error)
|
|
2212
|
-
}
|
|
2078
|
+
throw BundleStorageError.downloadFailed(
|
|
2079
|
+
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2080
|
+
NSLocalizedDescriptionKey: "Changed asset fileUrl missing and patch could not be applied: \(assetPath)"
|
|
2081
|
+
])
|
|
2082
|
+
)
|
|
2083
|
+
}
|
|
2213
2084
|
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2085
|
+
let changedAssetDownloadPath = downloadPath(
|
|
2086
|
+
assetPath: assetPath,
|
|
2087
|
+
changedAsset: changedAsset
|
|
2088
|
+
)
|
|
2089
|
+
let downloadDestinationPath: String
|
|
2090
|
+
if changedAsset.fileCompression == "br" {
|
|
2091
|
+
downloadDestinationPath = compressedTempPath(
|
|
2092
|
+
tempDirectory: tempDirectory,
|
|
2093
|
+
assetPath: assetPath,
|
|
2094
|
+
compression: "br"
|
|
2217
2095
|
)
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
}
|
|
2222
|
-
throw BundleStorageError.unknown(nil)
|
|
2223
|
-
}
|
|
2096
|
+
} else {
|
|
2097
|
+
downloadDestinationPath = destinationPath
|
|
2098
|
+
}
|
|
2224
2099
|
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2100
|
+
switch self.downloadFileSynchronously(
|
|
2101
|
+
from: changedAssetFileUrl,
|
|
2102
|
+
to: downloadDestinationPath,
|
|
2103
|
+
progressHandler: { progress in
|
|
2104
|
+
self.updateDiffProgressFile(
|
|
2105
|
+
files: &diffFiles,
|
|
2106
|
+
assetPath: assetPath,
|
|
2107
|
+
status: "downloading",
|
|
2108
|
+
progress: progress.progress,
|
|
2109
|
+
downloadPath: changedAssetDownloadPath,
|
|
2110
|
+
downloadedBytes: progress.downloadedBytes,
|
|
2111
|
+
totalBytes: progress.totalBytes
|
|
2112
|
+
)
|
|
2113
|
+
self.emitDiffProgress(
|
|
2114
|
+
progressHandler: progressHandler,
|
|
2115
|
+
phase: "downloading",
|
|
2116
|
+
files: diffFiles
|
|
2117
|
+
)
|
|
2118
|
+
}
|
|
2119
|
+
) {
|
|
2120
|
+
case .success(let downloadedFileURL):
|
|
2121
|
+
do {
|
|
2122
|
+
try materializeDownloadedAsset(
|
|
2123
|
+
downloadedPath: downloadedFileURL.path,
|
|
2124
|
+
destinationPath: destinationPath,
|
|
2125
|
+
changedAsset: changedAsset
|
|
2126
|
+
)
|
|
2127
|
+
try verifyManifestAssetFile(
|
|
2128
|
+
atPath: destinationPath,
|
|
2129
|
+
asset: expectedAsset
|
|
2130
|
+
)
|
|
2131
|
+
} catch {
|
|
2132
|
+
updateDiffProgressFile(
|
|
2133
|
+
files: &diffFiles,
|
|
2134
|
+
assetPath: assetPath,
|
|
2135
|
+
status: "failed",
|
|
2136
|
+
progress: 1
|
|
2137
|
+
)
|
|
2138
|
+
self.emitDiffProgress(
|
|
2139
|
+
progressHandler: progressHandler,
|
|
2140
|
+
phase: "downloading",
|
|
2141
|
+
files: diffFiles
|
|
2142
|
+
)
|
|
2143
|
+
throw error
|
|
2231
2144
|
}
|
|
2145
|
+
updateDiffProgressFile(
|
|
2146
|
+
files: &diffFiles,
|
|
2147
|
+
assetPath: assetPath,
|
|
2148
|
+
status: "downloaded",
|
|
2149
|
+
progress: 1
|
|
2150
|
+
)
|
|
2232
2151
|
self.emitDiffProgress(
|
|
2233
2152
|
progressHandler: progressHandler,
|
|
2234
|
-
phase: diffFiles.
|
|
2153
|
+
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
2154
|
+
? "finalizing"
|
|
2155
|
+
: "downloading",
|
|
2235
2156
|
files: diffFiles
|
|
2236
2157
|
)
|
|
2237
|
-
|
|
2238
|
-
let
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2158
|
+
case .failure(let error):
|
|
2159
|
+
let lastKnownProgress = diffFiles.first(where: { $0.path == assetPath })?.progress ?? 0
|
|
2160
|
+
updateDiffProgressFile(
|
|
2161
|
+
files: &diffFiles,
|
|
2162
|
+
assetPath: assetPath,
|
|
2163
|
+
status: "failed",
|
|
2164
|
+
progress: lastKnownProgress
|
|
2165
|
+
)
|
|
2166
|
+
self.emitDiffProgress(
|
|
2167
|
+
progressHandler: progressHandler,
|
|
2168
|
+
phase: "downloading",
|
|
2169
|
+
files: diffFiles
|
|
2170
|
+
)
|
|
2171
|
+
if let downloadError = error as? DownloadError,
|
|
2172
|
+
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2173
|
+
throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
2247
2174
|
}
|
|
2175
|
+
throw BundleStorageError.downloadFailed(error)
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2248
2178
|
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
}
|
|
2179
|
+
// Retain only target files on retry; partial files are rehashed before reuse.
|
|
2180
|
+
private func pruneStagingDirectory(_ directory: String, assets: Set<String>, prefix: String = "") throws {
|
|
2181
|
+
for name in try fileSystem.contentsOfDirectory(atPath: directory) {
|
|
2182
|
+
let relativePath = prefix + name
|
|
2183
|
+
let path = (directory as NSString).appendingPathComponent(name)
|
|
2184
|
+
let type = try fileSystem.attributesOfItem(atPath: path)[.type] as? FileAttributeType
|
|
2185
|
+
if type == .typeDirectory, assets.contains(where: { $0.hasPrefix(relativePath + "/") }) {
|
|
2186
|
+
try pruneStagingDirectory(path, assets: assets, prefix: relativePath + "/")
|
|
2187
|
+
} else if type != .typeRegular || !assets.contains(relativePath) {
|
|
2188
|
+
try fileSystem.removeItem(atPath: path)
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2263
2192
|
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2193
|
+
private func installArchive(
|
|
2194
|
+
archive: ParsedArchive,
|
|
2195
|
+
archiveUrl: URL,
|
|
2196
|
+
targetManifest: ParsedBundleManifest,
|
|
2197
|
+
preparedDirectory: String,
|
|
2198
|
+
tempDirectory: String,
|
|
2199
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void
|
|
2200
|
+
) throws -> [UpdateProgressPayload.DiffProgressFileSnapshot] {
|
|
2201
|
+
let archiveScratch = (tempDirectory as NSString).appendingPathComponent("archive")
|
|
2202
|
+
try? fileSystem.removeItem(atPath: archiveScratch)
|
|
2203
|
+
guard fileSystem.createDirectory(atPath: archiveScratch) else {
|
|
2204
|
+
throw BundleStorageError.directoryCreationFailed
|
|
2205
|
+
}
|
|
2206
|
+
defer { try? fileSystem.removeItem(atPath: archiveScratch) }
|
|
2274
2207
|
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2208
|
+
let compressedPath = (archiveScratch as NSString).appendingPathComponent("bundle.tar.br")
|
|
2209
|
+
let tarPath = (archiveScratch as NSString).appendingPathComponent("bundle.tar")
|
|
2210
|
+
let extractedDirectory = (archiveScratch as NSString).appendingPathComponent("extracted")
|
|
2211
|
+
var progressFiles = [UpdateProgressPayload.DiffProgressFileSnapshot(
|
|
2212
|
+
path: "bundle.tar.br",
|
|
2213
|
+
downloadPath: "bundle.tar.br",
|
|
2214
|
+
status: "pending",
|
|
2215
|
+
progress: 0,
|
|
2216
|
+
order: 0
|
|
2217
|
+
)]
|
|
2218
|
+
emitDiffProgress(
|
|
2219
|
+
progressHandler: progressHandler,
|
|
2220
|
+
phase: "downloading",
|
|
2221
|
+
files: progressFiles
|
|
2222
|
+
)
|
|
2284
2223
|
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2224
|
+
switch downloadFileSynchronously(
|
|
2225
|
+
from: archiveUrl,
|
|
2226
|
+
to: compressedPath,
|
|
2227
|
+
progressHandler: { progress in
|
|
2228
|
+
self.updateDiffProgressFile(
|
|
2229
|
+
files: &progressFiles,
|
|
2230
|
+
assetPath: "bundle.tar.br",
|
|
2231
|
+
status: "downloading",
|
|
2232
|
+
progress: progress.progress,
|
|
2233
|
+
downloadPath: "bundle.tar.br",
|
|
2234
|
+
downloadedBytes: progress.downloadedBytes,
|
|
2235
|
+
totalBytes: progress.totalBytes
|
|
2236
|
+
)
|
|
2237
|
+
self.emitDiffProgress(
|
|
2238
|
+
progressHandler: progressHandler,
|
|
2239
|
+
phase: "downloading",
|
|
2240
|
+
files: progressFiles
|
|
2241
|
+
)
|
|
2242
|
+
}
|
|
2243
|
+
) {
|
|
2244
|
+
case .success:
|
|
2245
|
+
break
|
|
2246
|
+
case .failure(let error):
|
|
2247
|
+
throw BundleStorageError.downloadFailed(error)
|
|
2248
|
+
}
|
|
2289
2249
|
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
phase: "downloading",
|
|
2300
|
-
files: diffFiles
|
|
2301
|
-
)
|
|
2302
|
-
throw BundleStorageError.downloadFailed(
|
|
2303
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2304
|
-
NSLocalizedDescriptionKey: "Changed asset missing from update response: \(assetPath)"
|
|
2305
|
-
])
|
|
2306
|
-
)
|
|
2307
|
-
}
|
|
2250
|
+
let compressedAttributes = try fileSystem.attributesOfItem(atPath: compressedPath)
|
|
2251
|
+
guard let compressedSize = compressedAttributes[.size] as? NSNumber,
|
|
2252
|
+
compressedSize.int64Value == archive.downloadByteSize,
|
|
2253
|
+
HashUtils.verifyHash(
|
|
2254
|
+
fileURL: URL(fileURLWithPath: compressedPath),
|
|
2255
|
+
expectedHash: archive.downloadFileHash
|
|
2256
|
+
) else {
|
|
2257
|
+
throw BundleStorageError.invalidBundle
|
|
2258
|
+
}
|
|
2308
2259
|
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
progress: 0
|
|
2315
|
-
)
|
|
2316
|
-
self.emitDiffProgress(
|
|
2317
|
-
progressHandler: progressHandler,
|
|
2318
|
-
phase: "downloading",
|
|
2319
|
-
files: diffFiles
|
|
2320
|
-
)
|
|
2321
|
-
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
2322
|
-
}
|
|
2260
|
+
try BrotliFileDecompressor.decompress(
|
|
2261
|
+
from: compressedPath,
|
|
2262
|
+
to: tarPath,
|
|
2263
|
+
expectedOutputByteSize: archive.tarByteSize
|
|
2264
|
+
)
|
|
2323
2265
|
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
destinationPath: destinationPath,
|
|
2330
|
-
expectedHash: expectedHash,
|
|
2331
|
-
tempDirectory: tempDirectory,
|
|
2332
|
-
files: &diffFiles,
|
|
2333
|
-
progressHandler: progressHandler
|
|
2334
|
-
)
|
|
2335
|
-
if patched {
|
|
2336
|
-
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2337
|
-
updateDiffProgressFile(
|
|
2338
|
-
files: &diffFiles,
|
|
2339
|
-
assetPath: assetPath,
|
|
2340
|
-
status: "downloaded",
|
|
2341
|
-
progress: 1
|
|
2342
|
-
)
|
|
2343
|
-
self.emitDiffProgress(
|
|
2344
|
-
progressHandler: progressHandler,
|
|
2345
|
-
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
2346
|
-
? "finalizing"
|
|
2347
|
-
: "downloading",
|
|
2348
|
-
files: diffFiles
|
|
2349
|
-
)
|
|
2350
|
-
continue
|
|
2266
|
+
let expectedFiles = try targetManifest.assets.reduce(
|
|
2267
|
+
into: [String: Int64](),
|
|
2268
|
+
{ result, entry in
|
|
2269
|
+
guard let byteSize = entry.value.byteSize else {
|
|
2270
|
+
throw BundleStorageError.invalidBundle
|
|
2351
2271
|
}
|
|
2272
|
+
result[entry.key] = byteSize
|
|
2273
|
+
}
|
|
2274
|
+
)
|
|
2275
|
+
try TarArchiveExtractor.extract(
|
|
2276
|
+
from: tarPath,
|
|
2277
|
+
to: extractedDirectory,
|
|
2278
|
+
expectedFiles: expectedFiles
|
|
2279
|
+
)
|
|
2280
|
+
for (path, asset) in targetManifest.assets {
|
|
2281
|
+
let extractedPath = try FileUtilities.fileURL(
|
|
2282
|
+
for: path,
|
|
2283
|
+
destinationRoot: extractedDirectory
|
|
2284
|
+
).path
|
|
2285
|
+
try verifyManifestAssetFile(atPath: extractedPath, asset: asset)
|
|
2286
|
+
}
|
|
2352
2287
|
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
status: "failed",
|
|
2358
|
-
progress: 0
|
|
2359
|
-
)
|
|
2360
|
-
self.emitDiffProgress(
|
|
2361
|
-
progressHandler: progressHandler,
|
|
2362
|
-
phase: "downloading",
|
|
2363
|
-
files: diffFiles
|
|
2364
|
-
)
|
|
2365
|
-
throw BundleStorageError.downloadFailed(
|
|
2366
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2367
|
-
NSLocalizedDescriptionKey: "Changed asset fileUrl missing and patch could not be applied: \(assetPath)"
|
|
2368
|
-
])
|
|
2369
|
-
)
|
|
2370
|
-
}
|
|
2288
|
+
try promoteVerifiedArchiveDirectory(
|
|
2289
|
+
preparedDirectory,
|
|
2290
|
+
withArchiveDirectory: extractedDirectory
|
|
2291
|
+
)
|
|
2371
2292
|
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2293
|
+
updateDiffProgressFile(
|
|
2294
|
+
files: &progressFiles,
|
|
2295
|
+
assetPath: "bundle.tar.br",
|
|
2296
|
+
status: "downloaded",
|
|
2297
|
+
progress: 1,
|
|
2298
|
+
downloadedBytes: archive.downloadByteSize,
|
|
2299
|
+
totalBytes: archive.downloadByteSize
|
|
2300
|
+
)
|
|
2301
|
+
emitDiffProgress(
|
|
2302
|
+
progressHandler: progressHandler,
|
|
2303
|
+
phase: "finalizing",
|
|
2304
|
+
files: progressFiles
|
|
2305
|
+
)
|
|
2306
|
+
return progressFiles
|
|
2307
|
+
}
|
|
2386
2308
|
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
status: "downloading",
|
|
2395
|
-
progress: progress.progress,
|
|
2396
|
-
downloadPath: changedAssetDownloadPath,
|
|
2397
|
-
downloadedBytes: progress.downloadedBytes,
|
|
2398
|
-
totalBytes: progress.totalBytes
|
|
2399
|
-
)
|
|
2400
|
-
self.emitDiffProgress(
|
|
2401
|
-
progressHandler: progressHandler,
|
|
2402
|
-
phase: "downloading",
|
|
2403
|
-
files: diffFiles
|
|
2404
|
-
)
|
|
2405
|
-
}
|
|
2406
|
-
) {
|
|
2407
|
-
case .success(let downloadedFileURL):
|
|
2408
|
-
do {
|
|
2409
|
-
try materializeDownloadedAsset(
|
|
2410
|
-
downloadedPath: downloadedFileURL.path,
|
|
2411
|
-
destinationPath: destinationPath,
|
|
2412
|
-
changedAsset: changedAsset
|
|
2413
|
-
)
|
|
2414
|
-
try verifyManifestAssetFile(
|
|
2415
|
-
atPath: destinationPath,
|
|
2416
|
-
asset: expectedAsset
|
|
2417
|
-
)
|
|
2418
|
-
} catch {
|
|
2419
|
-
updateDiffProgressFile(
|
|
2420
|
-
files: &diffFiles,
|
|
2421
|
-
assetPath: assetPath,
|
|
2422
|
-
status: "failed",
|
|
2423
|
-
progress: 1
|
|
2424
|
-
)
|
|
2425
|
-
self.emitDiffProgress(
|
|
2426
|
-
progressHandler: progressHandler,
|
|
2427
|
-
phase: "downloading",
|
|
2428
|
-
files: diffFiles
|
|
2429
|
-
)
|
|
2430
|
-
throw error
|
|
2431
|
-
}
|
|
2432
|
-
updateDiffProgressFile(
|
|
2433
|
-
files: &diffFiles,
|
|
2434
|
-
assetPath: assetPath,
|
|
2435
|
-
status: "downloaded",
|
|
2436
|
-
progress: 1
|
|
2437
|
-
)
|
|
2438
|
-
self.emitDiffProgress(
|
|
2439
|
-
progressHandler: progressHandler,
|
|
2440
|
-
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
2441
|
-
? "finalizing"
|
|
2442
|
-
: "downloading",
|
|
2443
|
-
files: diffFiles
|
|
2444
|
-
)
|
|
2445
|
-
case .failure(let error):
|
|
2446
|
-
let lastKnownProgress = diffFiles.first(where: { $0.path == assetPath })?.progress ?? 0
|
|
2447
|
-
updateDiffProgressFile(
|
|
2448
|
-
files: &diffFiles,
|
|
2449
|
-
assetPath: assetPath,
|
|
2450
|
-
status: "failed",
|
|
2451
|
-
progress: lastKnownProgress
|
|
2452
|
-
)
|
|
2453
|
-
self.emitDiffProgress(
|
|
2454
|
-
progressHandler: progressHandler,
|
|
2455
|
-
phase: "downloading",
|
|
2456
|
-
files: diffFiles
|
|
2457
|
-
)
|
|
2458
|
-
if let downloadError = error as? DownloadError,
|
|
2459
|
-
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2460
|
-
throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
2461
|
-
}
|
|
2462
|
-
throw BundleStorageError.downloadFailed(error)
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2309
|
+
private func promoteVerifiedArchiveDirectory(
|
|
2310
|
+
_ preparedDirectory: String,
|
|
2311
|
+
withArchiveDirectory archiveDirectory: String
|
|
2312
|
+
) throws {
|
|
2313
|
+
let backupDirectory = preparedDirectory + ".archive-backup"
|
|
2314
|
+
try? fileSystem.removeItem(atPath: backupDirectory)
|
|
2315
|
+
try fileSystem.moveItem(atPath: preparedDirectory, toPath: backupDirectory)
|
|
2465
2316
|
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2317
|
+
do {
|
|
2318
|
+
try fileSystem.moveItem(
|
|
2319
|
+
atPath: archiveDirectory,
|
|
2320
|
+
toPath: preparedDirectory
|
|
2470
2321
|
)
|
|
2322
|
+
} catch {
|
|
2323
|
+
try? fileSystem.removeItem(atPath: preparedDirectory)
|
|
2324
|
+
do {
|
|
2325
|
+
try fileSystem.moveItem(atPath: backupDirectory, toPath: preparedDirectory)
|
|
2326
|
+
} catch let restoreError {
|
|
2327
|
+
throw BundleStorageError.moveOperationFailed(restoreError)
|
|
2328
|
+
}
|
|
2329
|
+
throw error
|
|
2330
|
+
}
|
|
2331
|
+
try? fileSystem.removeItem(atPath: backupDirectory)
|
|
2332
|
+
}
|
|
2471
2333
|
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2334
|
+
private func recoverInterruptedPromotions() {
|
|
2335
|
+
guard case .success(let storeDir) = bundleStoreDir(),
|
|
2336
|
+
let entries = try? fileSystem.contentsOfDirectory(atPath: storeDir) else { return }
|
|
2337
|
+
for entry in entries where entry.hasSuffix(".install-backup") {
|
|
2338
|
+
let finalDirectory = (storeDir as NSString).appendingPathComponent(String(entry.dropLast(".install-backup".count)))
|
|
2339
|
+
do {
|
|
2340
|
+
try recoverInterruptedPromotion(finalDirectory: finalDirectory)
|
|
2341
|
+
} catch {
|
|
2342
|
+
NSLog("[BundleStorage] Cannot recover interrupted promotion: \(error)")
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2480
2346
|
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2347
|
+
private func recoverInterruptedPromotion(finalDirectory: String) throws {
|
|
2348
|
+
let backup = finalDirectory + ".install-backup"
|
|
2349
|
+
guard fileSystem.fileExists(atPath: backup) else { return }
|
|
2350
|
+
if fileSystem.fileExists(atPath: finalDirectory) {
|
|
2351
|
+
if isCompleteBundleDirectory(finalDirectory) {
|
|
2352
|
+
try fileSystem.removeItem(atPath: backup)
|
|
2353
|
+
return
|
|
2354
|
+
}
|
|
2355
|
+
try fileSystem.removeItem(atPath: finalDirectory)
|
|
2356
|
+
}
|
|
2357
|
+
try fileSystem.moveItem(atPath: backup, toPath: finalDirectory)
|
|
2358
|
+
}
|
|
2484
2359
|
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2360
|
+
private func isCompleteBundleDirectory(_ directory: String) -> Bool {
|
|
2361
|
+
let bundleId = (directory as NSString).lastPathComponent
|
|
2362
|
+
guard let manifest = readManifest(in: directory),
|
|
2363
|
+
let parsed = parseBundleManifest(from: manifest),
|
|
2364
|
+
parsed.bundleId == bundleId,
|
|
2365
|
+
case .success(let bundlePath) = findBundleFile(in: directory, expectedBundleId: bundleId),
|
|
2366
|
+
bundlePath != nil else { return false }
|
|
2367
|
+
return parsed.assets.allSatisfy { path, asset in
|
|
2368
|
+
guard let file = try? FileUtilities.fileURL(for: path, destinationRoot: directory) else { return false }
|
|
2369
|
+
return (try? verifyManifestAssetFile(atPath: file.path, asset: asset)) != nil
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2490
2372
|
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
)
|
|
2524
|
-
completion(.success(true))
|
|
2525
|
-
case .failure(let error):
|
|
2526
|
-
try? self.fileSystem.removeItem(atPath: realDir)
|
|
2527
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2528
|
-
completion(.failure(error))
|
|
2529
|
-
}
|
|
2530
|
-
case .failure(let error):
|
|
2531
|
-
throw error
|
|
2373
|
+
private func promoteStagedBundle(
|
|
2374
|
+
bundleId: String,
|
|
2375
|
+
stagingDirectory: String,
|
|
2376
|
+
finalDirectory: String,
|
|
2377
|
+
bundlePathInStaging: String
|
|
2378
|
+
) throws -> BundleMetadata {
|
|
2379
|
+
releaseStateLock.lock()
|
|
2380
|
+
defer { releaseStateLock.unlock() }
|
|
2381
|
+
let currentMetadata = loadMetadataOrNull() ?? createInitialMetadata()
|
|
2382
|
+
guard let updatedMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
|
|
2383
|
+
throw BundleStorageError.unknown(NSError(domain: "HotUpdater", code: 0,
|
|
2384
|
+
userInfo: [NSLocalizedDescriptionKey: "Release catalog selection is stale"]))
|
|
2385
|
+
}
|
|
2386
|
+
let finalBundlePath = try resolveBundlePathAfterMove(
|
|
2387
|
+
bundlePathInStaging, from: stagingDirectory, to: finalDirectory
|
|
2388
|
+
)
|
|
2389
|
+
let previousPreference = try preferences.getItem(forKey: "HotUpdaterBundleURL")
|
|
2390
|
+
let backup = finalDirectory + ".install-backup"
|
|
2391
|
+
try recoverInterruptedPromotion(finalDirectory: finalDirectory)
|
|
2392
|
+
if fileSystem.fileExists(atPath: finalDirectory) {
|
|
2393
|
+
try fileSystem.moveItem(atPath: finalDirectory, toPath: backup)
|
|
2394
|
+
}
|
|
2395
|
+
var promoted = false
|
|
2396
|
+
do {
|
|
2397
|
+
try fileSystem.moveItem(atPath: stagingDirectory, toPath: finalDirectory)
|
|
2398
|
+
promoted = true
|
|
2399
|
+
try setBundleURL(localPath: finalBundlePath).get()
|
|
2400
|
+
// Metadata is the durable activation point. Launch never uses an
|
|
2401
|
+
// untracked preference if this write fails or the process terminates.
|
|
2402
|
+
guard saveMetadata(updatedMetadata) else {
|
|
2403
|
+
throw BundleStorageError.unknown(NSError(domain: "HotUpdater", code: 0,
|
|
2404
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to persist bundle activation metadata"]))
|
|
2532
2405
|
}
|
|
2533
2406
|
} catch {
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
progressHandler: progressHandler,
|
|
2543
|
-
completion: completion
|
|
2544
|
-
)
|
|
2407
|
+
let installError = error
|
|
2408
|
+
let preferenceResult = setBundleURL(localPath: previousPreference)
|
|
2409
|
+
if promoted { try fileSystem.removeItem(atPath: finalDirectory) }
|
|
2410
|
+
if fileSystem.fileExists(atPath: backup) {
|
|
2411
|
+
try fileSystem.moveItem(atPath: backup, toPath: finalDirectory)
|
|
2412
|
+
}
|
|
2413
|
+
try preferenceResult.get()
|
|
2414
|
+
throw installError
|
|
2545
2415
|
}
|
|
2416
|
+
try? fileSystem.removeItem(atPath: backup)
|
|
2417
|
+
return updatedMetadata
|
|
2546
2418
|
}
|
|
2547
2419
|
|
|
2548
|
-
private func
|
|
2420
|
+
private func updateBundleFromManifest(
|
|
2549
2421
|
bundleId: String,
|
|
2550
|
-
|
|
2551
|
-
|
|
2422
|
+
manifestUrl: URL,
|
|
2423
|
+
manifestFileHash: String,
|
|
2424
|
+
archiveUrl: URL?,
|
|
2425
|
+
changedAssets: [String: ChangedAssetDescriptor],
|
|
2552
2426
|
storeDir: String,
|
|
2553
2427
|
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
2554
2428
|
completion: @escaping (Result<Bool, Error>) -> Void
|
|
2555
2429
|
) {
|
|
2556
|
-
// 1) Prepare temp directory for download
|
|
2557
2430
|
let tempDirResult = tempDir()
|
|
2558
2431
|
guard case .success(let tempDirectory) = tempDirResult else {
|
|
2559
2432
|
completion(.failure(tempDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
2560
2433
|
return
|
|
2561
2434
|
}
|
|
2562
|
-
|
|
2563
|
-
// 2) Clean up any previous temp dir
|
|
2435
|
+
|
|
2564
2436
|
try? self.fileSystem.removeItem(atPath: tempDirectory)
|
|
2565
|
-
|
|
2566
|
-
// 3) Create temp dir
|
|
2567
|
-
if !self.fileSystem.createDirectory(atPath: tempDirectory) {
|
|
2437
|
+
guard self.fileSystem.createDirectory(atPath: tempDirectory) else {
|
|
2568
2438
|
completion(.failure(BundleStorageError.directoryCreationFailed))
|
|
2569
2439
|
return
|
|
2570
2440
|
}
|
|
2571
2441
|
|
|
2572
|
-
|
|
2573
|
-
let
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
var
|
|
2580
|
-
|
|
2581
|
-
_ = self.downloadService.downloadFile(
|
|
2582
|
-
from: fileUrl,
|
|
2583
|
-
to: tempBundleFile,
|
|
2584
|
-
fileSizeHandler: { fileSize in
|
|
2585
|
-
// This will be called when Content-Length is received
|
|
2586
|
-
NSLog("[BundleStorage] File size received: \(fileSize) bytes")
|
|
2587
|
-
|
|
2588
|
-
// Check available disk space
|
|
2589
|
-
do {
|
|
2590
|
-
let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory())
|
|
2591
|
-
if let freeSize = attributes[.systemFreeSize] as? Int64 {
|
|
2592
|
-
let requiredSpace = fileSize * 2 // ZIP + extracted files
|
|
2593
|
-
|
|
2594
|
-
NSLog("[BundleStorage] Available: \(freeSize) bytes, Required: \(requiredSpace) bytes")
|
|
2442
|
+
let currentBundleId = self.getCachedBundleId()
|
|
2443
|
+
let currentBundleDir = currentBundleId.map {
|
|
2444
|
+
(storeDir as NSString).appendingPathComponent($0)
|
|
2445
|
+
}
|
|
2446
|
+
let tempManifestPath = (tempDirectory as NSString).appendingPathComponent("manifest.json")
|
|
2447
|
+
let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
|
|
2448
|
+
let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
2449
|
+
var diffFiles: [UpdateProgressPayload.DiffProgressFileSnapshot] = []
|
|
2595
2450
|
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2451
|
+
do {
|
|
2452
|
+
try recoverInterruptedPromotion(finalDirectory: realDir)
|
|
2453
|
+
self.emitDiffProgress(
|
|
2454
|
+
progressHandler: progressHandler,
|
|
2455
|
+
phase: "manifest",
|
|
2456
|
+
files: diffFiles,
|
|
2457
|
+
manifestProgress: 0
|
|
2458
|
+
)
|
|
2459
|
+
switch self.downloadFileSynchronously(
|
|
2460
|
+
from: manifestUrl,
|
|
2461
|
+
to: tempManifestPath,
|
|
2462
|
+
progressHandler: { progress in
|
|
2463
|
+
self.emitDiffProgress(
|
|
2464
|
+
progressHandler: progressHandler,
|
|
2465
|
+
phase: "manifest",
|
|
2466
|
+
files: diffFiles,
|
|
2467
|
+
manifestProgress: progress.progress
|
|
2468
|
+
)
|
|
2604
2469
|
}
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
downloadedBytes: downloadProgress.downloadedBytes,
|
|
2615
|
-
totalBytes: downloadProgress.totalBytes
|
|
2616
|
-
)
|
|
2617
|
-
},
|
|
2618
|
-
completion: { [weak self] result in
|
|
2619
|
-
guard let self = self else {
|
|
2620
|
-
let error = NSError(domain: "HotUpdaterError", code: 998,
|
|
2621
|
-
userInfo: [NSLocalizedDescriptionKey: "Self deallocated during download"])
|
|
2622
|
-
completion(.failure(error))
|
|
2623
|
-
return
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
// Check for disk space error first before processing download result
|
|
2627
|
-
if let diskError = diskSpaceError {
|
|
2628
|
-
NSLog("[BundleStorage] Throwing disk space error")
|
|
2629
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2630
|
-
completion(.failure(diskError))
|
|
2631
|
-
return
|
|
2470
|
+
) {
|
|
2471
|
+
case .success:
|
|
2472
|
+
break
|
|
2473
|
+
case .failure(let error):
|
|
2474
|
+
if let downloadError = error as? DownloadError,
|
|
2475
|
+
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2476
|
+
throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
2477
|
+
}
|
|
2478
|
+
throw BundleStorageError.downloadFailed(error)
|
|
2632
2479
|
}
|
|
2633
2480
|
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
storeDir: storeDir,
|
|
2642
|
-
bundleId: bundleId,
|
|
2643
|
-
tempDirectory: tempDirectory,
|
|
2644
|
-
progressHandler: progressHandler,
|
|
2645
|
-
completion: completion)
|
|
2646
|
-
case .failure(let error):
|
|
2647
|
-
NSLog("[BundleStorage] Download failed: \(error.localizedDescription)")
|
|
2648
|
-
self.cleanupTemporaryFiles([tempDirectory]) // Sync cleanup
|
|
2649
|
-
|
|
2650
|
-
// Map DownloadError.incompleteDownload to BundleStorageError.incompleteDownload
|
|
2651
|
-
if let downloadError = error as? DownloadError,
|
|
2652
|
-
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2653
|
-
completion(.failure(BundleStorageError.incompleteDownload(expected: expected, actual: actual)))
|
|
2654
|
-
} else {
|
|
2655
|
-
completion(.failure(BundleStorageError.downloadFailed(error)))
|
|
2656
|
-
}
|
|
2481
|
+
let manifestVerificationResult = SignatureVerifier.verifyBundle(
|
|
2482
|
+
fileURL: URL(fileURLWithPath: tempManifestPath),
|
|
2483
|
+
fileHash: manifestFileHash
|
|
2484
|
+
)
|
|
2485
|
+
guard case .success = manifestVerificationResult else {
|
|
2486
|
+
if case .failure(let error) = manifestVerificationResult {
|
|
2487
|
+
throw BundleStorageError.signatureVerificationFailed(error)
|
|
2657
2488
|
}
|
|
2489
|
+
throw BundleStorageError.unknown(nil)
|
|
2658
2490
|
}
|
|
2659
|
-
self.fileOperationQueue.async(execute: workItem)
|
|
2660
|
-
}
|
|
2661
|
-
)
|
|
2662
|
-
}
|
|
2663
|
-
|
|
2664
|
-
/**
|
|
2665
|
-
* Logs detailed diagnostic information about a file system path.
|
|
2666
|
-
* @param path The path to diagnose
|
|
2667
|
-
* @param context Additional context for logging
|
|
2668
|
-
*/
|
|
2669
|
-
private func logFileSystemDiagnostics(path: String, context: String) {
|
|
2670
|
-
let fileManager = FileManager.default
|
|
2671
|
-
|
|
2672
|
-
// Check if path exists
|
|
2673
|
-
let exists = fileManager.fileExists(atPath: path)
|
|
2674
|
-
NSLog("[BundleStorage] [\(context)] Path exists: \(exists) - \(path)")
|
|
2675
2491
|
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
} catch {
|
|
2683
|
-
NSLog("[BundleStorage] [\(context)] Failed to get attributes: \(error.localizedDescription)")
|
|
2492
|
+
guard let manifestData = try? Data(contentsOf: URL(fileURLWithPath: tempManifestPath)),
|
|
2493
|
+
let manifestJson = try JSONSerialization.jsonObject(with: manifestData) as? [String: Any],
|
|
2494
|
+
let targetManifest = parseBundleManifest(from: manifestJson),
|
|
2495
|
+
targetManifest.bundleId == bundleId
|
|
2496
|
+
else {
|
|
2497
|
+
throw BundleStorageError.invalidBundle
|
|
2684
2498
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
* @param fileHash Combined hash string for verification (sig:<signature> or <hex_hash>)
|
|
2699
|
-
* @param storeDir Path to the bundle-store directory
|
|
2700
|
-
* @param bundleId ID of the bundle being processed
|
|
2701
|
-
* @param tempDirectory Temporary directory for processing
|
|
2702
|
-
* @param progressHandler Callback for download/apply progress (0.0 to 1.0)
|
|
2703
|
-
* @param completion Callback with result of the operation
|
|
2704
|
-
*/
|
|
2705
|
-
private func processDownloadedFileWithTmp(
|
|
2706
|
-
location: URL,
|
|
2707
|
-
tempBundleFile: String,
|
|
2708
|
-
fileHash: String?,
|
|
2709
|
-
storeDir: String,
|
|
2710
|
-
bundleId: String,
|
|
2711
|
-
tempDirectory: String,
|
|
2712
|
-
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
2713
|
-
completion: @escaping (Result<Bool, Error>) -> Void
|
|
2714
|
-
) {
|
|
2715
|
-
let currentBundleId = self.getCachedBundleId()
|
|
2716
|
-
NSLog("[BundleStorage] Processing downloaded file atPath: \(location.path)")
|
|
2717
|
-
|
|
2718
|
-
// 1) Ensure the bundle file exists
|
|
2719
|
-
guard self.fileSystem.fileExists(atPath: location.path) else {
|
|
2720
|
-
logFileSystemDiagnostics(path: location.path, context: "Download Location Missing")
|
|
2721
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2722
|
-
completion(.failure(BundleStorageError.downloadFailed(NSError(
|
|
2723
|
-
domain: "HotUpdaterError",
|
|
2724
|
-
code: 1,
|
|
2725
|
-
userInfo: [NSLocalizedDescriptionKey: "Downloaded file does not exist atPath: \(location.path)"]
|
|
2726
|
-
))))
|
|
2727
|
-
return
|
|
2728
|
-
}
|
|
2729
|
-
|
|
2730
|
-
// 2) Define tmpDir and realDir
|
|
2731
|
-
let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
|
|
2732
|
-
let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
2733
|
-
|
|
2734
|
-
do {
|
|
2735
|
-
// 3) Remove any existing tmpDir
|
|
2736
|
-
if self.fileSystem.fileExists(atPath: tmpDir) {
|
|
2737
|
-
try self.fileSystem.removeItem(atPath: tmpDir)
|
|
2738
|
-
NSLog("[BundleStorage] Removed existing tmpDir: \(tmpDir)")
|
|
2499
|
+
// Validate the entire wire contract even when every byte can be reused.
|
|
2500
|
+
guard Set(targetManifest.assets.keys) == Set(changedAssets.keys) else {
|
|
2501
|
+
throw BundleStorageError.invalidBundle
|
|
2502
|
+
}
|
|
2503
|
+
for (path, asset) in targetManifest.assets {
|
|
2504
|
+
guard FileUtilities.normalizedRelativePath(from: path) == path,
|
|
2505
|
+
path != "manifest.json",
|
|
2506
|
+
let descriptor = changedAssets[path],
|
|
2507
|
+
descriptor.fileUrl != nil,
|
|
2508
|
+
descriptor.fileHash.caseInsensitiveCompare(asset.fileHash) == .orderedSame else {
|
|
2509
|
+
throw BundleStorageError.invalidBundle
|
|
2510
|
+
}
|
|
2511
|
+
_ = try FileUtilities.fileURL(for: path, destinationRoot: tmpDir)
|
|
2739
2512
|
}
|
|
2740
2513
|
|
|
2741
|
-
// 4) Create tmpDir
|
|
2742
2514
|
guard self.fileSystem.createDirectory(atPath: tmpDir) else {
|
|
2743
2515
|
throw BundleStorageError.directoryCreationFailed
|
|
2744
2516
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
progressHandler
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
self.
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
)
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
self.
|
|
2767
|
-
|
|
2768
|
-
|
|
2517
|
+
try pruneStagingDirectory(tmpDir, assets: Set(targetManifest.assets.keys))
|
|
2518
|
+
let sortedAssets = targetManifest.assets.sorted { $0.key < $1.key }
|
|
2519
|
+
let localDirectories = [currentBundleDir, realDir].compactMap { $0 }.filter { fileSystem.fileExists(atPath: $0) }
|
|
2520
|
+
var downloads: [String: ChangedAssetDescriptor] = [:]
|
|
2521
|
+
for (index, asset) in sortedAssets.enumerated() {
|
|
2522
|
+
// Local preparation has its own progress interval, with no download files.
|
|
2523
|
+
progressHandler(UpdateProgressPayload(
|
|
2524
|
+
progress: 0.15 + 0.05 * Double(index) / Double(max(1, sortedAssets.count)),
|
|
2525
|
+
artifactType: "diff",
|
|
2526
|
+
details: .init(totalFilesCount: 0, completedFilesCount: 0)
|
|
2527
|
+
))
|
|
2528
|
+
let assetPath = asset.key
|
|
2529
|
+
let expectedAsset = asset.value
|
|
2530
|
+
let destinationPath = try FileUtilities.fileURL(for: assetPath, destinationRoot: tmpDir).path
|
|
2531
|
+
guard self.fileSystem.createDirectory(atPath: (destinationPath as NSString).deletingLastPathComponent) else {
|
|
2532
|
+
throw BundleStorageError.directoryCreationFailed
|
|
2533
|
+
}
|
|
2534
|
+
if (try? fileSystem.attributesOfItem(atPath: destinationPath)[.type] as? FileAttributeType) == .typeRegular,
|
|
2535
|
+
(try? verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)) != nil {
|
|
2536
|
+
continue
|
|
2537
|
+
}
|
|
2538
|
+
if fileSystem.fileExists(atPath: destinationPath) { try self.fileSystem.removeItem(atPath: destinationPath) }
|
|
2539
|
+
|
|
2540
|
+
var reused = false
|
|
2541
|
+
// The cached target is only a candidate, just like the active OTA.
|
|
2542
|
+
for directory in localDirectories {
|
|
2543
|
+
guard let sourcePath = try? FileUtilities.fileURL(for: assetPath, destinationRoot: directory).path,
|
|
2544
|
+
(try? fileSystem.attributesOfItem(atPath: sourcePath)[.type] as? FileAttributeType) == .typeRegular,
|
|
2545
|
+
(try? verifyManifestAssetFile(atPath: sourcePath, asset: expectedAsset)) != nil else { continue }
|
|
2546
|
+
do {
|
|
2547
|
+
try self.fileSystem.copyItem(atPath: sourcePath, toPath: destinationPath)
|
|
2548
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2549
|
+
reused = true
|
|
2550
|
+
break
|
|
2551
|
+
} catch {
|
|
2552
|
+
try? self.fileSystem.removeItem(atPath: destinationPath)
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
if reused { continue }
|
|
2556
|
+
if builtInAssetResolver.copyIfMatches(assetPath: assetPath, expectedHash: expectedAsset.fileHash, destination: destinationPath) {
|
|
2557
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2558
|
+
continue
|
|
2559
|
+
}
|
|
2560
|
+
downloads[assetPath] = changedAssets[assetPath]
|
|
2769
2561
|
}
|
|
2770
2562
|
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2563
|
+
var archiveInstalled = false
|
|
2564
|
+
if shouldUseArchive(
|
|
2565
|
+
archive: targetManifest.archive,
|
|
2566
|
+
archiveUrl: archiveUrl,
|
|
2567
|
+
downloads: downloads,
|
|
2568
|
+
assets: targetManifest.assets
|
|
2569
|
+
), let archive = targetManifest.archive, let archiveUrl {
|
|
2570
|
+
do {
|
|
2571
|
+
diffFiles = try installArchive(
|
|
2572
|
+
archive: archive,
|
|
2573
|
+
archiveUrl: archiveUrl,
|
|
2574
|
+
targetManifest: targetManifest,
|
|
2575
|
+
preparedDirectory: tmpDir,
|
|
2576
|
+
tempDirectory: tempDirectory,
|
|
2577
|
+
progressHandler: progressHandler
|
|
2783
2578
|
)
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
2792
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2793
|
-
completion(.failure(BundleStorageError.extractionFormatError(error)))
|
|
2794
|
-
return
|
|
2579
|
+
archiveInstalled = true
|
|
2580
|
+
} catch {
|
|
2581
|
+
NSLog("[BundleStorage] tar.br optimization failed; falling back to individual files: \(error.localizedDescription)")
|
|
2582
|
+
guard fileSystem.fileExists(atPath: tmpDir) else {
|
|
2583
|
+
throw error
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2795
2586
|
}
|
|
2796
2587
|
|
|
2797
|
-
|
|
2798
|
-
|
|
2588
|
+
if !archiveInstalled {
|
|
2589
|
+
diffFiles = createDiffProgressFiles(changedAssets: downloads)
|
|
2590
|
+
emitDiffProgress(progressHandler: progressHandler, phase: "downloading", files: diffFiles)
|
|
2591
|
+
let progressLock = NSLock()
|
|
2592
|
+
let workers = OperationQueue()
|
|
2593
|
+
workers.maxConcurrentOperationCount = 4
|
|
2594
|
+
var installError: Error?
|
|
2595
|
+
for progressFile in diffFiles {
|
|
2596
|
+
let assetPath = progressFile.path
|
|
2597
|
+
let expectedAsset = targetManifest.assets[assetPath]!
|
|
2598
|
+
let descriptor = downloads[assetPath]!
|
|
2599
|
+
let destinationPath = try FileUtilities.fileURL(for: assetPath, destinationRoot: tmpDir).path
|
|
2600
|
+
// Separate temp namespaces also avoid collisions between flattened asset paths.
|
|
2601
|
+
let workerTemp = (tempDirectory as NSString).appendingPathComponent(String(progressFile.order))
|
|
2602
|
+
workers.addOperation {
|
|
2603
|
+
progressLock.lock()
|
|
2604
|
+
let shouldSkip = installError != nil
|
|
2605
|
+
progressLock.unlock()
|
|
2606
|
+
if shouldSkip { return }
|
|
2607
|
+
do {
|
|
2608
|
+
guard self.fileSystem.createDirectory(atPath: workerTemp) else { throw BundleStorageError.directoryCreationFailed }
|
|
2609
|
+
try self.downloadManifestAsset(
|
|
2610
|
+
assetPath: assetPath, expectedAsset: expectedAsset, changedAsset: descriptor,
|
|
2611
|
+
currentBundleId: currentBundleId, currentBundleDir: currentBundleDir,
|
|
2612
|
+
destinationPath: destinationPath, tempDirectory: workerTemp, progressFile: progressFile,
|
|
2613
|
+
progressHandler: { payload in
|
|
2614
|
+
progressLock.lock()
|
|
2615
|
+
defer { progressLock.unlock() }
|
|
2616
|
+
if let snapshot = payload.details?.files.first,
|
|
2617
|
+
let index = diffFiles.firstIndex(where: { $0.path == assetPath }) {
|
|
2618
|
+
diffFiles[index] = snapshot
|
|
2619
|
+
}
|
|
2620
|
+
self.emitDiffProgress(progressHandler: progressHandler, phase: "downloading", files: diffFiles)
|
|
2621
|
+
}
|
|
2622
|
+
)
|
|
2623
|
+
} catch {
|
|
2624
|
+
progressLock.lock()
|
|
2625
|
+
if installError == nil { installError = error }
|
|
2626
|
+
progressLock.unlock()
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
// Join all workers before cleaning scratch files, even after a failure.
|
|
2631
|
+
workers.waitUntilAllOperationsAreFinished()
|
|
2632
|
+
if let installError { throw installError }
|
|
2633
|
+
}
|
|
2799
2634
|
|
|
2800
|
-
|
|
2801
|
-
self.emitArchiveProgress(
|
|
2635
|
+
self.emitDiffProgress(
|
|
2802
2636
|
progressHandler: progressHandler,
|
|
2803
|
-
|
|
2637
|
+
phase: "finalizing",
|
|
2638
|
+
files: diffFiles
|
|
2804
2639
|
)
|
|
2805
|
-
switch self.findBundleFile(in: tmpDir, expectedBundleId: bundleId) {
|
|
2806
|
-
case .success(let maybeBundlePath):
|
|
2807
|
-
if let bundlePathInTmp = maybeBundlePath {
|
|
2808
|
-
NSLog("[BundleStorage] Found valid bundle in tmpDir: \(bundlePathInTmp)")
|
|
2809
|
-
logFileSystemDiagnostics(path: bundlePathInTmp, context: "Bundle Found")
|
|
2810
|
-
|
|
2811
|
-
// 9) Remove any existing realDir
|
|
2812
|
-
if self.fileSystem.fileExists(atPath: realDir) {
|
|
2813
|
-
try self.fileSystem.removeItem(atPath: realDir)
|
|
2814
|
-
NSLog("[BundleStorage] Removed existing realDir: \(realDir)")
|
|
2815
|
-
}
|
|
2816
2640
|
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
try self.fileSystem.moveItem(atPath: tmpDir, toPath: realDir)
|
|
2820
|
-
NSLog("[BundleStorage] Renamed tmpDir to realDir: \(realDir)")
|
|
2821
|
-
logFileSystemDiagnostics(path: realDir, context: "After Move")
|
|
2822
|
-
} catch {
|
|
2823
|
-
let nsError = error as NSError
|
|
2824
|
-
NSLog("[BundleStorage] Move operation failed - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
2825
|
-
logFileSystemDiagnostics(path: tmpDir, context: "Move Failed - Source")
|
|
2826
|
-
logFileSystemDiagnostics(path: realDir, context: "Move Failed - Destination")
|
|
2827
|
-
throw BundleStorageError.moveOperationFailed(error)
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
// 11) Construct final bundlePath for preferences
|
|
2831
|
-
let finalBundlePath = try resolveBundlePathAfterMove(
|
|
2832
|
-
bundlePathInTmp,
|
|
2833
|
-
from: tmpDir,
|
|
2834
|
-
to: realDir
|
|
2835
|
-
)
|
|
2641
|
+
let manifestDestination = (tmpDir as NSString).appendingPathComponent("manifest.json")
|
|
2642
|
+
try copyVerifiedManifest(from: tempManifestPath, to: manifestDestination)
|
|
2836
2643
|
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
completion(.failure(BundleStorageError.unknown(
|
|
2842
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2843
|
-
NSLocalizedDescriptionKey: "Release catalog selection is stale"
|
|
2844
|
-
])
|
|
2845
|
-
)))
|
|
2846
|
-
return
|
|
2847
|
-
}
|
|
2848
|
-
let setResult = self.setBundleURL(localPath: finalBundlePath)
|
|
2849
|
-
switch setResult {
|
|
2850
|
-
case .success:
|
|
2851
|
-
NSLog("[BundleStorage] Successfully set bundle URL: \(finalBundlePath)")
|
|
2852
|
-
|
|
2853
|
-
// 13) Set staging metadata for rollback support
|
|
2854
|
-
let _ = self.saveMetadata(updatedMetadata)
|
|
2855
|
-
NSLog("[BundleStorage] Set staging bundle: \(bundleId), verificationPending: true")
|
|
2856
|
-
|
|
2857
|
-
// 14) Clean up the temporary directory
|
|
2858
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2859
|
-
|
|
2860
|
-
self.emitArchiveProgress(
|
|
2861
|
-
progressHandler: progressHandler,
|
|
2862
|
-
progress: UpdateProgress.activationReady
|
|
2863
|
-
)
|
|
2864
|
-
self.scheduleCleanupOldBundles(
|
|
2865
|
-
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
2866
|
-
)
|
|
2867
|
-
|
|
2868
|
-
// 15) Complete with success
|
|
2869
|
-
self.emitArchiveProgress(
|
|
2870
|
-
progressHandler: progressHandler,
|
|
2871
|
-
progress: UpdateProgress.complete
|
|
2872
|
-
)
|
|
2873
|
-
completion(.success(true))
|
|
2874
|
-
case .failure(let err):
|
|
2875
|
-
let nsError = err as NSError
|
|
2876
|
-
NSLog("[BundleStorage] Failed to set bundle URL - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
2877
|
-
// Preferences save failed → remove realDir and clean up
|
|
2878
|
-
try? self.fileSystem.removeItem(atPath: realDir)
|
|
2879
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2880
|
-
completion(.failure(err))
|
|
2881
|
-
}
|
|
2882
|
-
} else {
|
|
2883
|
-
// No valid .jsbundle found → delete tmpDir and fail
|
|
2884
|
-
NSLog("[BundleStorage] No valid bundle file found in tmpDir")
|
|
2885
|
-
logFileSystemDiagnostics(path: tmpDir, context: "Invalid Bundle")
|
|
2886
|
-
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
2887
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2888
|
-
completion(.failure(BundleStorageError.invalidBundle))
|
|
2644
|
+
switch self.findBundleFile(in: tmpDir, expectedBundleId: bundleId) {
|
|
2645
|
+
case .success(let maybeBundlePath):
|
|
2646
|
+
guard let bundlePathInTmp = maybeBundlePath else {
|
|
2647
|
+
throw BundleStorageError.invalidBundle
|
|
2889
2648
|
}
|
|
2890
|
-
|
|
2891
|
-
let
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2649
|
+
|
|
2650
|
+
let updatedMetadata = try promoteStagedBundle(
|
|
2651
|
+
bundleId: bundleId,
|
|
2652
|
+
stagingDirectory: tmpDir,
|
|
2653
|
+
finalDirectory: realDir,
|
|
2654
|
+
bundlePathInStaging: bundlePathInTmp
|
|
2655
|
+
)
|
|
2895
2656
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
2896
|
-
|
|
2657
|
+
self.scheduleCleanupOldBundles(
|
|
2658
|
+
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
2659
|
+
)
|
|
2660
|
+
progressHandler(UpdateProgressPayload(
|
|
2661
|
+
progress: 1.0,
|
|
2662
|
+
artifactType: "diff",
|
|
2663
|
+
details: .init(
|
|
2664
|
+
totalFilesCount: diffFiles.count,
|
|
2665
|
+
completedFilesCount: diffFiles.filter { $0.status == "downloaded" }.count,
|
|
2666
|
+
files: diffFiles
|
|
2667
|
+
)
|
|
2668
|
+
))
|
|
2669
|
+
completion(.success(true))
|
|
2670
|
+
case .failure(let error):
|
|
2671
|
+
throw error
|
|
2897
2672
|
}
|
|
2898
|
-
} catch
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
NSLog("[BundleStorage] Error during tmpDir processing - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
2902
|
-
logFileSystemDiagnostics(path: tmpDir, context: "Processing Error")
|
|
2903
|
-
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
2673
|
+
} catch {
|
|
2674
|
+
NSLog("[BundleStorage] Manifest-driven install failed: \(error.localizedDescription).")
|
|
2675
|
+
// Preserve staging for a hash-checked retry. Never trust its presence alone.
|
|
2904
2676
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
2905
|
-
|
|
2906
|
-
// Re-throw specific BundleStorageError if it is one, otherwise wrap as unknown
|
|
2907
|
-
if let bundleError = error as? BundleStorageError {
|
|
2908
|
-
completion(.failure(bundleError))
|
|
2909
|
-
} else {
|
|
2910
|
-
completion(.failure(BundleStorageError.unknown(error)))
|
|
2911
|
-
}
|
|
2677
|
+
completion(.failure(error))
|
|
2912
2678
|
}
|
|
2913
2679
|
}
|
|
2914
2680
|
|
|
2915
|
-
private static func mapProgress(_ value: Double, start: Double, end: Double) -> Double {
|
|
2916
|
-
let clampedValue = min(max(value, 0), 1)
|
|
2917
|
-
return start + (clampedValue * (end - start))
|
|
2918
|
-
}
|
|
2919
|
-
|
|
2920
|
-
// MARK: - Rollback Support
|
|
2921
|
-
|
|
2922
|
-
/**
|
|
2923
|
-
* Marks the current launch as successful after the first content appeared.
|
|
2924
|
-
*/
|
|
2925
2681
|
func markLaunchCompleted(bundleId: String?) {
|
|
2926
2682
|
guard let bundleId,
|
|
2927
2683
|
var metadata = loadMetadataOrNull(),
|
|
@@ -2935,6 +2691,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2935
2691
|
metadata.stableBundleId = nil
|
|
2936
2692
|
metadata.stableSelection = nil
|
|
2937
2693
|
metadata.verificationPending = false
|
|
2694
|
+
metadata.launchInProgress = false
|
|
2938
2695
|
metadata.pendingTransition = nil
|
|
2939
2696
|
metadata.pendingSelectionTransition = nil
|
|
2940
2697
|
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|