@hot-updater/react-native 0.18.2 → 0.18.4

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.
@@ -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
- // Extract the zip file
145
- if (!unzipService.extractZipFile(tempZipFile.absolutePath, extractedDir.absolutePath)) {
146
- Log.d("BundleStorage", "Failed to extract zip file.")
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 the bundle file
152
- val indexFileExtracted = extractedDir.walk().find { it.name == "index.android.bundle" }
153
- if (indexFileExtracted == null) {
154
- Log.d("BundleStorage", "index.android.bundle not found in extracted files.")
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
- // Move to final location
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
- if (!fileSystem.moveItem(extractedDir.absolutePath, finalBundleDir.absolutePath)) {
165
- fileSystem.copyItem(extractedDir.absolutePath, finalBundleDir.absolutePath)
166
- extractedDir.deleteRecursively()
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 final directory.")
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
- cleanupOldBundles(bundleStoreDir)
201
+
202
+ // 9) Clean up temporary and download folders
181
203
  tempDir.deleteRecursively()
182
204
 
183
- Log.d("BundleStorage", "Downloaded and extracted file successfully.")
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
- val bundles = bundleStoreDir.listFiles { file -> file.isDirectory }?.toList() ?: return
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
  }
@@ -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
- if fullPath == currentBundlePath {
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, dispatching the main logic to fileOperationQueue
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)) // Or consider .success(true) if main operation succeeded
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 (must be called on a thread that can continue,
377
- // but prepareAndDownloadBundle will manage its own threading for download)
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, finalBundleDir: finalBundleDir, completion: completion)
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 finalBundleDir Final directory for the bundle
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(bundleId: String, fileUrl: URL, finalBundleDir: String, completion: @escaping (Result<Bool, Error>) -> Void) {
402
- // tempDir() is now synchronous. The rest of this function manages async download.
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
- // The rest of the operations (cleanup, dir creation, download) should still be on a background thread
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 necessary directories (sync operation)
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, to: tempZipFile, progressHandler: { progress in
436
- // Progress updates handled by notification system
437
- }, completion: { [weak self] result in
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.processDownloadedFile(
450
- location: location,
451
- tempZipFile: tempZipFile,
452
- extractedDir: extractedDir,
453
- finalBundleDir: finalBundleDir,
454
- bundleId: bundleId,
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 extractedDir Directory to extract contents to
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 processDownloadedFile(
490
+ private func processDownloadedFileWithTmp(
485
491
  location: URL,
486
492
  tempZipFile: String,
487
- extractedDir: String,
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. Check if source file exists
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",
@@ -503,79 +507,81 @@ class BundleFileStorageService: BundleStorageService {
503
507
  ))))
504
508
  return
505
509
  }
506
-
507
- // 2. Create target directory
510
+
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
- let tempZipFileURL = URL(fileURLWithPath: tempZipFile)
510
- let tempZipFileDirectory = tempZipFileURL.deletingLastPathComponent()
511
-
512
- if !self.fileSystem.fileExists(atPath: tempZipFileDirectory.path) {
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
- NSLog("[BundleStorage] Successfully downloaded file to: \(tempZipFile)")
517
-
518
- // 3. Unzip the file
519
- try self.unzipService.unzip(file: tempZipFile, to: extractedDir)
520
- NSLog("[BundleStorage] Successfully extracted to: \(extractedDir)")
521
521
 
522
- // 6. Remove temporary zip file
522
+ // 4) Create tmpDir
523
+ try self.fileSystem.createDirectory(atPath: tmpDir)
524
+ NSLog("[BundleStorage] Created tmpDir: \(tmpDir)")
525
+
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)")
530
+
531
+ // 6) Remove the downloaded ZIP file
523
532
  try? self.fileSystem.removeItem(atPath: tempZipFile)
524
533
 
525
- // 7. Search for bundle file
526
- switch self.findBundleFile(in: extractedDir) {
527
- case .success(let bundlePath):
528
- if let bundlePath = bundlePath {
529
- NSLog("[BundleStorage] Found bundle atPath: \(bundlePath)")
530
-
531
- // 8. Create final bundle directory
532
- if !self.fileSystem.fileExists(atPath: finalBundleDir) {
533
- try self.fileSystem.createDirectory(atPath: finalBundleDir)
534
- 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)")
535
542
  }
536
543
 
537
- // 9. Move entire extracted directory to final location
538
- if self.fileSystem.fileExists(atPath: finalBundleDir) {
539
- try self.fileSystem.removeItem(atPath: finalBundleDir)
540
- }
541
- try self.fileSystem.moveItem(atPath: extractedDir, toPath: finalBundleDir)
542
- 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)")
543
547
 
544
- let findResult = self.findBundleFile(in: finalBundleDir)
545
- switch findResult {
546
- case .success(let finalBundlePath):
547
- if let finalBundlePath = finalBundlePath {
548
- let setResult = self.setBundleURL(localPath: finalBundlePath)
549
- switch setResult {
550
- case .success:
551
- self.cleanupTemporaryFiles([tempDirectory])
552
- completion(.success(true))
553
- case .failure(let error):
554
- self.cleanupTemporaryFiles([tempDirectory])
555
- completion(.failure(error))
556
- }
557
- } else {
558
- NSLog("[BundleStorage] No bundle file found in final directory")
559
- self.cleanupTemporaryFiles([tempDirectory])
560
- completion(.failure(BundleStorageError.invalidBundle))
561
- }
562
- case .failure(let error):
563
- 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
564
556
  self.cleanupTemporaryFiles([tempDirectory])
565
- completion(.failure(error))
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))
566
568
  }
567
569
  } else {
568
- NSLog("[BundleStorage] No bundle file found in extracted directory")
570
+ // No valid .jsbundle found delete tmpDir and fail
571
+ try? self.fileSystem.removeItem(atPath: tmpDir)
569
572
  self.cleanupTemporaryFiles([tempDirectory])
570
573
  completion(.failure(BundleStorageError.invalidBundle))
571
574
  }
572
- case .failure(let error):
573
- NSLog("[BundleStorage] Error finding bundle file: \(error.localizedDescription)")
575
+ case .failure(let findError):
576
+ // Error scanning tmpDir delete tmpDir and fail
577
+ try? self.fileSystem.removeItem(atPath: tmpDir)
574
578
  self.cleanupTemporaryFiles([tempDirectory])
575
- completion(.failure(error))
579
+ completion(.failure(findError))
576
580
  }
577
581
  } catch let error {
578
- NSLog("[BundleStorage] Error processing downloaded file: \(error.localizedDescription)")
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)
579
585
  self.cleanupTemporaryFiles([tempDirectory])
580
586
  completion(.failure(BundleStorageError.fileSystemError(error)))
581
587
  }
@@ -588,4 +594,4 @@ extension Result {
588
594
  guard case .failure(let error) = self else { return nil }
589
595
  return error
590
596
  }
591
- }
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/react-native",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
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.2",
122
- "@hot-updater/js": "0.18.2"
121
+ "@hot-updater/core": "0.18.4",
122
+ "@hot-updater/js": "0.18.4"
123
123
  },
124
124
  "scripts": {
125
125
  "build": "bob build && tsc -p plugin/tsconfig.json",