@hot-updater/react-native 1.0.0-rc.16 → 1.0.0-rc.17
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/src/main/java/com/hotupdater/BundleFileStorageService.kt +491 -388
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +5 -11
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +290 -12
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +35 -19
- 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 +110 -52
- 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/newarch/HotUpdaterModule.kt +74 -69
- package/android/src/oldarch/HotUpdaterModule.kt +63 -59
- package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +1 -1
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +446 -61
- package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +220 -0
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +19 -27
- package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +71 -0
- package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +106 -0
- package/android/src/test/java/com/hotupdater/NoBackupContext.kt +11 -0
- package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +309 -23
- package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +57 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +238 -157
- package/ios/HotUpdater/Internal/BundleMetadata.swift +3 -53
- package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +8 -18
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +48 -29
- package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +108 -1
- package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
- package/lib/commonjs/appReady.js +111 -0
- package/lib/commonjs/appReady.js.map +1 -0
- package/lib/commonjs/appReady.spec.js +360 -0
- package/lib/commonjs/appReady.spec.js.map +1 -0
- package/lib/commonjs/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
- package/lib/commonjs/appReady.test-utils.js.map +1 -0
- package/lib/commonjs/baseURL.js +16 -0
- package/lib/commonjs/baseURL.js.map +1 -0
- package/lib/commonjs/checkForUpdate.js +104 -56
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +337 -74
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/commonjs/clientPlugin.js +49 -0
- package/lib/commonjs/clientPlugin.js.map +1 -0
- package/lib/commonjs/entryImportGraph.spec.js +50 -0
- package/lib/commonjs/entryImportGraph.spec.js.map +1 -0
- package/lib/commonjs/fetchJSON.js +2 -6
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/httpClient.js +18 -165
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +26 -199
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +49 -36
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +108 -123
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +67 -48
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +80 -58
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/pluginHost.js +208 -0
- package/lib/commonjs/pluginHost.js.map +1 -0
- package/lib/commonjs/pluginHost.spec.js +292 -0
- package/lib/commonjs/pluginHost.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/eventId.js +129 -0
- package/lib/commonjs/plugins/insights/eventId.js.map +1 -0
- package/lib/commonjs/plugins/insights/eventId.spec.js +47 -0
- package/lib/commonjs/plugins/insights/eventId.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/index.js +272 -0
- package/lib/commonjs/plugins/insights/index.js.map +1 -0
- package/lib/commonjs/plugins/insights/insights.spec.js +640 -0
- package/lib/commonjs/plugins/insights/insights.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/sender.js +128 -0
- package/lib/commonjs/plugins/insights/sender.js.map +1 -0
- package/lib/commonjs/plugins/insights/sender.spec.js +265 -0
- package/lib/commonjs/plugins/insights/sender.spec.js.map +1 -0
- package/lib/commonjs/plugins/insights/state.js +145 -0
- package/lib/commonjs/plugins/insights/state.js.map +1 -0
- package/lib/commonjs/releaseCatalogCache.js +13 -2
- package/lib/commonjs/releaseCatalogCache.js.map +1 -1
- package/lib/commonjs/releaseCatalogCache.spec.js +18 -0
- package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/updateError.js +132 -0
- package/lib/commonjs/updateError.js.map +1 -0
- package/lib/commonjs/updateError.spec.js +149 -0
- package/lib/commonjs/updateError.spec.js.map +1 -0
- package/lib/commonjs/wrap.js +43 -25
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.progress.spec.js +195 -0
- package/lib/commonjs/wrap.progress.spec.js.map +1 -0
- package/lib/commonjs/wrap.spec.js +94 -230
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/appReady.js +103 -0
- package/lib/module/appReady.js.map +1 -0
- package/lib/module/appReady.spec.js +359 -0
- package/lib/module/appReady.spec.js.map +1 -0
- package/lib/module/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
- package/lib/module/appReady.test-utils.js.map +1 -0
- package/lib/module/baseURL.js +11 -0
- package/lib/module/baseURL.js.map +1 -0
- package/lib/module/checkForUpdate.js +103 -57
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +337 -74
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/module/clientPlugin.js +44 -0
- package/lib/module/clientPlugin.js.map +1 -0
- package/lib/module/entryImportGraph.spec.js +50 -0
- package/lib/module/entryImportGraph.spec.js.map +1 -0
- package/lib/module/fetchJSON.js +2 -6
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/httpClient.js +18 -165
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +27 -200
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +44 -38
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +108 -123
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +63 -44
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +80 -58
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/pluginHost.js +200 -0
- package/lib/module/pluginHost.js.map +1 -0
- package/lib/module/pluginHost.spec.js +291 -0
- package/lib/module/pluginHost.spec.js.map +1 -0
- package/lib/module/plugins/insights/eventId.js +122 -0
- package/lib/module/plugins/insights/eventId.js.map +1 -0
- package/lib/module/plugins/insights/eventId.spec.js +47 -0
- package/lib/module/plugins/insights/eventId.spec.js.map +1 -0
- package/lib/module/plugins/insights/index.js +268 -0
- package/lib/module/plugins/insights/index.js.map +1 -0
- package/lib/module/plugins/insights/insights.spec.js +639 -0
- package/lib/module/plugins/insights/insights.spec.js.map +1 -0
- package/lib/module/plugins/insights/sender.js +123 -0
- package/lib/module/plugins/insights/sender.js.map +1 -0
- package/lib/module/plugins/insights/sender.spec.js +265 -0
- package/lib/module/plugins/insights/sender.spec.js.map +1 -0
- package/lib/module/plugins/insights/state.js +139 -0
- package/lib/module/plugins/insights/state.js.map +1 -0
- package/lib/module/releaseCatalogCache.js +13 -2
- package/lib/module/releaseCatalogCache.js.map +1 -1
- package/lib/module/releaseCatalogCache.spec.js +18 -0
- package/lib/module/releaseCatalogCache.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/updateError.js +126 -0
- package/lib/module/updateError.js.map +1 -0
- package/lib/module/updateError.spec.js +149 -0
- package/lib/module/updateError.spec.js.map +1 -0
- package/lib/module/wrap.js +43 -25
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.progress.spec.js +196 -0
- package/lib/module/wrap.progress.spec.js.map +1 -0
- package/lib/module/wrap.spec.js +93 -229
- package/lib/module/wrap.spec.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/appReady.test-utils.d.ts +9 -0
- package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +1 -0
- package/lib/typescript/commonjs/baseURL.d.ts +4 -0
- package/lib/typescript/commonjs/baseURL.d.ts.map +1 -0
- package/lib/typescript/commonjs/checkForUpdate.d.ts +15 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/clientPlugin.d.ts +185 -0
- package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts +2 -3
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +2 -36
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -6
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +29 -15
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/pluginHost.d.ts +29 -0
- package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +18 -0
- package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/index.d.ts +40 -0
- package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/sender.d.ts +74 -0
- package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +1 -0
- package/lib/typescript/commonjs/plugins/insights/state.d.ts +46 -0
- package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +1 -0
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts +5 -1
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +34 -12
- package/lib/typescript/commonjs/specs/NativeHotUpdater.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 -10
- 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/appReady.test-utils.d.ts +9 -0
- package/lib/typescript/module/appReady.test-utils.d.ts.map +1 -0
- package/lib/typescript/module/baseURL.d.ts +4 -0
- package/lib/typescript/module/baseURL.d.ts.map +1 -0
- package/lib/typescript/module/checkForUpdate.d.ts +15 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/clientPlugin.d.ts +185 -0
- package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
- package/lib/typescript/module/fetchJSON.d.ts +2 -3
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +2 -36
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -6
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +29 -15
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/pluginHost.d.ts +29 -0
- package/lib/typescript/module/pluginHost.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/eventId.d.ts +18 -0
- package/lib/typescript/module/plugins/insights/eventId.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/index.d.ts +40 -0
- package/lib/typescript/module/plugins/insights/index.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/sender.d.ts +74 -0
- package/lib/typescript/module/plugins/insights/sender.d.ts.map +1 -0
- package/lib/typescript/module/plugins/insights/state.d.ts +46 -0
- package/lib/typescript/module/plugins/insights/state.d.ts.map +1 -0
- package/lib/typescript/module/releaseCatalogCache.d.ts +5 -1
- package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +34 -12
- package/lib/typescript/module/specs/NativeHotUpdater.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 -10
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +14 -2
- package/plugins/insights/package.json +8 -0
- package/src/appReady.spec.ts +393 -0
- package/src/{notifyAppReadyInsights.test-utils.ts → appReady.test-utils.ts} +7 -7
- package/src/appReady.ts +149 -0
- package/src/baseURL.ts +15 -0
- package/src/checkForUpdate.releaseCatalog.spec.ts +370 -76
- package/src/checkForUpdate.ts +135 -80
- package/src/clientPlugin.ts +231 -0
- package/src/entryImportGraph.spec.ts +65 -0
- package/src/fetchJSON.ts +3 -10
- package/src/httpClient.spec.ts +30 -227
- package/src/httpClient.ts +20 -256
- package/src/index.spec.ts +117 -93
- package/src/index.ts +67 -52
- package/src/native.spec.ts +98 -72
- package/src/native.ts +93 -77
- package/src/pluginHost.spec.ts +297 -0
- package/src/pluginHost.ts +262 -0
- package/src/plugins/insights/eventId.spec.ts +60 -0
- package/src/plugins/insights/eventId.ts +171 -0
- package/src/plugins/insights/index.ts +367 -0
- package/src/plugins/insights/insights.spec.ts +732 -0
- package/src/plugins/insights/sender.spec.ts +287 -0
- package/src/plugins/insights/sender.ts +221 -0
- package/src/plugins/insights/state.ts +184 -0
- package/src/releaseCatalogCache.spec.ts +22 -0
- package/src/releaseCatalogCache.ts +19 -4
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +34 -13
- package/src/updateError.spec.ts +144 -0
- package/src/updateError.ts +153 -0
- package/src/wrap.progress.spec.ts +232 -0
- package/src/wrap.spec.ts +100 -283
- package/src/wrap.tsx +57 -26
- package/src/wrap.types.ts +6 -12
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -225
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.js +0 -188
- package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -378
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -171
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/commonjs/uuidv7.js +0 -41
- package/lib/commonjs/uuidv7.js.map +0 -1
- package/lib/commonjs/uuidv7.spec.js +0 -30
- package/lib/commonjs/uuidv7.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -224
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.js +0 -181
- package/lib/module/notifyAppReadyInsights.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.spec.js +0 -377
- package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -170
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
- package/lib/module/uuidv7.js +0 -36
- package/lib/module/uuidv7.js.map +0 -1
- package/lib/module/uuidv7.spec.js +0 -30
- package/lib/module/uuidv7.spec.js.map +0 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -25
- 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/commonjs/uuidv7.d.ts +0 -10
- package/lib/typescript/commonjs/uuidv7.d.ts.map +0 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -25
- 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/lib/typescript/module/uuidv7.d.ts +0 -10
- package/lib/typescript/module/uuidv7.d.ts.map +0 -1
- package/src/notifyAppReadyInsights.boundary.spec.ts +0 -242
- package/src/notifyAppReadyInsights.spec.ts +0 -362
- package/src/notifyAppReadyInsights.transport.spec.ts +0 -196
- package/src/notifyAppReadyInsights.ts +0 -293
- package/src/uuidv7.spec.ts +0 -25
- package/src/uuidv7.ts +0 -38
|
@@ -383,6 +383,8 @@ public struct LaunchReport: Codable {
|
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
/// The random id created once per app installation. It lives in
|
|
387
|
+
/// `NoBackupStorage`, so a device backup never carries it to another device.
|
|
386
388
|
public struct InstallIdentity: Codable {
|
|
387
389
|
static let installIdentityFilename = "install-identity.json"
|
|
388
390
|
|
|
@@ -412,11 +414,7 @@ public struct InstallIdentity: Codable {
|
|
|
412
414
|
let encoder = JSONEncoder()
|
|
413
415
|
encoder.outputFormatting = .prettyPrinted
|
|
414
416
|
let data = try encoder.encode(self)
|
|
415
|
-
|
|
416
|
-
if !FileManager.default.fileExists(atPath: directory.path) {
|
|
417
|
-
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
418
|
-
}
|
|
419
|
-
try data.write(to: file)
|
|
417
|
+
try NoBackupStorage.write(data, to: file)
|
|
420
418
|
return true
|
|
421
419
|
} catch {
|
|
422
420
|
print("[InstallIdentity] Failed to save install identity: \(error)")
|
|
@@ -424,51 +422,3 @@ public struct InstallIdentity: Codable {
|
|
|
424
422
|
}
|
|
425
423
|
}
|
|
426
424
|
}
|
|
427
|
-
|
|
428
|
-
public struct UserIdentity: Codable {
|
|
429
|
-
static let userIdentityFilename = "user-identity.json"
|
|
430
|
-
|
|
431
|
-
let userId: String?
|
|
432
|
-
let username: String?
|
|
433
|
-
|
|
434
|
-
init(userId: String?, username: String?) {
|
|
435
|
-
self.userId = userId
|
|
436
|
-
self.username = username
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
var isEmpty: Bool {
|
|
440
|
-
userId == nil && username == nil
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
static func load(from file: URL) -> UserIdentity? {
|
|
444
|
-
guard FileManager.default.fileExists(atPath: file.path) else {
|
|
445
|
-
return nil
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
do {
|
|
449
|
-
let data = try Data(contentsOf: file)
|
|
450
|
-
let identity = try JSONDecoder().decode(UserIdentity.self, from: data)
|
|
451
|
-
return identity.isEmpty ? nil : identity
|
|
452
|
-
} catch {
|
|
453
|
-
print("[UserIdentity] Failed to load user identity: \(error)")
|
|
454
|
-
return nil
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
func save(to file: URL) -> Bool {
|
|
459
|
-
do {
|
|
460
|
-
let encoder = JSONEncoder()
|
|
461
|
-
encoder.outputFormatting = .prettyPrinted
|
|
462
|
-
let data = try encoder.encode(self)
|
|
463
|
-
let directory = file.deletingLastPathComponent()
|
|
464
|
-
if !FileManager.default.fileExists(atPath: directory.path) {
|
|
465
|
-
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
466
|
-
}
|
|
467
|
-
try data.write(to: file)
|
|
468
|
-
return true
|
|
469
|
-
} catch {
|
|
470
|
-
print("[UserIdentity] Failed to save user identity: \(error)")
|
|
471
|
-
return false
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
@@ -17,6 +17,45 @@ protocol FileSystemService {
|
|
|
17
17
|
func contentsOfDirectory(atPath path: String) throws -> [String]
|
|
18
18
|
func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any]
|
|
19
19
|
func documentsPath() -> String
|
|
20
|
+
/// Base directory for data that lives as long as the installation.
|
|
21
|
+
func applicationSupportPath() -> String
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// `Application Support/HotUpdater`: data the SDK keeps for the life of an
|
|
25
|
+
/// installation, such as the install id and plugin storage. Device backups
|
|
26
|
+
/// skip it, so restoring a backup onto another device never copies it there.
|
|
27
|
+
enum NoBackupStorage {
|
|
28
|
+
static let directoryName = "HotUpdater"
|
|
29
|
+
|
|
30
|
+
static func directory(in fileSystem: FileSystemService) -> URL {
|
|
31
|
+
URL(fileURLWithPath: fileSystem.applicationSupportPath(), isDirectory: true)
|
|
32
|
+
.appendingPathComponent(directoryName, isDirectory: true)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Atomically replaces `file` with `data`, creating its directory when
|
|
36
|
+
/// needed, and excludes the directory and the file from device backups.
|
|
37
|
+
static func write(_ data: Data, to file: URL) throws {
|
|
38
|
+
let directory = file.deletingLastPathComponent()
|
|
39
|
+
try FileManager.default.createDirectory(
|
|
40
|
+
at: directory,
|
|
41
|
+
withIntermediateDirectories: true
|
|
42
|
+
)
|
|
43
|
+
excludeFromBackup(directory)
|
|
44
|
+
try data.write(to: file, options: .atomic)
|
|
45
|
+
// An atomic write replaces the file, which drops its previous flag.
|
|
46
|
+
excludeFromBackup(file)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static func excludeFromBackup(_ url: URL) {
|
|
50
|
+
var url = url
|
|
51
|
+
var values = URLResourceValues()
|
|
52
|
+
values.isExcludedFromBackup = true
|
|
53
|
+
do {
|
|
54
|
+
try url.setResourceValues(values)
|
|
55
|
+
} catch {
|
|
56
|
+
NSLog("[NoBackupStorage] Failed to exclude \(url.path) from backups: \(error)")
|
|
57
|
+
}
|
|
58
|
+
}
|
|
20
59
|
}
|
|
21
60
|
|
|
22
61
|
class FileManagerService: FileSystemService {
|
|
@@ -89,4 +128,13 @@ class FileManagerService: FileSystemService {
|
|
|
89
128
|
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
|
|
90
129
|
#endif
|
|
91
130
|
}
|
|
131
|
+
|
|
132
|
+
func applicationSupportPath() -> String {
|
|
133
|
+
#if os(tvOS)
|
|
134
|
+
// tvOS apps can only write purgeable files, so use Caches like documentsPath().
|
|
135
|
+
return NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
|
|
136
|
+
#else
|
|
137
|
+
return NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0]
|
|
138
|
+
#endif
|
|
139
|
+
}
|
|
92
140
|
}
|
|
@@ -660,19 +660,14 @@ RCT_EXPORT_MODULE();
|
|
|
660
660
|
return [impl getInstallId];
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
-
- (NSString * _Nullable)
|
|
663
|
+
- (NSString * _Nullable)getStorageItem:(NSString *)key {
|
|
664
664
|
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
665
|
-
return [impl
|
|
665
|
+
return [impl getStorageItem:key];
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
-
- (NSString * _Nullable)
|
|
668
|
+
- (void)setStorageItem:(NSString *)key value:(NSString * _Nullable)value {
|
|
669
669
|
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
- (void)setUser:(NSString * _Nullable)userId username:(NSString * _Nullable)username {
|
|
674
|
-
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
675
|
-
[impl setUser:userId username:username];
|
|
670
|
+
[impl setStorageItem:key value:value];
|
|
676
671
|
}
|
|
677
672
|
|
|
678
673
|
- (NSString * _Nullable)getBaseURL {
|
|
@@ -830,19 +825,14 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getInstallId) {
|
|
|
830
825
|
return [impl getInstallId];
|
|
831
826
|
}
|
|
832
827
|
|
|
833
|
-
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(
|
|
834
|
-
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
835
|
-
return [impl getUserId];
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getUsername) {
|
|
828
|
+
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getStorageItem:(NSString *)key) {
|
|
839
829
|
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
840
|
-
return [impl
|
|
830
|
+
return [impl getStorageItem:key];
|
|
841
831
|
}
|
|
842
832
|
|
|
843
|
-
RCT_EXPORT_METHOD(
|
|
833
|
+
RCT_EXPORT_METHOD(setStorageItem:(NSString *)key value:(NSString * _Nullable)value) {
|
|
844
834
|
HotUpdaterImpl *impl = [HotUpdater sharedImpl];
|
|
845
|
-
[impl
|
|
835
|
+
[impl setStorageItem:key value:value];
|
|
846
836
|
}
|
|
847
837
|
|
|
848
838
|
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getBaseURL) {
|
|
@@ -56,6 +56,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
56
56
|
private let cohortService: CohortService
|
|
57
57
|
private let recoveryManager: HotUpdaterRecoveryManager
|
|
58
58
|
private let releaseCatalogCache: ReleaseCatalogCacheService
|
|
59
|
+
private let keyValueStorage: KeyValueStorageService
|
|
59
60
|
private var currentLaunchSelection: LaunchSelection?
|
|
60
61
|
|
|
61
62
|
private static let DEFAULT_CHANNEL = "production"
|
|
@@ -99,7 +100,10 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
99
100
|
bundleStorage: bundleStorage,
|
|
100
101
|
preferences: preferences,
|
|
101
102
|
recoveryManager: recoveryManager,
|
|
102
|
-
releaseCatalogCache: ReleaseCatalogCacheService()
|
|
103
|
+
releaseCatalogCache: ReleaseCatalogCacheService(),
|
|
104
|
+
keyValueStorage: KeyValueStorageService(
|
|
105
|
+
directory: NoBackupStorage.directory(in: fileSystem)
|
|
106
|
+
)
|
|
103
107
|
)
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -107,18 +111,23 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
107
111
|
* Primary initializer with dependency injection.
|
|
108
112
|
* @param bundleStorage Service for bundle storage operations
|
|
109
113
|
* @param preferences Service for preference storage
|
|
114
|
+
* @param keyValueStorage Persistent key-value store for client plugins
|
|
110
115
|
*/
|
|
111
116
|
internal init(
|
|
112
117
|
bundleStorage: BundleStorageService,
|
|
113
118
|
preferences: PreferencesService,
|
|
114
119
|
recoveryManager: HotUpdaterRecoveryManager,
|
|
115
|
-
releaseCatalogCache: ReleaseCatalogCacheService = ReleaseCatalogCacheService()
|
|
120
|
+
releaseCatalogCache: ReleaseCatalogCacheService = ReleaseCatalogCacheService(),
|
|
121
|
+
keyValueStorage: KeyValueStorageService = KeyValueStorageService(
|
|
122
|
+
directory: NoBackupStorage.directory(in: FileManagerService())
|
|
123
|
+
)
|
|
116
124
|
) {
|
|
117
125
|
self.bundleStorage = bundleStorage
|
|
118
126
|
self.preferences = preferences
|
|
119
127
|
self.cohortService = CohortService()
|
|
120
128
|
self.recoveryManager = recoveryManager
|
|
121
129
|
self.releaseCatalogCache = releaseCatalogCache
|
|
130
|
+
self.keyValueStorage = keyValueStorage
|
|
122
131
|
super.init()
|
|
123
132
|
|
|
124
133
|
// Configure preferences with isolation key
|
|
@@ -244,8 +253,10 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
244
253
|
* Updates the bundle from JavaScript bridge.
|
|
245
254
|
* This method acts as the primary error boundary for all bundle operations.
|
|
246
255
|
* @param params Dictionary with bundleId, manifest, and asset parameters
|
|
247
|
-
* @param resolve Promise resolve callback
|
|
248
|
-
*
|
|
256
|
+
* @param resolve Promise resolve callback, called with the staged bundle's
|
|
257
|
+
* `{ delivery, patchFallback }`
|
|
258
|
+
* @param reject Promise reject callback; download and install failures
|
|
259
|
+
* carry `stage`, `reason`, and their details in the error's userInfo
|
|
249
260
|
*/
|
|
250
261
|
public func updateBundle(_ params: NSDictionary?,
|
|
251
262
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
@@ -260,33 +271,35 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
260
271
|
return
|
|
261
272
|
}
|
|
262
273
|
|
|
274
|
+
// Invalid update parameters are an invalid update response.
|
|
263
275
|
guard let bundleId = data["bundleId"] as? String, !bundleId.isEmpty else {
|
|
264
|
-
let error =
|
|
265
|
-
userInfo: [NSLocalizedDescriptionKey: "Missing or empty 'bundleId'"])
|
|
276
|
+
let error = UpdateFailure.invalidResponse.error(description: "Missing or empty 'bundleId'")
|
|
266
277
|
reject("MISSING_BUNDLE_ID", error.localizedDescription, error)
|
|
267
278
|
return
|
|
268
279
|
}
|
|
269
280
|
|
|
270
281
|
guard let manifestFileHash = data["manifestFileHash"] as? String,
|
|
271
282
|
!manifestFileHash.isEmpty else {
|
|
272
|
-
let error =
|
|
273
|
-
|
|
283
|
+
let error = UpdateFailure.invalidResponse.with(resource: .manifest)
|
|
284
|
+
.error(description: "Missing manifest file hash")
|
|
274
285
|
reject("INVALID_MANIFEST", error.localizedDescription, error)
|
|
275
286
|
return
|
|
276
287
|
}
|
|
277
288
|
let channel = data["channel"] as? String
|
|
278
289
|
let manifestUrlString = data["manifestUrl"] as? String ?? ""
|
|
279
290
|
guard let manifestUrl = URL(string: manifestUrlString) else {
|
|
280
|
-
let error =
|
|
281
|
-
|
|
291
|
+
let error = UpdateFailure.invalidResponse.with(resource: .manifest).error(
|
|
292
|
+
description: "Invalid 'manifestUrl' provided: \(manifestUrlString)"
|
|
293
|
+
)
|
|
282
294
|
reject("INVALID_FILE_URL", error.localizedDescription, error)
|
|
283
295
|
return
|
|
284
296
|
}
|
|
285
297
|
let archiveUrl: URL?
|
|
286
298
|
if let archiveUrlString = data["archiveUrl"] as? String {
|
|
287
299
|
guard let parsedArchiveUrl = URL(string: archiveUrlString) else {
|
|
288
|
-
let error =
|
|
289
|
-
|
|
300
|
+
let error = UpdateFailure.invalidResponse.with(resource: .archive).error(
|
|
301
|
+
description: "Invalid 'archiveUrl' provided: \(archiveUrlString)"
|
|
302
|
+
)
|
|
290
303
|
reject("INVALID_FILE_URL", error.localizedDescription, error)
|
|
291
304
|
return
|
|
292
305
|
}
|
|
@@ -295,8 +308,8 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
295
308
|
archiveUrl = nil
|
|
296
309
|
}
|
|
297
310
|
guard let assetsPayload = data["assets"] as? [String: [String: Any]] else {
|
|
298
|
-
let error =
|
|
299
|
-
|
|
311
|
+
let error = UpdateFailure.invalidResponse.with(resource: .manifest)
|
|
312
|
+
.error(description: "Missing manifest assets")
|
|
300
313
|
reject("INVALID_MANIFEST", error.localizedDescription, error)
|
|
301
314
|
return
|
|
302
315
|
}
|
|
@@ -344,11 +357,8 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
344
357
|
}
|
|
345
358
|
let selection = (data["selection"] as? [String: Any]).flatMap(Self.parseSelection)
|
|
346
359
|
if let selection, !bundleStorage.stageReleaseSelection(selection) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
code: 0,
|
|
350
|
-
userInfo: [NSLocalizedDescriptionKey: "Release catalog selection is stale"]
|
|
351
|
-
)
|
|
360
|
+
// Not an update failure, so the rejection carries no stage or reason.
|
|
361
|
+
let error = StaleReleaseSelectionError() as NSError
|
|
352
362
|
reject("UNKNOWN_ERROR", error.localizedDescription, error)
|
|
353
363
|
return
|
|
354
364
|
}
|
|
@@ -383,7 +393,7 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
383
393
|
// Return results on main thread for React Native bridge
|
|
384
394
|
DispatchQueue.main.async {
|
|
385
395
|
switch result {
|
|
386
|
-
case .success:
|
|
396
|
+
case .success(let delivery):
|
|
387
397
|
NSLog("[HotUpdaterImpl] Update successful for \(bundleId). Resolving promise.")
|
|
388
398
|
if let channel, !channel.isEmpty {
|
|
389
399
|
do {
|
|
@@ -396,12 +406,14 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
396
406
|
NSLog("[HotUpdaterImpl] Failed to persist channel override: \(error)")
|
|
397
407
|
}
|
|
398
408
|
}
|
|
399
|
-
|
|
409
|
+
// { delivery: "patch" | "manifest" | "archive", patchFallback }
|
|
410
|
+
resolve(delivery.dictionary)
|
|
400
411
|
case .failure(let error):
|
|
401
412
|
NSLog("[HotUpdaterImpl] Update failed for \(bundleId) - Error: \(error)")
|
|
402
413
|
|
|
403
414
|
let normalizedCode = HotUpdaterImpl.normalizeErrorCode(from: error)
|
|
404
|
-
|
|
415
|
+
// A classified failure adds stage, reason, and its details to userInfo.
|
|
416
|
+
let nsError = UpdateFailureError.rejectionError(for: error)
|
|
405
417
|
reject(normalizedCode, nsError.localizedDescription, nsError)
|
|
406
418
|
}
|
|
407
419
|
}
|
|
@@ -536,6 +548,8 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
536
548
|
* Rare or platform-specific codes are collapsed to UNKNOWN_ERROR to reduce surface area.
|
|
537
549
|
*/
|
|
538
550
|
private static func normalizeErrorCode(from error: Error) -> String {
|
|
551
|
+
// Classification wraps an error without changing its code.
|
|
552
|
+
let error = UpdateFailureError.unwrap(error)
|
|
539
553
|
let baseCode: String
|
|
540
554
|
|
|
541
555
|
if let storageError = error as? BundleStorageError {
|
|
@@ -601,16 +615,21 @@ private func hotUpdaterGetMinBundleId() -> String {
|
|
|
601
615
|
return bundleStorage.getInstallId()
|
|
602
616
|
}
|
|
603
617
|
|
|
604
|
-
|
|
605
|
-
return bundleStorage.getUserId()
|
|
606
|
-
}
|
|
618
|
+
// MARK: - Client Plugin Storage
|
|
607
619
|
|
|
608
|
-
|
|
609
|
-
|
|
620
|
+
/**
|
|
621
|
+
* Reads a value from the persistent key-value store for client plugins.
|
|
622
|
+
* @return The stored value or nil when the key has none
|
|
623
|
+
*/
|
|
624
|
+
public func getStorageItem(_ key: String) -> String? {
|
|
625
|
+
return keyValueStorage.getItem(key)
|
|
610
626
|
}
|
|
611
627
|
|
|
612
|
-
|
|
613
|
-
|
|
628
|
+
/**
|
|
629
|
+
* Writes a value to the persistent key-value store; nil removes the key.
|
|
630
|
+
*/
|
|
631
|
+
public func setStorageItem(_ key: String, value: String?) {
|
|
632
|
+
keyValueStorage.setItem(key, value: value)
|
|
614
633
|
}
|
|
615
634
|
|
|
616
635
|
/**
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Persistent string key-value store for client plugins, kept in
|
|
4
|
+
/// `Application Support/HotUpdater/storage.json` outside device backups.
|
|
5
|
+
///
|
|
6
|
+
/// Keys are opaque; JavaScript namespaces them per plugin. The file is read
|
|
7
|
+
/// into memory on first use, and every write replaces the whole file.
|
|
8
|
+
final class KeyValueStorageService {
|
|
9
|
+
static let storageFilename = "storage.json"
|
|
10
|
+
|
|
11
|
+
private let fileURL: URL
|
|
12
|
+
private let lock = NSLock()
|
|
13
|
+
private var items: [String: String]?
|
|
14
|
+
|
|
15
|
+
init(directory: URL) {
|
|
16
|
+
self.fileURL = directory.appendingPathComponent(Self.storageFilename)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func getItem(_ key: String) -> String? {
|
|
20
|
+
lock.lock()
|
|
21
|
+
defer { lock.unlock() }
|
|
22
|
+
return loadedItems()[key]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Stores `value` under `key`, or removes the key when `value` is nil.
|
|
26
|
+
/// Returns false when the file could not be written.
|
|
27
|
+
@discardableResult
|
|
28
|
+
func setItem(_ key: String, value: String?) -> Bool {
|
|
29
|
+
lock.lock()
|
|
30
|
+
defer { lock.unlock() }
|
|
31
|
+
|
|
32
|
+
var updatedItems = loadedItems()
|
|
33
|
+
updatedItems[key] = value
|
|
34
|
+
// Memory stays the source of truth for this process, so the next
|
|
35
|
+
// successful write also persists a value whose own write failed.
|
|
36
|
+
items = updatedItems
|
|
37
|
+
|
|
38
|
+
do {
|
|
39
|
+
let data = try JSONSerialization.data(
|
|
40
|
+
withJSONObject: updatedItems,
|
|
41
|
+
options: [.sortedKeys]
|
|
42
|
+
)
|
|
43
|
+
try NoBackupStorage.write(data, to: fileURL)
|
|
44
|
+
return true
|
|
45
|
+
} catch {
|
|
46
|
+
NSLog("[KeyValueStorage] Failed to write \(fileURL.path): \(error)")
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private func loadedItems() -> [String: String] {
|
|
52
|
+
if let items {
|
|
53
|
+
return items
|
|
54
|
+
}
|
|
55
|
+
let loadedItems = Self.readItems(from: fileURL)
|
|
56
|
+
items = loadedItems
|
|
57
|
+
return loadedItems
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// A missing, unreadable, or corrupt file reads as an empty store.
|
|
61
|
+
private static func readItems(from file: URL) -> [String: String] {
|
|
62
|
+
guard let data = try? Data(contentsOf: file) else {
|
|
63
|
+
return [:]
|
|
64
|
+
}
|
|
65
|
+
guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
66
|
+
NSLog("[KeyValueStorage] Ignoring unreadable store at \(file.path)")
|
|
67
|
+
return [:]
|
|
68
|
+
}
|
|
69
|
+
return object.compactMapValues { $0 as? String }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -23,9 +23,108 @@ protocol DownloadService {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
enum DownloadError: Error {
|
|
26
|
+
enum DownloadError: Error, Equatable {
|
|
27
27
|
case incompleteDownload(expected: Int64, actual: Int64)
|
|
28
28
|
case invalidContentLength
|
|
29
|
+
/// The server answered with a status outside 200-299. `originCode` is the
|
|
30
|
+
/// `<Code>` of a storage origin's XML error body, when it has one.
|
|
31
|
+
case httpStatus(Int, originCode: String? = nil)
|
|
32
|
+
|
|
33
|
+
/// How much of an error body is searched for a storage origin's code.
|
|
34
|
+
static let maximumErrorBodyPrefixByteCount = 4 * 1024
|
|
35
|
+
private static let maximumOriginCodeLength = 64
|
|
36
|
+
|
|
37
|
+
/// The failure for an HTTP response outside 200-299, or nil for a
|
|
38
|
+
/// successful or non-HTTP response. `body` is the saved response body.
|
|
39
|
+
static func httpStatusError(for response: URLResponse?, body: URL?) -> DownloadError? {
|
|
40
|
+
guard let httpResponse = response as? HTTPURLResponse,
|
|
41
|
+
!(200..<300).contains(httpResponse.statusCode) else {
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
let originCode = body.flatMap { body -> String? in
|
|
45
|
+
guard let handle = try? FileHandle(forReadingFrom: body) else {
|
|
46
|
+
return nil
|
|
47
|
+
}
|
|
48
|
+
defer { try? handle.close() }
|
|
49
|
+
let prefix = try? FileUtilities.readUpToCount(
|
|
50
|
+
from: handle,
|
|
51
|
+
count: maximumErrorBodyPrefixByteCount
|
|
52
|
+
)
|
|
53
|
+
return prefix.flatMap { storageOriginErrorCode(in: $0) }
|
|
54
|
+
}
|
|
55
|
+
return .httpStatus(httpResponse.statusCode, originCode: originCode)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// The `<Code>` of an S3, R2, or GCS style `<Error>` document in the
|
|
59
|
+
/// first 4 KB of `body`, such as "AccessDenied". Nothing else from the
|
|
60
|
+
/// body is kept, and a code must be 1-64 characters of [A-Za-z0-9._-].
|
|
61
|
+
static func storageOriginErrorCode(in body: Data) -> String? {
|
|
62
|
+
let text = String(decoding: body.prefix(maximumErrorBodyPrefixByteCount), as: UTF8.self)
|
|
63
|
+
guard let errorElement = text.range(of: "<Error"),
|
|
64
|
+
let codeStart = text.range(of: "<Code>", range: errorElement.upperBound..<text.endIndex),
|
|
65
|
+
let codeEnd = text.range(of: "</Code>", range: codeStart.upperBound..<text.endIndex) else {
|
|
66
|
+
return nil
|
|
67
|
+
}
|
|
68
|
+
let code = text[codeStart.upperBound..<codeEnd.lowerBound]
|
|
69
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
70
|
+
guard (1...maximumOriginCodeLength).contains(code.unicodeScalars.count),
|
|
71
|
+
code.unicodeScalars.allSatisfy(isOriginCodeCharacter) else {
|
|
72
|
+
return nil
|
|
73
|
+
}
|
|
74
|
+
return code
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static func isOriginCodeCharacter(_ scalar: Unicode.Scalar) -> Bool {
|
|
78
|
+
switch scalar {
|
|
79
|
+
case "A"..."Z", "a"..."z", "0"..."9", ".", "_", "-":
|
|
80
|
+
return true
|
|
81
|
+
default:
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
extension DownloadError {
|
|
88
|
+
/// Attempts per download that retries, the first one included.
|
|
89
|
+
static let maximumAttempts = 3
|
|
90
|
+
|
|
91
|
+
/// Whether a failed download may succeed if tried again: a network
|
|
92
|
+
/// failure or timeout, a body that ended early, or a 408, 429, or 5xx
|
|
93
|
+
/// answer. Another 4xx, a TLS failure, a cancelled task, and a local file
|
|
94
|
+
/// failure would fail the same way again.
|
|
95
|
+
static func isRetryable(_ error: Error) -> Bool {
|
|
96
|
+
if let downloadError = error as? DownloadError {
|
|
97
|
+
switch downloadError {
|
|
98
|
+
case .httpStatus(let status, _):
|
|
99
|
+
return status == 408 || status == 429 || status >= 500
|
|
100
|
+
case .incompleteDownload:
|
|
101
|
+
return true
|
|
102
|
+
case .invalidContentLength:
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let nsError = error as NSError
|
|
107
|
+
guard nsError.domain == NSURLErrorDomain else {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
switch URLError.Code(rawValue: nsError.code) {
|
|
111
|
+
case .timedOut,
|
|
112
|
+
.cannotFindHost,
|
|
113
|
+
.cannotConnectToHost,
|
|
114
|
+
.networkConnectionLost,
|
|
115
|
+
.dnsLookupFailed,
|
|
116
|
+
.notConnectedToInternet,
|
|
117
|
+
.cannotLoadFromNetwork,
|
|
118
|
+
.callIsActive,
|
|
119
|
+
.dataNotAllowed,
|
|
120
|
+
.internationalRoamingOff,
|
|
121
|
+
.backgroundSessionWasDisconnected,
|
|
122
|
+
.badServerResponse:
|
|
123
|
+
return true
|
|
124
|
+
default:
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
29
128
|
}
|
|
30
129
|
|
|
31
130
|
class URLSessionDownloadService: NSObject, DownloadService {
|
|
@@ -107,6 +206,14 @@ extension URLSessionDownloadService: URLSessionDownloadDelegate {
|
|
|
107
206
|
return
|
|
108
207
|
}
|
|
109
208
|
|
|
209
|
+
// URLSession saves error bodies too, so reject them before they reach verification.
|
|
210
|
+
if let httpError = DownloadError.httpStatusError(for: downloadTask.response, body: location) {
|
|
211
|
+
NSLog("[DownloadService] Download failed: \(httpError)")
|
|
212
|
+
try? FileManager.default.removeItem(at: location)
|
|
213
|
+
completion?(.failure(httpError))
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
110
217
|
// Verify file size
|
|
111
218
|
let expectedSize = downloadTask.response?.expectedContentLength ?? -1
|
|
112
219
|
let actualSize: Int64?
|