@hot-updater/react-native 0.30.12 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HotUpdater.podspec +1 -0
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/hotupdater/BsdiffPatch.kt +284 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +885 -5
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +13 -2
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +37 -8
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +48 -12
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +60 -22
- package/android/src/newarch/HotUpdater.kt +14 -2
- package/android/src/newarch/HotUpdaterModule.kt +112 -1
- package/android/src/oldarch/HotUpdater.kt +14 -2
- package/android/src/oldarch/HotUpdaterModule.kt +107 -3
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +46 -0
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +29 -1
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -1
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.h +16 -0
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.mm +343 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1093 -47
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +2 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +12 -2
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +64 -5
- package/ios/HotUpdater/Internal/NotificationExtension.swift +2 -1
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +28 -5
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +11 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +11 -5
- package/lib/commonjs/DefaultResolver.js +6 -2
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/DefaultResolver.spec.js +79 -3
- package/lib/commonjs/DefaultResolver.spec.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +6 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +53 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +117 -1
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +40 -16
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +119 -3
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +8 -0
- package/lib/commonjs/sdkVersion.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +72 -5
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.spec.js +265 -8
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/wrap.js +19 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +74 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/DefaultResolver.js +6 -2
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/DefaultResolver.spec.js +78 -3
- package/lib/module/DefaultResolver.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +6 -0
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +53 -18
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +117 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +40 -16
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +119 -3
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +4 -0
- package/lib/module/sdkVersion.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +72 -5
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.spec.js +265 -8
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/wrap.js +19 -5
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +74 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +36 -8
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +56 -3
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +2 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +5 -0
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +43 -29
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +2 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +36 -8
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +56 -3
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +2 -0
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +5 -0
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +43 -29
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +20 -10
- package/src/DefaultResolver.spec.ts +103 -3
- package/src/DefaultResolver.ts +9 -2
- package/src/checkForUpdate.ts +9 -2
- package/src/index.spec.ts +149 -1
- package/src/index.ts +99 -40
- package/src/native.spec.ts +139 -1
- package/src/native.ts +145 -31
- package/src/sdkVersion.ts +1 -0
- package/src/specs/NativeHotUpdater.ts +28 -0
- package/src/store.spec.ts +324 -9
- package/src/store.ts +133 -7
- package/src/wrap.spec.ts +91 -0
- package/src/wrap.tsx +79 -42
|
@@ -1,7 +1,173 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
|
+
@_silgen_name("HotUpdaterApplyBsdiffPatch")
|
|
4
|
+
private func hotUpdaterApplyBsdiffPatchSymbol(
|
|
5
|
+
_ patchPath: NSString,
|
|
6
|
+
_ basePath: NSString,
|
|
7
|
+
_ outputPath: NSString
|
|
8
|
+
) -> ObjCBool
|
|
9
|
+
|
|
10
|
+
private func hotUpdaterApplyBsdiffPatch(
|
|
11
|
+
patchPath: String,
|
|
12
|
+
basePath: String,
|
|
13
|
+
outputPath: String
|
|
14
|
+
) -> Bool {
|
|
15
|
+
return hotUpdaterApplyBsdiffPatchSymbol(
|
|
16
|
+
patchPath as NSString,
|
|
17
|
+
basePath as NSString,
|
|
18
|
+
outputPath as NSString
|
|
19
|
+
).boolValue
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
public typealias ManifestAssets = [String: Any]
|
|
4
23
|
|
|
24
|
+
public struct ChangedAssetDescriptor {
|
|
25
|
+
public let fileUrl: URL?
|
|
26
|
+
public let fileHash: String
|
|
27
|
+
public let fileCompression: String?
|
|
28
|
+
public let patch: BsdiffPatchDescriptor?
|
|
29
|
+
|
|
30
|
+
public init(
|
|
31
|
+
fileUrl: URL?,
|
|
32
|
+
fileHash: String,
|
|
33
|
+
fileCompression: String? = nil,
|
|
34
|
+
patch: BsdiffPatchDescriptor? = nil
|
|
35
|
+
) {
|
|
36
|
+
self.fileUrl = fileUrl
|
|
37
|
+
self.fileHash = fileHash
|
|
38
|
+
self.fileCompression = fileCompression
|
|
39
|
+
self.patch = patch
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public struct BsdiffPatchDescriptor {
|
|
44
|
+
public let algorithm: String
|
|
45
|
+
public let baseBundleId: String
|
|
46
|
+
public let baseFileHash: String
|
|
47
|
+
public let patchFileHash: String
|
|
48
|
+
public let patchUrl: URL
|
|
49
|
+
|
|
50
|
+
public init(
|
|
51
|
+
algorithm: String,
|
|
52
|
+
baseBundleId: String,
|
|
53
|
+
baseFileHash: String,
|
|
54
|
+
patchFileHash: String,
|
|
55
|
+
patchUrl: URL
|
|
56
|
+
) {
|
|
57
|
+
self.algorithm = algorithm
|
|
58
|
+
self.baseBundleId = baseBundleId
|
|
59
|
+
self.baseFileHash = baseFileHash
|
|
60
|
+
self.patchFileHash = patchFileHash
|
|
61
|
+
self.patchUrl = patchUrl
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public struct UpdateProgressPayload {
|
|
66
|
+
public let progress: Double
|
|
67
|
+
public let artifactType: String
|
|
68
|
+
public let downloadedBytes: Int64?
|
|
69
|
+
public let totalBytes: Int64?
|
|
70
|
+
public let details: DiffProgressDetails?
|
|
71
|
+
|
|
72
|
+
public struct DiffProgressFileSnapshot {
|
|
73
|
+
public let path: String
|
|
74
|
+
public let downloadPath: String
|
|
75
|
+
public let status: String
|
|
76
|
+
public let progress: Double
|
|
77
|
+
public let order: Int
|
|
78
|
+
public let downloadedBytes: Int64?
|
|
79
|
+
public let totalBytes: Int64?
|
|
80
|
+
|
|
81
|
+
public init(
|
|
82
|
+
path: String,
|
|
83
|
+
downloadPath: String? = nil,
|
|
84
|
+
status: String,
|
|
85
|
+
progress: Double,
|
|
86
|
+
order: Int,
|
|
87
|
+
downloadedBytes: Int64? = nil,
|
|
88
|
+
totalBytes: Int64? = nil
|
|
89
|
+
) {
|
|
90
|
+
self.path = path
|
|
91
|
+
self.downloadPath = downloadPath ?? path
|
|
92
|
+
self.status = status
|
|
93
|
+
self.progress = progress
|
|
94
|
+
self.order = order
|
|
95
|
+
self.downloadedBytes = downloadedBytes
|
|
96
|
+
self.totalBytes = totalBytes
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public var userInfo: [String: Any] {
|
|
100
|
+
var info: [String: Any] = [
|
|
101
|
+
"path": path,
|
|
102
|
+
"downloadPath": downloadPath,
|
|
103
|
+
"status": status,
|
|
104
|
+
"progress": progress,
|
|
105
|
+
"order": order
|
|
106
|
+
]
|
|
107
|
+
if let downloadedBytes {
|
|
108
|
+
info["downloadedBytes"] = downloadedBytes
|
|
109
|
+
}
|
|
110
|
+
if let totalBytes {
|
|
111
|
+
info["totalBytes"] = totalBytes
|
|
112
|
+
}
|
|
113
|
+
return info
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public struct DiffProgressDetails {
|
|
118
|
+
public let totalFilesCount: Int
|
|
119
|
+
public let completedFilesCount: Int
|
|
120
|
+
public let files: [DiffProgressFileSnapshot]
|
|
121
|
+
|
|
122
|
+
public init(
|
|
123
|
+
totalFilesCount: Int,
|
|
124
|
+
completedFilesCount: Int,
|
|
125
|
+
files: [DiffProgressFileSnapshot] = []
|
|
126
|
+
) {
|
|
127
|
+
self.totalFilesCount = totalFilesCount
|
|
128
|
+
self.completedFilesCount = completedFilesCount
|
|
129
|
+
self.files = files
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public var userInfo: [String: Any] {
|
|
133
|
+
return [
|
|
134
|
+
"totalFilesCount": totalFilesCount,
|
|
135
|
+
"completedFilesCount": completedFilesCount,
|
|
136
|
+
"files": files.map(\.userInfo)
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public init(
|
|
142
|
+
progress: Double,
|
|
143
|
+
artifactType: String,
|
|
144
|
+
downloadedBytes: Int64? = nil,
|
|
145
|
+
totalBytes: Int64? = nil,
|
|
146
|
+
details: DiffProgressDetails? = nil
|
|
147
|
+
) {
|
|
148
|
+
self.progress = progress
|
|
149
|
+
self.artifactType = artifactType
|
|
150
|
+
self.downloadedBytes = downloadedBytes
|
|
151
|
+
self.totalBytes = totalBytes
|
|
152
|
+
self.details = details
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public var userInfo: [String: Any] {
|
|
156
|
+
var info: [String: Any] = [
|
|
157
|
+
"artifactType": artifactType,
|
|
158
|
+
"progress": progress,
|
|
159
|
+
"details": details?.userInfo ?? NSNull()
|
|
160
|
+
]
|
|
161
|
+
if let downloadedBytes {
|
|
162
|
+
info["downloadedBytes"] = downloadedBytes
|
|
163
|
+
}
|
|
164
|
+
if let totalBytes {
|
|
165
|
+
info["totalBytes"] = totalBytes
|
|
166
|
+
}
|
|
167
|
+
return info
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
5
171
|
public enum BundleStorageError: Error, CustomNSError {
|
|
6
172
|
case directoryCreationFailed
|
|
7
173
|
case downloadFailed(Error)
|
|
@@ -108,7 +274,7 @@ public protocol BundleStorageService {
|
|
|
108
274
|
func prepareLaunch(bundle: Bundle, pendingRecovery: PendingCrashRecovery?) -> LaunchSelection
|
|
109
275
|
|
|
110
276
|
// Bundle update
|
|
111
|
-
func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, progressHandler: @escaping (
|
|
277
|
+
func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, manifestUrl: URL?, manifestFileHash: String?, changedAssets: [String: ChangedAssetDescriptor]?, progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<Bool, Error>) -> Void)
|
|
112
278
|
|
|
113
279
|
// Rollback support
|
|
114
280
|
func markLaunchCompleted(bundleId: String?)
|
|
@@ -149,6 +315,16 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
149
315
|
let manifest: ManifestAssets
|
|
150
316
|
}
|
|
151
317
|
|
|
318
|
+
private struct ParsedBundleManifest {
|
|
319
|
+
let bundleId: String
|
|
320
|
+
let assets: [String: ParsedManifestAsset]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private struct ParsedManifestAsset {
|
|
324
|
+
let fileHash: String
|
|
325
|
+
let signature: String?
|
|
326
|
+
}
|
|
327
|
+
|
|
152
328
|
private enum UpdateProgress {
|
|
153
329
|
static let downloadEnd = 0.7
|
|
154
330
|
static let verificationStart = 0.72
|
|
@@ -177,6 +353,342 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
177
353
|
private let activeBundleMetadataLock = NSLock()
|
|
178
354
|
private var activeBundleMetadataSnapshot: ActiveBundleMetadataSnapshot?
|
|
179
355
|
|
|
356
|
+
private func emitArchiveProgress(
|
|
357
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
358
|
+
progress: Double,
|
|
359
|
+
downloadedBytes: Int64? = nil,
|
|
360
|
+
totalBytes: Int64? = nil
|
|
361
|
+
) {
|
|
362
|
+
progressHandler(
|
|
363
|
+
UpdateProgressPayload(
|
|
364
|
+
progress: max(0, min(progress, 1)),
|
|
365
|
+
artifactType: "archive",
|
|
366
|
+
downloadedBytes: downloadedBytes,
|
|
367
|
+
totalBytes: totalBytes
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private func createDiffProgressFiles(
|
|
373
|
+
changedAssets: [String: ChangedAssetDescriptor]
|
|
374
|
+
) -> [UpdateProgressPayload.DiffProgressFileSnapshot] {
|
|
375
|
+
return changedAssets.keys.sorted().enumerated().map { index, path in
|
|
376
|
+
UpdateProgressPayload.DiffProgressFileSnapshot(
|
|
377
|
+
path: path,
|
|
378
|
+
downloadPath: path,
|
|
379
|
+
status: "pending",
|
|
380
|
+
progress: 0,
|
|
381
|
+
order: index
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private func updateDiffProgressFile(
|
|
387
|
+
files: inout [UpdateProgressPayload.DiffProgressFileSnapshot],
|
|
388
|
+
assetPath: String,
|
|
389
|
+
status: String,
|
|
390
|
+
progress: Double,
|
|
391
|
+
downloadPath: String? = nil,
|
|
392
|
+
downloadedBytes: Int64? = nil,
|
|
393
|
+
totalBytes: Int64? = nil
|
|
394
|
+
) {
|
|
395
|
+
guard let fileIndex = files.firstIndex(where: { $0.path == assetPath }) else {
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
files[fileIndex] = UpdateProgressPayload.DiffProgressFileSnapshot(
|
|
400
|
+
path: files[fileIndex].path,
|
|
401
|
+
downloadPath: downloadPath ?? files[fileIndex].downloadPath,
|
|
402
|
+
status: status,
|
|
403
|
+
progress: max(0, min(progress, 1)),
|
|
404
|
+
order: files[fileIndex].order,
|
|
405
|
+
downloadedBytes: downloadedBytes ?? files[fileIndex].downloadedBytes,
|
|
406
|
+
totalBytes: totalBytes ?? files[fileIndex].totalBytes
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private func calculateDiffOverallProgress(
|
|
411
|
+
phase: String,
|
|
412
|
+
files: [UpdateProgressPayload.DiffProgressFileSnapshot],
|
|
413
|
+
manifestProgress: Double = 0
|
|
414
|
+
) -> Double {
|
|
415
|
+
let normalizedManifestProgress = max(0, min(manifestProgress, 1))
|
|
416
|
+
switch phase {
|
|
417
|
+
case "manifest":
|
|
418
|
+
return normalizedManifestProgress * 0.15
|
|
419
|
+
case "downloading":
|
|
420
|
+
guard !files.isEmpty else {
|
|
421
|
+
return 0.92
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
let completedFilesCount = files.filter { $0.status == "downloaded" }.count
|
|
425
|
+
let activeProgressUnits = files
|
|
426
|
+
.filter { $0.status == "downloading" }
|
|
427
|
+
.reduce(0.0) { partialResult, file in
|
|
428
|
+
partialResult + max(0, min(file.progress, 1))
|
|
429
|
+
}
|
|
430
|
+
let normalizedProgress =
|
|
431
|
+
(Double(completedFilesCount) + activeProgressUnits) / Double(files.count)
|
|
432
|
+
return max(0.2, min(0.2 + normalizedProgress * 0.72, 0.92))
|
|
433
|
+
case "finalizing":
|
|
434
|
+
return 0.97
|
|
435
|
+
case "completed":
|
|
436
|
+
return 1.0
|
|
437
|
+
default:
|
|
438
|
+
return 0
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private func emitDiffProgress(
|
|
443
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
444
|
+
phase: String,
|
|
445
|
+
files: [UpdateProgressPayload.DiffProgressFileSnapshot],
|
|
446
|
+
manifestProgress: Double = 0
|
|
447
|
+
) {
|
|
448
|
+
let completedFilesCount = files.filter { $0.status == "downloaded" }.count
|
|
449
|
+
progressHandler(
|
|
450
|
+
UpdateProgressPayload(
|
|
451
|
+
progress: calculateDiffOverallProgress(
|
|
452
|
+
phase: phase,
|
|
453
|
+
files: files,
|
|
454
|
+
manifestProgress: manifestProgress
|
|
455
|
+
),
|
|
456
|
+
artifactType: "diff",
|
|
457
|
+
details: UpdateProgressPayload.DiffProgressDetails(
|
|
458
|
+
totalFilesCount: files.count,
|
|
459
|
+
completedFilesCount: completedFilesCount,
|
|
460
|
+
files: files
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private func resetDiffProgressFile(
|
|
467
|
+
files: inout [UpdateProgressPayload.DiffProgressFileSnapshot],
|
|
468
|
+
assetPath: String,
|
|
469
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void
|
|
470
|
+
) {
|
|
471
|
+
updateDiffProgressFile(
|
|
472
|
+
files: &files,
|
|
473
|
+
assetPath: assetPath,
|
|
474
|
+
status: "pending",
|
|
475
|
+
progress: 0,
|
|
476
|
+
downloadPath: assetPath
|
|
477
|
+
)
|
|
478
|
+
emitDiffProgress(
|
|
479
|
+
progressHandler: progressHandler,
|
|
480
|
+
phase: "downloading",
|
|
481
|
+
files: files
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private func patchTempPath(
|
|
486
|
+
tempDirectory: String,
|
|
487
|
+
assetPath: String
|
|
488
|
+
) -> String {
|
|
489
|
+
let safeName = assetPath
|
|
490
|
+
.replacingOccurrences(of: "/", with: "__")
|
|
491
|
+
.replacingOccurrences(of: "\\", with: "__")
|
|
492
|
+
let patchDirectory = (tempDirectory as NSString).appendingPathComponent("patches")
|
|
493
|
+
_ = fileSystem.createDirectory(atPath: patchDirectory)
|
|
494
|
+
return (patchDirectory as NSString).appendingPathComponent("\(safeName).bsdiff")
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private func patchDownloadPath(assetPath: String) -> String {
|
|
498
|
+
return "\(assetPath).bsdiff"
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private func downloadPath(
|
|
502
|
+
assetPath: String,
|
|
503
|
+
changedAsset: ChangedAssetDescriptor
|
|
504
|
+
) -> String {
|
|
505
|
+
if changedAsset.fileCompression == "br" {
|
|
506
|
+
return "\(assetPath).br"
|
|
507
|
+
}
|
|
508
|
+
return assetPath
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private func compressedTempPath(
|
|
512
|
+
tempDirectory: String,
|
|
513
|
+
assetPath: String,
|
|
514
|
+
compression: String
|
|
515
|
+
) -> String {
|
|
516
|
+
let safeName = assetPath
|
|
517
|
+
.replacingOccurrences(of: "/", with: "__")
|
|
518
|
+
.replacingOccurrences(of: "\\", with: "__")
|
|
519
|
+
let compressedDirectory = (tempDirectory as NSString).appendingPathComponent("compressed")
|
|
520
|
+
_ = fileSystem.createDirectory(atPath: compressedDirectory)
|
|
521
|
+
return (compressedDirectory as NSString).appendingPathComponent("\(safeName).\(compression)")
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private func materializeDownloadedAsset(
|
|
525
|
+
downloadedPath: String,
|
|
526
|
+
destinationPath: String,
|
|
527
|
+
changedAsset: ChangedAssetDescriptor
|
|
528
|
+
) throws {
|
|
529
|
+
guard changedAsset.fileCompression == "br" else {
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
try StreamingTarArchiveExtractor.decompressBrotliFile(
|
|
534
|
+
from: downloadedPath,
|
|
535
|
+
to: destinationPath
|
|
536
|
+
)
|
|
537
|
+
try? fileSystem.removeItem(atPath: downloadedPath)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private func applyPatchAssetIfPossible(
|
|
541
|
+
assetPath: String,
|
|
542
|
+
changedAsset: ChangedAssetDescriptor,
|
|
543
|
+
currentBundleId: String?,
|
|
544
|
+
currentBundleDir: String?,
|
|
545
|
+
destinationPath: String,
|
|
546
|
+
expectedHash: String,
|
|
547
|
+
tempDirectory: String,
|
|
548
|
+
files: inout [UpdateProgressPayload.DiffProgressFileSnapshot],
|
|
549
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void
|
|
550
|
+
) -> Bool {
|
|
551
|
+
guard let patch = changedAsset.patch,
|
|
552
|
+
patch.algorithm == "bsdiff",
|
|
553
|
+
currentBundleId == patch.baseBundleId,
|
|
554
|
+
let currentBundleDir
|
|
555
|
+
else {
|
|
556
|
+
return false
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
let sourcePath = (currentBundleDir as NSString).appendingPathComponent(assetPath)
|
|
560
|
+
guard self.fileSystem.fileExists(atPath: sourcePath),
|
|
561
|
+
HashUtils.verifyHash(
|
|
562
|
+
fileURL: URL(fileURLWithPath: sourcePath),
|
|
563
|
+
expectedHash: patch.baseFileHash
|
|
564
|
+
)
|
|
565
|
+
else {
|
|
566
|
+
return false
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let patchPath = patchTempPath(
|
|
570
|
+
tempDirectory: tempDirectory,
|
|
571
|
+
assetPath: assetPath
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
defer {
|
|
575
|
+
try? self.fileSystem.removeItem(atPath: patchPath)
|
|
576
|
+
if self.fileSystem.fileExists(atPath: destinationPath),
|
|
577
|
+
!HashUtils.verifyHash(
|
|
578
|
+
fileURL: URL(fileURLWithPath: destinationPath),
|
|
579
|
+
expectedHash: expectedHash
|
|
580
|
+
) {
|
|
581
|
+
try? self.fileSystem.removeItem(atPath: destinationPath)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
var updatedFiles = files
|
|
586
|
+
defer {
|
|
587
|
+
files = updatedFiles
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
switch self.downloadFileSynchronously(
|
|
591
|
+
from: patch.patchUrl,
|
|
592
|
+
to: patchPath,
|
|
593
|
+
progressHandler: { progress in
|
|
594
|
+
self.updateDiffProgressFile(
|
|
595
|
+
files: &updatedFiles,
|
|
596
|
+
assetPath: assetPath,
|
|
597
|
+
status: "downloading",
|
|
598
|
+
progress: progress.progress,
|
|
599
|
+
downloadPath: self.patchDownloadPath(assetPath: assetPath),
|
|
600
|
+
downloadedBytes: progress.downloadedBytes,
|
|
601
|
+
totalBytes: progress.totalBytes
|
|
602
|
+
)
|
|
603
|
+
self.emitDiffProgress(
|
|
604
|
+
progressHandler: progressHandler,
|
|
605
|
+
phase: "downloading",
|
|
606
|
+
files: updatedFiles
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
) {
|
|
610
|
+
case .success(let patchFileURL):
|
|
611
|
+
guard HashUtils.verifyHash(
|
|
612
|
+
fileURL: patchFileURL,
|
|
613
|
+
expectedHash: patch.patchFileHash
|
|
614
|
+
) else {
|
|
615
|
+
resetDiffProgressFile(
|
|
616
|
+
files: &updatedFiles,
|
|
617
|
+
assetPath: assetPath,
|
|
618
|
+
progressHandler: progressHandler
|
|
619
|
+
)
|
|
620
|
+
return false
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
let applied = hotUpdaterApplyBsdiffPatch(
|
|
624
|
+
patchPath: patchPath,
|
|
625
|
+
basePath: sourcePath,
|
|
626
|
+
outputPath: destinationPath
|
|
627
|
+
)
|
|
628
|
+
guard applied else {
|
|
629
|
+
resetDiffProgressFile(
|
|
630
|
+
files: &updatedFiles,
|
|
631
|
+
assetPath: assetPath,
|
|
632
|
+
progressHandler: progressHandler
|
|
633
|
+
)
|
|
634
|
+
return false
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
guard HashUtils.verifyHash(
|
|
638
|
+
fileURL: URL(fileURLWithPath: destinationPath),
|
|
639
|
+
expectedHash: expectedHash
|
|
640
|
+
) else {
|
|
641
|
+
resetDiffProgressFile(
|
|
642
|
+
files: &updatedFiles,
|
|
643
|
+
assetPath: assetPath,
|
|
644
|
+
progressHandler: progressHandler
|
|
645
|
+
)
|
|
646
|
+
return false
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
NSLog("[BundleStorage] HotUpdaterBsdiffPatchApplied asset=\(assetPath) baseBundleId=\(patch.baseBundleId)")
|
|
650
|
+
return true
|
|
651
|
+
case .failure:
|
|
652
|
+
resetDiffProgressFile(
|
|
653
|
+
files: &updatedFiles,
|
|
654
|
+
assetPath: assetPath,
|
|
655
|
+
progressHandler: progressHandler
|
|
656
|
+
)
|
|
657
|
+
return false
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private func verifyManifestAssetFile(
|
|
662
|
+
atPath path: String,
|
|
663
|
+
asset: ParsedManifestAsset
|
|
664
|
+
) throws {
|
|
665
|
+
guard let actualHash = HashUtils.calculateSHA256(fileURL: URL(fileURLWithPath: path)) else {
|
|
666
|
+
throw BundleStorageError.signatureVerificationFailed(.fileReadFailed)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
guard actualHash.caseInsensitiveCompare(asset.fileHash) == .orderedSame else {
|
|
670
|
+
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
guard SignatureVerifier.isSigningEnabled() else {
|
|
674
|
+
return
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
guard let signature = asset.signature, !signature.isEmpty else {
|
|
678
|
+
throw BundleStorageError.signatureVerificationFailed(.invalidSignatureFormat)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
switch SignatureVerifier.verifyHashSignature(
|
|
682
|
+
fileHash: asset.fileHash,
|
|
683
|
+
signatureBase64: signature
|
|
684
|
+
) {
|
|
685
|
+
case .success:
|
|
686
|
+
return
|
|
687
|
+
case .failure(let error):
|
|
688
|
+
throw BundleStorageError.signatureVerificationFailed(error)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
180
692
|
public init(fileSystem: FileSystemService,
|
|
181
693
|
downloadService: DownloadService,
|
|
182
694
|
decompressService: DecompressService,
|
|
@@ -349,6 +861,23 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
349
861
|
}
|
|
350
862
|
}
|
|
351
863
|
|
|
864
|
+
func canUseManifestDrivenInstall() -> Bool {
|
|
865
|
+
guard let currentBundleURL = getCachedBundleURL(),
|
|
866
|
+
case .success(let storeDir) = bundleStoreDir() else {
|
|
867
|
+
return false
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
let currentBundleId = currentBundleURL.deletingLastPathComponent().lastPathComponent
|
|
871
|
+
let currentBundleDir = (storeDir as NSString).appendingPathComponent(currentBundleId)
|
|
872
|
+
guard fileSystem.fileExists(atPath: currentBundleDir),
|
|
873
|
+
let snapshot = getActiveBundleMetadataSnapshot(),
|
|
874
|
+
let currentManifest = parseBundleManifest(from: snapshot.manifest) else {
|
|
875
|
+
return false
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return currentManifest.assets.isEmpty == false
|
|
879
|
+
}
|
|
880
|
+
|
|
352
881
|
private func resolveActiveBundleMetadataSnapshot(
|
|
353
882
|
activeBundleId: String,
|
|
354
883
|
bundleDirectory: String
|
|
@@ -429,6 +958,65 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
429
958
|
}
|
|
430
959
|
}
|
|
431
960
|
|
|
961
|
+
private func parseBundleManifest(from manifest: [String: Any]) -> ParsedBundleManifest? {
|
|
962
|
+
guard let manifestBundleId = (manifest["bundleId"] as? String)?
|
|
963
|
+
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
964
|
+
!manifestBundleId.isEmpty,
|
|
965
|
+
let rawAssets = manifest["assets"] as? [String: Any]
|
|
966
|
+
else {
|
|
967
|
+
return nil
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
var assets: [String: ParsedManifestAsset] = [:]
|
|
971
|
+
for (assetPath, assetValue) in rawAssets {
|
|
972
|
+
guard let asset = assetValue as? [String: Any],
|
|
973
|
+
let fileHash = asset["fileHash"] as? String,
|
|
974
|
+
!fileHash.isEmpty
|
|
975
|
+
else {
|
|
976
|
+
return nil
|
|
977
|
+
}
|
|
978
|
+
let signature = (asset["signature"] as? String)?
|
|
979
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
980
|
+
assets[assetPath] = ParsedManifestAsset(
|
|
981
|
+
fileHash: fileHash,
|
|
982
|
+
signature: signature?.isEmpty == false ? signature : nil
|
|
983
|
+
)
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return ParsedBundleManifest(bundleId: manifestBundleId, assets: assets)
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
private func parseBundleManifest(fromFile manifestPath: String) -> ParsedBundleManifest? {
|
|
990
|
+
guard let manifest = readManifest(in: (manifestPath as NSString).deletingLastPathComponent) else {
|
|
991
|
+
return nil
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return parseBundleManifest(from: manifest)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private func writeManifestFile(_ manifest: ParsedBundleManifest, to destination: String) throws {
|
|
998
|
+
let manifestDirectory = (destination as NSString).deletingLastPathComponent
|
|
999
|
+
guard fileSystem.createDirectory(atPath: manifestDirectory) else {
|
|
1000
|
+
throw BundleStorageError.directoryCreationFailed
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
let assets = manifest.assets
|
|
1004
|
+
.sorted { $0.key < $1.key }
|
|
1005
|
+
.reduce(into: [String: [String: String]]()) { partialResult, entry in
|
|
1006
|
+
var assetPayload = ["fileHash": entry.value.fileHash]
|
|
1007
|
+
if let signature = entry.value.signature, !signature.isEmpty {
|
|
1008
|
+
assetPayload["signature"] = signature
|
|
1009
|
+
}
|
|
1010
|
+
partialResult[entry.key] = assetPayload
|
|
1011
|
+
}
|
|
1012
|
+
let payload: [String: Any] = [
|
|
1013
|
+
"bundleId": manifest.bundleId,
|
|
1014
|
+
"assets": assets,
|
|
1015
|
+
]
|
|
1016
|
+
let data = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted])
|
|
1017
|
+
try data.write(to: URL(fileURLWithPath: destination))
|
|
1018
|
+
}
|
|
1019
|
+
|
|
432
1020
|
/**
|
|
433
1021
|
* Checks if isolationKey has changed and cleans up old bundles if needed.
|
|
434
1022
|
* This handles migration when isolationKey format changes.
|
|
@@ -648,6 +1236,28 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
648
1236
|
}
|
|
649
1237
|
DispatchQueue.global(qos: .background).async(execute: workItem)
|
|
650
1238
|
}
|
|
1239
|
+
|
|
1240
|
+
private func downloadFileSynchronously(
|
|
1241
|
+
from url: URL,
|
|
1242
|
+
to destination: String,
|
|
1243
|
+
progressHandler: @escaping (DownloadProgress) -> Void
|
|
1244
|
+
) -> Result<URL, Error> {
|
|
1245
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
1246
|
+
var finalResult: Result<URL, Error> = .failure(BundleStorageError.unknown(nil))
|
|
1247
|
+
|
|
1248
|
+
_ = downloadService.downloadFile(
|
|
1249
|
+
from: url,
|
|
1250
|
+
to: destination,
|
|
1251
|
+
fileSizeHandler: nil,
|
|
1252
|
+
progressHandler: progressHandler
|
|
1253
|
+
) { result in
|
|
1254
|
+
finalResult = result
|
|
1255
|
+
semaphore.signal()
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
semaphore.wait()
|
|
1259
|
+
return finalResult
|
|
1260
|
+
}
|
|
651
1261
|
|
|
652
1262
|
// MARK: - Bundle File Operations
|
|
653
1263
|
|
|
@@ -881,7 +1491,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
881
1491
|
* @param progressHandler Callback for download and extraction progress (0.0 to 1.0)
|
|
882
1492
|
* @param completion Callback with result of the operation
|
|
883
1493
|
*/
|
|
884
|
-
func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, progressHandler: @escaping (
|
|
1494
|
+
func updateBundle(bundleId: String, fileUrl: URL?, fileHash: String?, manifestUrl: URL?, manifestFileHash: String?, changedAssets: [String: ChangedAssetDescriptor]?, progressHandler: @escaping (UpdateProgressPayload) -> Void, completion: @escaping (Result<Bool, Error>) -> Void) {
|
|
885
1495
|
// Check if bundle is in crashed history
|
|
886
1496
|
let crashedHistory = loadCrashedHistory()
|
|
887
1497
|
if crashedHistory.contains(bundleId) {
|
|
@@ -944,7 +1554,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
944
1554
|
let _ = self.saveMetadata(updatedMetadata)
|
|
945
1555
|
NSLog("[BundleStorage] Set staging bundle (cached): \(bundleId), verificationPending: true")
|
|
946
1556
|
|
|
947
|
-
|
|
1557
|
+
self.emitArchiveProgress(
|
|
1558
|
+
progressHandler: progressHandler,
|
|
1559
|
+
progress: UpdateProgress.complete
|
|
1560
|
+
)
|
|
948
1561
|
self.scheduleCleanupOldBundles(
|
|
949
1562
|
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
950
1563
|
)
|
|
@@ -958,7 +1571,27 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
958
1571
|
do {
|
|
959
1572
|
try self.fileSystem.removeItem(atPath: finalBundleDir)
|
|
960
1573
|
// Continue with download process on success
|
|
961
|
-
|
|
1574
|
+
if let manifestUrl,
|
|
1575
|
+
let manifestFileHash,
|
|
1576
|
+
let changedAssets,
|
|
1577
|
+
self.canUseManifestDrivenInstall() {
|
|
1578
|
+
self.updateBundleFromManifest(
|
|
1579
|
+
bundleId: bundleId,
|
|
1580
|
+
fileUrl: validFileUrl,
|
|
1581
|
+
fileHash: fileHash,
|
|
1582
|
+
manifestUrl: manifestUrl,
|
|
1583
|
+
manifestFileHash: manifestFileHash,
|
|
1584
|
+
changedAssets: changedAssets,
|
|
1585
|
+
storeDir: storeDir,
|
|
1586
|
+
progressHandler: progressHandler,
|
|
1587
|
+
completion: completion
|
|
1588
|
+
)
|
|
1589
|
+
} else {
|
|
1590
|
+
if manifestUrl != nil && manifestFileHash != nil && changedAssets != nil {
|
|
1591
|
+
NSLog("[BundleStorage] Skipping manifest-driven install for \(bundleId) because no active OTA manifest is available. Using archive.")
|
|
1592
|
+
}
|
|
1593
|
+
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
|
|
1594
|
+
}
|
|
962
1595
|
} catch let error {
|
|
963
1596
|
NSLog("[BundleStorage] Failed to remove invalid bundle dir: \(error.localizedDescription)")
|
|
964
1597
|
completion(.failure(BundleStorageError.unknown(error)))
|
|
@@ -968,7 +1601,27 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
968
1601
|
completion(.failure(error))
|
|
969
1602
|
}
|
|
970
1603
|
} else {
|
|
971
|
-
|
|
1604
|
+
if let manifestUrl,
|
|
1605
|
+
let manifestFileHash,
|
|
1606
|
+
let changedAssets,
|
|
1607
|
+
self.canUseManifestDrivenInstall() {
|
|
1608
|
+
self.updateBundleFromManifest(
|
|
1609
|
+
bundleId: bundleId,
|
|
1610
|
+
fileUrl: validFileUrl,
|
|
1611
|
+
fileHash: fileHash,
|
|
1612
|
+
manifestUrl: manifestUrl,
|
|
1613
|
+
manifestFileHash: manifestFileHash,
|
|
1614
|
+
changedAssets: changedAssets,
|
|
1615
|
+
storeDir: storeDir,
|
|
1616
|
+
progressHandler: progressHandler,
|
|
1617
|
+
completion: completion
|
|
1618
|
+
)
|
|
1619
|
+
} else {
|
|
1620
|
+
if manifestUrl != nil && manifestFileHash != nil && changedAssets != nil {
|
|
1621
|
+
NSLog("[BundleStorage] Skipping manifest-driven install for \(bundleId) because no active OTA manifest is available. Using archive.")
|
|
1622
|
+
}
|
|
1623
|
+
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, fileHash: fileHash, storeDir: storeDir, progressHandler: progressHandler, completion: completion)
|
|
1624
|
+
}
|
|
972
1625
|
}
|
|
973
1626
|
}
|
|
974
1627
|
}
|
|
@@ -983,12 +1636,391 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
983
1636
|
* @param progressHandler Callback for download and extraction progress
|
|
984
1637
|
* @param completion Callback with result of the operation
|
|
985
1638
|
*/
|
|
1639
|
+
private func updateBundleFromManifest(
|
|
1640
|
+
bundleId: String,
|
|
1641
|
+
fileUrl: URL,
|
|
1642
|
+
fileHash: String?,
|
|
1643
|
+
manifestUrl: URL,
|
|
1644
|
+
manifestFileHash: String,
|
|
1645
|
+
changedAssets: [String: ChangedAssetDescriptor],
|
|
1646
|
+
storeDir: String,
|
|
1647
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
1648
|
+
completion: @escaping (Result<Bool, Error>) -> Void
|
|
1649
|
+
) {
|
|
1650
|
+
let tempDirResult = tempDir()
|
|
1651
|
+
guard case .success(let tempDirectory) = tempDirResult else {
|
|
1652
|
+
completion(.failure(tempDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
1653
|
+
return
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
try? self.fileSystem.removeItem(atPath: tempDirectory)
|
|
1657
|
+
guard self.fileSystem.createDirectory(atPath: tempDirectory) else {
|
|
1658
|
+
completion(.failure(BundleStorageError.directoryCreationFailed))
|
|
1659
|
+
return
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
|
|
1663
|
+
let currentBundleDir = currentBundleId.map {
|
|
1664
|
+
(storeDir as NSString).appendingPathComponent($0)
|
|
1665
|
+
}
|
|
1666
|
+
let tempManifestPath = (tempDirectory as NSString).appendingPathComponent("manifest.json")
|
|
1667
|
+
let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
|
|
1668
|
+
let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
1669
|
+
var diffFiles = createDiffProgressFiles(changedAssets: changedAssets)
|
|
1670
|
+
|
|
1671
|
+
do {
|
|
1672
|
+
self.emitDiffProgress(
|
|
1673
|
+
progressHandler: progressHandler,
|
|
1674
|
+
phase: "manifest",
|
|
1675
|
+
files: diffFiles,
|
|
1676
|
+
manifestProgress: 0
|
|
1677
|
+
)
|
|
1678
|
+
switch self.downloadFileSynchronously(
|
|
1679
|
+
from: manifestUrl,
|
|
1680
|
+
to: tempManifestPath,
|
|
1681
|
+
progressHandler: { progress in
|
|
1682
|
+
self.emitDiffProgress(
|
|
1683
|
+
progressHandler: progressHandler,
|
|
1684
|
+
phase: "manifest",
|
|
1685
|
+
files: diffFiles,
|
|
1686
|
+
manifestProgress: progress.progress
|
|
1687
|
+
)
|
|
1688
|
+
}
|
|
1689
|
+
) {
|
|
1690
|
+
case .success:
|
|
1691
|
+
break
|
|
1692
|
+
case .failure(let error):
|
|
1693
|
+
if let downloadError = error as? DownloadError,
|
|
1694
|
+
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
1695
|
+
throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
1696
|
+
}
|
|
1697
|
+
throw BundleStorageError.downloadFailed(error)
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
let manifestVerificationResult = SignatureVerifier.verifyBundle(
|
|
1701
|
+
fileURL: URL(fileURLWithPath: tempManifestPath),
|
|
1702
|
+
fileHash: manifestFileHash
|
|
1703
|
+
)
|
|
1704
|
+
guard case .success = manifestVerificationResult else {
|
|
1705
|
+
if case .failure(let error) = manifestVerificationResult {
|
|
1706
|
+
throw BundleStorageError.signatureVerificationFailed(error)
|
|
1707
|
+
}
|
|
1708
|
+
throw BundleStorageError.unknown(nil)
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
guard let manifestData = try? Data(contentsOf: URL(fileURLWithPath: tempManifestPath)),
|
|
1712
|
+
let manifestJson = try JSONSerialization.jsonObject(with: manifestData) as? [String: Any],
|
|
1713
|
+
let targetManifest = parseBundleManifest(from: manifestJson),
|
|
1714
|
+
targetManifest.bundleId == bundleId
|
|
1715
|
+
else {
|
|
1716
|
+
throw BundleStorageError.invalidBundle
|
|
1717
|
+
}
|
|
1718
|
+
self.emitDiffProgress(
|
|
1719
|
+
progressHandler: progressHandler,
|
|
1720
|
+
phase: diffFiles.isEmpty ? "finalizing" : "downloading",
|
|
1721
|
+
files: diffFiles
|
|
1722
|
+
)
|
|
1723
|
+
|
|
1724
|
+
let currentManifest = getActiveBundleMetadataSnapshot().flatMap { snapshot in
|
|
1725
|
+
parseBundleManifest(from: snapshot.manifest)
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
if self.fileSystem.fileExists(atPath: tmpDir) {
|
|
1729
|
+
try self.fileSystem.removeItem(atPath: tmpDir)
|
|
1730
|
+
}
|
|
1731
|
+
guard self.fileSystem.createDirectory(atPath: tmpDir) else {
|
|
1732
|
+
throw BundleStorageError.directoryCreationFailed
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
let sortedAssets = targetManifest.assets.sorted { $0.key < $1.key }
|
|
1736
|
+
|
|
1737
|
+
for asset in sortedAssets {
|
|
1738
|
+
let assetPath = asset.key
|
|
1739
|
+
let expectedAsset = asset.value
|
|
1740
|
+
let expectedHash = expectedAsset.fileHash
|
|
1741
|
+
let destinationPath = (tmpDir as NSString).appendingPathComponent(assetPath)
|
|
1742
|
+
let destinationDir = (destinationPath as NSString).deletingLastPathComponent
|
|
1743
|
+
guard self.fileSystem.createDirectory(atPath: destinationDir) else {
|
|
1744
|
+
throw BundleStorageError.directoryCreationFailed
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
if currentManifest?.assets[assetPath]?.fileHash == expectedHash {
|
|
1748
|
+
guard let currentBundleDir,
|
|
1749
|
+
self.fileSystem.fileExists(atPath: currentBundleDir)
|
|
1750
|
+
else {
|
|
1751
|
+
throw BundleStorageError.downloadFailed(
|
|
1752
|
+
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
1753
|
+
NSLocalizedDescriptionKey: "Current bundle directory unavailable for reused asset: \(assetPath)"
|
|
1754
|
+
])
|
|
1755
|
+
)
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
let sourcePath = (currentBundleDir as NSString).appendingPathComponent(assetPath)
|
|
1759
|
+
guard self.fileSystem.fileExists(atPath: sourcePath),
|
|
1760
|
+
HashUtils.verifyHash(fileURL: URL(fileURLWithPath: sourcePath), expectedHash: expectedHash)
|
|
1761
|
+
else {
|
|
1762
|
+
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
try self.fileSystem.copyItem(atPath: sourcePath, toPath: destinationPath)
|
|
1766
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
1767
|
+
continue
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
guard let changedAsset = changedAssets[assetPath] else {
|
|
1771
|
+
updateDiffProgressFile(
|
|
1772
|
+
files: &diffFiles,
|
|
1773
|
+
assetPath: assetPath,
|
|
1774
|
+
status: "failed",
|
|
1775
|
+
progress: 0
|
|
1776
|
+
)
|
|
1777
|
+
self.emitDiffProgress(
|
|
1778
|
+
progressHandler: progressHandler,
|
|
1779
|
+
phase: "downloading",
|
|
1780
|
+
files: diffFiles
|
|
1781
|
+
)
|
|
1782
|
+
throw BundleStorageError.downloadFailed(
|
|
1783
|
+
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
1784
|
+
NSLocalizedDescriptionKey: "Changed asset missing from update response: \(assetPath)"
|
|
1785
|
+
])
|
|
1786
|
+
)
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
guard changedAsset.fileHash.caseInsensitiveCompare(expectedHash) == .orderedSame else {
|
|
1790
|
+
updateDiffProgressFile(
|
|
1791
|
+
files: &diffFiles,
|
|
1792
|
+
assetPath: assetPath,
|
|
1793
|
+
status: "failed",
|
|
1794
|
+
progress: 0
|
|
1795
|
+
)
|
|
1796
|
+
self.emitDiffProgress(
|
|
1797
|
+
progressHandler: progressHandler,
|
|
1798
|
+
phase: "downloading",
|
|
1799
|
+
files: diffFiles
|
|
1800
|
+
)
|
|
1801
|
+
throw BundleStorageError.signatureVerificationFailed(.fileHashMismatch)
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
let patched = applyPatchAssetIfPossible(
|
|
1805
|
+
assetPath: assetPath,
|
|
1806
|
+
changedAsset: changedAsset,
|
|
1807
|
+
currentBundleId: currentBundleId,
|
|
1808
|
+
currentBundleDir: currentBundleDir,
|
|
1809
|
+
destinationPath: destinationPath,
|
|
1810
|
+
expectedHash: expectedHash,
|
|
1811
|
+
tempDirectory: tempDirectory,
|
|
1812
|
+
files: &diffFiles,
|
|
1813
|
+
progressHandler: progressHandler
|
|
1814
|
+
)
|
|
1815
|
+
if patched {
|
|
1816
|
+
try verifyManifestAssetFile(atPath: destinationPath, asset: expectedAsset)
|
|
1817
|
+
updateDiffProgressFile(
|
|
1818
|
+
files: &diffFiles,
|
|
1819
|
+
assetPath: assetPath,
|
|
1820
|
+
status: "downloaded",
|
|
1821
|
+
progress: 1
|
|
1822
|
+
)
|
|
1823
|
+
self.emitDiffProgress(
|
|
1824
|
+
progressHandler: progressHandler,
|
|
1825
|
+
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
1826
|
+
? "finalizing"
|
|
1827
|
+
: "downloading",
|
|
1828
|
+
files: diffFiles
|
|
1829
|
+
)
|
|
1830
|
+
continue
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
guard let changedAssetFileUrl = changedAsset.fileUrl else {
|
|
1834
|
+
updateDiffProgressFile(
|
|
1835
|
+
files: &diffFiles,
|
|
1836
|
+
assetPath: assetPath,
|
|
1837
|
+
status: "failed",
|
|
1838
|
+
progress: 0
|
|
1839
|
+
)
|
|
1840
|
+
self.emitDiffProgress(
|
|
1841
|
+
progressHandler: progressHandler,
|
|
1842
|
+
phase: "downloading",
|
|
1843
|
+
files: diffFiles
|
|
1844
|
+
)
|
|
1845
|
+
throw BundleStorageError.downloadFailed(
|
|
1846
|
+
NSError(domain: "HotUpdater", code: 0, userInfo: [
|
|
1847
|
+
NSLocalizedDescriptionKey: "Changed asset fileUrl missing and patch could not be applied: \(assetPath)"
|
|
1848
|
+
])
|
|
1849
|
+
)
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
let changedAssetDownloadPath = downloadPath(
|
|
1853
|
+
assetPath: assetPath,
|
|
1854
|
+
changedAsset: changedAsset
|
|
1855
|
+
)
|
|
1856
|
+
let downloadDestinationPath: String
|
|
1857
|
+
if changedAsset.fileCompression == "br" {
|
|
1858
|
+
downloadDestinationPath = compressedTempPath(
|
|
1859
|
+
tempDirectory: tempDirectory,
|
|
1860
|
+
assetPath: assetPath,
|
|
1861
|
+
compression: "br"
|
|
1862
|
+
)
|
|
1863
|
+
} else {
|
|
1864
|
+
downloadDestinationPath = destinationPath
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
switch self.downloadFileSynchronously(
|
|
1868
|
+
from: changedAssetFileUrl,
|
|
1869
|
+
to: downloadDestinationPath,
|
|
1870
|
+
progressHandler: { progress in
|
|
1871
|
+
self.updateDiffProgressFile(
|
|
1872
|
+
files: &diffFiles,
|
|
1873
|
+
assetPath: assetPath,
|
|
1874
|
+
status: "downloading",
|
|
1875
|
+
progress: progress.progress,
|
|
1876
|
+
downloadPath: changedAssetDownloadPath,
|
|
1877
|
+
downloadedBytes: progress.downloadedBytes,
|
|
1878
|
+
totalBytes: progress.totalBytes
|
|
1879
|
+
)
|
|
1880
|
+
self.emitDiffProgress(
|
|
1881
|
+
progressHandler: progressHandler,
|
|
1882
|
+
phase: "downloading",
|
|
1883
|
+
files: diffFiles
|
|
1884
|
+
)
|
|
1885
|
+
}
|
|
1886
|
+
) {
|
|
1887
|
+
case .success(let downloadedFileURL):
|
|
1888
|
+
do {
|
|
1889
|
+
try materializeDownloadedAsset(
|
|
1890
|
+
downloadedPath: downloadedFileURL.path,
|
|
1891
|
+
destinationPath: destinationPath,
|
|
1892
|
+
changedAsset: changedAsset
|
|
1893
|
+
)
|
|
1894
|
+
try verifyManifestAssetFile(
|
|
1895
|
+
atPath: destinationPath,
|
|
1896
|
+
asset: expectedAsset
|
|
1897
|
+
)
|
|
1898
|
+
} catch {
|
|
1899
|
+
updateDiffProgressFile(
|
|
1900
|
+
files: &diffFiles,
|
|
1901
|
+
assetPath: assetPath,
|
|
1902
|
+
status: "failed",
|
|
1903
|
+
progress: 1
|
|
1904
|
+
)
|
|
1905
|
+
self.emitDiffProgress(
|
|
1906
|
+
progressHandler: progressHandler,
|
|
1907
|
+
phase: "downloading",
|
|
1908
|
+
files: diffFiles
|
|
1909
|
+
)
|
|
1910
|
+
throw error
|
|
1911
|
+
}
|
|
1912
|
+
updateDiffProgressFile(
|
|
1913
|
+
files: &diffFiles,
|
|
1914
|
+
assetPath: assetPath,
|
|
1915
|
+
status: "downloaded",
|
|
1916
|
+
progress: 1
|
|
1917
|
+
)
|
|
1918
|
+
self.emitDiffProgress(
|
|
1919
|
+
progressHandler: progressHandler,
|
|
1920
|
+
phase: diffFiles.allSatisfy { $0.status == "downloaded" }
|
|
1921
|
+
? "finalizing"
|
|
1922
|
+
: "downloading",
|
|
1923
|
+
files: diffFiles
|
|
1924
|
+
)
|
|
1925
|
+
case .failure(let error):
|
|
1926
|
+
let lastKnownProgress = diffFiles.first(where: { $0.path == assetPath })?.progress ?? 0
|
|
1927
|
+
updateDiffProgressFile(
|
|
1928
|
+
files: &diffFiles,
|
|
1929
|
+
assetPath: assetPath,
|
|
1930
|
+
status: "failed",
|
|
1931
|
+
progress: lastKnownProgress
|
|
1932
|
+
)
|
|
1933
|
+
self.emitDiffProgress(
|
|
1934
|
+
progressHandler: progressHandler,
|
|
1935
|
+
phase: "downloading",
|
|
1936
|
+
files: diffFiles
|
|
1937
|
+
)
|
|
1938
|
+
if let downloadError = error as? DownloadError,
|
|
1939
|
+
case .incompleteDownload(let expected, let actual) = downloadError {
|
|
1940
|
+
throw BundleStorageError.incompleteDownload(expected: expected, actual: actual)
|
|
1941
|
+
}
|
|
1942
|
+
throw BundleStorageError.downloadFailed(error)
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
self.emitDiffProgress(
|
|
1947
|
+
progressHandler: progressHandler,
|
|
1948
|
+
phase: "finalizing",
|
|
1949
|
+
files: diffFiles
|
|
1950
|
+
)
|
|
1951
|
+
|
|
1952
|
+
let manifestDestination = (tmpDir as NSString).appendingPathComponent("manifest.json")
|
|
1953
|
+
try writeManifestFile(targetManifest, to: manifestDestination)
|
|
1954
|
+
|
|
1955
|
+
switch self.findBundleFile(in: tmpDir) {
|
|
1956
|
+
case .success(let maybeBundlePath):
|
|
1957
|
+
guard let bundlePathInTmp = maybeBundlePath else {
|
|
1958
|
+
throw BundleStorageError.invalidBundle
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
if self.fileSystem.fileExists(atPath: realDir) {
|
|
1962
|
+
try self.fileSystem.removeItem(atPath: realDir)
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
do {
|
|
1966
|
+
try self.fileSystem.moveItem(atPath: tmpDir, toPath: realDir)
|
|
1967
|
+
} catch {
|
|
1968
|
+
throw BundleStorageError.moveOperationFailed(error)
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
let finalBundlePath = (realDir as NSString).appendingPathComponent(
|
|
1972
|
+
(bundlePathInTmp as NSString).lastPathComponent
|
|
1973
|
+
)
|
|
1974
|
+
switch self.setBundleURL(localPath: finalBundlePath) {
|
|
1975
|
+
case .success:
|
|
1976
|
+
let currentMetadata = self.loadMetadataOrNull() ?? self.createInitialMetadata()
|
|
1977
|
+
let updatedMetadata = self.prepareMetadataForNewStagingBundle(currentMetadata, bundleId: bundleId)
|
|
1978
|
+
let _ = self.saveMetadata(updatedMetadata)
|
|
1979
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
1980
|
+
self.scheduleCleanupOldBundles(
|
|
1981
|
+
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
1982
|
+
)
|
|
1983
|
+
progressHandler(
|
|
1984
|
+
UpdateProgressPayload(
|
|
1985
|
+
progress: UpdateProgress.complete,
|
|
1986
|
+
artifactType: "diff",
|
|
1987
|
+
details: UpdateProgressPayload.DiffProgressDetails(
|
|
1988
|
+
totalFilesCount: diffFiles.count,
|
|
1989
|
+
completedFilesCount: diffFiles.filter { $0.status == "downloaded" }.count,
|
|
1990
|
+
files: diffFiles
|
|
1991
|
+
)
|
|
1992
|
+
)
|
|
1993
|
+
)
|
|
1994
|
+
completion(.success(true))
|
|
1995
|
+
case .failure(let error):
|
|
1996
|
+
try? self.fileSystem.removeItem(atPath: realDir)
|
|
1997
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
1998
|
+
completion(.failure(error))
|
|
1999
|
+
}
|
|
2000
|
+
case .failure(let error):
|
|
2001
|
+
throw error
|
|
2002
|
+
}
|
|
2003
|
+
} catch {
|
|
2004
|
+
NSLog("[BundleStorage] Manifest-driven install failed: \(error.localizedDescription). Falling back to archive.")
|
|
2005
|
+
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
2006
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
2007
|
+
self.prepareAndDownloadBundle(
|
|
2008
|
+
bundleId: bundleId,
|
|
2009
|
+
fileUrl: fileUrl,
|
|
2010
|
+
fileHash: fileHash,
|
|
2011
|
+
storeDir: storeDir,
|
|
2012
|
+
progressHandler: progressHandler,
|
|
2013
|
+
completion: completion
|
|
2014
|
+
)
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
986
2018
|
private func prepareAndDownloadBundle(
|
|
987
2019
|
bundleId: String,
|
|
988
2020
|
fileUrl: URL,
|
|
989
2021
|
fileHash: String?,
|
|
990
2022
|
storeDir: String,
|
|
991
|
-
progressHandler: @escaping (
|
|
2023
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
992
2024
|
completion: @escaping (Result<Bool, Error>) -> Void
|
|
993
2025
|
) {
|
|
994
2026
|
// 1) Prepare temp directory for download
|
|
@@ -1019,10 +2051,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1019
2051
|
_ = self.downloadService.downloadFile(
|
|
1020
2052
|
from: fileUrl,
|
|
1021
2053
|
to: tempBundleFile,
|
|
1022
|
-
fileSizeHandler: {
|
|
2054
|
+
fileSizeHandler: { fileSize in
|
|
1023
2055
|
// This will be called when Content-Length is received
|
|
1024
|
-
guard let self = self else { return }
|
|
1025
|
-
|
|
1026
2056
|
NSLog("[BundleStorage] File size received: \(fileSize) bytes")
|
|
1027
2057
|
|
|
1028
2058
|
// Check available disk space
|
|
@@ -1044,7 +2074,16 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1044
2074
|
}
|
|
1045
2075
|
},
|
|
1046
2076
|
progressHandler: { downloadProgress in
|
|
1047
|
-
|
|
2077
|
+
self.emitArchiveProgress(
|
|
2078
|
+
progressHandler: progressHandler,
|
|
2079
|
+
progress: Self.mapProgress(
|
|
2080
|
+
downloadProgress.progress,
|
|
2081
|
+
start: 0,
|
|
2082
|
+
end: UpdateProgress.downloadEnd
|
|
2083
|
+
),
|
|
2084
|
+
downloadedBytes: downloadProgress.downloadedBytes,
|
|
2085
|
+
totalBytes: downloadProgress.totalBytes
|
|
2086
|
+
)
|
|
1048
2087
|
},
|
|
1049
2088
|
completion: { [weak self] result in
|
|
1050
2089
|
guard let self = self else {
|
|
@@ -1140,7 +2179,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1140
2179
|
storeDir: String,
|
|
1141
2180
|
bundleId: String,
|
|
1142
2181
|
tempDirectory: String,
|
|
1143
|
-
progressHandler: @escaping (
|
|
2182
|
+
progressHandler: @escaping (UpdateProgressPayload) -> Void,
|
|
1144
2183
|
completion: @escaping (Result<Bool, Error>) -> Void
|
|
1145
2184
|
) {
|
|
1146
2185
|
let currentBundleId = self.getCachedBundleURL()?.deletingLastPathComponent().lastPathComponent
|
|
@@ -1178,13 +2217,19 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1178
2217
|
|
|
1179
2218
|
// 5) Verify bundle integrity (hash or signature based on fileHash format)
|
|
1180
2219
|
NSLog("[BundleStorage] Verifying bundle integrity...")
|
|
1181
|
-
|
|
2220
|
+
self.emitArchiveProgress(
|
|
2221
|
+
progressHandler: progressHandler,
|
|
2222
|
+
progress: UpdateProgress.verificationStart
|
|
2223
|
+
)
|
|
1182
2224
|
let tempBundleURL = URL(fileURLWithPath: tempBundleFile)
|
|
1183
2225
|
let verificationResult = SignatureVerifier.verifyBundle(fileURL: tempBundleURL, fileHash: fileHash)
|
|
1184
2226
|
switch verificationResult {
|
|
1185
2227
|
case .success:
|
|
1186
2228
|
NSLog("[BundleStorage] Bundle verification completed successfully")
|
|
1187
|
-
|
|
2229
|
+
self.emitArchiveProgress(
|
|
2230
|
+
progressHandler: progressHandler,
|
|
2231
|
+
progress: UpdateProgress.verificationEnd
|
|
2232
|
+
)
|
|
1188
2233
|
case .failure(let error):
|
|
1189
2234
|
NSLog("[BundleStorage] Bundle verification failed: \(error)")
|
|
1190
2235
|
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
@@ -1198,12 +2243,14 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1198
2243
|
logFileSystemDiagnostics(path: tempBundleFile, context: "Before Extraction")
|
|
1199
2244
|
do {
|
|
1200
2245
|
try self.decompressService.unzip(file: tempBundleFile, to: tmpDir, progressHandler: { unzipProgress in
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
2246
|
+
self.emitArchiveProgress(
|
|
2247
|
+
progressHandler: progressHandler,
|
|
2248
|
+
progress: Self.mapProgress(
|
|
2249
|
+
unzipProgress,
|
|
2250
|
+
start: UpdateProgress.extractionStart,
|
|
2251
|
+
end: UpdateProgress.extractionEnd
|
|
2252
|
+
)
|
|
1205
2253
|
)
|
|
1206
|
-
progressHandler(progress)
|
|
1207
2254
|
})
|
|
1208
2255
|
NSLog("[BundleStorage] Extraction complete at \(tmpDir)")
|
|
1209
2256
|
logFileSystemDiagnostics(path: tmpDir, context: "After Extraction")
|
|
@@ -1221,7 +2268,10 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1221
2268
|
try? self.fileSystem.removeItem(atPath: tempBundleFile)
|
|
1222
2269
|
|
|
1223
2270
|
// 8) Verify that a valid bundle file exists inside tmpDir
|
|
1224
|
-
|
|
2271
|
+
self.emitArchiveProgress(
|
|
2272
|
+
progressHandler: progressHandler,
|
|
2273
|
+
progress: UpdateProgress.bundleValidation
|
|
2274
|
+
)
|
|
1225
2275
|
switch self.findBundleFile(in: tmpDir) {
|
|
1226
2276
|
case .success(let maybeBundlePath):
|
|
1227
2277
|
if let bundlePathInTmp = maybeBundlePath {
|
|
@@ -1265,13 +2315,19 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1265
2315
|
// 14) Clean up the temporary directory
|
|
1266
2316
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
1267
2317
|
|
|
1268
|
-
|
|
2318
|
+
self.emitArchiveProgress(
|
|
2319
|
+
progressHandler: progressHandler,
|
|
2320
|
+
progress: UpdateProgress.activationReady
|
|
2321
|
+
)
|
|
1269
2322
|
self.scheduleCleanupOldBundles(
|
|
1270
2323
|
bundleIdsToKeep: [currentBundleId, updatedMetadata.stableBundleId, bundleId].compactMap { $0 }
|
|
1271
2324
|
)
|
|
1272
2325
|
|
|
1273
2326
|
// 15) Complete with success
|
|
1274
|
-
|
|
2327
|
+
self.emitArchiveProgress(
|
|
2328
|
+
progressHandler: progressHandler,
|
|
2329
|
+
progress: UpdateProgress.complete
|
|
2330
|
+
)
|
|
1275
2331
|
completion(.success(true))
|
|
1276
2332
|
case .failure(let err):
|
|
1277
2333
|
let nsError = err as NSError
|
|
@@ -1372,41 +2428,31 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
1372
2428
|
* Returns the file:// URL to the bundle directory without trailing slash
|
|
1373
2429
|
*/
|
|
1374
2430
|
func getBaseURL() -> String {
|
|
1375
|
-
|
|
1376
|
-
if let
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
return "file://\(bundleDir)"
|
|
1381
|
-
}
|
|
2431
|
+
if let bundleId = getActiveBundleId() {
|
|
2432
|
+
if case .success(let storeDir) = bundleStoreDir() {
|
|
2433
|
+
let bundleDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
2434
|
+
if fileSystem.fileExists(atPath: bundleDir) {
|
|
2435
|
+
return "file://\(bundleDir)"
|
|
1382
2436
|
}
|
|
1383
2437
|
}
|
|
1384
|
-
|
|
1385
|
-
return ""
|
|
1386
|
-
} catch {
|
|
1387
|
-
NSLog("[BundleStorage] Error getting base URL: \(error)")
|
|
1388
|
-
return ""
|
|
1389
2438
|
}
|
|
2439
|
+
|
|
2440
|
+
return ""
|
|
1390
2441
|
}
|
|
1391
2442
|
|
|
1392
2443
|
func getBaseURL(forBundleId bundleId: String?) -> String {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
let bundleDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
1401
|
-
guard fileSystem.fileExists(atPath: bundleDir) else {
|
|
1402
|
-
return ""
|
|
1403
|
-
}
|
|
2444
|
+
guard let bundleId = bundleId?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
2445
|
+
!bundleId.isEmpty,
|
|
2446
|
+
case .success(let storeDir) = bundleStoreDir() else {
|
|
2447
|
+
return ""
|
|
2448
|
+
}
|
|
1404
2449
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
NSLog("[BundleStorage] Error getting base URL for bundle \(bundleId ?? "nil"): \(error)")
|
|
2450
|
+
let bundleDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
2451
|
+
guard fileSystem.fileExists(atPath: bundleDir) else {
|
|
1408
2452
|
return ""
|
|
1409
2453
|
}
|
|
2454
|
+
|
|
2455
|
+
return "file://\(bundleDir)"
|
|
1410
2456
|
}
|
|
1411
2457
|
|
|
1412
2458
|
func getBundleId() -> String? {
|