@hot-updater/react-native 1.0.0-rc.2 → 1.0.0-rc.22
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 +17 -2
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +954 -837
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +15 -11
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +287 -22
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +43 -31
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
- package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
- package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
- package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +127 -50
- package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -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 +109 -84
- package/android/src/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +98 -74
- package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +950 -1112
- package/ios/HotUpdater/Internal/BundleMetadata.swift +8 -53
- package/ios/HotUpdater/Internal/FileManagerService.swift +48 -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 +13 -45
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +190 -73
- package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
- package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +119 -71
- package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
- package/lib/commonjs/appReady.js +111 -0
- package/lib/commonjs/appReady.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +125 -70
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/clientPlugin.js +13 -0
- package/lib/commonjs/clientPlugin.js.map +1 -0
- package/lib/commonjs/error.js +4 -13
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchJSON.js +5 -2
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/httpClient.js +55 -92
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/index.js +68 -20
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +82 -62
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/pluginHost.js +44 -0
- package/lib/commonjs/pluginHost.js.map +1 -0
- package/lib/commonjs/releaseCatalogCache.js +20 -61
- package/lib/commonjs/releaseCatalogCache.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/sdkVersion.js.map +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/updateError.js +132 -0
- package/lib/commonjs/updateError.js.map +1 -0
- package/lib/commonjs/wrap.js +44 -27
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/appReady.js +103 -0
- package/lib/module/appReady.js.map +1 -0
- package/lib/module/checkForUpdate.js +118 -65
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/clientPlugin.js +4 -0
- package/lib/module/clientPlugin.js.map +1 -0
- package/lib/module/error.js +4 -13
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchJSON.js +3 -1
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/httpClient.js +53 -90
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/index.js +55 -22
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +74 -54
- package/lib/module/native.js.map +1 -1
- package/lib/module/pluginHost.js +35 -0
- package/lib/module/pluginHost.js.map +1 -0
- package/lib/module/releaseCatalogCache.js +16 -56
- package/lib/module/releaseCatalogCache.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/sdkVersion.js.map +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/updateError.js +126 -0
- package/lib/module/updateError.js.map +1 -0
- package/lib/module/wrap.js +45 -28
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/appReady.d.ts +23 -0
- package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +16 -4
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/clientPlugin.d.ts +2 -0
- package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
- 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 +3 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +3 -32
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +7 -12
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +33 -23
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/pluginHost.d.ts +4 -0
- package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts +6 -14
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +42 -47
- 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/types.d.ts +1 -1
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/updateError.d.ts +26 -0
- package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +6 -7
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/appReady.d.ts +23 -0
- package/lib/typescript/module/appReady.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +16 -4
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/clientPlugin.d.ts +2 -0
- package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
- 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 +3 -0
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +3 -32
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +7 -12
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +33 -23
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/pluginHost.d.ts +4 -0
- package/lib/typescript/module/pluginHost.d.ts.map +1 -0
- package/lib/typescript/module/releaseCatalogCache.d.ts +6 -14
- package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +42 -47
- 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/types.d.ts +1 -1
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/updateError.d.ts +26 -0
- package/lib/typescript/module/updateError.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +6 -7
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/appReady.ts +149 -0
- package/src/checkForUpdate.ts +152 -89
- package/src/clientPlugin.ts +17 -0
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +5 -1
- package/src/httpClient.ts +78 -157
- package/src/index.ts +87 -29
- package/src/native.ts +109 -104
- package/src/pluginHost.ts +45 -0
- package/src/releaseCatalogCache.ts +26 -140
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +41 -48
- package/src/store.ts +2 -23
- package/src/types.ts +1 -1
- package/src/updateError.ts +153 -0
- package/src/wrap.tsx +59 -28
- package/src/wrap.types.ts +6 -7
- 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/BsdiffPatchTest.kt +0 -46
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -799
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -273
- package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
- package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -111
- package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
- package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
- 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
- package/lib/commonjs/catalogCacheNative.spec.js +0 -31
- package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -604
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/commonjs/httpClient.spec.js +0 -102
- package/lib/commonjs/httpClient.spec.js.map +0 -1
- package/lib/commonjs/index.spec.js +0 -298
- package/lib/commonjs/index.spec.js.map +0 -1
- package/lib/commonjs/iosInterop.spec.js +0 -18
- package/lib/commonjs/iosInterop.spec.js.map +0 -1
- package/lib/commonjs/native.spec.js +0 -795
- package/lib/commonjs/native.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -218
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.js +0 -133
- package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -246
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js +0 -24
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -117
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/commonjs/releaseCatalogCache.spec.js +0 -195
- package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/commonjs/store.notification.spec.js +0 -76
- package/lib/commonjs/store.notification.spec.js.map +0 -1
- package/lib/commonjs/store.spec.js +0 -316
- package/lib/commonjs/store.spec.js.map +0 -1
- package/lib/commonjs/wrap.spec.js +0 -296
- package/lib/commonjs/wrap.spec.js.map +0 -1
- package/lib/module/catalogCacheNative.spec.js +0 -31
- package/lib/module/catalogCacheNative.spec.js.map +0 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -603
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/module/httpClient.spec.js +0 -102
- package/lib/module/httpClient.spec.js.map +0 -1
- package/lib/module/index.spec.js +0 -297
- package/lib/module/index.spec.js.map +0 -1
- package/lib/module/iosInterop.spec.js +0 -18
- package/lib/module/iosInterop.spec.js.map +0 -1
- package/lib/module/native.spec.js +0 -794
- package/lib/module/native.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -217
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.js +0 -128
- package/lib/module/notifyAppReadyInsights.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.spec.js +0 -245
- package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.test-utils.js +0 -18
- package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -116
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/module/releaseCatalogCache.spec.js +0 -195
- package/lib/module/releaseCatalogCache.spec.js.map +0 -1
- package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/module/store.notification.spec.js +0 -77
- package/lib/module/store.notification.spec.js.map +0 -1
- package/lib/module/store.spec.js +0 -315
- package/lib/module/store.spec.js.map +0 -1
- package/lib/module/wrap.spec.js +0 -295
- package/lib/module/wrap.spec.js.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -13
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -13
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
- package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
- package/src/catalogCacheNative.spec.ts +0 -63
- package/src/checkForUpdate.releaseCatalog.spec.ts +0 -596
- package/src/httpClient.spec.ts +0 -132
- package/src/index.spec.ts +0 -331
- package/src/iosInterop.spec.ts +0 -30
- package/src/native.spec.ts +0 -855
- package/src/notifyAppReadyInsights.boundary.spec.ts +0 -234
- package/src/notifyAppReadyInsights.spec.ts +0 -243
- package/src/notifyAppReadyInsights.test-utils.ts +0 -31
- package/src/notifyAppReadyInsights.transport.spec.ts +0 -130
- package/src/notifyAppReadyInsights.ts +0 -194
- package/src/releaseCatalogCache.spec.ts +0 -282
- package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
- package/src/store.notification.spec.ts +0 -92
- package/src/store.spec.ts +0 -372
- package/src/wrap.spec.ts +0 -367
|
@@ -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,47 @@ 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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// How a staged bundle arrived. `updateBundle` resolves with `dictionary`.
|
|
168
|
+
public struct UpdateDelivery: Equatable {
|
|
169
|
+
public enum Method: String {
|
|
170
|
+
/// A bsdiff patch produced at least one file.
|
|
171
|
+
case patch
|
|
172
|
+
/// Only changed files were downloaded, possibly none.
|
|
173
|
+
case manifest
|
|
174
|
+
/// The full tar.br archive was downloaded.
|
|
175
|
+
case archive
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public let method: Method
|
|
179
|
+
/// A file's patch was tried, but the full file was downloaded instead.
|
|
180
|
+
public let patchFallback: Bool
|
|
181
|
+
|
|
182
|
+
public init(method: Method, patchFallback: Bool) {
|
|
183
|
+
self.method = method
|
|
184
|
+
self.patchFallback = patchFallback
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public var dictionary: [String: Any] {
|
|
188
|
+
[
|
|
189
|
+
"delivery": method.rawValue,
|
|
190
|
+
"patchFallback": patchFallback,
|
|
191
|
+
]
|
|
174
192
|
}
|
|
175
193
|
}
|
|
176
194
|
|
|
@@ -178,9 +196,7 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
178
196
|
case directoryCreationFailed
|
|
179
197
|
case downloadFailed(Error)
|
|
180
198
|
case incompleteDownload(expected: Int64, actual: Int64)
|
|
181
|
-
case extractionFormatError(Error)
|
|
182
199
|
case invalidBundle
|
|
183
|
-
case insufficientDiskSpace
|
|
184
200
|
case signatureVerificationFailed(SignatureVerificationError)
|
|
185
201
|
case moveOperationFailed(Error)
|
|
186
202
|
case bundleInCrashedHistory(String)
|
|
@@ -200,9 +216,7 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
200
216
|
case .directoryCreationFailed: return "DIRECTORY_CREATION_FAILED"
|
|
201
217
|
case .downloadFailed: return "DOWNLOAD_FAILED"
|
|
202
218
|
case .incompleteDownload: return "INCOMPLETE_DOWNLOAD"
|
|
203
|
-
case .extractionFormatError: return "EXTRACTION_FORMAT_ERROR"
|
|
204
219
|
case .invalidBundle: return "INVALID_BUNDLE"
|
|
205
|
-
case .insufficientDiskSpace: return "INSUFFICIENT_DISK_SPACE"
|
|
206
220
|
case .signatureVerificationFailed: return "SIGNATURE_VERIFICATION_FAILED"
|
|
207
221
|
case .moveOperationFailed: return "MOVE_OPERATION_FAILED"
|
|
208
222
|
case .bundleInCrashedHistory: return "BUNDLE_IN_CRASHED_HISTORY"
|
|
@@ -227,19 +241,10 @@ public enum BundleStorageError: Error, CustomNSError {
|
|
|
227
241
|
userInfo[NSLocalizedDescriptionKey] = "Download incomplete: received \(actual) bytes, expected \(expected) bytes"
|
|
228
242
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "The download was interrupted. Check network connection and try again"
|
|
229
243
|
|
|
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
244
|
case .invalidBundle:
|
|
236
245
|
userInfo[NSLocalizedDescriptionKey] = "Bundle missing required platform files (index.ios.bundle or main.jsbundle)"
|
|
237
246
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = "Verify the bundle was built correctly with metro bundler"
|
|
238
247
|
|
|
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
248
|
case .signatureVerificationFailed(let underlyingError):
|
|
244
249
|
userInfo[NSLocalizedDescriptionKey] = "Bundle signature verification failed"
|
|
245
250
|
userInfo[NSUnderlyingErrorKey] = underlyingError
|
|
@@ -280,7 +285,7 @@ public protocol BundleStorageService {
|
|
|
280
285
|
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection
|
|
281
286
|
|
|
282
287
|
// Bundle update
|
|
283
|
-
func updateBundle(bundleId: String,
|
|
288
|
+
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL?, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void)
|
|
284
289
|
func stageReleaseSelection(_ selection: PersistedSelection) -> Bool
|
|
285
290
|
func acceptReleaseCatalog(catalogId: String, scopeKey: String, generation: Int64, catalogHash: String, channel: String, selectionContextHash: String) -> Bool
|
|
286
291
|
func getActiveUpdateState() -> [String: Any]
|
|
@@ -293,10 +298,7 @@ public protocol BundleStorageService {
|
|
|
293
298
|
func getCrashHistory() -> CrashedHistory
|
|
294
299
|
func clearCrashHistory() -> Bool
|
|
295
300
|
func getInstallId() -> String
|
|
296
|
-
|
|
297
|
-
func getUsername() -> String?
|
|
298
|
-
func setUser(userId: String?, username: String?)
|
|
299
|
-
|
|
301
|
+
|
|
300
302
|
/**
|
|
301
303
|
* Gets the base URL for the current active bundle directory
|
|
302
304
|
* @return Base URL string (e.g., "file:///data/.../bundle-store/abc123") or empty string
|
|
@@ -306,7 +308,7 @@ public protocol BundleStorageService {
|
|
|
306
308
|
|
|
307
309
|
/**
|
|
308
310
|
* Gets the current active bundle ID from bundle storage.
|
|
309
|
-
* Reads
|
|
311
|
+
* Reads the required OTA manifest.json.
|
|
310
312
|
*/
|
|
311
313
|
func getBundleId() -> String?
|
|
312
314
|
|
|
@@ -332,6 +334,8 @@ public extension BundleStorageService {
|
|
|
332
334
|
}
|
|
333
335
|
|
|
334
336
|
class BundleFileStorageService: BundleStorageService {
|
|
337
|
+
private static let maximumSafeByteSize: Int64 = 9_007_199_254_740_991
|
|
338
|
+
|
|
335
339
|
private struct ActiveBundleMetadataSnapshot {
|
|
336
340
|
let activeBundleId: String
|
|
337
341
|
let bundleId: String?
|
|
@@ -341,80 +345,52 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
341
345
|
private struct ParsedBundleManifest {
|
|
342
346
|
let bundleId: String
|
|
343
347
|
let assets: [String: ParsedManifestAsset]
|
|
348
|
+
let archive: ParsedArchive?
|
|
344
349
|
}
|
|
345
350
|
|
|
346
351
|
private struct ParsedManifestAsset {
|
|
347
352
|
let fileHash: String
|
|
348
353
|
let signature: String?
|
|
354
|
+
let byteSize: Int64?
|
|
355
|
+
let downloadByteSize: Int64?
|
|
349
356
|
}
|
|
350
357
|
|
|
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
|
|
358
|
+
private struct ParsedArchive {
|
|
359
|
+
let downloadFileHash: String
|
|
360
|
+
let downloadByteSize: Int64
|
|
361
|
+
let tarByteSize: Int64
|
|
360
362
|
}
|
|
361
363
|
|
|
362
364
|
private let fileSystem: FileSystemService
|
|
363
365
|
private let downloadService: DownloadService
|
|
364
|
-
|
|
366
|
+
/// The wait before a download's second attempt; it doubles for each later one.
|
|
367
|
+
private let downloadRetryDelay: TimeInterval
|
|
365
368
|
private let preferences: PreferencesService
|
|
366
369
|
private let isolationKey: String
|
|
367
370
|
|
|
368
|
-
private let id = Int.random(in: 1..<100)
|
|
369
|
-
|
|
370
371
|
// Queue for potentially long-running sequences within updateBundle or for explicit background tasks.
|
|
371
372
|
private let fileOperationQueue: DispatchQueue
|
|
372
373
|
|
|
373
|
-
private var
|
|
374
|
-
|
|
374
|
+
private var hasPreparedLaunch = false
|
|
375
375
|
private var currentLaunchReport: LaunchReport?
|
|
376
376
|
private let activeBundleMetadataLock = NSLock()
|
|
377
377
|
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot?
|
|
378
378
|
private let builtInBundleIdProvider: () -> String
|
|
379
|
+
private let builtInAssetResolver: BuiltInAssetResolver
|
|
379
380
|
private let updateStrategyProvider: () -> UpdateStrategy
|
|
380
381
|
private let releaseStateLock = NSRecursiveLock()
|
|
381
382
|
private var pendingInstallSelections: [String: PersistedSelection] = [:]
|
|
382
383
|
|
|
384
|
+
private let installIdentityLock = NSLock()
|
|
383
385
|
private var currentInstallIdentity: InstallIdentity?
|
|
384
|
-
private var currentUserIdentity: UserIdentity?
|
|
385
386
|
|
|
386
387
|
private let protectedBundleStoreEntries: Set<String> = [
|
|
387
388
|
BundleMetadata.metadataFilename,
|
|
388
389
|
CrashedHistory.crashedHistoryFilename,
|
|
389
390
|
LaunchReport.launchReportFilename,
|
|
390
|
-
|
|
391
|
-
UserIdentity.userIdentityFilename,
|
|
391
|
+
"builtin-index-v1.json",
|
|
392
392
|
]
|
|
393
393
|
|
|
394
|
-
private func normalizeIdentityValue(_ value: String?) -> String? {
|
|
395
|
-
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
396
|
-
!trimmed.isEmpty else {
|
|
397
|
-
return nil
|
|
398
|
-
}
|
|
399
|
-
return trimmed
|
|
400
|
-
}
|
|
401
|
-
|
|
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
394
|
private func createDiffProgressFiles(
|
|
419
395
|
changedAssets: [String: ChangedAssetDescriptor]
|
|
420
396
|
) -> [UpdateProgressPayload.DiffProgressFileSnapshot] {
|
|
@@ -576,10 +552,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
576
552
|
return
|
|
577
553
|
}
|
|
578
554
|
|
|
579
|
-
try
|
|
580
|
-
from: downloadedPath,
|
|
581
|
-
to: destinationPath
|
|
582
|
-
)
|
|
555
|
+
try BrotliFileDecompressor.decompress(from: downloadedPath, to: destinationPath)
|
|
583
556
|
try? fileSystem.removeItem(atPath: downloadedPath)
|
|
584
557
|
}
|
|
585
558
|
|
|
@@ -604,7 +577,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
604
577
|
|
|
605
578
|
let sourcePath: String
|
|
606
579
|
do {
|
|
607
|
-
sourcePath = try
|
|
580
|
+
sourcePath = try FileUtilities.fileURL(
|
|
608
581
|
for: assetPath,
|
|
609
582
|
destinationRoot: currentBundleDir
|
|
610
583
|
).path
|
|
@@ -722,6 +695,14 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
722
695
|
atPath path: String,
|
|
723
696
|
asset: ParsedManifestAsset
|
|
724
697
|
) throws {
|
|
698
|
+
if let expectedByteSize = asset.byteSize {
|
|
699
|
+
let attributes = try fileSystem.attributesOfItem(atPath: path)
|
|
700
|
+
guard let size = attributes[.size] as? NSNumber,
|
|
701
|
+
size.int64Value == expectedByteSize else {
|
|
702
|
+
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
725
706
|
guard let actualHash = HashUtils.calculateSHA256(fileURL: URL(fileURLWithPath: path)) else {
|
|
726
707
|
throw BundleStorageError.signatureVerificationFailed(.fileReadFailed)
|
|
727
708
|
}
|
|
@@ -751,22 +732,28 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
751
732
|
|
|
752
733
|
public init(fileSystem: FileSystemService,
|
|
753
734
|
downloadService: DownloadService,
|
|
754
|
-
decompressService: DecompressService,
|
|
755
735
|
preferences: PreferencesService,
|
|
756
736
|
isolationKey: String,
|
|
757
737
|
builtInBundleIdProvider: @escaping () -> String = { "" },
|
|
738
|
+
builtInAssetResolver: BuiltInAssetResolver? = nil,
|
|
758
739
|
updateStrategyProvider: @escaping () -> UpdateStrategy = {
|
|
759
740
|
let fingerprintHash = HotUpdaterConfig.shared.fingerprintHash
|
|
760
741
|
?? Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_FINGERPRINT_HASH") as? String
|
|
761
742
|
return (fingerprintHash?.isEmpty == false) ? .fingerprint : .appVersion
|
|
762
|
-
}
|
|
743
|
+
},
|
|
744
|
+
downloadRetryDelay: TimeInterval = 1) {
|
|
763
745
|
|
|
764
746
|
self.fileSystem = fileSystem
|
|
747
|
+
self.downloadRetryDelay = downloadRetryDelay
|
|
765
748
|
self.downloadService = downloadService
|
|
766
|
-
self.decompressService = decompressService
|
|
767
749
|
self.preferences = preferences
|
|
768
750
|
self.isolationKey = isolationKey
|
|
769
751
|
self.builtInBundleIdProvider = builtInBundleIdProvider
|
|
752
|
+
self.builtInAssetResolver = builtInAssetResolver ?? IOSBuiltInAssetResolver(
|
|
753
|
+
cacheURL: URL(fileURLWithPath: fileSystem.documentsPath())
|
|
754
|
+
.appendingPathComponent("bundle-store", isDirectory: true)
|
|
755
|
+
.appendingPathComponent("builtin-index-v1.json")
|
|
756
|
+
)
|
|
770
757
|
self.updateStrategyProvider = updateStrategyProvider
|
|
771
758
|
|
|
772
759
|
// Create queue for file operations
|
|
@@ -779,6 +766,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
779
766
|
|
|
780
767
|
// Clean up old bundles if isolationKey format changed
|
|
781
768
|
checkAndCleanupIfIsolationKeyChanged()
|
|
769
|
+
recoverInterruptedPromotions()
|
|
782
770
|
}
|
|
783
771
|
|
|
784
772
|
// MARK: - Metadata File Paths
|
|
@@ -804,18 +792,9 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
804
792
|
return URL(fileURLWithPath: storeDir).appendingPathComponent(LaunchReport.launchReportFilename)
|
|
805
793
|
}
|
|
806
794
|
|
|
807
|
-
private func installIdentityFileURL() -> URL
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
}
|
|
811
|
-
return URL(fileURLWithPath: storeDir).appendingPathComponent(InstallIdentity.installIdentityFilename)
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
private func userIdentityFileURL() -> URL? {
|
|
815
|
-
guard case .success(let storeDir) = bundleStoreDir() else {
|
|
816
|
-
return nil
|
|
817
|
-
}
|
|
818
|
-
return URL(fileURLWithPath: storeDir).appendingPathComponent(UserIdentity.userIdentityFilename)
|
|
795
|
+
private func installIdentityFileURL() -> URL {
|
|
796
|
+
NoBackupStorage.directory(in: fileSystem)
|
|
797
|
+
.appendingPathComponent(InstallIdentity.installIdentityFilename)
|
|
819
798
|
}
|
|
820
799
|
|
|
821
800
|
// MARK: - Metadata Operations
|
|
@@ -864,54 +843,28 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
864
843
|
_ = report.save(to: file)
|
|
865
844
|
}
|
|
866
845
|
|
|
867
|
-
|
|
846
|
+
/// Loads the install identity, creating it once when its file is missing.
|
|
847
|
+
@discardableResult
|
|
848
|
+
private func loadOrCreateInstallIdentity() -> InstallIdentity {
|
|
849
|
+
installIdentityLock.lock()
|
|
850
|
+
defer { installIdentityLock.unlock() }
|
|
851
|
+
|
|
868
852
|
if let currentInstallIdentity {
|
|
869
853
|
return currentInstallIdentity
|
|
870
854
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
855
|
+
let file = installIdentityFileURL()
|
|
856
|
+
if let identity = InstallIdentity.load(from: file) {
|
|
857
|
+
currentInstallIdentity = identity
|
|
858
|
+
return identity
|
|
874
859
|
}
|
|
875
|
-
currentInstallIdentity = identity
|
|
876
|
-
return identity
|
|
877
|
-
}
|
|
878
860
|
|
|
879
|
-
|
|
861
|
+
let identity = InstallIdentity()
|
|
862
|
+
// Keep the new id for this process even when it cannot be saved.
|
|
880
863
|
currentInstallIdentity = identity
|
|
881
|
-
|
|
882
|
-
return false
|
|
883
|
-
}
|
|
884
|
-
return identity.save(to: file)
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
private func loadUserIdentity() -> UserIdentity? {
|
|
888
|
-
if let currentUserIdentity {
|
|
889
|
-
return currentUserIdentity
|
|
890
|
-
}
|
|
891
|
-
guard let file = userIdentityFileURL(),
|
|
892
|
-
let identity = UserIdentity.load(from: file) else {
|
|
893
|
-
return nil
|
|
894
|
-
}
|
|
895
|
-
currentUserIdentity = identity
|
|
864
|
+
_ = identity.save(to: file)
|
|
896
865
|
return identity
|
|
897
866
|
}
|
|
898
867
|
|
|
899
|
-
private func saveUserIdentity(_ identity: UserIdentity?) -> Bool {
|
|
900
|
-
currentUserIdentity = identity
|
|
901
|
-
guard let file = userIdentityFileURL() else {
|
|
902
|
-
return false
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
guard let identity else {
|
|
906
|
-
if FileManager.default.fileExists(atPath: file.path) {
|
|
907
|
-
try? FileManager.default.removeItem(at: file)
|
|
908
|
-
}
|
|
909
|
-
return true
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
return identity.save(to: file)
|
|
913
|
-
}
|
|
914
|
-
|
|
915
868
|
private func resolveBuiltInBundleId() -> String? {
|
|
916
869
|
let bundleId = builtInBundleIdProvider().trimmingCharacters(in: .whitespacesAndNewlines)
|
|
917
870
|
return bundleId.isEmpty ? nil : bundleId
|
|
@@ -938,11 +891,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
938
891
|
}
|
|
939
892
|
|
|
940
893
|
private func createInitialMetadata() -> BundleMetadata {
|
|
941
|
-
let currentBundleId = getCachedBundleId()
|
|
942
894
|
return BundleMetadata(
|
|
943
895
|
isolationKey: isolationKey,
|
|
944
896
|
stableBundleId: nil,
|
|
945
|
-
stagingBundleId:
|
|
897
|
+
stagingBundleId: nil,
|
|
946
898
|
verificationPending: false,
|
|
947
899
|
pendingTransition: nil
|
|
948
900
|
)
|
|
@@ -956,18 +908,18 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
956
908
|
}
|
|
957
909
|
|
|
958
910
|
private func getActiveBundleId() -> String? {
|
|
959
|
-
|
|
911
|
+
guard let metadata = loadMetadataOrNull() else { return nil }
|
|
912
|
+
if let cachedBundleId = getCachedBundleId(),
|
|
913
|
+
cachedBundleId == metadata.stagingBundleId || cachedBundleId == metadata.stableBundleId {
|
|
960
914
|
return cachedBundleId
|
|
961
915
|
}
|
|
962
916
|
|
|
963
|
-
let
|
|
964
|
-
|
|
965
|
-
if let stagingBundleId = metadata?.stagingBundleId,
|
|
966
|
-
metadata?.verificationPending == false {
|
|
917
|
+
if let stagingBundleId = metadata.stagingBundleId,
|
|
918
|
+
metadata.verificationPending == false {
|
|
967
919
|
return stagingBundleId
|
|
968
920
|
}
|
|
969
921
|
|
|
970
|
-
if let stableBundleId = metadata
|
|
922
|
+
if let stableBundleId = metadata.stableBundleId {
|
|
971
923
|
return stableBundleId
|
|
972
924
|
}
|
|
973
925
|
|
|
@@ -1017,22 +969,6 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1017
969
|
}
|
|
1018
970
|
}
|
|
1019
971
|
|
|
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
972
|
private func resolveActiveBundleMetadataSnapshot(
|
|
1037
973
|
activeBundleId: String,
|
|
1038
974
|
bundleDirectory: String
|
|
@@ -1041,9 +977,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1041
977
|
let manifestBundleId =
|
|
1042
978
|
(manifest["bundleId"] as? String)?
|
|
1043
979
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1044
|
-
let resolvedBundleId =
|
|
1045
|
-
(manifestBundleId?.isEmpty == false ? manifestBundleId : nil) ??
|
|
1046
|
-
readCompatibilityBundleId(in: bundleDirectory)
|
|
980
|
+
let resolvedBundleId = manifestBundleId?.isEmpty == false ? manifestBundleId : nil
|
|
1047
981
|
|
|
1048
982
|
return ActiveBundleMetadataSnapshot(
|
|
1049
983
|
activeBundleId: activeBundleId,
|
|
@@ -1070,34 +1004,6 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1070
1004
|
)
|
|
1071
1005
|
}
|
|
1072
1006
|
|
|
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
1007
|
private func readManifest(in bundleDirectory: String) -> [String: Any]? {
|
|
1102
1008
|
let manifestPath = (bundleDirectory as NSString).appendingPathComponent("manifest.json")
|
|
1103
1009
|
guard fileSystem.fileExists(atPath: manifestPath) else {
|
|
@@ -1124,7 +1030,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1124
1030
|
|
|
1125
1031
|
var assets: [String: ParsedManifestAsset] = [:]
|
|
1126
1032
|
for (assetPath, assetValue) in rawAssets {
|
|
1127
|
-
guard
|
|
1033
|
+
guard FileUtilities.normalizedRelativePath(from: assetPath) == assetPath else {
|
|
1128
1034
|
return nil
|
|
1129
1035
|
}
|
|
1130
1036
|
guard let asset = assetValue as? [String: Any],
|
|
@@ -1137,42 +1043,136 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1137
1043
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1138
1044
|
assets[assetPath] = ParsedManifestAsset(
|
|
1139
1045
|
fileHash: fileHash,
|
|
1140
|
-
signature: signature?.isEmpty == false ? signature : nil
|
|
1046
|
+
signature: signature?.isEmpty == false ? signature : nil,
|
|
1047
|
+
byteSize: Self.parseSafeByteSize(asset["byteSize"]),
|
|
1048
|
+
downloadByteSize: Self.parseSafeByteSize(asset["downloadByteSize"])
|
|
1049
|
+
)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
let archive: ParsedArchive?
|
|
1053
|
+
if let payload = manifest["archive"] as? [String: Any],
|
|
1054
|
+
let downloadFileHash = payload["downloadFileHash"] as? String,
|
|
1055
|
+
Self.isPlainSHA256(downloadFileHash),
|
|
1056
|
+
let downloadByteSize = Self.parseSafeByteSize(payload["downloadByteSize"]),
|
|
1057
|
+
downloadByteSize > 0,
|
|
1058
|
+
let tarByteSize = Self.parseSafeByteSize(payload["tarByteSize"]),
|
|
1059
|
+
tarByteSize >= 1024 {
|
|
1060
|
+
archive = ParsedArchive(
|
|
1061
|
+
downloadFileHash: downloadFileHash,
|
|
1062
|
+
downloadByteSize: downloadByteSize,
|
|
1063
|
+
tarByteSize: tarByteSize
|
|
1141
1064
|
)
|
|
1065
|
+
} else {
|
|
1066
|
+
archive = nil
|
|
1142
1067
|
}
|
|
1143
1068
|
|
|
1144
|
-
return ParsedBundleManifest(
|
|
1069
|
+
return ParsedBundleManifest(
|
|
1070
|
+
bundleId: manifestBundleId,
|
|
1071
|
+
assets: assets,
|
|
1072
|
+
archive: archive
|
|
1073
|
+
)
|
|
1145
1074
|
}
|
|
1146
1075
|
|
|
1147
|
-
private func
|
|
1148
|
-
guard let
|
|
1076
|
+
private static func parseSafeByteSize(_ value: Any?) -> Int64? {
|
|
1077
|
+
guard let number = value as? NSNumber,
|
|
1078
|
+
String(cString: number.objCType) != "c" else {
|
|
1079
|
+
return nil
|
|
1080
|
+
}
|
|
1081
|
+
let doubleValue = number.doubleValue
|
|
1082
|
+
guard doubleValue.isFinite,
|
|
1083
|
+
doubleValue >= 0,
|
|
1084
|
+
doubleValue <= Double(maximumSafeByteSize),
|
|
1085
|
+
doubleValue.rounded(.towardZero) == doubleValue else {
|
|
1149
1086
|
return nil
|
|
1150
1087
|
}
|
|
1088
|
+
return Int64(doubleValue)
|
|
1089
|
+
}
|
|
1151
1090
|
|
|
1152
|
-
|
|
1091
|
+
private static func isPlainSHA256(_ value: String) -> Bool {
|
|
1092
|
+
value.count == 64 && value.allSatisfy {
|
|
1093
|
+
("0"..."9").contains($0) || ("a"..."f").contains($0)
|
|
1094
|
+
}
|
|
1153
1095
|
}
|
|
1154
1096
|
|
|
1155
|
-
private func
|
|
1097
|
+
private func copyVerifiedManifest(from source: String, to destination: String) throws {
|
|
1156
1098
|
let manifestDirectory = (destination as NSString).deletingLastPathComponent
|
|
1157
1099
|
guard fileSystem.createDirectory(atPath: manifestDirectory) else {
|
|
1158
1100
|
throw BundleStorageError.directoryCreationFailed
|
|
1159
1101
|
}
|
|
1102
|
+
if fileSystem.fileExists(atPath: destination) {
|
|
1103
|
+
try fileSystem.removeItem(atPath: destination)
|
|
1104
|
+
}
|
|
1105
|
+
do {
|
|
1106
|
+
try fileSystem.copyItem(atPath: source, toPath: destination)
|
|
1107
|
+
} catch {
|
|
1108
|
+
throw BundleStorageError.moveOperationFailed(error)
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
private func shouldUseArchive(
|
|
1113
|
+
archive: ParsedArchive?,
|
|
1114
|
+
archiveUrl: URL?,
|
|
1115
|
+
downloads: [String: ChangedAssetDescriptor],
|
|
1116
|
+
assets: [String: ParsedManifestAsset]
|
|
1117
|
+
) -> Bool {
|
|
1118
|
+
guard downloads.count >= 2,
|
|
1119
|
+
archive != nil,
|
|
1120
|
+
archiveUrl != nil,
|
|
1121
|
+
assets.values.allSatisfy({ $0.byteSize != nil }) else {
|
|
1122
|
+
return false
|
|
1123
|
+
}
|
|
1160
1124
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1125
|
+
var individualLowerBound: Int64 = 0
|
|
1126
|
+
var hasOfferedPatch = false
|
|
1127
|
+
for (path, descriptor) in downloads {
|
|
1128
|
+
guard let originalByteSize = assets[path]?.downloadByteSize else {
|
|
1129
|
+
return false
|
|
1130
|
+
}
|
|
1131
|
+
let plannedByteSize: Int64
|
|
1132
|
+
if let patch = descriptor.patch {
|
|
1133
|
+
hasOfferedPatch = true
|
|
1134
|
+
guard let patchByteSize = patch.byteSize,
|
|
1135
|
+
patchByteSize >= 0,
|
|
1136
|
+
patchByteSize <= Self.maximumSafeByteSize else {
|
|
1137
|
+
return false
|
|
1167
1138
|
}
|
|
1168
|
-
|
|
1139
|
+
plannedByteSize = min(originalByteSize, patchByteSize)
|
|
1140
|
+
} else {
|
|
1141
|
+
plannedByteSize = originalByteSize
|
|
1169
1142
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1143
|
+
let (sum, overflowed) = individualLowerBound.addingReportingOverflow(plannedByteSize)
|
|
1144
|
+
guard !overflowed,
|
|
1145
|
+
sum <= Self.maximumSafeByteSize else { return false }
|
|
1146
|
+
individualLowerBound = sum
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
guard let archive else { return false }
|
|
1150
|
+
var logicalByteSize: Int64 = 0
|
|
1151
|
+
for asset in assets.values {
|
|
1152
|
+
guard let byteSize = asset.byteSize else { return false }
|
|
1153
|
+
let (sum, overflowed) = logicalByteSize.addingReportingOverflow(byteSize)
|
|
1154
|
+
guard !overflowed,
|
|
1155
|
+
sum <= Self.maximumSafeByteSize else { return false }
|
|
1156
|
+
logicalByteSize = sum
|
|
1157
|
+
}
|
|
1158
|
+
let (tarOverhead, overheadUnderflowed) = archive.tarByteSize
|
|
1159
|
+
.subtractingReportingOverflow(logicalByteSize)
|
|
1160
|
+
guard !overheadUnderflowed,
|
|
1161
|
+
tarOverhead >= 0 else { return false }
|
|
1162
|
+
|
|
1163
|
+
if archive.downloadByteSize <= individualLowerBound {
|
|
1164
|
+
return true
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
guard downloads.count == assets.count,
|
|
1168
|
+
!hasOfferedPatch else {
|
|
1169
|
+
return false
|
|
1170
|
+
}
|
|
1171
|
+
let (archiveBound, boundOverflowed) = individualLowerBound
|
|
1172
|
+
.addingReportingOverflow(tarOverhead)
|
|
1173
|
+
guard !boundOverflowed,
|
|
1174
|
+
archiveBound <= Self.maximumSafeByteSize else { return false }
|
|
1175
|
+
return archive.downloadByteSize <= archiveBound
|
|
1176
1176
|
}
|
|
1177
1177
|
|
|
1178
1178
|
/**
|
|
@@ -1539,6 +1539,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1539
1539
|
metadata.stagingBundleId = nil
|
|
1540
1540
|
metadata.stagingSelection = selection
|
|
1541
1541
|
metadata.verificationPending = false
|
|
1542
|
+
metadata.launchInProgress = false
|
|
1542
1543
|
metadata.pendingTransition = nil
|
|
1543
1544
|
metadata.pendingSelectionTransition = nil
|
|
1544
1545
|
}
|
|
@@ -1619,7 +1620,42 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1619
1620
|
DispatchQueue.global(qos: .background).async(execute: workItem)
|
|
1620
1621
|
}
|
|
1621
1622
|
|
|
1623
|
+
/**
|
|
1624
|
+
* Downloads `url`, trying again after a retryable failure (see
|
|
1625
|
+
* `DownloadError.isRetryable`) up to `DownloadError.maximumAttempts`
|
|
1626
|
+
* attempts in all, 1 and then 2 seconds apart, unless `retrying` is
|
|
1627
|
+
* false. The last attempt's result is returned, so a failure keeps what
|
|
1628
|
+
* classifies it. Blocks the calling thread.
|
|
1629
|
+
*/
|
|
1622
1630
|
private func downloadFileSynchronously(
|
|
1631
|
+
from url: URL,
|
|
1632
|
+
to destination: String,
|
|
1633
|
+
retrying: Bool = true,
|
|
1634
|
+
progressHandler: @escaping (DownloadProgress) -> Void
|
|
1635
|
+
) -> Result<URL, Error> {
|
|
1636
|
+
var attempt = 1
|
|
1637
|
+
while true {
|
|
1638
|
+
let result = downloadFileOnce(
|
|
1639
|
+
from: url,
|
|
1640
|
+
to: destination,
|
|
1641
|
+
progressHandler: progressHandler
|
|
1642
|
+
)
|
|
1643
|
+
guard case .failure(let error) = result,
|
|
1644
|
+
retrying,
|
|
1645
|
+
attempt < DownloadError.maximumAttempts,
|
|
1646
|
+
DownloadError.isRetryable(error) else {
|
|
1647
|
+
return result
|
|
1648
|
+
}
|
|
1649
|
+
let delay = downloadRetryDelay * pow(2, Double(attempt - 1))
|
|
1650
|
+
NSLog("[BundleStorage] Download failed (attempt \(attempt)/\(DownloadError.maximumAttempts)): \(error.localizedDescription). Retrying in \(delay)s")
|
|
1651
|
+
if delay > 0 {
|
|
1652
|
+
Thread.sleep(forTimeInterval: delay)
|
|
1653
|
+
}
|
|
1654
|
+
attempt += 1
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
private func downloadFileOnce(
|
|
1623
1659
|
from url: URL,
|
|
1624
1660
|
to destination: String,
|
|
1625
1661
|
progressHandler: @escaping (DownloadProgress) -> Void
|
|
@@ -1666,7 +1702,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1666
1702
|
do {
|
|
1667
1703
|
var bundlePaths: [String] = []
|
|
1668
1704
|
for assetPath in parsedManifest.assets.keys {
|
|
1669
|
-
let assetURL = try
|
|
1705
|
+
let assetURL = try FileUtilities.fileURL(
|
|
1670
1706
|
for: assetPath,
|
|
1671
1707
|
destinationRoot: directoryPath
|
|
1672
1708
|
)
|
|
@@ -1688,38 +1724,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1688
1724
|
}
|
|
1689
1725
|
}
|
|
1690
1726
|
|
|
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
|
-
}
|
|
1727
|
+
return .success(nil)
|
|
1723
1728
|
}
|
|
1724
1729
|
|
|
1725
1730
|
/**
|
|
@@ -1752,7 +1757,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1752
1757
|
let bundles = contents.compactMap { item -> String? in
|
|
1753
1758
|
let fullPath = (storeDir as NSString).appendingPathComponent(item)
|
|
1754
1759
|
|
|
1755
|
-
if protectedBundleStoreEntries.contains(item) {
|
|
1760
|
+
if protectedBundleStoreEntries.contains(item) || item.hasSuffix(".install-backup") {
|
|
1756
1761
|
return nil
|
|
1757
1762
|
}
|
|
1758
1763
|
|
|
@@ -1864,11 +1869,11 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1864
1869
|
}
|
|
1865
1870
|
|
|
1866
1871
|
let relativePath = String(bundleURL.path.dropFirst(sourcePrefix.count))
|
|
1867
|
-
guard
|
|
1872
|
+
guard FileUtilities.normalizedRelativePath(from: relativePath) == relativePath else {
|
|
1868
1873
|
throw BundleStorageError.invalidBundle
|
|
1869
1874
|
}
|
|
1870
1875
|
|
|
1871
|
-
return try
|
|
1876
|
+
return try FileUtilities.fileURL(
|
|
1872
1877
|
for: relativePath,
|
|
1873
1878
|
destinationRoot: destinationDirectory
|
|
1874
1879
|
).path
|
|
@@ -1924,8 +1929,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1924
1929
|
private func selectLaunch(bundle: Bundle) -> LaunchSelection {
|
|
1925
1930
|
guard let metadata = loadMetadataOrNull() else {
|
|
1926
1931
|
return LaunchSelection(
|
|
1927
|
-
bundleURL:
|
|
1928
|
-
launchedBundleId:
|
|
1932
|
+
bundleURL: getFallbackBundleURL(bundle: bundle),
|
|
1933
|
+
launchedBundleId: nil,
|
|
1929
1934
|
shouldRollbackOnCrash: false
|
|
1930
1935
|
)
|
|
1931
1936
|
}
|
|
@@ -1972,13 +1977,26 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1972
1977
|
}
|
|
1973
1978
|
|
|
1974
1979
|
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection {
|
|
1980
|
+
builtInAssetResolver.use(bundle: bundle)
|
|
1975
1981
|
saveLaunchReport(nil)
|
|
1976
|
-
|
|
1977
|
-
let identity = InstallIdentity()
|
|
1978
|
-
_ = saveInstallIdentity(identity)
|
|
1979
|
-
}
|
|
1982
|
+
loadOrCreateInstallIdentity()
|
|
1980
1983
|
applyPendingRecoveryIfNeeded(pendingRecovery)
|
|
1981
|
-
|
|
1984
|
+
// Only a new storage instance consumes an unfinished launch. Repeated bundle
|
|
1985
|
+
// lookups or bridge reloads in this process must not reject its own launch.
|
|
1986
|
+
if !hasPreparedLaunch,
|
|
1987
|
+
let metadata = loadMetadataOrNull(),
|
|
1988
|
+
metadata.verificationPending,
|
|
1989
|
+
metadata.launchInProgress,
|
|
1990
|
+
let stagingBundleId = metadata.stagingBundleId {
|
|
1991
|
+
rollbackPendingBundle(stagingBundleId)
|
|
1992
|
+
}
|
|
1993
|
+
hasPreparedLaunch = true
|
|
1994
|
+
let selection = selectLaunch(bundle: bundle)
|
|
1995
|
+
if selection.shouldRollbackOnCrash, var metadata = loadMetadataOrNull() {
|
|
1996
|
+
metadata.launchInProgress = true
|
|
1997
|
+
_ = saveMetadata(metadata)
|
|
1998
|
+
}
|
|
1999
|
+
return selection
|
|
1982
2000
|
}
|
|
1983
2001
|
|
|
1984
2002
|
// MARK: - Bundle Update
|
|
@@ -1986,12 +2004,12 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1986
2004
|
/**
|
|
1987
2005
|
* Updates the bundle from the specified URL. This operation is asynchronous.
|
|
1988
2006
|
* @param bundleId ID of the bundle to update
|
|
1989
|
-
* @param
|
|
1990
|
-
* @param
|
|
2007
|
+
* @param manifestUrl URL of the complete target manifest
|
|
2008
|
+
* @param manifestFileHash Hash or signature of the target manifest
|
|
1991
2009
|
* @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
|
|
1992
2010
|
* @param completion Callback with result of the operation
|
|
1993
2011
|
*/
|
|
1994
|
-
func updateBundle(bundleId: String,
|
|
2012
|
+
func updateBundle(bundleId: String, manifestUrl: URL, manifestFileHash: String, archiveUrl: URL? = nil, assets: [String: ChangedAssetDescriptor], progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<UpdateDelivery, Error>) -> Void) {
|
|
1995
2013
|
// Check if bundle is in crashed history
|
|
1996
2014
|
let crashedHistory = loadCrashedHistory()
|
|
1997
2015
|
if crashedHistory.contains(bundleId) {
|
|
@@ -2000,928 +2018,774 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2000
2018
|
return
|
|
2001
2019
|
}
|
|
2002
2020
|
|
|
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
|
-
|
|
2021
|
+
fileOperationQueue.async(flags: .barrier) {
|
|
2039
2022
|
let storeDirResult = self.bundleStoreDir()
|
|
2040
2023
|
guard case .success(let storeDir) = storeDirResult else {
|
|
2041
|
-
completion(.failure(
|
|
2024
|
+
completion(.failure(UpdateFailureError.classifying(
|
|
2025
|
+
storeDirResult.failureError ?? BundleStorageError.unknown(nil),
|
|
2026
|
+
during: .storage
|
|
2027
|
+
)))
|
|
2042
2028
|
return
|
|
2043
2029
|
}
|
|
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
|
-
}
|
|
2030
|
+
self.updateBundleFromManifest(
|
|
2031
|
+
bundleId: bundleId,
|
|
2032
|
+
manifestUrl: manifestUrl,
|
|
2033
|
+
manifestFileHash: manifestFileHash,
|
|
2034
|
+
archiveUrl: archiveUrl,
|
|
2035
|
+
changedAssets: assets,
|
|
2036
|
+
storeDir: storeDir,
|
|
2037
|
+
progressHandler: progressHandler,
|
|
2038
|
+
completion: completion
|
|
2039
|
+
)
|
|
2140
2040
|
}
|
|
2141
2041
|
}
|
|
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
2042
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2043
|
+
/// How the per-file install produced one file.
|
|
2044
|
+
private enum AssetInstallOutcome {
|
|
2045
|
+
case patched
|
|
2046
|
+
/// `patchFallback` is true when a patch for the running bundle was
|
|
2047
|
+
/// offered but the original file was downloaded instead.
|
|
2048
|
+
case downloaded(patchFallback: Bool)
|
|
2049
|
+
}
|
|
2175
2050
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2051
|
+
private func downloadManifestAsset(
|
|
2052
|
+
assetPath: String,
|
|
2053
|
+
expectedAsset: ParsedManifestAsset,
|
|
2054
|
+
changedAsset: ChangedAssetDescriptor,
|
|
2055
|
+
currentBundleId: String?,
|
|
2056
|
+
currentBundleDir: String?,
|
|
2057
|
+
destinationPath: String,
|
|
2058
|
+
tempDirectory: String,
|
|
2059
|
+
progressFile: UpdateProgressPayload.DiffProgressFileSnapshot,
|
|
2060
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void
|
|
2061
|
+
) throws -> AssetInstallOutcome {
|
|
2062
|
+
let expectedHash = expectedAsset.fileHash
|
|
2063
|
+
var diffFiles = [progressFile]
|
|
2064
|
+
let patched = applyPatchAssetIfPossible(
|
|
2065
|
+
assetPath: assetPath,
|
|
2066
|
+
changedAsset: changedAsset,
|
|
2067
|
+
currentBundleId: currentBundleId,
|
|
2068
|
+
currentBundleDir: currentBundleDir,
|
|
2069
|
+
destinationPath: destinationPath,
|
|
2070
|
+
expectedHash: expectedHash,
|
|
2071
|
+
tempDirectory: tempDirectory,
|
|
2072
|
+
files: &diffFiles,
|
|
2073
|
+
progressHandler: progressHandler
|
|
2074
|
+
)
|
|
2075
|
+
if patched {
|
|
2076
|
+
// The patch produced these bytes, so a mismatch is a patch failure.
|
|
2077
|
+
try UpdateFailureError.during(.patch, resource: .patch) {
|
|
2078
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2079
|
+
}
|
|
2080
|
+
updateDiffProgressFile(
|
|
2081
|
+
files: &diffFiles,
|
|
2082
|
+
assetPath: assetPath,
|
|
2083
|
+
status: "downloaded",
|
|
2084
|
+
progress: 1
|
|
2085
|
+
)
|
|
2086
|
+
self.emitDiffProgress(
|
|
2087
|
+
progressHandler: progressHandler,
|
|
2088
|
+
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
2089
|
+
? "finalizing"
|
|
2090
|
+
: "downloading",
|
|
2091
|
+
files: diffFiles
|
|
2092
|
+
)
|
|
2093
|
+
return .patched
|
|
2179
2094
|
}
|
|
2180
|
-
let tempManifestPath = (tempDirectory as NSString).appendingPathComponent("manifest.json")
|
|
2181
|
-
let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
|
|
2182
|
-
let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
2183
|
-
var diffFiles = createDiffProgressFiles(changedAssets: changedAssets)
|
|
2184
2095
|
|
|
2185
|
-
|
|
2096
|
+
guard let changedAssetFileUrl = changedAsset.fileUrl else {
|
|
2097
|
+
updateDiffProgressFile(
|
|
2098
|
+
files: &diffFiles,
|
|
2099
|
+
assetPath: assetPath,
|
|
2100
|
+
status: "failed",
|
|
2101
|
+
progress: 0
|
|
2102
|
+
)
|
|
2186
2103
|
self.emitDiffProgress(
|
|
2187
2104
|
progressHandler: progressHandler,
|
|
2188
|
-
phase: "
|
|
2189
|
-
files: diffFiles
|
|
2190
|
-
manifestProgress: 0
|
|
2105
|
+
phase: "downloading",
|
|
2106
|
+
files: diffFiles
|
|
2191
2107
|
)
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
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
|
-
}
|
|
2213
|
-
|
|
2214
|
-
let manifestVerificationResult = SignatureVerifier.verifyBundle(
|
|
2215
|
-
fileURL: URL(fileURLWithPath: tempManifestPath),
|
|
2216
|
-
fileHash: manifestFileHash
|
|
2108
|
+
// With no original file to fall back to, the patch decides the install.
|
|
2109
|
+
throw UpdateFailureError(
|
|
2110
|
+
underlying: BundleStorageError.downloadFailed(
|
|
2111
|
+
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2112
|
+
NSLocalizedDescriptionKey: "Changed asset fileUrl missing and patch could not be applied: \(assetPath)"
|
|
2113
|
+
])
|
|
2114
|
+
),
|
|
2115
|
+
failure: UpdateFailure.patch.with(resource: .patch)
|
|
2217
2116
|
)
|
|
2218
|
-
|
|
2219
|
-
if case .failure(let error) = manifestVerificationResult {
|
|
2220
|
-
throw BundleStorageError.signatureVerificationFailed(error)
|
|
2221
|
-
}
|
|
2222
|
-
throw BundleStorageError.unknown(nil)
|
|
2223
|
-
}
|
|
2117
|
+
}
|
|
2224
2118
|
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
files: diffFiles
|
|
2119
|
+
let changedAssetDownloadPath = downloadPath(
|
|
2120
|
+
assetPath: assetPath,
|
|
2121
|
+
changedAsset: changedAsset
|
|
2122
|
+
)
|
|
2123
|
+
let downloadDestinationPath: String
|
|
2124
|
+
if changedAsset.fileCompression == "br" {
|
|
2125
|
+
downloadDestinationPath = compressedTempPath(
|
|
2126
|
+
tempDirectory: tempDirectory,
|
|
2127
|
+
assetPath: assetPath,
|
|
2128
|
+
compression: "br"
|
|
2236
2129
|
)
|
|
2130
|
+
} else {
|
|
2131
|
+
downloadDestinationPath = destinationPath
|
|
2132
|
+
}
|
|
2237
2133
|
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
try self.fileSystem.removeItem(atPath: tmpDir)
|
|
2244
|
-
}
|
|
2245
|
-
guard self.fileSystem.createDirectory(atPath: tmpDir) else {
|
|
2246
|
-
throw BundleStorageError.directoryCreationFailed
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
let sortedAssets = targetManifest.assets.sorted { $0.key < $1.key }
|
|
2250
|
-
|
|
2251
|
-
for asset in sortedAssets {
|
|
2252
|
-
let assetPath = asset.key
|
|
2253
|
-
let expectedAsset = asset.value
|
|
2254
|
-
let expectedHash = expectedAsset.fileHash
|
|
2255
|
-
let destinationPath = try ArchiveExtractionUtilities.extractionURL(
|
|
2256
|
-
for: assetPath,
|
|
2257
|
-
destinationRoot: tmpDir
|
|
2258
|
-
).path
|
|
2259
|
-
let destinationDir = (destinationPath as NSString).deletingLastPathComponent
|
|
2260
|
-
guard self.fileSystem.createDirectory(atPath: destinationDir) else {
|
|
2261
|
-
throw BundleStorageError.directoryCreationFailed
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
if currentManifest?.assets[assetPath]?.fileHash == expectedHash {
|
|
2265
|
-
guard let currentBundleDir,
|
|
2266
|
-
self.fileSystem.fileExists(atPath: currentBundleDir)
|
|
2267
|
-
else {
|
|
2268
|
-
throw BundleStorageError.downloadFailed(
|
|
2269
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2270
|
-
NSLocalizedDescriptionKey: "Current bundle directory unavailable for reused asset: \(assetPath)"
|
|
2271
|
-
])
|
|
2272
|
-
)
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
let sourcePath = try ArchiveExtractionUtilities.extractionURL(
|
|
2276
|
-
for: assetPath,
|
|
2277
|
-
destinationRoot: currentBundleDir
|
|
2278
|
-
).path
|
|
2279
|
-
guard self.fileSystem.fileExists(atPath: sourcePath),
|
|
2280
|
-
HashUtils.verifyHash(fileURL: URL(fileURLWithPath: sourcePath), expectedHash: expectedHash)
|
|
2281
|
-
else {
|
|
2282
|
-
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
try self.fileSystem.copyItem(atPath: sourcePath, toPath: destinationPath)
|
|
2286
|
-
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2287
|
-
continue
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
guard let changedAsset = changedAssets[assetPath] else {
|
|
2291
|
-
updateDiffProgressFile(
|
|
2292
|
-
files: &diffFiles,
|
|
2293
|
-
assetPath: assetPath,
|
|
2294
|
-
status: "failed",
|
|
2295
|
-
progress: 0
|
|
2296
|
-
)
|
|
2297
|
-
self.emitDiffProgress(
|
|
2298
|
-
progressHandler: progressHandler,
|
|
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
|
-
}
|
|
2308
|
-
|
|
2309
|
-
guard changedAsset.fileHash.caseInsensitiveCompare(expectedHash) == .orderedSame else {
|
|
2310
|
-
updateDiffProgressFile(
|
|
2311
|
-
files: &diffFiles,
|
|
2312
|
-
assetPath: assetPath,
|
|
2313
|
-
status: "failed",
|
|
2314
|
-
progress: 0
|
|
2315
|
-
)
|
|
2316
|
-
self.emitDiffProgress(
|
|
2317
|
-
progressHandler: progressHandler,
|
|
2318
|
-
phase: "downloading",
|
|
2319
|
-
files: diffFiles
|
|
2320
|
-
)
|
|
2321
|
-
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
let patched = applyPatchAssetIfPossible(
|
|
2325
|
-
assetPath: assetPath,
|
|
2326
|
-
changedAsset: changedAsset,
|
|
2327
|
-
currentBundleId: currentBundleId,
|
|
2328
|
-
currentBundleDir: currentBundleDir,
|
|
2329
|
-
destinationPath: destinationPath,
|
|
2330
|
-
expectedHash: expectedHash,
|
|
2331
|
-
tempDirectory: tempDirectory,
|
|
2134
|
+
switch self.downloadFileSynchronously(
|
|
2135
|
+
from: changedAssetFileUrl,
|
|
2136
|
+
to: downloadDestinationPath,
|
|
2137
|
+
progressHandler: { progress in
|
|
2138
|
+
self.updateDiffProgressFile(
|
|
2332
2139
|
files: &diffFiles,
|
|
2333
|
-
|
|
2140
|
+
assetPath: assetPath,
|
|
2141
|
+
status: "downloading",
|
|
2142
|
+
progress: progress.progress,
|
|
2143
|
+
downloadPath: changedAssetDownloadPath,
|
|
2144
|
+
downloadedBytes: progress.downloadedBytes,
|
|
2145
|
+
totalBytes: progress.totalBytes
|
|
2334
2146
|
)
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2147
|
+
self.emitDiffProgress(
|
|
2148
|
+
progressHandler: progressHandler,
|
|
2149
|
+
phase: "downloading",
|
|
2150
|
+
files: diffFiles
|
|
2151
|
+
)
|
|
2152
|
+
}
|
|
2153
|
+
) {
|
|
2154
|
+
case .success(let downloadedFileURL):
|
|
2155
|
+
do {
|
|
2156
|
+
try UpdateFailureError.during(.extraction, resource: .file) {
|
|
2157
|
+
try materializeDownloadedAsset(
|
|
2158
|
+
downloadedPath: downloadedFileURL.path,
|
|
2159
|
+
destinationPath: destinationPath,
|
|
2160
|
+
changedAsset: changedAsset
|
|
2349
2161
|
)
|
|
2350
|
-
continue
|
|
2351
2162
|
}
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
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
|
-
])
|
|
2163
|
+
// Compressed files are checked after decoding, so their
|
|
2164
|
+
// mismatch is an extraction failure.
|
|
2165
|
+
let verificationPhase: UpdatePhase =
|
|
2166
|
+
changedAsset.fileCompression == "br" ? .extraction : .verification
|
|
2167
|
+
try UpdateFailureError.during(verificationPhase, resource: .file) {
|
|
2168
|
+
try verifyManifestAssetFile(
|
|
2169
|
+
atPath: destinationPath,
|
|
2170
|
+
asset: expectedAsset
|
|
2369
2171
|
)
|
|
2370
2172
|
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2173
|
+
} catch {
|
|
2174
|
+
updateDiffProgressFile(
|
|
2175
|
+
files: &diffFiles,
|
|
2373
2176
|
assetPath: assetPath,
|
|
2374
|
-
|
|
2177
|
+
status: "failed",
|
|
2178
|
+
progress: 1
|
|
2375
2179
|
)
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
)
|
|
2383
|
-
} else {
|
|
2384
|
-
downloadDestinationPath = destinationPath
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
switch self.downloadFileSynchronously(
|
|
2388
|
-
from: changedAssetFileUrl,
|
|
2389
|
-
to: downloadDestinationPath,
|
|
2390
|
-
progressHandler: { progress in
|
|
2391
|
-
self.updateDiffProgressFile(
|
|
2392
|
-
files: &diffFiles,
|
|
2393
|
-
assetPath: assetPath,
|
|
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
|
-
}
|
|
2180
|
+
self.emitDiffProgress(
|
|
2181
|
+
progressHandler: progressHandler,
|
|
2182
|
+
phase: "downloading",
|
|
2183
|
+
files: diffFiles
|
|
2184
|
+
)
|
|
2185
|
+
throw error
|
|
2464
2186
|
}
|
|
2465
|
-
|
|
2187
|
+
updateDiffProgressFile(
|
|
2188
|
+
files: &diffFiles,
|
|
2189
|
+
assetPath: assetPath,
|
|
2190
|
+
status: "downloaded",
|
|
2191
|
+
progress: 1
|
|
2192
|
+
)
|
|
2466
2193
|
self.emitDiffProgress(
|
|
2467
2194
|
progressHandler: progressHandler,
|
|
2468
|
-
phase: "
|
|
2195
|
+
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
2196
|
+
? "finalizing"
|
|
2197
|
+
: "downloading",
|
|
2469
2198
|
files: diffFiles
|
|
2470
2199
|
)
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
try self.fileSystem.moveItem(atPath: tmpDir, toPath: realDir)
|
|
2487
|
-
} catch {
|
|
2488
|
-
throw BundleStorageError.moveOperationFailed(error)
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
let finalBundlePath = try resolveBundlePathAfterMove(
|
|
2492
|
-
bundlePathInTmp,
|
|
2493
|
-
from: tmpDir,
|
|
2494
|
-
to: realDir
|
|
2495
|
-
)
|
|
2496
|
-
let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
|
|
2497
|
-
guard let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
|
|
2498
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2499
|
-
completion(.failure(BundleStorageError.unknown(
|
|
2500
|
-
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
2501
|
-
NSLocalizedDescriptionKey: "Release catalog selection is stale"
|
|
2502
|
-
])
|
|
2503
|
-
)))
|
|
2504
|
-
return
|
|
2505
|
-
}
|
|
2506
|
-
switch self.setBundleURL(localPath: finalBundlePath) {
|
|
2507
|
-
case .success:
|
|
2508
|
-
let _ = self.saveMetadata(updatedMetadata)
|
|
2509
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2510
|
-
self.scheduleCleanupOldBundles(
|
|
2511
|
-
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
2512
|
-
)
|
|
2513
|
-
progressHandler(
|
|
2514
|
-
UpdateProgressPayload(
|
|
2515
|
-
progress: UpdateProgress.complete,
|
|
2516
|
-
artifactType: "diff",
|
|
2517
|
-
details: UpdateProgressPayload.DiffProgressDetails(
|
|
2518
|
-
totalFilesCount: diffFiles.count,
|
|
2519
|
-
completedFilesCount: diffFiles.filter { $0.status == "downloaded" }.count,
|
|
2520
|
-
files: diffFiles
|
|
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
|
|
2532
|
-
}
|
|
2533
|
-
} catch {
|
|
2534
|
-
NSLog("[BundleStorage] Manifest-driven install failed: \(error.localizedDescription). Falling back to archive.")
|
|
2535
|
-
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
2536
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2537
|
-
self.prepareAndDownloadBundle(
|
|
2538
|
-
bundleId: bundleId,
|
|
2539
|
-
fileUrl: fileUrl,
|
|
2540
|
-
fileHash: fileHash,
|
|
2541
|
-
storeDir: storeDir,
|
|
2200
|
+
// A patch for the running bundle was offered, but the original
|
|
2201
|
+
// file came instead; a patch for another base was never tried.
|
|
2202
|
+
let patchWasTried = changedAsset.patch.map {
|
|
2203
|
+
$0.algorithm == "bsdiff" && $0.baseBundleId == currentBundleId
|
|
2204
|
+
} ?? false
|
|
2205
|
+
return .downloaded(patchFallback: patchWasTried)
|
|
2206
|
+
case .failure(let error):
|
|
2207
|
+
let lastKnownProgress = diffFiles.first(where: { $0.path == assetPath })?.progress ?? 0
|
|
2208
|
+
updateDiffProgressFile(
|
|
2209
|
+
files: &diffFiles,
|
|
2210
|
+
assetPath: assetPath,
|
|
2211
|
+
status: "failed",
|
|
2212
|
+
progress: lastKnownProgress
|
|
2213
|
+
)
|
|
2214
|
+
self.emitDiffProgress(
|
|
2542
2215
|
progressHandler: progressHandler,
|
|
2543
|
-
|
|
2216
|
+
phase: "downloading",
|
|
2217
|
+
files: diffFiles
|
|
2544
2218
|
)
|
|
2219
|
+
let transferError: Error
|
|
2220
|
+
if let downloadError = error as? DownloadError,
|
|
2221
|
+
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2222
|
+
transferError = BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
2223
|
+
} else {
|
|
2224
|
+
transferError = BundleStorageError.downloadFailed(error)
|
|
2225
|
+
}
|
|
2226
|
+
throw UpdateFailureError.classifying(transferError, during: .transfer, resource: .file)
|
|
2545
2227
|
}
|
|
2546
2228
|
}
|
|
2547
2229
|
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
completion(.failure(tempDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
2560
|
-
return
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
// 2) Clean up any previous temp dir
|
|
2564
|
-
try? self.fileSystem.removeItem(atPath: tempDirectory)
|
|
2565
|
-
|
|
2566
|
-
// 3) Create temp dir
|
|
2567
|
-
if !self.fileSystem.createDirectory(atPath: tempDirectory) {
|
|
2568
|
-
completion(.failure(BundleStorageError.directoryCreationFailed))
|
|
2569
|
-
return
|
|
2230
|
+
// Retain only target files on retry; partial files are rehashed before reuse.
|
|
2231
|
+
private func pruneStagingDirectory(_ directory: String, assets: Set<String>, prefix: String = "") throws {
|
|
2232
|
+
for name in try fileSystem.contentsOfDirectory(atPath: directory) {
|
|
2233
|
+
let relativePath = prefix + name
|
|
2234
|
+
let path = (directory as NSString).appendingPathComponent(name)
|
|
2235
|
+
let type = try fileSystem.attributesOfItem(atPath: path)[.type] as? FileAttributeType
|
|
2236
|
+
if type == .typeDirectory, assets.contains(where: { $0.hasPrefix(relativePath + "/") }) {
|
|
2237
|
+
try pruneStagingDirectory(path, assets: assets, prefix: relativePath + "/")
|
|
2238
|
+
} else if type != .typeRegular || !assets.contains(relativePath) {
|
|
2239
|
+
try fileSystem.removeItem(atPath: path)
|
|
2240
|
+
}
|
|
2570
2241
|
}
|
|
2242
|
+
}
|
|
2571
2243
|
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
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
|
|
2244
|
+
private func installArchive(
|
|
2245
|
+
archive: ParsedArchive,
|
|
2246
|
+
archiveUrl: URL,
|
|
2247
|
+
targetManifest: ParsedBundleManifest,
|
|
2248
|
+
preparedDirectory: String,
|
|
2249
|
+
tempDirectory: String,
|
|
2250
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void
|
|
2251
|
+
) throws -> [UpdateProgressPayload.DiffProgressFileSnapshot] {
|
|
2252
|
+
let archiveScratch = (tempDirectory as NSString).appendingPathComponent("archive")
|
|
2253
|
+
try? fileSystem.removeItem(atPath: archiveScratch)
|
|
2254
|
+
guard fileSystem.createDirectory(atPath: archiveScratch) else {
|
|
2255
|
+
throw BundleStorageError.directoryCreationFailed
|
|
2256
|
+
}
|
|
2257
|
+
defer { try? fileSystem.removeItem(atPath: archiveScratch) }
|
|
2593
2258
|
|
|
2594
|
-
|
|
2259
|
+
let compressedPath = (archiveScratch as NSString).appendingPathComponent("bundle.tar.br")
|
|
2260
|
+
let tarPath = (archiveScratch as NSString).appendingPathComponent("bundle.tar")
|
|
2261
|
+
let extractedDirectory = (archiveScratch as NSString).appendingPathComponent("extracted")
|
|
2262
|
+
var progressFiles = [UpdateProgressPayload.DiffProgressFileSnapshot(
|
|
2263
|
+
path: "bundle.tar.br",
|
|
2264
|
+
downloadPath: "bundle.tar.br",
|
|
2265
|
+
status: "pending",
|
|
2266
|
+
progress: 0,
|
|
2267
|
+
order: 0
|
|
2268
|
+
)]
|
|
2269
|
+
emitDiffProgress(
|
|
2270
|
+
progressHandler: progressHandler,
|
|
2271
|
+
phase: "downloading",
|
|
2272
|
+
files: progressFiles
|
|
2273
|
+
)
|
|
2595
2274
|
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2275
|
+
// One attempt: a failed archive falls back to per-file downloads, which retry.
|
|
2276
|
+
switch downloadFileSynchronously(
|
|
2277
|
+
from: archiveUrl,
|
|
2278
|
+
to: compressedPath,
|
|
2279
|
+
retrying: false,
|
|
2280
|
+
progressHandler: { progress in
|
|
2281
|
+
self.updateDiffProgressFile(
|
|
2282
|
+
files: &progressFiles,
|
|
2283
|
+
assetPath: "bundle.tar.br",
|
|
2284
|
+
status: "downloading",
|
|
2285
|
+
progress: progress.progress,
|
|
2286
|
+
downloadPath: "bundle.tar.br",
|
|
2287
|
+
downloadedBytes: progress.downloadedBytes,
|
|
2288
|
+
totalBytes: progress.totalBytes
|
|
2289
|
+
)
|
|
2290
|
+
self.emitDiffProgress(
|
|
2608
2291
|
progressHandler: progressHandler,
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
start: 0,
|
|
2612
|
-
end: UpdateProgress.downloadEnd
|
|
2613
|
-
),
|
|
2614
|
-
downloadedBytes: downloadProgress.downloadedBytes,
|
|
2615
|
-
totalBytes: downloadProgress.totalBytes
|
|
2292
|
+
phase: "downloading",
|
|
2293
|
+
files: progressFiles
|
|
2616
2294
|
)
|
|
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
2295
|
}
|
|
2296
|
+
) {
|
|
2297
|
+
case .success:
|
|
2298
|
+
break
|
|
2299
|
+
case .failure(let error):
|
|
2300
|
+
throw BundleStorageError.downloadFailed(error)
|
|
2301
|
+
}
|
|
2625
2302
|
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2303
|
+
let compressedAttributes = try fileSystem.attributesOfItem(atPath: compressedPath)
|
|
2304
|
+
guard let compressedSize = compressedAttributes[.size] as? NSNumber,
|
|
2305
|
+
compressedSize.int64Value == archive.downloadByteSize,
|
|
2306
|
+
HashUtils.verifyHash(
|
|
2307
|
+
fileURL: URL(fileURLWithPath: compressedPath),
|
|
2308
|
+
expectedHash: archive.downloadFileHash
|
|
2309
|
+
) else {
|
|
2310
|
+
throw UpdateFailureError(
|
|
2311
|
+
underlying: BundleStorageError.invalidBundle,
|
|
2312
|
+
failure: .hashMismatch
|
|
2313
|
+
)
|
|
2314
|
+
}
|
|
2633
2315
|
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
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
|
-
}
|
|
2316
|
+
try UpdateFailureError.during(.extraction) {
|
|
2317
|
+
try BrotliFileDecompressor.decompress(
|
|
2318
|
+
from: compressedPath,
|
|
2319
|
+
to: tarPath,
|
|
2320
|
+
expectedOutputByteSize: archive.tarByteSize
|
|
2321
|
+
)
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
let expectedFiles = try targetManifest.assets.reduce(
|
|
2325
|
+
into: [String: Int64](),
|
|
2326
|
+
{ result, entry in
|
|
2327
|
+
guard let byteSize = entry.value.byteSize else {
|
|
2328
|
+
throw BundleStorageError.invalidBundle
|
|
2657
2329
|
}
|
|
2330
|
+
result[entry.key] = byteSize
|
|
2331
|
+
}
|
|
2332
|
+
)
|
|
2333
|
+
try UpdateFailureError.during(.extraction) {
|
|
2334
|
+
try TarArchiveExtractor.extract(
|
|
2335
|
+
from: tarPath,
|
|
2336
|
+
to: extractedDirectory,
|
|
2337
|
+
expectedFiles: expectedFiles
|
|
2338
|
+
)
|
|
2339
|
+
for (path, asset) in targetManifest.assets {
|
|
2340
|
+
let extractedPath = try FileUtilities.fileURL(
|
|
2341
|
+
for: path,
|
|
2342
|
+
destinationRoot: extractedDirectory
|
|
2343
|
+
).path
|
|
2344
|
+
try verifyManifestAssetFile(atPath: extractedPath, asset: asset)
|
|
2658
2345
|
}
|
|
2659
|
-
self.fileOperationQueue.async(execute: workItem)
|
|
2660
2346
|
}
|
|
2347
|
+
|
|
2348
|
+
try promoteVerifiedArchiveDirectory(
|
|
2349
|
+
preparedDirectory,
|
|
2350
|
+
withArchiveDirectory: extractedDirectory
|
|
2351
|
+
)
|
|
2352
|
+
|
|
2353
|
+
updateDiffProgressFile(
|
|
2354
|
+
files: &progressFiles,
|
|
2355
|
+
assetPath: "bundle.tar.br",
|
|
2356
|
+
status: "downloaded",
|
|
2357
|
+
progress: 1,
|
|
2358
|
+
downloadedBytes: archive.downloadByteSize,
|
|
2359
|
+
totalBytes: archive.downloadByteSize
|
|
2360
|
+
)
|
|
2361
|
+
emitDiffProgress(
|
|
2362
|
+
progressHandler: progressHandler,
|
|
2363
|
+
phase: "finalizing",
|
|
2364
|
+
files: progressFiles
|
|
2661
2365
|
)
|
|
2366
|
+
return progressFiles
|
|
2662
2367
|
}
|
|
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
2368
|
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2369
|
+
private func promoteVerifiedArchiveDirectory(
|
|
2370
|
+
_ preparedDirectory: String,
|
|
2371
|
+
withArchiveDirectory archiveDirectory: String
|
|
2372
|
+
) throws {
|
|
2373
|
+
let backupDirectory = preparedDirectory + ".archive-backup"
|
|
2374
|
+
try? fileSystem.removeItem(atPath: backupDirectory)
|
|
2375
|
+
try fileSystem.moveItem(atPath: preparedDirectory, toPath: backupDirectory)
|
|
2675
2376
|
|
|
2676
|
-
|
|
2377
|
+
do {
|
|
2378
|
+
try fileSystem.moveItem(
|
|
2379
|
+
atPath: archiveDirectory,
|
|
2380
|
+
toPath: preparedDirectory
|
|
2381
|
+
)
|
|
2382
|
+
} catch {
|
|
2383
|
+
try? fileSystem.removeItem(atPath: preparedDirectory)
|
|
2677
2384
|
do {
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2385
|
+
try fileSystem.moveItem(atPath: backupDirectory, toPath: preparedDirectory)
|
|
2386
|
+
} catch let restoreError {
|
|
2387
|
+
throw BundleStorageError.moveOperationFailed(restoreError)
|
|
2388
|
+
}
|
|
2389
|
+
throw error
|
|
2390
|
+
}
|
|
2391
|
+
try? fileSystem.removeItem(atPath: backupDirectory)
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
private func recoverInterruptedPromotions() {
|
|
2395
|
+
guard case .success(let storeDir) = bundleStoreDir(),
|
|
2396
|
+
let entries = try? fileSystem.contentsOfDirectory(atPath: storeDir) else { return }
|
|
2397
|
+
for entry in entries where entry.hasSuffix(".install-backup") {
|
|
2398
|
+
let finalDirectory = (storeDir as NSString).appendingPathComponent(String(entry.dropLast(".install-backup".count)))
|
|
2399
|
+
do {
|
|
2400
|
+
try recoverInterruptedPromotion(finalDirectory: finalDirectory)
|
|
2682
2401
|
} catch {
|
|
2683
|
-
NSLog("[BundleStorage]
|
|
2402
|
+
NSLog("[BundleStorage] Cannot recover interrupted promotion: \(error)")
|
|
2684
2403
|
}
|
|
2685
2404
|
}
|
|
2405
|
+
}
|
|
2686
2406
|
|
|
2687
|
-
|
|
2688
|
-
let
|
|
2689
|
-
|
|
2690
|
-
|
|
2407
|
+
private func recoverInterruptedPromotion(finalDirectory: String) throws {
|
|
2408
|
+
let backup = finalDirectory + ".install-backup"
|
|
2409
|
+
guard fileSystem.fileExists(atPath: backup) else { return }
|
|
2410
|
+
if fileSystem.fileExists(atPath: finalDirectory) {
|
|
2411
|
+
if isCompleteBundleDirectory(finalDirectory) {
|
|
2412
|
+
try fileSystem.removeItem(atPath: backup)
|
|
2413
|
+
return
|
|
2414
|
+
}
|
|
2415
|
+
try fileSystem.removeItem(atPath: finalDirectory)
|
|
2416
|
+
}
|
|
2417
|
+
try fileSystem.moveItem(atPath: backup, toPath: finalDirectory)
|
|
2691
2418
|
}
|
|
2692
2419
|
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
tempBundleFile: String,
|
|
2708
|
-
fileHash: String?,
|
|
2709
|
-
storeDir: String,
|
|
2420
|
+
private func isCompleteBundleDirectory(_ directory: String) -> Bool {
|
|
2421
|
+
let bundleId = (directory as NSString).lastPathComponent
|
|
2422
|
+
guard let manifest = readManifest(in: directory),
|
|
2423
|
+
let parsed = parseBundleManifest(from: manifest),
|
|
2424
|
+
parsed.bundleId == bundleId,
|
|
2425
|
+
case .success(let bundlePath) = findBundleFile(in: directory, expectedBundleId: bundleId),
|
|
2426
|
+
bundlePath != nil else { return false }
|
|
2427
|
+
return parsed.assets.allSatisfy { path, asset in
|
|
2428
|
+
guard let file = try? FileUtilities.fileURL(for: path, destinationRoot: directory) else { return false }
|
|
2429
|
+
return (try? verifyManifestAssetFile(atPath: file.path, asset: asset)) != nil
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
private func promoteStagedBundle(
|
|
2710
2434
|
bundleId: String,
|
|
2711
|
-
|
|
2435
|
+
stagingDirectory: String,
|
|
2436
|
+
finalDirectory: String,
|
|
2437
|
+
bundlePathInStaging: String
|
|
2438
|
+
) throws -> BundleMetadata {
|
|
2439
|
+
releaseStateLock.lock()
|
|
2440
|
+
defer { releaseStateLock.unlock() }
|
|
2441
|
+
let currentMetadata = loadMetadataOrNull() ?? createInitialMetadata()
|
|
2442
|
+
guard let updatedMetadata = prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId) else {
|
|
2443
|
+
throw BundleStorageError.unknown(StaleReleaseSelectionError())
|
|
2444
|
+
}
|
|
2445
|
+
let finalBundlePath = try resolveBundlePathAfterMove(
|
|
2446
|
+
bundlePathInStaging, from: stagingDirectory, to: finalDirectory
|
|
2447
|
+
)
|
|
2448
|
+
let previousPreference = try preferences.getItem(forKey: "HotUpdaterBundleURL")
|
|
2449
|
+
let backup = finalDirectory + ".install-backup"
|
|
2450
|
+
try recoverInterruptedPromotion(finalDirectory: finalDirectory)
|
|
2451
|
+
if fileSystem.fileExists(atPath: finalDirectory) {
|
|
2452
|
+
try fileSystem.moveItem(atPath: finalDirectory, toPath: backup)
|
|
2453
|
+
}
|
|
2454
|
+
var promoted = false
|
|
2455
|
+
do {
|
|
2456
|
+
try fileSystem.moveItem(atPath: stagingDirectory, toPath: finalDirectory)
|
|
2457
|
+
promoted = true
|
|
2458
|
+
try setBundleURL(localPath: finalBundlePath).get()
|
|
2459
|
+
// Metadata is the durable activation point. Launch never uses an
|
|
2460
|
+
// untracked preference if this write fails or the process terminates.
|
|
2461
|
+
guard saveMetadata(updatedMetadata) else {
|
|
2462
|
+
throw UpdateFailureError(
|
|
2463
|
+
underlying: BundleStorageError.unknown(NSError(domain: "HotUpdater", code: 0,
|
|
2464
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to persist bundle activation metadata"])),
|
|
2465
|
+
failure: .storage
|
|
2466
|
+
)
|
|
2467
|
+
}
|
|
2468
|
+
} catch {
|
|
2469
|
+
let installError = error
|
|
2470
|
+
let preferenceResult = setBundleURL(localPath: previousPreference)
|
|
2471
|
+
if promoted { try fileSystem.removeItem(atPath: finalDirectory) }
|
|
2472
|
+
if fileSystem.fileExists(atPath: backup) {
|
|
2473
|
+
try fileSystem.moveItem(atPath: backup, toPath: finalDirectory)
|
|
2474
|
+
}
|
|
2475
|
+
try preferenceResult.get()
|
|
2476
|
+
throw installError
|
|
2477
|
+
}
|
|
2478
|
+
try? fileSystem.removeItem(atPath: backup)
|
|
2479
|
+
return updatedMetadata
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
private func updateBundleFromManifest(
|
|
2483
|
+
bundleId: String,
|
|
2484
|
+
manifestUrl: URL,
|
|
2485
|
+
manifestFileHash: String,
|
|
2486
|
+
archiveUrl: URL?,
|
|
2487
|
+
changedAssets: [String: ChangedAssetDescriptor],
|
|
2488
|
+
storeDir: String,
|
|
2712
2489
|
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
2713
|
-
completion: @escaping (Result<
|
|
2490
|
+
completion: @escaping (Result<UpdateDelivery, Error>) -> Void
|
|
2714
2491
|
) {
|
|
2715
|
-
let
|
|
2716
|
-
|
|
2492
|
+
let tempDirResult = tempDir()
|
|
2493
|
+
guard case .success(let tempDirectory) = tempDirResult else {
|
|
2494
|
+
completion(.failure(UpdateFailureError.classifying(
|
|
2495
|
+
tempDirResult.failureError ?? BundleStorageError.unknown(nil),
|
|
2496
|
+
during: .storage
|
|
2497
|
+
)))
|
|
2498
|
+
return
|
|
2499
|
+
}
|
|
2717
2500
|
|
|
2718
|
-
|
|
2719
|
-
guard self.fileSystem.
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
code: 1,
|
|
2725
|
-
userInfo: [NSLocalizedDescriptionKey: "Downloaded file does not exist atPath: \(location.path)"]
|
|
2726
|
-
))))
|
|
2501
|
+
try? self.fileSystem.removeItem(atPath: tempDirectory)
|
|
2502
|
+
guard self.fileSystem.createDirectory(atPath: tempDirectory) else {
|
|
2503
|
+
completion(.failure(UpdateFailureError.classifying(
|
|
2504
|
+
BundleStorageError.directoryCreationFailed,
|
|
2505
|
+
during: .storage
|
|
2506
|
+
)))
|
|
2727
2507
|
return
|
|
2728
2508
|
}
|
|
2729
2509
|
|
|
2730
|
-
|
|
2510
|
+
let currentBundleId = self.getCachedBundleId()
|
|
2511
|
+
let currentBundleDir = currentBundleId.map {
|
|
2512
|
+
(storeDir as NSString).appendingPathComponent($0)
|
|
2513
|
+
}
|
|
2514
|
+
let tempManifestPath = (tempDirectory as NSString).appendingPathComponent("manifest.json")
|
|
2731
2515
|
let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
|
|
2732
2516
|
let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
2517
|
+
var diffFiles: [UpdateProgressPayload.DiffProgressFileSnapshot] = []
|
|
2518
|
+
// The running step classifies the errors whose type does not.
|
|
2519
|
+
var failurePhase = UpdatePhase.storage
|
|
2520
|
+
var failureResource: UpdateFailureResource?
|
|
2733
2521
|
|
|
2734
2522
|
do {
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
// 4) Create tmpDir
|
|
2742
|
-
guard self.fileSystem.createDirectory(atPath: tmpDir) else {
|
|
2743
|
-
throw BundleStorageError.directoryCreationFailed
|
|
2744
|
-
}
|
|
2745
|
-
NSLog("[BundleStorage] Created tmpDir: \(tmpDir)")
|
|
2746
|
-
logFileSystemDiagnostics(path: tmpDir, context: "TmpDir Created")
|
|
2747
|
-
|
|
2748
|
-
// 5) Verify bundle integrity (hash or signature based on fileHash format)
|
|
2749
|
-
NSLog("[BundleStorage] Verifying bundle integrity...")
|
|
2750
|
-
self.emitArchiveProgress(
|
|
2523
|
+
try recoverInterruptedPromotion(finalDirectory: realDir)
|
|
2524
|
+
failurePhase = .transfer
|
|
2525
|
+
failureResource = .manifest
|
|
2526
|
+
self.emitDiffProgress(
|
|
2751
2527
|
progressHandler: progressHandler,
|
|
2752
|
-
|
|
2528
|
+
phase: "manifest",
|
|
2529
|
+
files: diffFiles,
|
|
2530
|
+
manifestProgress: 0
|
|
2753
2531
|
)
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2532
|
+
switch self.downloadFileSynchronously(
|
|
2533
|
+
from: manifestUrl,
|
|
2534
|
+
to: tempManifestPath,
|
|
2535
|
+
progressHandler: { progress in
|
|
2536
|
+
self.emitDiffProgress(
|
|
2537
|
+
progressHandler: progressHandler,
|
|
2538
|
+
phase: "manifest",
|
|
2539
|
+
files: diffFiles,
|
|
2540
|
+
manifestProgress: progress.progress
|
|
2541
|
+
)
|
|
2542
|
+
}
|
|
2543
|
+
) {
|
|
2757
2544
|
case .success:
|
|
2758
|
-
|
|
2759
|
-
self.emitArchiveProgress(
|
|
2760
|
-
progressHandler: progressHandler,
|
|
2761
|
-
progress: UpdateProgress.verificationEnd
|
|
2762
|
-
)
|
|
2545
|
+
break
|
|
2763
2546
|
case .failure(let error):
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2547
|
+
if let downloadError = error as? DownloadError,
|
|
2548
|
+
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
2549
|
+
throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
2550
|
+
}
|
|
2551
|
+
throw BundleStorageError.downloadFailed(error)
|
|
2769
2552
|
}
|
|
2770
2553
|
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
end: UpdateProgress.extractionEnd
|
|
2782
|
-
)
|
|
2783
|
-
)
|
|
2784
|
-
})
|
|
2785
|
-
NSLog("[BundleStorage] Extraction complete at \(tmpDir)")
|
|
2786
|
-
logFileSystemDiagnostics(path: tmpDir, context: "After Extraction")
|
|
2787
|
-
} catch {
|
|
2788
|
-
let nsError = error as NSError
|
|
2789
|
-
NSLog("[BundleStorage] Extraction failed - Domain: \(nsError.domain), Code: \(nsError.code), Description: \(nsError.localizedDescription)")
|
|
2790
|
-
logFileSystemDiagnostics(path: tmpDir, context: "Extraction Failed")
|
|
2791
|
-
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
2792
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
2793
|
-
completion(.failure(BundleStorageError.extractionFormatError(error)))
|
|
2794
|
-
return
|
|
2554
|
+
failurePhase = .verification
|
|
2555
|
+
let manifestVerificationResult = SignatureVerifier.verifyBundle(
|
|
2556
|
+
fileURL: URL(fileURLWithPath: tempManifestPath),
|
|
2557
|
+
fileHash: manifestFileHash
|
|
2558
|
+
)
|
|
2559
|
+
guard case .success = manifestVerificationResult else {
|
|
2560
|
+
if case .failure(let error) = manifestVerificationResult {
|
|
2561
|
+
throw BundleStorageError.signatureVerificationFailed(error)
|
|
2562
|
+
}
|
|
2563
|
+
throw BundleStorageError.unknown(nil)
|
|
2795
2564
|
}
|
|
2796
2565
|
|
|
2797
|
-
|
|
2798
|
-
try?
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2566
|
+
failurePhase = .manifest
|
|
2567
|
+
guard let manifestData = try? Data(contentsOf: URL(fileURLWithPath: tempManifestPath)),
|
|
2568
|
+
let manifestJson = try JSONSerialization.jsonObject(with: manifestData) as? [String: Any],
|
|
2569
|
+
let targetManifest = parseBundleManifest(from: manifestJson),
|
|
2570
|
+
targetManifest.bundleId == bundleId
|
|
2571
|
+
else {
|
|
2572
|
+
throw BundleStorageError.invalidBundle
|
|
2573
|
+
}
|
|
2574
|
+
// Validate the entire wire contract even when every byte can be reused.
|
|
2575
|
+
guard Set(targetManifest.assets.keys) == Set(changedAssets.keys) else {
|
|
2576
|
+
throw BundleStorageError.invalidBundle
|
|
2577
|
+
}
|
|
2578
|
+
for (path, asset) in targetManifest.assets {
|
|
2579
|
+
guard FileUtilities.normalizedRelativePath(from: path) == path,
|
|
2580
|
+
path != "manifest.json",
|
|
2581
|
+
let descriptor = changedAssets[path],
|
|
2582
|
+
descriptor.fileUrl != nil,
|
|
2583
|
+
descriptor.fileHash.caseInsensitiveCompare(asset.fileHash) == .orderedSame else {
|
|
2584
|
+
throw BundleStorageError.invalidBundle
|
|
2585
|
+
}
|
|
2586
|
+
_ = try FileUtilities.fileURL(for: path, destinationRoot: tmpDir)
|
|
2587
|
+
}
|
|
2816
2588
|
|
|
2817
|
-
|
|
2589
|
+
// Downloads, patches, and extraction below classify their own errors.
|
|
2590
|
+
failurePhase = .storage
|
|
2591
|
+
failureResource = nil
|
|
2592
|
+
guard self.fileSystem.createDirectory(atPath: tmpDir) else {
|
|
2593
|
+
throw BundleStorageError.directoryCreationFailed
|
|
2594
|
+
}
|
|
2595
|
+
try pruneStagingDirectory(tmpDir, assets: Set(targetManifest.assets.keys))
|
|
2596
|
+
let sortedAssets = targetManifest.assets.sorted { $0.key < $1.key }
|
|
2597
|
+
let localDirectories = [currentBundleDir, realDir].compactMap { $0 }.filter { fileSystem.fileExists(atPath: $0) }
|
|
2598
|
+
var downloads: [String: ChangedAssetDescriptor] = [:]
|
|
2599
|
+
for (index, asset) in sortedAssets.enumerated() {
|
|
2600
|
+
// Local preparation has its own progress interval, with no download files.
|
|
2601
|
+
progressHandler(UpdateProgressPayload(
|
|
2602
|
+
progress: 0.15 + 0.05 * Double(index) / Double(max(1, sortedAssets.count)),
|
|
2603
|
+
artifactType: "diff",
|
|
2604
|
+
details: .init(totalFilesCount: 0, completedFilesCount: 0)
|
|
2605
|
+
))
|
|
2606
|
+
let assetPath = asset.key
|
|
2607
|
+
let expectedAsset = asset.value
|
|
2608
|
+
let destinationPath = try FileUtilities.fileURL(for: assetPath, destinationRoot: tmpDir).path
|
|
2609
|
+
guard self.fileSystem.createDirectory(atPath: (destinationPath as NSString).deletingLastPathComponent) else {
|
|
2610
|
+
throw BundleStorageError.directoryCreationFailed
|
|
2611
|
+
}
|
|
2612
|
+
if (try? fileSystem.attributesOfItem(atPath: destinationPath)[.type] as? FileAttributeType) == .typeRegular,
|
|
2613
|
+
(try? verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)) != nil {
|
|
2614
|
+
continue
|
|
2615
|
+
}
|
|
2616
|
+
if fileSystem.fileExists(atPath: destinationPath) { try self.fileSystem.removeItem(atPath: destinationPath) }
|
|
2617
|
+
|
|
2618
|
+
var reused = false
|
|
2619
|
+
// The cached target is only a candidate, just like the active OTA.
|
|
2620
|
+
for directory in localDirectories {
|
|
2621
|
+
guard let sourcePath = try? FileUtilities.fileURL(for: assetPath, destinationRoot: directory).path,
|
|
2622
|
+
(try? fileSystem.attributesOfItem(atPath: sourcePath)[.type] as? FileAttributeType) == .typeRegular,
|
|
2623
|
+
(try? verifyManifestAssetFile(atPath: sourcePath, asset: expectedAsset)) != nil else { continue }
|
|
2818
2624
|
do {
|
|
2819
|
-
try self.fileSystem.
|
|
2820
|
-
|
|
2821
|
-
|
|
2625
|
+
try self.fileSystem.copyItem(atPath: sourcePath, toPath: destinationPath)
|
|
2626
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2627
|
+
reused = true
|
|
2628
|
+
break
|
|
2822
2629
|
} catch {
|
|
2823
|
-
|
|
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)
|
|
2630
|
+
try? self.fileSystem.removeItem(atPath: destinationPath)
|
|
2828
2631
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2632
|
+
}
|
|
2633
|
+
if reused { continue }
|
|
2634
|
+
if builtInAssetResolver.copyIfMatches(assetPath: assetPath, expectedHash: expectedAsset.fileHash, destination: destinationPath) {
|
|
2635
|
+
try UpdateFailureError.during(.verification, resource: .file) {
|
|
2636
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
2637
|
+
}
|
|
2638
|
+
continue
|
|
2639
|
+
}
|
|
2640
|
+
downloads[assetPath] = changedAssets[assetPath]
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
var archiveInstalled = false
|
|
2644
|
+
var patchedFiles = false
|
|
2645
|
+
// The archive is chosen before any patch is tried, so only the
|
|
2646
|
+
// per-file install can fall back from a patch.
|
|
2647
|
+
var patchFallbacks = false
|
|
2648
|
+
if shouldUseArchive(
|
|
2649
|
+
archive: targetManifest.archive,
|
|
2650
|
+
archiveUrl: archiveUrl,
|
|
2651
|
+
downloads: downloads,
|
|
2652
|
+
assets: targetManifest.assets
|
|
2653
|
+
), let archive = targetManifest.archive, let archiveUrl {
|
|
2654
|
+
do {
|
|
2655
|
+
diffFiles = try UpdateFailureError.during(.storage, resource: .archive) {
|
|
2656
|
+
try installArchive(
|
|
2657
|
+
archive: archive,
|
|
2658
|
+
archiveUrl: archiveUrl,
|
|
2659
|
+
targetManifest: targetManifest,
|
|
2660
|
+
preparedDirectory: tmpDir,
|
|
2661
|
+
tempDirectory: tempDirectory,
|
|
2662
|
+
progressHandler: progressHandler
|
|
2663
|
+
)
|
|
2664
|
+
}
|
|
2665
|
+
archiveInstalled = true
|
|
2666
|
+
} catch {
|
|
2667
|
+
NSLog("[BundleStorage] tar.br optimization failed; falling back to individual files: \(error.localizedDescription)")
|
|
2668
|
+
guard fileSystem.fileExists(atPath: tmpDir) else {
|
|
2669
|
+
throw error
|
|
2847
2670
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
case .success:
|
|
2851
|
-
NSLog("[BundleStorage] Successfully set bundle URL: \(finalBundlePath)")
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2852
2673
|
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2674
|
+
if !archiveInstalled {
|
|
2675
|
+
diffFiles = createDiffProgressFiles(changedAssets: downloads)
|
|
2676
|
+
emitDiffProgress(progressHandler: progressHandler, phase: "downloading", files: diffFiles)
|
|
2677
|
+
let progressLock = NSLock()
|
|
2678
|
+
let workers = OperationQueue()
|
|
2679
|
+
workers.maxConcurrentOperationCount = 4
|
|
2680
|
+
var installError: Error?
|
|
2681
|
+
var installOutcomes: [AssetInstallOutcome] = []
|
|
2682
|
+
for progressFile in diffFiles {
|
|
2683
|
+
let assetPath = progressFile.path
|
|
2684
|
+
let expectedAsset = targetManifest.assets[assetPath]!
|
|
2685
|
+
let descriptor = downloads[assetPath]!
|
|
2686
|
+
let destinationPath = try FileUtilities.fileURL(for: assetPath, destinationRoot: tmpDir).path
|
|
2687
|
+
// Separate temp namespaces also avoid collisions between flattened asset paths.
|
|
2688
|
+
let workerTemp = (tempDirectory as NSString).appendingPathComponent(String(progressFile.order))
|
|
2689
|
+
workers.addOperation {
|
|
2690
|
+
progressLock.lock()
|
|
2691
|
+
let shouldSkip = installError != nil
|
|
2692
|
+
progressLock.unlock()
|
|
2693
|
+
if shouldSkip { return }
|
|
2694
|
+
do {
|
|
2695
|
+
guard self.fileSystem.createDirectory(atPath: workerTemp) else { throw BundleStorageError.directoryCreationFailed }
|
|
2696
|
+
let outcome = try self.downloadManifestAsset(
|
|
2697
|
+
assetPath: assetPath, expectedAsset: expectedAsset, changedAsset: descriptor,
|
|
2698
|
+
currentBundleId: currentBundleId, currentBundleDir: currentBundleDir,
|
|
2699
|
+
destinationPath: destinationPath, tempDirectory: workerTemp, progressFile: progressFile,
|
|
2700
|
+
progressHandler: { payload in
|
|
2701
|
+
progressLock.lock()
|
|
2702
|
+
defer { progressLock.unlock() }
|
|
2703
|
+
if let snapshot = payload.details?.files.first,
|
|
2704
|
+
let index = diffFiles.firstIndex(where: { $0.path == assetPath }) {
|
|
2705
|
+
diffFiles[index] = snapshot
|
|
2706
|
+
}
|
|
2707
|
+
self.emitDiffProgress(progressHandler: progressHandler, phase: "downloading", files: diffFiles)
|
|
2708
|
+
}
|
|
2709
|
+
)
|
|
2710
|
+
progressLock.lock()
|
|
2711
|
+
installOutcomes.append(outcome)
|
|
2712
|
+
progressLock.unlock()
|
|
2713
|
+
} catch {
|
|
2714
|
+
progressLock.lock()
|
|
2715
|
+
if installError == nil { installError = error }
|
|
2716
|
+
progressLock.unlock()
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
// Join all workers before cleaning scratch files, even after a failure.
|
|
2721
|
+
workers.waitUntilAllOperationsAreFinished()
|
|
2722
|
+
if let installError { throw installError }
|
|
2723
|
+
for outcome in installOutcomes {
|
|
2724
|
+
switch outcome {
|
|
2725
|
+
case .patched:
|
|
2726
|
+
patchedFiles = true
|
|
2727
|
+
case .downloaded(let patchFallback):
|
|
2728
|
+
patchFallbacks = patchFallbacks || patchFallback
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2856
2732
|
|
|
2857
|
-
|
|
2858
|
-
|
|
2733
|
+
self.emitDiffProgress(
|
|
2734
|
+
progressHandler: progressHandler,
|
|
2735
|
+
phase: "finalizing",
|
|
2736
|
+
files: diffFiles
|
|
2737
|
+
)
|
|
2859
2738
|
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
progress: UpdateProgress.activationReady
|
|
2863
|
-
)
|
|
2864
|
-
self.scheduleCleanupOldBundles(
|
|
2865
|
-
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
2866
|
-
)
|
|
2739
|
+
let manifestDestination = (tmpDir as NSString).appendingPathComponent("manifest.json")
|
|
2740
|
+
try copyVerifiedManifest(from: tempManifestPath, to: manifestDestination)
|
|
2867
2741
|
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
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))
|
|
2742
|
+
switch self.findBundleFile(in: tmpDir, expectedBundleId: bundleId) {
|
|
2743
|
+
case .success(let maybeBundlePath):
|
|
2744
|
+
guard let bundlePathInTmp = maybeBundlePath else {
|
|
2745
|
+
// The manifest does not name exactly one platform bundle file.
|
|
2746
|
+
throw UpdateFailureError(
|
|
2747
|
+
underlying: BundleStorageError.invalidBundle,
|
|
2748
|
+
failure: UpdateFailure.invalidResponse.with(resource: .manifest)
|
|
2749
|
+
)
|
|
2889
2750
|
}
|
|
2890
|
-
|
|
2891
|
-
let
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2751
|
+
|
|
2752
|
+
let updatedMetadata = try promoteStagedBundle(
|
|
2753
|
+
bundleId: bundleId,
|
|
2754
|
+
stagingDirectory: tmpDir,
|
|
2755
|
+
finalDirectory: realDir,
|
|
2756
|
+
bundlePathInStaging: bundlePathInTmp
|
|
2757
|
+
)
|
|
2895
2758
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
2896
|
-
|
|
2759
|
+
self.scheduleCleanupOldBundles(
|
|
2760
|
+
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
2761
|
+
)
|
|
2762
|
+
progressHandler(UpdateProgressPayload(
|
|
2763
|
+
progress: 1.0,
|
|
2764
|
+
artifactType: "diff",
|
|
2765
|
+
details: .init(
|
|
2766
|
+
totalFilesCount: diffFiles.count,
|
|
2767
|
+
completedFilesCount: diffFiles.filter { $0.status == "downloaded" }.count,
|
|
2768
|
+
files: diffFiles
|
|
2769
|
+
)
|
|
2770
|
+
))
|
|
2771
|
+
let delivery: UpdateDelivery.Method =
|
|
2772
|
+
archiveInstalled ? .archive : (patchedFiles ? .patch : .manifest)
|
|
2773
|
+
completion(.success(UpdateDelivery(method: delivery, patchFallback: patchFallbacks)))
|
|
2774
|
+
case .failure(let error):
|
|
2775
|
+
throw error
|
|
2897
2776
|
}
|
|
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)
|
|
2777
|
+
} catch {
|
|
2778
|
+
NSLog("[BundleStorage] Manifest-driven install failed: \(error.localizedDescription).")
|
|
2779
|
+
// Preserve staging for a hash-checked retry. Never trust its presence alone.
|
|
2904
2780
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
completion(.failure(BundleStorageError.unknown(error)))
|
|
2911
|
-
}
|
|
2781
|
+
completion(.failure(UpdateFailureError.classifying(
|
|
2782
|
+
error,
|
|
2783
|
+
during: failurePhase,
|
|
2784
|
+
resource: failureResource
|
|
2785
|
+
)))
|
|
2912
2786
|
}
|
|
2913
2787
|
}
|
|
2914
2788
|
|
|
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
2789
|
func markLaunchCompleted(bundleId: String?) {
|
|
2926
2790
|
guard let bundleId,
|
|
2927
2791
|
var metadata = loadMetadataOrNull(),
|
|
@@ -2935,6 +2799,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2935
2799
|
metadata.stableBundleId = nil
|
|
2936
2800
|
metadata.stableSelection = nil
|
|
2937
2801
|
metadata.verificationPending = false
|
|
2802
|
+
metadata.launchInProgress = false
|
|
2938
2803
|
metadata.pendingTransition = nil
|
|
2939
2804
|
metadata.pendingSelectionTransition = nil
|
|
2940
2805
|
metadata.updatedAt = Date().timeIntervalSince1970 * 1000
|
|
@@ -2999,34 +2864,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
2999
2864
|
}
|
|
3000
2865
|
|
|
3001
2866
|
func getInstallId() -> String {
|
|
3002
|
-
|
|
3003
|
-
return installId
|
|
3004
|
-
}
|
|
3005
|
-
|
|
3006
|
-
let identity = InstallIdentity()
|
|
3007
|
-
guard saveInstallIdentity(identity) else {
|
|
3008
|
-
return identity.installId
|
|
3009
|
-
}
|
|
3010
|
-
return identity.installId
|
|
3011
|
-
}
|
|
3012
|
-
|
|
3013
|
-
func getUserId() -> String? {
|
|
3014
|
-
loadUserIdentity()?.userId
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
func getUsername() -> String? {
|
|
3018
|
-
loadUserIdentity()?.username
|
|
3019
|
-
}
|
|
3020
|
-
|
|
3021
|
-
func setUser(userId: String?, username: String?) {
|
|
3022
|
-
let normalizedUserId = normalizeIdentityValue(userId)
|
|
3023
|
-
let normalizedUsername = normalizeIdentityValue(username)
|
|
3024
|
-
let identity = UserIdentity(userId: normalizedUserId, username: normalizedUsername)
|
|
3025
|
-
if identity.isEmpty {
|
|
3026
|
-
_ = saveUserIdentity(nil)
|
|
3027
|
-
return
|
|
3028
|
-
}
|
|
3029
|
-
_ = saveUserIdentity(identity)
|
|
2867
|
+
loadOrCreateInstallIdentity().installId
|
|
3030
2868
|
}
|
|
3031
2869
|
|
|
3032
2870
|
/**
|