@hot-updater/react-native 0.18.1 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +54 -17
- package/android/src/main/java/com/hotupdater/HotUpdaterFactory.kt +2 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +9 -0
- package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +2 -1
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +108 -104
- package/ios/HotUpdater/Internal/FileManagerService.swift +0 -10
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +17 -6
- package/ios/HotUpdater/Internal/VersionedPreferencesService.swift +2 -2
- package/package.json +3 -3
|
@@ -88,6 +88,7 @@ class BundleFileStorageService(
|
|
|
88
88
|
): Boolean {
|
|
89
89
|
Log.d("BundleStorage", "updateBundle bundleId $bundleId fileUrl $fileUrl")
|
|
90
90
|
|
|
91
|
+
// If no URL is provided, reset to fallback
|
|
91
92
|
if (fileUrl.isNullOrEmpty()) {
|
|
92
93
|
setBundleURL(null)
|
|
93
94
|
return true
|
|
@@ -104,11 +105,13 @@ class BundleFileStorageService(
|
|
|
104
105
|
Log.d("BundleStorage", "Bundle for bundleId $bundleId already exists. Using cached bundle.")
|
|
105
106
|
val existingIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
106
107
|
if (existingIndexFile != null) {
|
|
108
|
+
// Update last modified time and set the cached bundle URL
|
|
107
109
|
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
108
110
|
setBundleURL(existingIndexFile.absolutePath)
|
|
109
111
|
cleanupOldBundles(bundleStoreDir)
|
|
110
112
|
return true
|
|
111
113
|
} else {
|
|
114
|
+
// If index.android.bundle is missing, delete and re-download
|
|
112
115
|
finalBundleDir.deleteRecursively()
|
|
113
116
|
}
|
|
114
117
|
}
|
|
@@ -120,8 +123,6 @@ class BundleFileStorageService(
|
|
|
120
123
|
tempDir.mkdirs()
|
|
121
124
|
|
|
122
125
|
val tempZipFile = File(tempDir, "bundle.zip")
|
|
123
|
-
val extractedDir = File(tempDir, "extracted")
|
|
124
|
-
extractedDir.mkdirs()
|
|
125
126
|
|
|
126
127
|
return withContext(Dispatchers.IO) {
|
|
127
128
|
val downloadUrl = URL(fileUrl)
|
|
@@ -141,60 +142,96 @@ class BundleFileStorageService(
|
|
|
141
142
|
return@withContext false
|
|
142
143
|
}
|
|
143
144
|
is DownloadResult.Success -> {
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
// 1) Create a .tmp directory under bundle-store (to avoid colliding with an existing bundleId folder)
|
|
146
|
+
val tmpDir = File(bundleStoreDir, "$bundleId.tmp")
|
|
147
|
+
if (tmpDir.exists()) {
|
|
148
|
+
tmpDir.deleteRecursively()
|
|
149
|
+
}
|
|
150
|
+
tmpDir.mkdirs()
|
|
151
|
+
|
|
152
|
+
// 2) Unzip into tmpDir
|
|
153
|
+
Log.d("BundleStorage", "Unzipping $tempZipFile → $tmpDir")
|
|
154
|
+
if (!unzipService.extractZipFile(tempZipFile.absolutePath, tmpDir.absolutePath)) {
|
|
155
|
+
Log.d("BundleStorage", "Failed to extract zip into tmpDir.")
|
|
147
156
|
tempDir.deleteRecursively()
|
|
157
|
+
tmpDir.deleteRecursively()
|
|
148
158
|
return@withContext false
|
|
149
159
|
}
|
|
150
160
|
|
|
151
|
-
// Find
|
|
152
|
-
val
|
|
153
|
-
if (
|
|
154
|
-
Log.d("BundleStorage", "index.android.bundle not found in
|
|
161
|
+
// 3) Find index.android.bundle inside tmpDir
|
|
162
|
+
val extractedIndex = tmpDir.walk().find { it.name == "index.android.bundle" }
|
|
163
|
+
if (extractedIndex == null) {
|
|
164
|
+
Log.d("BundleStorage", "index.android.bundle not found in tmpDir.")
|
|
155
165
|
tempDir.deleteRecursively()
|
|
166
|
+
tmpDir.deleteRecursively()
|
|
156
167
|
return@withContext false
|
|
157
168
|
}
|
|
158
169
|
|
|
159
|
-
//
|
|
170
|
+
// 4) If the realDir (bundle-store/<bundleId>) exists, delete it
|
|
160
171
|
if (finalBundleDir.exists()) {
|
|
161
172
|
finalBundleDir.deleteRecursively()
|
|
162
173
|
}
|
|
163
174
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
// 5) Attempt to rename tmpDir → finalBundleDir (atomic within the same parent folder)
|
|
176
|
+
val renamed = tmpDir.renameTo(finalBundleDir)
|
|
177
|
+
if (!renamed) {
|
|
178
|
+
// If rename fails, use moveItem or copyItem
|
|
179
|
+
if (!fileSystem.moveItem(tmpDir.absolutePath, finalBundleDir.absolutePath)) {
|
|
180
|
+
fileSystem.copyItem(tmpDir.absolutePath, finalBundleDir.absolutePath)
|
|
181
|
+
tmpDir.deleteRecursively()
|
|
182
|
+
}
|
|
167
183
|
}
|
|
168
184
|
|
|
185
|
+
// 6) Verify index.android.bundle exists inside finalBundleDir
|
|
169
186
|
val finalIndexFile = finalBundleDir.walk().find { it.name == "index.android.bundle" }
|
|
170
187
|
if (finalIndexFile == null) {
|
|
171
|
-
Log.d("BundleStorage", "index.android.bundle not found in
|
|
188
|
+
Log.d("BundleStorage", "index.android.bundle not found in realDir.")
|
|
172
189
|
tempDir.deleteRecursively()
|
|
190
|
+
finalBundleDir.deleteRecursively()
|
|
173
191
|
return@withContext false
|
|
174
192
|
}
|
|
175
193
|
|
|
194
|
+
// 7) Update finalBundleDir's last modified time
|
|
176
195
|
finalBundleDir.setLastModified(System.currentTimeMillis())
|
|
196
|
+
|
|
197
|
+
// 8) Save the new bundle path in Preferences
|
|
177
198
|
val bundlePath = finalIndexFile.absolutePath
|
|
178
199
|
Log.d("BundleStorage", "Setting bundle URL: $bundlePath")
|
|
179
200
|
setBundleURL(bundlePath)
|
|
180
|
-
|
|
201
|
+
|
|
202
|
+
// 9) Clean up temporary and download folders
|
|
181
203
|
tempDir.deleteRecursively()
|
|
182
204
|
|
|
183
|
-
|
|
205
|
+
// 10) Remove old bundles
|
|
206
|
+
cleanupOldBundles(bundleStoreDir)
|
|
207
|
+
|
|
208
|
+
Log.d("BundleStorage", "Downloaded and activated bundle successfully.")
|
|
184
209
|
return@withContext true
|
|
185
210
|
}
|
|
186
211
|
}
|
|
187
212
|
}
|
|
188
213
|
}
|
|
189
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Removes older bundles and any leftover .tmp directories
|
|
217
|
+
*/
|
|
190
218
|
private fun cleanupOldBundles(bundleStoreDir: File) {
|
|
191
|
-
|
|
219
|
+
// List only directories that are not .tmp
|
|
220
|
+
val bundles = bundleStoreDir.listFiles { file -> file.isDirectory && !file.name.endsWith(".tmp") }?.toList() ?: return
|
|
221
|
+
// Sort bundles by last modified (newest first)
|
|
192
222
|
val sortedBundles = bundles.sortedByDescending { it.lastModified() }
|
|
193
223
|
if (sortedBundles.size > 1) {
|
|
224
|
+
// Keep the most recent bundle, delete the rest
|
|
194
225
|
sortedBundles.drop(1).forEach { oldBundle ->
|
|
195
226
|
Log.d("BundleStorage", "Removing old bundle: ${oldBundle.name}")
|
|
196
227
|
oldBundle.deleteRecursively()
|
|
197
228
|
}
|
|
198
229
|
}
|
|
230
|
+
|
|
231
|
+
// Remove any leftover .tmp directories
|
|
232
|
+
bundleStoreDir.listFiles { file -> file.isDirectory && file.name.endsWith(".tmp") }?.forEach { staleTmp ->
|
|
233
|
+
Log.d("BundleStorage", "Removing stale tmp directory: ${staleTmp.name}")
|
|
234
|
+
staleTmp.deleteRecursively()
|
|
235
|
+
}
|
|
199
236
|
}
|
|
200
237
|
}
|
|
@@ -27,10 +27,11 @@ object HotUpdaterFactory {
|
|
|
27
27
|
private fun createHotUpdaterImpl(context: Context): HotUpdaterImpl {
|
|
28
28
|
val appContext = context.applicationContext
|
|
29
29
|
val appVersion = HotUpdaterImpl.getAppVersion(appContext) ?: "unknown"
|
|
30
|
+
val appChannel = HotUpdaterImpl.getChannel(appContext)
|
|
30
31
|
|
|
31
32
|
// Create services
|
|
32
33
|
val fileSystem = FileManagerService(appContext)
|
|
33
|
-
val preferences = VersionedPreferencesService(appContext, appVersion)
|
|
34
|
+
val preferences = VersionedPreferencesService(appContext, appVersion, appChannel)
|
|
34
35
|
val downloadService = HttpDownloadService()
|
|
35
36
|
val unzipService = ZipFileUnzipService()
|
|
36
37
|
|
|
@@ -57,6 +57,15 @@ class HotUpdaterImpl(
|
|
|
57
57
|
} catch (e: Exception) {
|
|
58
58
|
null
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
fun getChannel(context: Context): String {
|
|
62
|
+
val id = context.resources.getIdentifier("hot_updater_channel", "string", context.packageName)
|
|
63
|
+
return if (id != 0) {
|
|
64
|
+
context.getString(id).takeIf { it.isNotEmpty() } ?: DEFAULT_CHANNEL
|
|
65
|
+
} else {
|
|
66
|
+
DEFAULT_CHANNEL
|
|
67
|
+
}
|
|
68
|
+
}
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
/**
|
|
@@ -33,11 +33,12 @@ interface PreferencesService {
|
|
|
33
33
|
class VersionedPreferencesService(
|
|
34
34
|
private val context: Context,
|
|
35
35
|
private val appVersion: String,
|
|
36
|
+
private val appChannel: String,
|
|
36
37
|
) : PreferencesService {
|
|
37
38
|
private val prefs: SharedPreferences
|
|
38
39
|
|
|
39
40
|
init {
|
|
40
|
-
val prefsName = "HotUpdaterPrefs_$appVersion"
|
|
41
|
+
val prefsName = "HotUpdaterPrefs_${appVersion}_$appChannel"
|
|
41
42
|
|
|
42
43
|
val sharedPrefsDir = File(context.applicationInfo.dataDir, "shared_prefs")
|
|
43
44
|
if (sharedPrefsDir.exists() && sharedPrefsDir.isDirectory) {
|
|
@@ -196,7 +196,12 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
196
196
|
for item in contents {
|
|
197
197
|
let fullPath = (storeDir as NSString).appendingPathComponent(item)
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
// Skip .tmp directories
|
|
200
|
+
if item.hasSuffix(".tmp") {
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if let currentPath = currentBundlePath, fullPath == currentPath {
|
|
200
205
|
continue
|
|
201
206
|
}
|
|
202
207
|
|
|
@@ -230,6 +235,18 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
230
235
|
var removedCount = 0
|
|
231
236
|
for item in contents {
|
|
232
237
|
let fullPath = (storeDir as NSString).appendingPathComponent(item)
|
|
238
|
+
// Skip .tmp directories as well
|
|
239
|
+
if item.hasSuffix(".tmp") {
|
|
240
|
+
// Clean up any stale .tmp directories
|
|
241
|
+
do {
|
|
242
|
+
try self.fileSystem.removeItem(atPath: fullPath)
|
|
243
|
+
NSLog("[BundleStorage] Removed stale tmp directory: \(fullPath)")
|
|
244
|
+
} catch {
|
|
245
|
+
NSLog("[BundleStorage] Failed to remove stale tmp directory \(fullPath): \(error)")
|
|
246
|
+
}
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
|
|
233
250
|
if !bundlesToKeep.contains(fullPath) {
|
|
234
251
|
do {
|
|
235
252
|
try self.fileSystem.removeItem(atPath: fullPath)
|
|
@@ -237,7 +254,6 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
237
254
|
NSLog("[BundleStorage] Removed old bundle: \(item)")
|
|
238
255
|
} catch {
|
|
239
256
|
NSLog("[BundleStorage] Failed to remove old bundle at \(fullPath): \(error)")
|
|
240
|
-
// Optionally, collect errors and return a multiple error type or first error
|
|
241
257
|
}
|
|
242
258
|
}
|
|
243
259
|
}
|
|
@@ -332,7 +348,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
332
348
|
return
|
|
333
349
|
}
|
|
334
350
|
|
|
335
|
-
// Start the bundle update process
|
|
351
|
+
// Start the bundle update process on a background queue
|
|
336
352
|
fileOperationQueue.async {
|
|
337
353
|
let storeDirResult = self.bundleStoreDir()
|
|
338
354
|
guard case .success(let storeDir) = storeDirResult else {
|
|
@@ -349,8 +365,6 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
349
365
|
if let bundlePath = existingBundlePath {
|
|
350
366
|
NSLog("[BundleStorage] Using cached bundle at path: \(bundlePath)")
|
|
351
367
|
do {
|
|
352
|
-
try self.fileSystem.setAttributes([.modificationDate: Date()], ofItemAtPath: finalBundleDir)
|
|
353
|
-
|
|
354
368
|
let setResult = self.setBundleURL(localPath: bundlePath)
|
|
355
369
|
switch setResult {
|
|
356
370
|
case .success:
|
|
@@ -360,7 +374,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
360
374
|
completion(.success(true))
|
|
361
375
|
case .failure(let error):
|
|
362
376
|
NSLog("[BundleStorage] Warning: Cleanup failed but bundle is set: \(error)")
|
|
363
|
-
completion(.failure(error))
|
|
377
|
+
completion(.failure(error))
|
|
364
378
|
}
|
|
365
379
|
case .failure(let error):
|
|
366
380
|
completion(.failure(error))
|
|
@@ -373,9 +387,8 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
373
387
|
NSLog("[BundleStorage] Cached directory exists but invalid, removing: \(finalBundleDir)")
|
|
374
388
|
do {
|
|
375
389
|
try self.fileSystem.removeItem(atPath: finalBundleDir)
|
|
376
|
-
// Continue with download process on success
|
|
377
|
-
|
|
378
|
-
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, finalBundleDir: finalBundleDir, completion: completion)
|
|
390
|
+
// Continue with download process on success
|
|
391
|
+
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, storeDir: storeDir, completion: completion)
|
|
379
392
|
} catch let error {
|
|
380
393
|
NSLog("[BundleStorage] Failed to remove invalid bundle dir: \(error.localizedDescription)")
|
|
381
394
|
completion(.failure(BundleStorageError.fileSystemError(error)))
|
|
@@ -385,7 +398,7 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
385
398
|
completion(.failure(error))
|
|
386
399
|
}
|
|
387
400
|
} else {
|
|
388
|
-
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl,
|
|
401
|
+
self.prepareAndDownloadBundle(bundleId: bundleId, fileUrl: validFileUrl, storeDir: storeDir, completion: completion)
|
|
389
402
|
}
|
|
390
403
|
}
|
|
391
404
|
}
|
|
@@ -395,46 +408,44 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
395
408
|
* This method is part of the asynchronous `updateBundle` flow.
|
|
396
409
|
* @param bundleId ID of the bundle to update
|
|
397
410
|
* @param fileUrl URL of the bundle file to download
|
|
398
|
-
* @param
|
|
411
|
+
* @param storeDir Path to the bundle-store directory
|
|
399
412
|
* @param completion Callback with result of the operation
|
|
400
413
|
*/
|
|
401
|
-
private func prepareAndDownloadBundle(
|
|
402
|
-
|
|
414
|
+
private func prepareAndDownloadBundle(
|
|
415
|
+
bundleId: String,
|
|
416
|
+
fileUrl: URL,
|
|
417
|
+
storeDir: String,
|
|
418
|
+
completion: @escaping (Result<Bool, Error>) -> Void
|
|
419
|
+
) {
|
|
420
|
+
// 1) Prepare temp directory for download
|
|
403
421
|
let tempDirResult = tempDir()
|
|
404
422
|
guard case .success(let tempDirectory) = tempDirResult else {
|
|
405
423
|
completion(.failure(tempDirResult.failureError ?? BundleStorageError.unknown(nil)))
|
|
406
424
|
return
|
|
407
425
|
}
|
|
408
426
|
|
|
409
|
-
//
|
|
410
|
-
// This is already within a fileOperationQueue.async block from updateBundle or needs to be if called directly.
|
|
411
|
-
// For safety, ensure this block runs on the fileOperationQueue if it's not already.
|
|
412
|
-
// However, prepareAndDownloadBundle is called from an existing fileOperationQueue.async block in updateBundle.
|
|
413
|
-
|
|
414
|
-
// Clean up any previous temp dir (sync operation)
|
|
427
|
+
// 2) Clean up any previous temp dir
|
|
415
428
|
try? self.fileSystem.removeItem(atPath: tempDirectory)
|
|
416
429
|
|
|
417
|
-
// Create
|
|
430
|
+
// 3) Create temp dir
|
|
418
431
|
if !self.fileSystem.createDirectory(atPath: tempDirectory) {
|
|
419
432
|
completion(.failure(BundleStorageError.directoryCreationFailed))
|
|
420
433
|
return
|
|
421
434
|
}
|
|
422
435
|
|
|
436
|
+
// 4) Define paths for ZIP file
|
|
423
437
|
let tempZipFile = (tempDirectory as NSString).appendingPathComponent("bundle.zip")
|
|
424
|
-
let extractedDir = (tempDirectory as NSString).appendingPathComponent("extracted")
|
|
425
|
-
|
|
426
|
-
if !self.fileSystem.createDirectory(atPath: extractedDir) {
|
|
427
|
-
completion(.failure(BundleStorageError.directoryCreationFailed))
|
|
428
|
-
return
|
|
429
|
-
}
|
|
430
438
|
|
|
431
439
|
NSLog("[BundleStorage] Starting download from \(fileUrl)")
|
|
432
440
|
|
|
433
|
-
// DownloadService handles its own threading for the download task.
|
|
441
|
+
// 5) DownloadService handles its own threading for the download task.
|
|
434
442
|
// The completion handler for downloadService.downloadFile is then dispatched to fileOperationQueue.
|
|
435
|
-
let task = self.downloadService.downloadFile(from: fileUrl,
|
|
436
|
-
|
|
437
|
-
|
|
443
|
+
let task = self.downloadService.downloadFile(from: fileUrl,
|
|
444
|
+
to: tempZipFile,
|
|
445
|
+
progressHandler: { _ in
|
|
446
|
+
// Progress updates handled by notification system
|
|
447
|
+
},
|
|
448
|
+
completion: { [weak self] result in
|
|
438
449
|
guard let self = self else {
|
|
439
450
|
let error = NSError(domain: "HotUpdaterError", code: 998,
|
|
440
451
|
userInfo: [NSLocalizedDescriptionKey: "Self deallocated during download"])
|
|
@@ -446,16 +457,12 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
446
457
|
let workItem = DispatchWorkItem {
|
|
447
458
|
switch result {
|
|
448
459
|
case .success(let location):
|
|
449
|
-
self.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
tempDirectory: tempDirectory,
|
|
456
|
-
completion: completion
|
|
457
|
-
)
|
|
458
|
-
|
|
460
|
+
self.processDownloadedFileWithTmp(location: location,
|
|
461
|
+
tempZipFile: tempZipFile,
|
|
462
|
+
storeDir: storeDir,
|
|
463
|
+
bundleId: bundleId,
|
|
464
|
+
tempDirectory: tempDirectory,
|
|
465
|
+
completion: completion)
|
|
459
466
|
case .failure(let error):
|
|
460
467
|
NSLog("[BundleStorage] Download failed: \(error.localizedDescription)")
|
|
461
468
|
self.cleanupTemporaryFiles([tempDirectory]) // Sync cleanup
|
|
@@ -471,30 +478,27 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
471
478
|
}
|
|
472
479
|
|
|
473
480
|
/**
|
|
474
|
-
* Processes a downloaded bundle file.
|
|
481
|
+
* Processes a downloaded bundle file using the “.tmp” rename approach.
|
|
475
482
|
* This method is part of the asynchronous `updateBundle` flow and is expected to run on a background thread.
|
|
476
483
|
* @param location URL of the downloaded file
|
|
477
484
|
* @param tempZipFile Path to store the downloaded zip file
|
|
478
|
-
* @param
|
|
479
|
-
* @param finalBundleDir Final directory for the bundle
|
|
485
|
+
* @param storeDir Path to the bundle-store directory
|
|
480
486
|
* @param bundleId ID of the bundle being processed
|
|
481
487
|
* @param tempDirectory Temporary directory for processing
|
|
482
488
|
* @param completion Callback with result of the operation
|
|
483
489
|
*/
|
|
484
|
-
private func
|
|
490
|
+
private func processDownloadedFileWithTmp(
|
|
485
491
|
location: URL,
|
|
486
492
|
tempZipFile: String,
|
|
487
|
-
|
|
488
|
-
finalBundleDir: String,
|
|
493
|
+
storeDir: String,
|
|
489
494
|
bundleId: String,
|
|
490
495
|
tempDirectory: String,
|
|
491
496
|
completion: @escaping (Result<Bool, Error>) -> Void
|
|
492
497
|
) {
|
|
493
498
|
NSLog("[BundleStorage] Processing downloaded file atPath: \(location.path)")
|
|
494
499
|
|
|
495
|
-
// 1
|
|
500
|
+
// 1) Ensure the ZIP file exists
|
|
496
501
|
guard self.fileSystem.fileExists(atPath: location.path) else {
|
|
497
|
-
NSLog("[BundleStorage] Source file does not exist atPath: \(location.path)")
|
|
498
502
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
499
503
|
completion(.failure(BundleStorageError.fileSystemError(NSError(
|
|
500
504
|
domain: "HotUpdaterError",
|
|
@@ -504,80 +508,80 @@ class BundleFileStorageService: BundleStorageService {
|
|
|
504
508
|
return
|
|
505
509
|
}
|
|
506
510
|
|
|
507
|
-
// 2
|
|
511
|
+
// 2) Define tmpDir and realDir
|
|
512
|
+
let tmpDir = (storeDir as NSString).appendingPathComponent("\(bundleId).tmp")
|
|
513
|
+
let realDir = (storeDir as NSString).appendingPathComponent(bundleId)
|
|
514
|
+
|
|
508
515
|
do {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
try self.fileSystem.createDirectory(atPath: tempZipFileDirectory.path)
|
|
514
|
-
NSLog("[BundleStorage] Created directory atPath: \(tempZipFileDirectory.path)")
|
|
516
|
+
// 3) Remove any existing tmpDir
|
|
517
|
+
if self.fileSystem.fileExists(atPath: tmpDir) {
|
|
518
|
+
try self.fileSystem.removeItem(atPath: tmpDir)
|
|
519
|
+
NSLog("[BundleStorage] Removed existing tmpDir: \(tmpDir)")
|
|
515
520
|
}
|
|
516
521
|
|
|
517
|
-
|
|
518
|
-
|
|
522
|
+
// 4) Create tmpDir
|
|
523
|
+
try self.fileSystem.createDirectory(atPath: tmpDir)
|
|
524
|
+
NSLog("[BundleStorage] Created tmpDir: \(tmpDir)")
|
|
519
525
|
|
|
520
|
-
|
|
521
|
-
NSLog("[BundleStorage]
|
|
526
|
+
// 5) Unzip directly into tmpDir
|
|
527
|
+
NSLog("[BundleStorage] Unzipping \(tempZipFile) → \(tmpDir)")
|
|
528
|
+
try self.unzipService.unzip(file: tempZipFile, to: tmpDir)
|
|
529
|
+
NSLog("[BundleStorage] Unzip complete at \(tmpDir)")
|
|
522
530
|
|
|
523
|
-
// 6
|
|
531
|
+
// 6) Remove the downloaded ZIP file
|
|
524
532
|
try? self.fileSystem.removeItem(atPath: tempZipFile)
|
|
525
533
|
|
|
526
|
-
// 7
|
|
527
|
-
switch self.findBundleFile(in:
|
|
528
|
-
case .success(let
|
|
529
|
-
if let
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
try self.fileSystem.createDirectory(atPath: finalBundleDir)
|
|
535
|
-
NSLog("[BundleStorage] Created final bundle directory atPath: \(finalBundleDir)")
|
|
534
|
+
// 7) Verify that a valid bundle file exists inside tmpDir
|
|
535
|
+
switch self.findBundleFile(in: tmpDir) {
|
|
536
|
+
case .success(let maybeBundlePath):
|
|
537
|
+
if let bundlePathInTmp = maybeBundlePath {
|
|
538
|
+
// 8) Remove any existing realDir
|
|
539
|
+
if self.fileSystem.fileExists(atPath: realDir) {
|
|
540
|
+
try self.fileSystem.removeItem(atPath: realDir)
|
|
541
|
+
NSLog("[BundleStorage] Removed existing realDir: \(realDir)")
|
|
536
542
|
}
|
|
537
543
|
|
|
538
|
-
// 9
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
try self.fileSystem.moveItem(atPath: extractedDir, toPath: finalBundleDir)
|
|
543
|
-
NSLog("[BundleStorage] Successfully moved entire bundle directory to: \(finalBundleDir)")
|
|
544
|
+
// 9) Rename (move) tmpDir → realDir
|
|
545
|
+
try self.fileSystem.moveItem(atPath: tmpDir, toPath: realDir)
|
|
546
|
+
NSLog("[BundleStorage] Renamed tmpDir to realDir: \(realDir)")
|
|
544
547
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
554
|
-
completion(.success(true))
|
|
555
|
-
case .failure(let error):
|
|
556
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
557
|
-
completion(.failure(error))
|
|
558
|
-
}
|
|
559
|
-
} else {
|
|
560
|
-
NSLog("[BundleStorage] No bundle file found in final directory")
|
|
561
|
-
self.cleanupTemporaryFiles([tempDirectory])
|
|
562
|
-
completion(.failure(BundleStorageError.invalidBundle))
|
|
563
|
-
}
|
|
564
|
-
case .failure(let error):
|
|
565
|
-
NSLog("[BundleStorage] Error finding bundle file: \(error.localizedDescription)")
|
|
548
|
+
// 10) Construct final bundlePath for preferences
|
|
549
|
+
let finalBundlePath = (realDir as NSString).appendingPathComponent((bundlePathInTmp as NSString).lastPathComponent)
|
|
550
|
+
|
|
551
|
+
// 11) Set the bundle URL in preferences
|
|
552
|
+
let setResult = self.setBundleURL(localPath: finalBundlePath)
|
|
553
|
+
switch setResult {
|
|
554
|
+
case .success:
|
|
555
|
+
// 12) Clean up the temporary directory
|
|
566
556
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
567
|
-
|
|
557
|
+
|
|
558
|
+
// 13) Clean up old bundles, preserving current and latest
|
|
559
|
+
let _ = self.cleanupOldBundles(currentBundleId: bundleId)
|
|
560
|
+
|
|
561
|
+
// 14) Complete with success
|
|
562
|
+
completion(.success(true))
|
|
563
|
+
case .failure(let err):
|
|
564
|
+
// Preferences save failed → remove realDir and clean up
|
|
565
|
+
try? self.fileSystem.removeItem(atPath: realDir)
|
|
566
|
+
self.cleanupTemporaryFiles([tempDirectory])
|
|
567
|
+
completion(.failure(err))
|
|
568
568
|
}
|
|
569
569
|
} else {
|
|
570
|
-
|
|
570
|
+
// No valid .jsbundle found → delete tmpDir and fail
|
|
571
|
+
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
571
572
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
572
573
|
completion(.failure(BundleStorageError.invalidBundle))
|
|
573
574
|
}
|
|
574
|
-
case .failure(let
|
|
575
|
-
|
|
575
|
+
case .failure(let findError):
|
|
576
|
+
// Error scanning tmpDir → delete tmpDir and fail
|
|
577
|
+
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
576
578
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
577
|
-
completion(.failure(
|
|
579
|
+
completion(.failure(findError))
|
|
578
580
|
}
|
|
579
581
|
} catch let error {
|
|
580
|
-
|
|
582
|
+
// Any failure during unzip or rename → clean tmpDir and fail
|
|
583
|
+
NSLog("[BundleStorage] Error during tmpDir processing: \(error)")
|
|
584
|
+
try? self.fileSystem.removeItem(atPath: tmpDir)
|
|
581
585
|
self.cleanupTemporaryFiles([tempDirectory])
|
|
582
586
|
completion(.failure(BundleStorageError.fileSystemError(error)))
|
|
583
587
|
}
|
|
@@ -590,4 +594,4 @@ extension Result {
|
|
|
590
594
|
guard case .failure(let error) = self else { return nil }
|
|
591
595
|
return error
|
|
592
596
|
}
|
|
593
|
-
}
|
|
597
|
+
}
|
|
@@ -15,7 +15,6 @@ protocol FileSystemService {
|
|
|
15
15
|
func moveItem(atPath srcPath: String, toPath dstPath: String) throws
|
|
16
16
|
func copyItem(atPath srcPath: String, toPath dstPath: String) throws
|
|
17
17
|
func contentsOfDirectory(atPath path: String) throws -> [String]
|
|
18
|
-
func setAttributes(_ attributes: [FileAttributeKey: Any], ofItemAtPath path: String) throws
|
|
19
18
|
func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any]
|
|
20
19
|
func documentsPath() -> String
|
|
21
20
|
}
|
|
@@ -72,15 +71,6 @@ class FileManagerService: FileSystemService {
|
|
|
72
71
|
throw FileSystemError.fileOperationFailed(path, error)
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
|
-
|
|
76
|
-
func setAttributes(_ attributes: [FileAttributeKey: Any], ofItemAtPath path: String) throws {
|
|
77
|
-
do {
|
|
78
|
-
try fileManager.setAttributes(attributes, ofItemAtPath: path)
|
|
79
|
-
} catch let error {
|
|
80
|
-
NSLog("[FileSystemService] Failed to set attributes for \(path): \(error)")
|
|
81
|
-
throw FileSystemError.fileOperationFailed(path, error)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
74
|
|
|
85
75
|
func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any] {
|
|
86
76
|
do {
|
|
@@ -4,7 +4,9 @@ import React
|
|
|
4
4
|
@objcMembers public class HotUpdaterImpl: NSObject {
|
|
5
5
|
private let bundleStorage: BundleStorageService
|
|
6
6
|
private let preferences: PreferencesService
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
private static let DEFAULT_CHANNEL = "production"
|
|
9
|
+
|
|
8
10
|
// MARK: - Initialization
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -35,10 +37,13 @@ import React
|
|
|
35
37
|
self.bundleStorage = bundleStorage
|
|
36
38
|
self.preferences = preferences
|
|
37
39
|
super.init()
|
|
38
|
-
|
|
40
|
+
|
|
39
41
|
// Configure preferences with app version
|
|
40
42
|
if let appVersion = HotUpdaterImpl.appVersion {
|
|
41
|
-
(preferences as? VersionedPreferencesService)?.configure(
|
|
43
|
+
(preferences as? VersionedPreferencesService)?.configure(
|
|
44
|
+
appVersion: appVersion,
|
|
45
|
+
appChannel: HotUpdaterImpl.appChannel
|
|
46
|
+
)
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
|
|
@@ -50,15 +55,21 @@ import React
|
|
|
50
55
|
public static var appVersion: String? {
|
|
51
56
|
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
|
|
52
57
|
}
|
|
53
|
-
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns the app version from main bundle info.
|
|
61
|
+
*/
|
|
62
|
+
public static var appChannel: String {
|
|
63
|
+
return Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_CHANNEL") as? String ?? Self.DEFAULT_CHANNEL
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
// MARK: - Channel Management
|
|
55
67
|
|
|
56
68
|
/**
|
|
57
69
|
* Gets the current update channel.
|
|
58
70
|
* @return The channel name or nil if not set
|
|
59
71
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
|
|
62
73
|
public func getChannel() -> String {
|
|
63
74
|
return Bundle.main.object(forInfoDictionaryKey: "HOT_UPDATER_CHANNEL") as? String ?? Self.DEFAULT_CHANNEL
|
|
64
75
|
}
|
|
@@ -23,8 +23,8 @@ class VersionedPreferencesService: PreferencesService {
|
|
|
23
23
|
* Configures the service with app version for key prefixing.
|
|
24
24
|
* @param appVersion The app version to use for key prefixing
|
|
25
25
|
*/
|
|
26
|
-
func configure(appVersion: String
|
|
27
|
-
self.keyPrefix = "hotupdater_\(appVersion ?? "unknown")_"
|
|
26
|
+
func configure(appVersion: String?, appChannel: String) {
|
|
27
|
+
self.keyPrefix = "hotupdater_\(appVersion ?? "unknown")_\(appChannel)_"
|
|
28
28
|
NSLog("[PreferencesService] Configured with appVersion: \(appVersion ?? "nil"). Key prefix: \(self.keyPrefix)")
|
|
29
29
|
}
|
|
30
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/react-native",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
4
4
|
"description": "React Native OTA solution for self-hosted",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
@@ -118,8 +118,8 @@
|
|
|
118
118
|
},
|
|
119
119
|
"dependencies": {
|
|
120
120
|
"use-sync-external-store": "1.5.0",
|
|
121
|
-
"@hot-updater/core": "0.18.
|
|
122
|
-
"@hot-updater/js": "0.18.
|
|
121
|
+
"@hot-updater/core": "0.18.3",
|
|
122
|
+
"@hot-updater/js": "0.18.3"
|
|
123
123
|
},
|
|
124
124
|
"scripts": {
|
|
125
125
|
"build": "bob build && tsc -p plugin/tsconfig.json",
|