@capawesome/capacitor-live-update 8.4.2 → 8.4.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.
- package/README.md +2 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdate.java +104 -58
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdateHttpClient.java +1 -1
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java +3 -24
- package/ios/Plugin/LiveUpdate.swift +48 -13
- package/ios/Plugin/LiveUpdatePlugin.swift +2 -19
- package/package.json +1 -1
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
|
|
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/)
|
|
@@ -57,6 +57,7 @@ import java.io.FileInputStream;
|
|
|
57
57
|
import java.io.FileOutputStream;
|
|
58
58
|
import java.io.IOException;
|
|
59
59
|
import java.io.InputStream;
|
|
60
|
+
import java.net.SocketTimeoutException;
|
|
60
61
|
import java.security.KeyFactory;
|
|
61
62
|
import java.security.MessageDigest;
|
|
62
63
|
import java.security.PublicKey;
|
|
@@ -104,6 +105,7 @@ public class LiveUpdate {
|
|
|
104
105
|
private final SharedPreferences.Editor webViewSettingsEditor;
|
|
105
106
|
|
|
106
107
|
private final String bundlesDirectory = "_capacitor_live_update_bundles"; // DO NOT CHANGE!
|
|
108
|
+
private final String downloadsDirectory = "capawesome_capacitor_live_update_downloads";
|
|
107
109
|
private final Handler rollbackHandler = new Handler(Looper.getMainLooper());
|
|
108
110
|
private final String manifestFileName = "capawesome-live-update-manifest.json"; // DO NOT CHANGE!
|
|
109
111
|
|
|
@@ -125,6 +127,9 @@ public class LiveUpdate {
|
|
|
125
127
|
// Set the device ID on the HTTP client (after any potential config reset)
|
|
126
128
|
this.httpClient.setDeviceId(getDeviceId());
|
|
127
129
|
|
|
130
|
+
// Delete the leftovers of downloads that were interrupted by a process kill
|
|
131
|
+
tryDeleteDownloadsDirectory();
|
|
132
|
+
|
|
128
133
|
// Start the rollback timer to rollback to the default bundle
|
|
129
134
|
// if the app is not ready after a certain time
|
|
130
135
|
startRollbackTimer();
|
|
@@ -176,7 +181,7 @@ public class LiveUpdate {
|
|
|
176
181
|
.host(config.getServerDomain())
|
|
177
182
|
.addPathSegment("v1")
|
|
178
183
|
.addPathSegment("apps")
|
|
179
|
-
.addPathSegment(
|
|
184
|
+
.addPathSegment(requireAppId())
|
|
180
185
|
.addPathSegment("channels");
|
|
181
186
|
if (options.getLimit() != null) {
|
|
182
187
|
urlBuilder.addQueryParameter("limit", String.valueOf(options.getLimit()));
|
|
@@ -536,7 +541,10 @@ public class LiveUpdate {
|
|
|
536
541
|
|
|
537
542
|
// Move the bundle directory to the bundles directory
|
|
538
543
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
539
|
-
indexHtmlFile.getParentFile().renameTo(bundleDirectory);
|
|
544
|
+
boolean moved = indexHtmlFile.getParentFile().renameTo(bundleDirectory);
|
|
545
|
+
if (!moved) {
|
|
546
|
+
throw new Exception(LiveUpdatePlugin.ERROR_BUNDLE_MOVE_FAILED);
|
|
547
|
+
}
|
|
540
548
|
}
|
|
541
549
|
|
|
542
550
|
private void addBundleOfTypeManifest(@NonNull String bundleId, @NonNull File directory) throws Exception {
|
|
@@ -545,7 +553,7 @@ public class LiveUpdate {
|
|
|
545
553
|
|
|
546
554
|
private void addBundleOfTypeZip(@NonNull String bundleId, @NonNull File zipFile) throws Exception {
|
|
547
555
|
// Unzip the file to the bundle directory
|
|
548
|
-
File unzippedDirectory =
|
|
556
|
+
File unzippedDirectory = unzipDownloadedBundle(zipFile);
|
|
549
557
|
// Add the bundle
|
|
550
558
|
addBundle(bundleId, unzippedDirectory);
|
|
551
559
|
}
|
|
@@ -558,14 +566,8 @@ public class LiveUpdate {
|
|
|
558
566
|
return new File(plugin.getContext().getFilesDir(), bundlesDirectory + "/" + bundleId);
|
|
559
567
|
}
|
|
560
568
|
|
|
561
|
-
private File
|
|
562
|
-
|
|
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);
|
|
569
|
+
private File buildDownloadsDirectory() {
|
|
570
|
+
return new File(plugin.getContext().getCacheDir(), downloadsDirectory);
|
|
569
571
|
}
|
|
570
572
|
|
|
571
573
|
private void copyCurrentBundleFile(@NonNull ManifestItem fileToCopy, @NonNull File destinationDirectory) throws IOException {
|
|
@@ -652,10 +654,10 @@ public class LiveUpdate {
|
|
|
652
654
|
}
|
|
653
655
|
}
|
|
654
656
|
|
|
655
|
-
private File
|
|
656
|
-
File
|
|
657
|
-
|
|
658
|
-
return
|
|
657
|
+
private File createTemporaryDownloadDirectory() {
|
|
658
|
+
File temporaryDirectory = new File(buildDownloadsDirectory(), UUID.randomUUID().toString());
|
|
659
|
+
temporaryDirectory.mkdirs();
|
|
660
|
+
return temporaryDirectory;
|
|
659
661
|
}
|
|
660
662
|
|
|
661
663
|
private void deleteBundleById(@NonNull String bundleId) {
|
|
@@ -681,6 +683,23 @@ public class LiveUpdate {
|
|
|
681
683
|
file.delete();
|
|
682
684
|
}
|
|
683
685
|
|
|
686
|
+
@NonNull
|
|
687
|
+
private EmptyCallback deleteTemporaryDirectoryOnCompletion(@NonNull File temporaryDirectory, @NonNull EmptyCallback callback) {
|
|
688
|
+
return new EmptyCallback() {
|
|
689
|
+
@Override
|
|
690
|
+
public void success() {
|
|
691
|
+
deleteFileRecursively(temporaryDirectory);
|
|
692
|
+
callback.success();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
@Override
|
|
696
|
+
public void error(@NonNull Exception exception) {
|
|
697
|
+
deleteFileRecursively(temporaryDirectory);
|
|
698
|
+
callback.error(exception);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
684
703
|
private void deleteUnusedBundles() {
|
|
685
704
|
String[] bundleIds = getDownloadedBundleIds();
|
|
686
705
|
for (String bundleId : bundleIds) {
|
|
@@ -721,6 +740,8 @@ public class LiveUpdate {
|
|
|
721
740
|
Logger.error(LiveUpdatePlugin.TAG, errorMessage, exception);
|
|
722
741
|
completionCallback.error(exception);
|
|
723
742
|
}
|
|
743
|
+
} catch (IOException e) {
|
|
744
|
+
handleDownloadFailure(e, completionCallback);
|
|
724
745
|
} catch (Exception e) {
|
|
725
746
|
completionCallback.error(e);
|
|
726
747
|
}
|
|
@@ -728,7 +749,7 @@ public class LiveUpdate {
|
|
|
728
749
|
|
|
729
750
|
@Override
|
|
730
751
|
public void error(@NonNull Exception exception) {
|
|
731
|
-
|
|
752
|
+
handleDownloadFailure(exception, completionCallback);
|
|
732
753
|
}
|
|
733
754
|
}
|
|
734
755
|
);
|
|
@@ -851,10 +872,10 @@ public class LiveUpdate {
|
|
|
851
872
|
@NonNull String downloadUrl,
|
|
852
873
|
@NonNull EmptyCallback completionCallback
|
|
853
874
|
) {
|
|
875
|
+
// Create a temporary directory that is deleted once the download completed or failed
|
|
876
|
+
File temporaryDirectory = createTemporaryDownloadDirectory();
|
|
877
|
+
EmptyCallback callback = deleteTemporaryDirectoryOnCompletion(temporaryDirectory, completionCallback);
|
|
854
878
|
try {
|
|
855
|
-
// Create a temporary directory
|
|
856
|
-
File temporaryDirectory = createTemporaryDirectory();
|
|
857
|
-
|
|
858
879
|
// Download the latest manifest
|
|
859
880
|
downloadBundleFile(
|
|
860
881
|
downloadUrl,
|
|
@@ -904,31 +925,31 @@ public class LiveUpdate {
|
|
|
904
925
|
try {
|
|
905
926
|
// Add the bundle
|
|
906
927
|
addBundleOfTypeManifest(bundleId, temporaryDirectory);
|
|
907
|
-
|
|
928
|
+
callback.success();
|
|
908
929
|
} catch (Exception e) {
|
|
909
|
-
|
|
930
|
+
callback.error(e);
|
|
910
931
|
}
|
|
911
932
|
}
|
|
912
933
|
|
|
913
934
|
@Override
|
|
914
935
|
public void error(@NonNull Exception exception) {
|
|
915
|
-
|
|
936
|
+
callback.error(exception);
|
|
916
937
|
}
|
|
917
938
|
}
|
|
918
939
|
);
|
|
919
940
|
} catch (Exception e) {
|
|
920
|
-
|
|
941
|
+
callback.error(e);
|
|
921
942
|
}
|
|
922
943
|
}
|
|
923
944
|
|
|
924
945
|
@Override
|
|
925
946
|
public void error(@NonNull Exception exception) {
|
|
926
|
-
|
|
947
|
+
callback.error(exception);
|
|
927
948
|
}
|
|
928
949
|
}
|
|
929
950
|
);
|
|
930
951
|
} catch (Exception e) {
|
|
931
|
-
|
|
952
|
+
callback.error(e);
|
|
932
953
|
}
|
|
933
954
|
}
|
|
934
955
|
|
|
@@ -939,39 +960,42 @@ public class LiveUpdate {
|
|
|
939
960
|
@NonNull String downloadUrl,
|
|
940
961
|
@NonNull EmptyCallback completionCallback
|
|
941
962
|
) {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
963
|
+
// Create a temporary directory that is deleted once the download completed or failed
|
|
964
|
+
File temporaryDirectory = createTemporaryDownloadDirectory();
|
|
965
|
+
EmptyCallback callback = deleteTemporaryDirectoryOnCompletion(temporaryDirectory, completionCallback);
|
|
966
|
+
File zipFile = new File(temporaryDirectory, "bundle.zip");
|
|
967
|
+
try {
|
|
968
|
+
// Download the bundle
|
|
969
|
+
downloadAndVerifyFile(
|
|
970
|
+
downloadUrl,
|
|
971
|
+
zipFile,
|
|
972
|
+
checksum,
|
|
973
|
+
signature,
|
|
974
|
+
(downloadedBytes, totalBytes) -> {
|
|
975
|
+
DownloadBundleProgressEvent event = new DownloadBundleProgressEvent(bundleId, downloadedBytes, totalBytes);
|
|
976
|
+
notifyDownloadBundleProgressListeners(event);
|
|
977
|
+
},
|
|
978
|
+
new EmptyCallback() {
|
|
979
|
+
@Override
|
|
980
|
+
public void success() {
|
|
981
|
+
try {
|
|
982
|
+
// Add the bundle
|
|
983
|
+
addBundleOfTypeZip(bundleId, zipFile);
|
|
984
|
+
callback.success();
|
|
985
|
+
} catch (Exception e) {
|
|
986
|
+
callback.error(e);
|
|
987
|
+
}
|
|
964
988
|
}
|
|
965
|
-
}
|
|
966
989
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
completionCallback.error(exception);
|
|
990
|
+
@Override
|
|
991
|
+
public void error(@NonNull Exception exception) {
|
|
992
|
+
callback.error(exception);
|
|
993
|
+
}
|
|
972
994
|
}
|
|
973
|
-
|
|
974
|
-
)
|
|
995
|
+
);
|
|
996
|
+
} catch (Exception e) {
|
|
997
|
+
callback.error(e);
|
|
998
|
+
}
|
|
975
999
|
}
|
|
976
1000
|
|
|
977
1001
|
private void fetchLatestBundleInternal(
|
|
@@ -985,7 +1009,7 @@ public class LiveUpdate {
|
|
|
985
1009
|
.host(config.getServerDomain())
|
|
986
1010
|
.addPathSegment("v1")
|
|
987
1011
|
.addPathSegment("apps")
|
|
988
|
-
.addPathSegment(
|
|
1012
|
+
.addPathSegment(requireAppId())
|
|
989
1013
|
.addPathSegment("bundles")
|
|
990
1014
|
.addPathSegment("latest")
|
|
991
1015
|
.addQueryParameter("appVersionCode", getVersionCodeAsString())
|
|
@@ -1186,6 +1210,15 @@ public class LiveUpdate {
|
|
|
1186
1210
|
return getPackageInfo().versionName;
|
|
1187
1211
|
}
|
|
1188
1212
|
|
|
1213
|
+
private void handleDownloadFailure(@NonNull Exception exception, @NonNull EmptyCallback callback) {
|
|
1214
|
+
Logger.error(LiveUpdatePlugin.TAG, exception.getMessage(), exception);
|
|
1215
|
+
if (exception instanceof SocketTimeoutException) {
|
|
1216
|
+
callback.error(exception);
|
|
1217
|
+
} else {
|
|
1218
|
+
callback.error(new Exception(LiveUpdatePlugin.ERROR_DOWNLOAD_FAILED));
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1189
1222
|
private boolean hasBundleById(@NonNull String bundleId) {
|
|
1190
1223
|
File bundleDirectory = buildBundleDirectoryFor(bundleId);
|
|
1191
1224
|
return bundleDirectory.exists();
|
|
@@ -1264,6 +1297,15 @@ public class LiveUpdate {
|
|
|
1264
1297
|
sync(options, callback);
|
|
1265
1298
|
}
|
|
1266
1299
|
|
|
1300
|
+
@NonNull
|
|
1301
|
+
private String requireAppId() throws Exception {
|
|
1302
|
+
String appId = getAppId();
|
|
1303
|
+
if (appId == null || appId.isEmpty()) {
|
|
1304
|
+
throw new Exception(LiveUpdatePlugin.ERROR_APP_ID_MISSING);
|
|
1305
|
+
}
|
|
1306
|
+
return appId;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1267
1309
|
private void rollback() {
|
|
1268
1310
|
// Set the rollback flag
|
|
1269
1311
|
rollbackPerformed = true;
|
|
@@ -1437,8 +1479,12 @@ public class LiveUpdate {
|
|
|
1437
1479
|
}
|
|
1438
1480
|
}
|
|
1439
1481
|
|
|
1440
|
-
private
|
|
1441
|
-
|
|
1482
|
+
private void tryDeleteDownloadsDirectory() {
|
|
1483
|
+
deleteFileRecursively(buildDownloadsDirectory());
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
private File unzipDownloadedBundle(@NonNull File zipFile) throws IOException {
|
|
1487
|
+
File destination = new File(zipFile.getParentFile(), "bundle");
|
|
1442
1488
|
String destinationPath = destination.getPath();
|
|
1443
1489
|
ZipFile zip = new ZipFile(zipFile);
|
|
1444
1490
|
// Clear stored Unix permissions to prevent EACCES errors on newer Android versions
|
package/android/src/main/java/io/capawesome/capacitorjs/plugins/liveupdate/LiveUpdatePlugin.java
CHANGED
|
@@ -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.
|
|
40
|
+
public static final String VERSION = "8.4.4";
|
|
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.";
|
|
@@ -59,9 +60,6 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
59
60
|
public static final String EVENT_NEXT_BUNDLE_SET = "nextBundleSet";
|
|
60
61
|
public static final String EVENT_RELOADED = "reloaded";
|
|
61
62
|
|
|
62
|
-
@Nullable
|
|
63
|
-
private LiveUpdateConfig config;
|
|
64
|
-
|
|
65
63
|
@Nullable
|
|
66
64
|
private LiveUpdate implementation;
|
|
67
65
|
|
|
@@ -69,8 +67,7 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
69
67
|
|
|
70
68
|
public void load() {
|
|
71
69
|
try {
|
|
72
|
-
|
|
73
|
-
implementation = new LiveUpdate(config, this);
|
|
70
|
+
implementation = new LiveUpdate(getLiveUpdateConfig(), this);
|
|
74
71
|
} catch (Exception exception) {
|
|
75
72
|
Logger.error(TAG, exception.getMessage(), exception);
|
|
76
73
|
}
|
|
@@ -182,12 +179,6 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
182
179
|
@PluginMethod
|
|
183
180
|
public void fetchChannels(PluginCall call) {
|
|
184
181
|
try {
|
|
185
|
-
String appId = config.getAppId();
|
|
186
|
-
if (appId == null || appId.isEmpty()) {
|
|
187
|
-
call.reject(ERROR_APP_ID_MISSING);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
182
|
FetchChannelsOptions options = new FetchChannelsOptions(call);
|
|
192
183
|
NonEmptyCallback<FetchChannelsResult> callback = new NonEmptyCallback<>() {
|
|
193
184
|
@Override
|
|
@@ -210,12 +201,6 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
210
201
|
@PluginMethod
|
|
211
202
|
public void fetchLatestBundle(PluginCall call) {
|
|
212
203
|
try {
|
|
213
|
-
String appId = config.getAppId();
|
|
214
|
-
if (appId == null || appId.isEmpty()) {
|
|
215
|
-
call.reject(ERROR_APP_ID_MISSING);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
204
|
FetchLatestBundleOptions options = new FetchLatestBundleOptions(call);
|
|
220
205
|
NonEmptyCallback<FetchLatestBundleResult> callback = new NonEmptyCallback<>() {
|
|
221
206
|
@Override
|
|
@@ -632,12 +617,6 @@ public class LiveUpdatePlugin extends Plugin {
|
|
|
632
617
|
@PluginMethod
|
|
633
618
|
public void sync(PluginCall call) {
|
|
634
619
|
try {
|
|
635
|
-
String appId = config.getAppId();
|
|
636
|
-
if (appId == null || appId.isEmpty()) {
|
|
637
|
-
call.reject(ERROR_APP_ID_MISSING);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
620
|
SyncOptions options = new SyncOptions(call);
|
|
642
621
|
NonEmptyCallback<Result> callback = new NonEmptyCallback<>() {
|
|
643
622
|
@Override
|
|
@@ -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()
|
|
@@ -94,7 +98,8 @@ import CommonCrypto
|
|
|
94
98
|
if let query = options.getQuery() {
|
|
95
99
|
parameters["query"] = query
|
|
96
100
|
}
|
|
97
|
-
|
|
101
|
+
let appId = try requireAppId()
|
|
102
|
+
var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(appId)/channels")!
|
|
98
103
|
if !parameters.isEmpty {
|
|
99
104
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
100
105
|
}
|
|
@@ -355,7 +360,7 @@ import CommonCrypto
|
|
|
355
360
|
|
|
356
361
|
private func addBundleOfTypeZip(bundleId: String, zipFile: URL) async throws {
|
|
357
362
|
// Unzip the bundle
|
|
358
|
-
let unzippedDirectory = try self.
|
|
363
|
+
let unzippedDirectory = try self.unzipDownloadedBundle(zipFile: zipFile)
|
|
359
364
|
// Add the bundle
|
|
360
365
|
try self.addBundle(bundleId: bundleId, directory: unzippedDirectory)
|
|
361
366
|
}
|
|
@@ -379,6 +384,10 @@ import CommonCrypto
|
|
|
379
384
|
return url
|
|
380
385
|
}
|
|
381
386
|
|
|
387
|
+
private func buildDownloadsDirectoryUrl() -> URL {
|
|
388
|
+
return cachesDirectoryUrl.appendingPathComponent(downloadsDirectory)
|
|
389
|
+
}
|
|
390
|
+
|
|
382
391
|
private func copyCurrentBundleFile(fileToCopy: ManifestItem, toDirectory: URL) throws {
|
|
383
392
|
let currentBundleId = getCurrentBundleId()
|
|
384
393
|
let destination = toDirectory.appendingPathComponent(fileToCopy.href)
|
|
@@ -428,8 +437,8 @@ import CommonCrypto
|
|
|
428
437
|
}
|
|
429
438
|
}
|
|
430
439
|
|
|
431
|
-
private func
|
|
432
|
-
let temporaryDirectory =
|
|
440
|
+
private func createTemporaryDownloadDirectory() throws -> URL {
|
|
441
|
+
let temporaryDirectory = buildDownloadsDirectoryUrl().appendingPathComponent(UUID().uuidString)
|
|
433
442
|
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
434
443
|
return temporaryDirectory
|
|
435
444
|
}
|
|
@@ -537,8 +546,9 @@ import CommonCrypto
|
|
|
537
546
|
}
|
|
538
547
|
|
|
539
548
|
private func downloadBundleOfTypeManifest(bundleId: String, url: String) async throws {
|
|
540
|
-
// Create a temporary directory
|
|
541
|
-
let temporaryDirectory = try
|
|
549
|
+
// Create a temporary directory that is deleted once the download completed or failed
|
|
550
|
+
let temporaryDirectory = try createTemporaryDownloadDirectory()
|
|
551
|
+
defer { tryDeleteDirectory(temporaryDirectory) }
|
|
542
552
|
// Download the latest manifest
|
|
543
553
|
let latestManifestFile = try await downloadBundleFile(baseUrl: url, href: manifestFileName, directory: temporaryDirectory, callback: nil)
|
|
544
554
|
let latestManifest = try loadManifest(file: latestManifestFile)
|
|
@@ -569,15 +579,17 @@ import CommonCrypto
|
|
|
569
579
|
}
|
|
570
580
|
|
|
571
581
|
private func downloadBundleOfTypeZip(bundleId: String, checksum: String?, signature: String?, url: String) async throws {
|
|
572
|
-
|
|
573
|
-
let
|
|
582
|
+
// Create a temporary directory that is deleted once the download completed or failed
|
|
583
|
+
let temporaryDirectory = try createTemporaryDownloadDirectory()
|
|
584
|
+
defer { tryDeleteDirectory(temporaryDirectory) }
|
|
585
|
+
let zipFile = temporaryDirectory.appendingPathComponent("bundle.zip")
|
|
574
586
|
// Download the bundle
|
|
575
|
-
try await downloadAndVerifyFile(url: url, file:
|
|
587
|
+
try await downloadAndVerifyFile(url: url, file: zipFile, checksum: checksum, signature: signature, callback: { progress in
|
|
576
588
|
let event = DownloadBundleProgressEvent(bundleId: bundleId, downloadedBytes: progress.completedUnitCount, totalBytes: progress.totalUnitCount)
|
|
577
589
|
self.notifyDownloadBundleProgressListeners(event)
|
|
578
590
|
})
|
|
579
591
|
// Add the bundle
|
|
580
|
-
try await addBundleOfTypeZip(bundleId: bundleId, zipFile:
|
|
592
|
+
try await addBundleOfTypeZip(bundleId: bundleId, zipFile: zipFile)
|
|
581
593
|
}
|
|
582
594
|
|
|
583
595
|
private func fetchLatestBundle(_ options: FetchLatestBundleOptions) async throws -> GetLatestBundleResponse? {
|
|
@@ -592,7 +604,8 @@ import CommonCrypto
|
|
|
592
604
|
parameters["osVersion"] = await UIDevice.current.systemVersion
|
|
593
605
|
parameters["platform"] = "1"
|
|
594
606
|
parameters["pluginVersion"] = LiveUpdatePlugin.version
|
|
595
|
-
|
|
607
|
+
let appId = try requireAppId()
|
|
608
|
+
var urlComponents = URLComponents(string: "https://\(config.serverDomain)/v1/apps/\(appId)/bundles/latest")!
|
|
596
609
|
urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
|
|
597
610
|
let url = try urlComponents.asURL()
|
|
598
611
|
CAPLog.print("[", LiveUpdatePlugin.tag, "] Fetching latest bundle: ", url)
|
|
@@ -791,6 +804,13 @@ import CommonCrypto
|
|
|
791
804
|
}
|
|
792
805
|
}
|
|
793
806
|
|
|
807
|
+
private func requireAppId() throws -> String {
|
|
808
|
+
guard let appId = getAppId(), !appId.isEmpty else {
|
|
809
|
+
throw CustomError.appIdMissing
|
|
810
|
+
}
|
|
811
|
+
return appId
|
|
812
|
+
}
|
|
813
|
+
|
|
794
814
|
private func rollback() {
|
|
795
815
|
// Set the rollback flag
|
|
796
816
|
rollbackPerformed = true
|
|
@@ -963,8 +983,23 @@ import CommonCrypto
|
|
|
963
983
|
}
|
|
964
984
|
}
|
|
965
985
|
|
|
966
|
-
private func
|
|
967
|
-
|
|
986
|
+
private func tryDeleteDirectory(_ url: URL) {
|
|
987
|
+
guard FileManager.default.fileExists(atPath: url.path) else {
|
|
988
|
+
return
|
|
989
|
+
}
|
|
990
|
+
do {
|
|
991
|
+
try FileManager.default.removeItem(at: url)
|
|
992
|
+
} catch {
|
|
993
|
+
CAPLog.print("[", LiveUpdatePlugin.tag, "] ", "Failed to delete directory: \(url.path)")
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private func tryDeleteDownloadsDirectory() {
|
|
998
|
+
tryDeleteDirectory(buildDownloadsDirectoryUrl())
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
private func unzipDownloadedBundle(zipFile: URL) throws -> URL {
|
|
1002
|
+
let destinationDirectory = zipFile.deletingLastPathComponent().appendingPathComponent("bundle")
|
|
968
1003
|
try FileManager.default.createDirectory(at: destinationDirectory, withIntermediateDirectories: true, attributes: nil)
|
|
969
1004
|
try FileManager.default.unzipItem(at: zipFile, to: destinationDirectory)
|
|
970
1005
|
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.
|
|
11
|
+
public static let version = "8.4.4"
|
|
12
12
|
public static let userDefaultsPrefix = "CapawesomeLiveUpdate" // DO NOT CHANGE
|
|
13
13
|
|
|
14
14
|
public let identifier = "LiveUpdatePlugin"
|
|
@@ -46,12 +46,10 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
46
46
|
private let eventNextBundleSet = "nextBundleSet"
|
|
47
47
|
private let eventReloaded = "reloaded"
|
|
48
48
|
|
|
49
|
-
private var config: LiveUpdateConfig?
|
|
50
49
|
private var implementation: LiveUpdate?
|
|
51
50
|
|
|
52
51
|
override public func load() {
|
|
53
|
-
self.
|
|
54
|
-
self.implementation = LiveUpdate(config: config!, plugin: self)
|
|
52
|
+
self.implementation = LiveUpdate(config: liveUpdateConfig(), plugin: self)
|
|
55
53
|
|
|
56
54
|
// Notify implementation about load
|
|
57
55
|
implementation?.handleLoad()
|
|
@@ -115,11 +113,6 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
115
113
|
@objc func fetchChannels(_ call: CAPPluginCall) {
|
|
116
114
|
Task {
|
|
117
115
|
do {
|
|
118
|
-
guard let appId = config?.appId, !appId.isEmpty else {
|
|
119
|
-
call.reject(CustomError.appIdMissing.localizedDescription)
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
|
|
123
116
|
let options = FetchChannelsOptions(call)
|
|
124
117
|
let result = try await implementation?.fetchChannels(options)
|
|
125
118
|
if let result = result?.toJSObject() as? JSObject {
|
|
@@ -134,11 +127,6 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
134
127
|
@objc func fetchLatestBundle(_ call: CAPPluginCall) {
|
|
135
128
|
Task {
|
|
136
129
|
do {
|
|
137
|
-
guard let appId = config?.appId, !appId.isEmpty else {
|
|
138
|
-
call.reject(CustomError.appIdMissing.localizedDescription)
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
|
|
142
130
|
let options = FetchLatestBundleOptions(call)
|
|
143
131
|
let result = try await implementation?.fetchLatestBundle(options)
|
|
144
132
|
if let result = result?.toJSObject() as? JSObject {
|
|
@@ -373,11 +361,6 @@ public class LiveUpdatePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
373
361
|
@objc func sync(_ call: CAPPluginCall) {
|
|
374
362
|
Task {
|
|
375
363
|
do {
|
|
376
|
-
guard let appId = config?.appId, !appId.isEmpty else {
|
|
377
|
-
call.reject(CustomError.appIdMissing.localizedDescription)
|
|
378
|
-
return
|
|
379
|
-
}
|
|
380
|
-
|
|
381
364
|
let options = SyncOptions(call)
|
|
382
365
|
let result = try await implementation?.sync(options)
|
|
383
366
|
if let result = result?.toJSObject() as? JSObject {
|
package/package.json
CHANGED