@capawesome/capacitor-live-update 8.4.2 → 8.4.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/README.md CHANGED
@@ -55,9 +55,10 @@ The Live Update plugin is typically used to ship changes to users without going
55
55
 
56
56
  - [Getting Started with Capawesome Cloud Live Updates](https://capawesome.io/docs/cloud/live-updates/setup/)
57
57
  - [Capacitor Live Updates: The Complete OTA Guide](https://capawesome.io/blog/capacitor-live-updates-guide/)
58
- - [Best OTA Update Tools for Capacitor (2026)](https://capawesome.io/blog/best-ota-update-tools-for-capacitor/)
58
+ - [Best Live Update Tools for Capacitor (2026)](https://capawesome.io/blog/best-ota-update-tools-for-capacitor/)
59
59
  - [Capacitor Live Updates: Signing vs Encryption](https://capawesome.io/blog/capacitor-live-updates-end-to-end-encryption/)
60
60
  - [Capacitor Live Updates: Reliability & Uptime](https://capawesome.io/blog/capacitor-live-updates-reliability-and-uptime/)
61
+ - [Capawesome vs. Capgo: Which Is More Reliable?](https://capawesome.io/blog/capawesome-cloud-vs-capgo-reliability/)
61
62
  - [Migrating from Ionic Appflow to Capawesome Cloud](https://capawesome.io/blog/migrating-from-ionic-appflow-to-capawesome-cloud/)
62
63
  - [Migrating from App Center to Capawesome Cloud](https://capawesome.io/blog/migrating-from-app-center-to-capawesome-cloud/)
63
64
  - [Live Updates for Nuxt Capacitor Apps](https://capawesome.io/blog/live-updates-for-nuxt-capacitor/)
@@ -104,6 +104,7 @@ public class LiveUpdate {
104
104
  private final SharedPreferences.Editor webViewSettingsEditor;
105
105
 
106
106
  private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
107
+ private final String downloadsDirectory = "capawesome_capacitor_live_update_downloads";
107
108
  private final Handler rollbackHandler = new Handler(Looper.getMainLooper());
108
109
  private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
109
110
 
@@ -125,6 +126,9 @@ public class LiveUpdate {
125
126
  // Set the device ID on the HTTP client (after any potential config reset)
126
127
  this.httpClient.setDeviceId(getDeviceId());
127
128
 
129
+ // Delete the leftovers of downloads that were interrupted by a process kill
130
+ tryDeleteDownloadsDirectory();
131
+
128
132
  // Start the rollback timer to rollback to the default bundle
129
133
  // if the app is not ready after a certain time
130
134
  startRollbackTimer();
@@ -536,7 +540,10 @@ public class LiveUpdate {
536
540
 
537
541
  // Move the bundle directory to the bundles directory
538
542
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
539
- indexHtmlFile.getParentFile().renameTo(bundleDirectory);
543
+ boolean moved = indexHtmlFile.getParentFile().renameTo(bundleDirectory);
544
+ if (!moved) {
545
+ throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_MOVE_FAILED);
546
+ }
540
547
  }
541
548
 
542
549
  private void addBundleOfTypeManifest(@NonNull String bundleId, @NonNull File directory) throws Exception {
@@ -545,7 +552,7 @@ public class LiveUpdate {
545
552
 
546
553
  private void addBundleOfTypeZip(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
547
554
  // Unzip the file to the bundle directory
548
- File unzippedDirectory = unzipFile(zipFile);
555
+ File unzippedDirectory = unzipDownloadedBundle(zipFile);
549
556
  // Add the bundle
550
557
  addBundle(bundleId, unzippedDirectory);
551
558
  }
@@ -558,14 +565,8 @@ public class LiveUpdate {
558
565
  return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
559
566
  }
560
567
 
561
- private File buildTemporaryDirectory() {
562
- String fileName = UUID.randomUUID().toString();
563
- return new File(plugin.getContext().getCacheDir(), fileName);
564
- }
565
-
566
- private File buildTemporaryZipFile() {
567
- String fileName = UUID.randomUUID().toString() + ".zip";
568
- return new File(plugin.getContext().getCacheDir(), fileName);
568
+ private File buildDownloadsDirectory() {
569
+ return new File(plugin.getContext().getCacheDir(), downloadsDirectory);
569
570
  }
570
571
 
571
572
  private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
@@ -652,10 +653,10 @@ public class LiveUpdate {
652
653
  }
653
654
  }
654
655
 
655
- private File createTemporaryDirectory() {
656
- File file = buildTemporaryDirectory();
657
- file.mkdir();
658
- return file;
656
+ private File createTemporaryDownloadDirectory() {
657
+ File temporaryDirectory = new File(buildDownloadsDirectory(), UUID.randomUUID().toString());
658
+ temporaryDirectory.mkdirs();
659
+ return temporaryDirectory;
659
660
  }
660
661
 
661
662
  private void deleteBundleById(@NonNull String bundleId) {
@@ -681,6 +682,23 @@ public class LiveUpdate {
681
682
  file.delete();
682
683
  }
683
684
 
685
+ @NonNull
686
+ private EmptyCallback deleteTemporaryDirectoryOnCompletion(@NonNull File temporaryDirectory, @NonNull EmptyCallback callback) {
687
+ return new EmptyCallback() {
688
+ @Override
689
+ public void success() {
690
+ deleteFileRecursively(temporaryDirectory);
691
+ callback.success();
692
+ }
693
+
694
+ @Override
695
+ public void error(@NonNull Exception exception) {
696
+ deleteFileRecursively(temporaryDirectory);
697
+ callback.error(exception);
698
+ }
699
+ };
700
+ }
701
+
684
702
  private void deleteUnusedBundles() {
685
703
  String[] bundleIds = getDownloadedBundleIds();
686
704
  for (String bundleId : bundleIds) {
@@ -851,10 +869,10 @@ public class LiveUpdate {
851
869
  @NonNull String downloadUrl,
852
870
  @NonNull EmptyCallback completionCallback
853
871
  ) {
872
+ // Create a temporary directory that is deleted once the download completed or failed
873
+ File temporaryDirectory = createTemporaryDownloadDirectory();
874
+ EmptyCallback callback = deleteTemporaryDirectoryOnCompletion(temporaryDirectory, completionCallback);
854
875
  try {
855
- // Create a temporary directory
856
- File temporaryDirectory = createTemporaryDirectory();
857
-
858
876
  // Download the latest manifest
859
877
  downloadBundleFile(
860
878
  downloadUrl,
@@ -904,31 +922,31 @@ public class LiveUpdate {
904
922
  try {
905
923
  // Add the bundle
906
924
  addBundleOfTypeManifest(bundleId, temporaryDirectory);
907
- completionCallback.success();
925
+ callback.success();
908
926
  } catch (Exception e) {
909
- completionCallback.error(e);
927
+ callback.error(e);
910
928
  }
911
929
  }
912
930
 
913
931
  @Override
914
932
  public void error(@NonNull Exception exception) {
915
- completionCallback.error(exception);
933
+ callback.error(exception);
916
934
  }
917
935
  }
918
936
  );
919
937
  } catch (Exception e) {
920
- completionCallback.error(e);
938
+ callback.error(e);
921
939
  }
922
940
  }
923
941
 
924
942
  @Override
925
943
  public void error(@NonNull Exception exception) {
926
- completionCallback.error(exception);
944
+ callback.error(exception);
927
945
  }
928
946
  }
929
947
  );
930
948
  } catch (Exception e) {
931
- completionCallback.error(e);
949
+ callback.error(e);
932
950
  }
933
951
  }
934
952
 
@@ -939,39 +957,42 @@ public class LiveUpdate {
939
957
  @NonNull String downloadUrl,
940
958
  @NonNull EmptyCallback completionCallback
941
959
  ) {
942
- File file = buildTemporaryZipFile();
943
- // Download the bundle
944
- downloadAndVerifyFile(
945
- downloadUrl,
946
- file,
947
- checksum,
948
- signature,
949
- (downloadedBytes, totalBytes) -> {
950
- DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
951
- notifyDownloadBundleProgressListeners(event);
952
- },
953
- new EmptyCallback() {
954
- @Override
955
- public void success() {
956
- try {
957
- // Add the bundle
958
- addBundleOfTypeZip(bundleId, file);
959
- // Delete the temporary file
960
- file.delete();
961
- completionCallback.success();
962
- } catch (Exception e) {
963
- completionCallback.error(e);
960
+ // Create a temporary directory that is deleted once the download completed or failed
961
+ File temporaryDirectory = createTemporaryDownloadDirectory();
962
+ EmptyCallback callback = deleteTemporaryDirectoryOnCompletion(temporaryDirectory, completionCallback);
963
+ File zipFile = new File(temporaryDirectory, "bundle.zip");
964
+ try {
965
+ // Download the bundle
966
+ downloadAndVerifyFile(
967
+ downloadUrl,
968
+ zipFile,
969
+ checksum,
970
+ signature,
971
+ (downloadedBytes, totalBytes) -> {
972
+ DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
973
+ notifyDownloadBundleProgressListeners(event);
974
+ },
975
+ new EmptyCallback() {
976
+ @Override
977
+ public void success() {
978
+ try {
979
+ // Add the bundle
980
+ addBundleOfTypeZip(bundleId, zipFile);
981
+ callback.success();
982
+ } catch (Exception e) {
983
+ callback.error(e);
984
+ }
964
985
  }
965
- }
966
986
 
967
- @Override
968
- public void error(@NonNull Exception exception) {
969
- // Delete the temporary file on error
970
- file.delete();
971
- completionCallback.error(exception);
987
+ @Override
988
+ public void error(@NonNull Exception exception) {
989
+ callback.error(exception);
990
+ }
972
991
  }
973
- }
974
- );
992
+ );
993
+ } catch (Exception e) {
994
+ callback.error(e);
995
+ }
975
996
  }
976
997
 
977
998
  private void fetchLatestBundleInternal(
@@ -1437,8 +1458,12 @@ public class LiveUpdate {
1437
1458
  }
1438
1459
  }
1439
1460
 
1440
- private File unzipFile(@NonNull File zipFile) throws IOException {
1441
- File destination = buildTemporaryDirectory();
1461
+ private void tryDeleteDownloadsDirectory() {
1462
+ deleteFileRecursively(buildDownloadsDirectory());
1463
+ }
1464
+
1465
+ private File unzipDownloadedBundle(@NonNull File zipFile) throws IOException {
1466
+ File destination = new File(zipFile.getParentFile(), "bundle");
1442
1467
  String destinationPath = destination.getPath();
1443
1468
  ZipFile zip = new ZipFile(zipFile);
1444
1469
  // Clear stored Unix permissions to prevent EACCES errors on newer Android versions
@@ -37,12 +37,13 @@ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
37
37
  public class LiveUpdatePlugin extends Plugin {
38
38
 
39
39
  public static final String TAG = "LiveUpdate";
40
- public static final String VERSION = "8.4.2";
40
+ public static final String VERSION = "8.4.3";
41
41
  public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
42
42
  public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
43
43
  public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
44
44
  public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
45
45
  public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
46
+ public static final String ERROR_BUNDLE_MOVE_FAILED = "Bundle could not be moved.";
46
47
  public static final String ERROR_BUNDLE_NOT_FOUND = "bundle not found.";
47
48
  public static final String ERROR_CHECKSUM_CALCULATION_FAILED = "Failed to calculate checksum.";
48
49
  public static final String ERROR_CHECKSUM_MISMATCH = "Checksum mismatch.";
@@ -13,6 +13,7 @@ import CommonCrypto
13
13
  private let config: LiveUpdateConfig
14
14
  private let defaultWebAssetDir = "public" // DO NOT CHANGE! (See https://dub.sh/Buvz4yj)
15
15
  private let defaultServerPathKey = "serverBasePath" // DO NOT CHANGE! (See https://dub.sh/ceDl0zT)
16
+ private let downloadsDirectory = "capawesome_capacitor_live_update_downloads"
16
17
  private let httpClient: LiveUpdateHttpClient
17
18
  private let libraryDirectoryUrl = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
18
19
  private let manifestFileName = "capawesome-live-update-manifest.json" // DO NOT CHANGE!
@@ -37,6 +38,9 @@ import CommonCrypto
37
38
  // Set the device ID on the HTTP client (after any potential config reset)
38
39
  self.httpClient.setDeviceId(getDeviceId())
39
40
 
41
+ // Delete the leftovers of downloads that were interrupted by a process kill
42
+ tryDeleteDownloadsDirectory()
43
+
40
44
  // Start the rollback timer to rollback to the default bundle
41
45
  // if the app is not ready after a certain time
42
46
  startRollbackTimer()
@@ -355,7 +359,7 @@ import CommonCrypto
355
359
 
356
360
  private func addBundleOfTypeZip(bundleId: String, zipFile: URL) async throws {
357
361
  // Unzip the bundle
358
- let unzippedDirectory = try self.unzipFile(zipFile: zipFile)
362
+ let unzippedDirectory = try self.unzipDownloadedBundle(zipFile: zipFile)
359
363
  // Add the bundle
360
364
  try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
361
365
  }
@@ -379,6 +383,10 @@ import CommonCrypto
379
383
  return url
380
384
  }
381
385
 
386
+ private func buildDownloadsDirectoryUrl() -> URL {
387
+ return cachesDirectoryUrl.appendingPathComponent(downloadsDirectory)
388
+ }
389
+
382
390
  private func copyCurrentBundleFile(fileToCopy: ManifestItem, toDirectory: URL) throws {
383
391
  let currentBundleId = getCurrentBundleId()
384
392
  let destination = toDirectory.appendingPathComponent(fileToCopy.href)
@@ -428,8 +436,8 @@ import CommonCrypto
428
436
  }
429
437
  }
430
438
 
431
- private func createTemporaryDirectory() throws -> URL {
432
- let temporaryDirectory = cachesDirectoryUrl.appendingPathComponent(UUID().uuidString)
439
+ private func createTemporaryDownloadDirectory() throws -> URL {
440
+ let temporaryDirectory = buildDownloadsDirectoryUrl().appendingPathComponent(UUID().uuidString)
433
441
  try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
434
442
  return temporaryDirectory
435
443
  }
@@ -537,8 +545,9 @@ import CommonCrypto
537
545
  }
538
546
 
539
547
  private func downloadBundleOfTypeManifest(bundleId: String, url: String) async throws {
540
- // Create a temporary directory
541
- let temporaryDirectory = try createTemporaryDirectory()
548
+ // Create a temporary directory that is deleted once the download completed or failed
549
+ let temporaryDirectory = try createTemporaryDownloadDirectory()
550
+ defer { tryDeleteDirectory(temporaryDirectory) }
542
551
  // Download the latest manifest
543
552
  let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory, callback: nil)
544
553
  let latestManifest = try loadManifest(file: latestManifestFile)
@@ -569,15 +578,17 @@ import CommonCrypto
569
578
  }
570
579
 
571
580
  private func downloadBundleOfTypeZip(bundleId: String, checksum: String?, signature: String?, url: String) async throws {
572
- let timestamp = String(Int(Date().timeIntervalSince1970))
573
- let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
581
+ // Create a temporary directory that is deleted once the download completed or failed
582
+ let temporaryDirectory = try createTemporaryDownloadDirectory()
583
+ defer { tryDeleteDirectory(temporaryDirectory) }
584
+ let zipFile = temporaryDirectory.appendingPathComponent("bundle.zip")
574
585
  // Download the bundle
575
- try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, checksum: checksum, signature: signature, callback: { progress in
586
+ try await downloadAndVerifyFile(url: url, file: zipFile, checksum: checksum, signature: signature, callback: { progress in
576
587
  let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
577
588
  self.notifyDownloadBundleProgressListeners(event)
578
589
  })
579
590
  // Add the bundle
580
- try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
591
+ try await addBundleOfTypeZip(bundleId: bundleId, zipFile: zipFile)
581
592
  }
582
593
 
583
594
  private func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> GetLatestBundleResponse? {
@@ -963,8 +974,23 @@ import CommonCrypto
963
974
  }
964
975
  }
965
976
 
966
- private func unzipFile(zipFile: URL) throws -> URL {
967
- let destinationDirectory = zipFile.deletingPathExtension()
977
+ private func tryDeleteDirectory(_ url: URL) {
978
+ guard FileManager.default.fileExists(atPath: url.path) else {
979
+ return
980
+ }
981
+ do {
982
+ try FileManager.default.removeItem(at: url)
983
+ } catch {
984
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete directory: \(url.path)")
985
+ }
986
+ }
987
+
988
+ private func tryDeleteDownloadsDirectory() {
989
+ tryDeleteDirectory(buildDownloadsDirectoryUrl())
990
+ }
991
+
992
+ private func unzipDownloadedBundle(zipFile: URL) throws -> URL {
993
+ let destinationDirectory = zipFile.deletingLastPathComponent().appendingPathComponent("bundle")
968
994
  try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true, attributes: nil)
969
995
  try FileManager.default.unzipItem(at: zipFile, to: destinationDirectory)
970
996
  return destinationDirectory
@@ -8,7 +8,7 @@ import Capacitor
8
8
  @objc(LiveUpdatePlugin)
9
9
  public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
10
10
  public static let tag = "LiveUpdate"
11
- public static let version = "8.4.2"
11
+ public static let version = "8.4.3"
12
12
  public static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
13
13
 
14
14
  public let identifier = "LiveUpdatePlugin"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capawesome/capacitor-live-update",
3
- "version": "8.4.2",
3
+ "version": "8.4.3",
4
4
  "description": "Capacitor plugin to update your app remotely in real-time on Android and iOS.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",