@capawesome/capacitor-live-update 6.9.0 → 6.9.2

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.
@@ -7,9 +7,11 @@ import android.content.pm.PackageManager;
7
7
  import android.content.res.AssetManager;
8
8
  import android.os.Build;
9
9
  import android.os.Handler;
10
+ import android.os.Looper;
10
11
  import android.util.Base64;
11
12
  import androidx.annotation.NonNull;
12
13
  import androidx.annotation.Nullable;
14
+ import androidx.core.content.pm.PackageInfoCompat;
13
15
  import com.getcapacitor.Bridge;
14
16
  import com.getcapacitor.Logger;
15
17
  import com.getcapacitor.plugin.WebView;
@@ -103,7 +105,8 @@ public class LiveUpdate {
103
105
  private final SharedPreferences.Editor webViewSettingsEditor;
104
106
 
105
107
  private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
106
- private final Handler rollbackHandler = new Handler();
108
+ private final String downloadsDirectory = "capawesome_capacitor_live_update_downloads";
109
+ private final Handler rollbackHandler = new Handler(Looper.getMainLooper());
107
110
  private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
108
111
 
109
112
  private boolean initialPageLoaded = false;
@@ -121,6 +124,9 @@ public class LiveUpdate {
121
124
  // Set the device ID on the HTTP client
122
125
  this.httpClient.setDeviceId(getDeviceId());
123
126
 
127
+ // Delete the leftovers of downloads that were interrupted by a process kill
128
+ tryDeleteDownloadsDirectory();
129
+
124
130
  if (config.getEnabled()) {
125
131
  boolean wasUpdated = wasUpdated();
126
132
  if (wasUpdated) {
@@ -560,7 +566,10 @@ public class LiveUpdate {
560
566
 
561
567
  // Move the bundle directory to the bundles directory
562
568
  File bundleDirectory = buildBundleDirectoryFor(bundleId);
563
- indexHtmlFile.getParentFile().renameTo(bundleDirectory);
569
+ boolean moved = indexHtmlFile.getParentFile().renameTo(bundleDirectory);
570
+ if (!moved) {
571
+ throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_MOVE_FAILED);
572
+ }
564
573
  }
565
574
 
566
575
  private void addBundleOfTypeManifest(@NonNull String bundleId, @NonNull File directory) throws Exception {
@@ -569,7 +578,7 @@ public class LiveUpdate {
569
578
 
570
579
  private void addBundleOfTypeZip(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
571
580
  // Unzip the file to the bundle directory
572
- File unzippedDirectory = unzipFile(zipFile);
581
+ File unzippedDirectory = unzipDownloadedBundle(zipFile);
573
582
  // Add the bundle
574
583
  addBundle(bundleId, unzippedDirectory);
575
584
  }
@@ -582,14 +591,8 @@ public class LiveUpdate {
582
591
  return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
583
592
  }
584
593
 
585
- private File buildTemporaryDirectory() {
586
- String fileName = UUID.randomUUID().toString();
587
- return new File(plugin.getContext().getCacheDir(), fileName);
588
- }
589
-
590
- private File buildTemporaryZipFile() {
591
- String fileName = UUID.randomUUID().toString() + ".zip";
592
- return new File(plugin.getContext().getCacheDir(), fileName);
594
+ private File buildDownloadsDirectory() {
595
+ return new File(plugin.getContext().getCacheDir(), downloadsDirectory);
593
596
  }
594
597
 
595
598
  private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
@@ -676,10 +679,10 @@ public class LiveUpdate {
676
679
  }
677
680
  }
678
681
 
679
- private File createTemporaryDirectory() {
680
- File file = buildTemporaryDirectory();
681
- file.mkdir();
682
- return file;
682
+ private File createTemporaryDownloadDirectory() {
683
+ File temporaryDirectory = new File(buildDownloadsDirectory(), UUID.randomUUID().toString());
684
+ temporaryDirectory.mkdirs();
685
+ return temporaryDirectory;
683
686
  }
684
687
 
685
688
  private void deleteBundleById(@NonNull String bundleId) {
@@ -705,6 +708,23 @@ public class LiveUpdate {
705
708
  file.delete();
706
709
  }
707
710
 
711
+ @NonNull
712
+ private EmptyCallback deleteTemporaryDirectoryOnCompletion(@NonNull File temporaryDirectory, @NonNull EmptyCallback callback) {
713
+ return new EmptyCallback() {
714
+ @Override
715
+ public void success() {
716
+ deleteFileRecursively(temporaryDirectory);
717
+ callback.success();
718
+ }
719
+
720
+ @Override
721
+ public void error(@NonNull Exception exception) {
722
+ deleteFileRecursively(temporaryDirectory);
723
+ callback.error(exception);
724
+ }
725
+ };
726
+ }
727
+
708
728
  private void deleteUnusedBundles() {
709
729
  String[] bundleIds = getDownloadedBundleIds();
710
730
  for (String bundleId : bundleIds) {
@@ -875,10 +895,10 @@ public class LiveUpdate {
875
895
  @NonNull String downloadUrl,
876
896
  @NonNull EmptyCallback completionCallback
877
897
  ) {
898
+ // Create a temporary directory that is deleted once the download completed or failed
899
+ File temporaryDirectory = createTemporaryDownloadDirectory();
900
+ EmptyCallback callback = deleteTemporaryDirectoryOnCompletion(temporaryDirectory, completionCallback);
878
901
  try {
879
- // Create a temporary directory
880
- File temporaryDirectory = createTemporaryDirectory();
881
-
882
902
  // Download the latest manifest
883
903
  downloadBundleFile(
884
904
  downloadUrl,
@@ -928,31 +948,31 @@ public class LiveUpdate {
928
948
  try {
929
949
  // Add the bundle
930
950
  addBundleOfTypeManifest(bundleId, temporaryDirectory);
931
- completionCallback.success();
951
+ callback.success();
932
952
  } catch (Exception e) {
933
- completionCallback.error(e);
953
+ callback.error(e);
934
954
  }
935
955
  }
936
956
 
937
957
  @Override
938
958
  public void error(@NonNull Exception exception) {
939
- completionCallback.error(exception);
959
+ callback.error(exception);
940
960
  }
941
961
  }
942
962
  );
943
963
  } catch (Exception e) {
944
- completionCallback.error(e);
964
+ callback.error(e);
945
965
  }
946
966
  }
947
967
 
948
968
  @Override
949
969
  public void error(@NonNull Exception exception) {
950
- completionCallback.error(exception);
970
+ callback.error(exception);
951
971
  }
952
972
  }
953
973
  );
954
974
  } catch (Exception e) {
955
- completionCallback.error(e);
975
+ callback.error(e);
956
976
  }
957
977
  }
958
978
 
@@ -963,39 +983,42 @@ public class LiveUpdate {
963
983
  @NonNull String downloadUrl,
964
984
  @NonNull EmptyCallback completionCallback
965
985
  ) {
966
- File file = buildTemporaryZipFile();
967
- // Download the bundle
968
- downloadAndVerifyFile(
969
- downloadUrl,
970
- file,
971
- checksum,
972
- signature,
973
- (downloadedBytes, totalBytes) -> {
974
- DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
975
- notifyDownloadBundleProgressListeners(event);
976
- },
977
- new EmptyCallback() {
978
- @Override
979
- public void success() {
980
- try {
981
- // Add the bundle
982
- addBundleOfTypeZip(bundleId, file);
983
- // Delete the temporary file
984
- file.delete();
985
- completionCallback.success();
986
- } catch (Exception e) {
987
- completionCallback.error(e);
986
+ // Create a temporary directory that is deleted once the download completed or failed
987
+ File temporaryDirectory = createTemporaryDownloadDirectory();
988
+ EmptyCallback callback = deleteTemporaryDirectoryOnCompletion(temporaryDirectory, completionCallback);
989
+ File zipFile = new File(temporaryDirectory, "bundle.zip");
990
+ try {
991
+ // Download the bundle
992
+ downloadAndVerifyFile(
993
+ downloadUrl,
994
+ zipFile,
995
+ checksum,
996
+ signature,
997
+ (downloadedBytes, totalBytes) -> {
998
+ DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
999
+ notifyDownloadBundleProgressListeners(event);
1000
+ },
1001
+ new EmptyCallback() {
1002
+ @Override
1003
+ public void success() {
1004
+ try {
1005
+ // Add the bundle
1006
+ addBundleOfTypeZip(bundleId, zipFile);
1007
+ callback.success();
1008
+ } catch (Exception e) {
1009
+ callback.error(e);
1010
+ }
988
1011
  }
989
- }
990
1012
 
991
- @Override
992
- public void error(@NonNull Exception exception) {
993
- // Delete the temporary file on error
994
- file.delete();
995
- completionCallback.error(exception);
1013
+ @Override
1014
+ public void error(@NonNull Exception exception) {
1015
+ callback.error(exception);
1016
+ }
996
1017
  }
997
- }
998
- );
1018
+ );
1019
+ } catch (Exception e) {
1020
+ callback.error(e);
1021
+ }
999
1022
  }
1000
1023
 
1001
1024
  private void fetchLatestBundleInternal(
@@ -1199,7 +1222,7 @@ public class LiveUpdate {
1199
1222
  }
1200
1223
 
1201
1224
  private int getVersionCodeAsInt() throws PackageManager.NameNotFoundException {
1202
- return getPackageInfo().versionCode;
1225
+ return (int) PackageInfoCompat.getLongVersionCode(getPackageInfo());
1203
1226
  }
1204
1227
 
1205
1228
  private String getVersionCodeAsString() throws PackageManager.NameNotFoundException {
@@ -1458,8 +1481,12 @@ public class LiveUpdate {
1458
1481
  }
1459
1482
  }
1460
1483
 
1461
- private File unzipFile(@NonNull File zipFile) throws IOException {
1462
- File destination = buildTemporaryDirectory();
1484
+ private void tryDeleteDownloadsDirectory() {
1485
+ deleteFileRecursively(buildDownloadsDirectory());
1486
+ }
1487
+
1488
+ private File unzipDownloadedBundle(@NonNull File zipFile) throws IOException {
1489
+ File destination = new File(zipFile.getParentFile(), "bundle");
1463
1490
  String destinationPath = destination.getPath();
1464
1491
  ZipFile zip = new ZipFile(zipFile);
1465
1492
  // Clear stored Unix permissions to prevent EACCES errors on newer Android versions
@@ -39,12 +39,13 @@ import io.capawesome.capacitorjs.plugins.liveupdate.interfaces.Result;
39
39
  public class LiveUpdatePlugin extends Plugin {
40
40
 
41
41
  public static final String TAG = "LiveUpdate";
42
- public static final String VERSION = "6.9.0";
42
+ public static final String VERSION = "6.9.2";
43
43
  public static final String SHARED_PREFERENCES_NAME = "CapawesomeLiveUpdate"; // DO NOT CHANGE
44
44
  public static final String ERROR_APP_ID_MISSING = "appId must be configured.";
45
45
  public static final String ERROR_BUNDLE_EXISTS = "bundle already exists.";
46
46
  public static final String ERROR_BUNDLE_ID_MISSING = "bundleId must be provided.";
47
47
  public static final String ERROR_BUNDLE_INDEX_HTML_MISSING = "The bundle does not contain an index.html file.";
48
+ public static final String ERROR_BUNDLE_MOVE_FAILED = "Bundle could not be moved.";
48
49
  public static final String ERROR_BUNDLE_NOT_FOUND = "bundle not found.";
49
50
  public static final String ERROR_CHECKSUM_CALCULATION_FAILED = "Failed to calculate checksum.";
50
51
  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!
@@ -34,6 +35,9 @@ import CommonCrypto
34
35
  // Set the device ID on the HTTP client
35
36
  httpClient.setDeviceId(getDeviceId())
36
37
 
38
+ // Delete the leftovers of downloads that were interrupted by a process kill
39
+ tryDeleteDownloadsDirectory()
40
+
37
41
  if config.enabled {
38
42
  let wasUpdated = wasUpdated()
39
43
  if wasUpdated {
@@ -381,7 +385,7 @@ import CommonCrypto
381
385
 
382
386
  private func addBundleOfTypeZip(bundleId: String, zipFile: URL) async throws {
383
387
  // Unzip the bundle
384
- let unzippedDirectory = try self.unzipFile(zipFile: zipFile)
388
+ let unzippedDirectory = try self.unzipDownloadedBundle(zipFile: zipFile)
385
389
  // Add the bundle
386
390
  try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
387
391
  }
@@ -405,6 +409,10 @@ import CommonCrypto
405
409
  return url
406
410
  }
407
411
 
412
+ private func buildDownloadsDirectoryUrl() -> URL {
413
+ return cachesDirectoryUrl.appendingPathComponent(downloadsDirectory)
414
+ }
415
+
408
416
  private func copyCurrentBundleFile(fileToCopy: ManifestItem, toDirectory: URL) throws {
409
417
  let currentBundleId = getCurrentBundleId()
410
418
  let destination = toDirectory.appendingPathComponent(fileToCopy.href)
@@ -454,8 +462,8 @@ import CommonCrypto
454
462
  }
455
463
  }
456
464
 
457
- private func createTemporaryDirectory() throws -> URL {
458
- let temporaryDirectory = cachesDirectoryUrl.appendingPathComponent(UUID().uuidString)
465
+ private func createTemporaryDownloadDirectory() throws -> URL {
466
+ let temporaryDirectory = buildDownloadsDirectoryUrl().appendingPathComponent(UUID().uuidString)
459
467
  try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
460
468
  return temporaryDirectory
461
469
  }
@@ -489,7 +497,8 @@ import CommonCrypto
489
497
 
490
498
  private func downloadAndVerifyFile(url: String, file: URL, checksum: String?, signature: String?, callback: ((Progress) -> Void)?) async throws {
491
499
  let destination: DownloadRequest.Destination = { _, _ in
492
- return (file, [.createIntermediateDirectories])
500
+ // `removePreviousFile` ensures that a leftover file from a failed attempt does not fail the download
501
+ return (file, [.createIntermediateDirectories, .removePreviousFile])
493
502
  }
494
503
  let urlComponents = URLComponents(string: url)!
495
504
  let result = try await httpClient.download(url: urlComponents.asURL(), destination: destination, callback: callback)
@@ -562,8 +571,9 @@ import CommonCrypto
562
571
  }
563
572
 
564
573
  private func downloadBundleOfTypeManifest(bundleId: String, url: String) async throws {
565
- // Create a temporary directory
566
- let temporaryDirectory = try createTemporaryDirectory()
574
+ // Create a temporary directory that is deleted once the download completed or failed
575
+ let temporaryDirectory = try createTemporaryDownloadDirectory()
576
+ defer { tryDeleteDirectory(temporaryDirectory) }
567
577
  // Download the latest manifest
568
578
  let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory, callback: nil)
569
579
  let latestManifest = try loadManifest(file: latestManifestFile)
@@ -594,15 +604,17 @@ import CommonCrypto
594
604
  }
595
605
 
596
606
  private func downloadBundleOfTypeZip(bundleId: String, checksum: String?, signature: String?, url: String) async throws {
597
- let timestamp = String(Int(Date().timeIntervalSince1970))
598
- let temporaryZipFileUrl = self.cachesDirectoryUrl.appendingPathComponent(timestamp + ".zip")
607
+ // Create a temporary directory that is deleted once the download completed or failed
608
+ let temporaryDirectory = try createTemporaryDownloadDirectory()
609
+ defer { tryDeleteDirectory(temporaryDirectory) }
610
+ let zipFile = temporaryDirectory.appendingPathComponent("bundle.zip")
599
611
  // Download the bundle
600
- try await downloadAndVerifyFile(url: url, file: temporaryZipFileUrl, checksum: checksum, signature: signature, callback: { progress in
612
+ try await downloadAndVerifyFile(url: url, file: zipFile, checksum: checksum, signature: signature, callback: { progress in
601
613
  let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
602
614
  self.notifyDownloadBundleProgressListeners(event)
603
615
  })
604
616
  // Add the bundle
605
- try await addBundleOfTypeZip(bundleId: bundleId, zipFile: temporaryZipFileUrl)
617
+ try await addBundleOfTypeZip(bundleId: bundleId, zipFile: zipFile)
606
618
  }
607
619
 
608
620
  private func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> GetLatestBundleResponse? {
@@ -984,8 +996,23 @@ import CommonCrypto
984
996
  }
985
997
  }
986
998
 
987
- private func unzipFile(zipFile: URL) throws -> URL {
988
- let destinationDirectory = zipFile.deletingPathExtension()
999
+ private func tryDeleteDirectory(_ url: URL) {
1000
+ guard FileManager.default.fileExists(atPath: url.path) else {
1001
+ return
1002
+ }
1003
+ do {
1004
+ try FileManager.default.removeItem(at: url)
1005
+ } catch {
1006
+ CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete directory: \(url.path)")
1007
+ }
1008
+ }
1009
+
1010
+ private func tryDeleteDownloadsDirectory() {
1011
+ tryDeleteDirectory(buildDownloadsDirectoryUrl())
1012
+ }
1013
+
1014
+ private func unzipDownloadedBundle(zipFile: URL) throws -> URL {
1015
+ let destinationDirectory = zipFile.deletingLastPathComponent().appendingPathComponent("bundle")
989
1016
  try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true, attributes: nil)
990
1017
  try FileManager.default.unzipItem(at: zipFile, to: destinationDirectory)
991
1018
  return destinationDirectory
@@ -27,13 +27,16 @@ public class LiveUpdateHttpClient: NSObject {
27
27
 
28
28
  public func download(url: URL, destination: @escaping DownloadRequest.Destination, callback: ((Progress) -> Void)?) async throws -> AFDownloadResponse<Data> {
29
29
  var request = URLRequest(url: url)
30
+ // Ignore the URL cache so that a cached response is never replayed for a deterministic request URL
31
+ request.cachePolicy = .reloadIgnoringLocalCacheData
30
32
  request.httpMethod = HTTPMethod.get.rawValue
31
33
  request.timeoutInterval = Double(config.httpTimeout) / 1000.0
32
34
  if let deviceId = deviceId, !deviceId.isEmpty {
33
35
  request.setValue(deviceId, forHTTPHeaderField: "X-Device-Id")
34
36
  }
35
37
  return try await withCheckedThrowingContinuation { continuation in
36
- AF.download(request, to: destination).downloadProgress { progress in
38
+ // `validate()` treats non-2xx responses as errors so that an error body is never saved as a bundle file
39
+ AF.download(request, to: destination).validate().downloadProgress { progress in
37
40
  if let callback = callback {
38
41
  callback(progress)
39
42
  }
@@ -45,6 +48,8 @@ public class LiveUpdateHttpClient: NSObject {
45
48
 
46
49
  public func request<T: Decodable>(url: URL, type: T.Type) async throws -> AFDataResponse<T> {
47
50
  var request = URLRequest(url: url)
51
+ // Ignore the URL cache so that a cached response is never replayed for a deterministic request URL
52
+ request.cachePolicy = .reloadIgnoringLocalCacheData
48
53
  request.httpMethod = HTTPMethod.get.rawValue
49
54
  request.timeoutInterval = Double(config.httpTimeout) / 1000.0
50
55
  if let deviceId = deviceId, !deviceId.isEmpty {
@@ -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 = "6.9.0"
11
+ public static let version = "6.9.2"
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": "6.9.0",
3
+ "version": "6.9.2",
4
4
  "description": "Capacitor plugin to update your app remotely in real-time.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",