@capgo/capacitor-updater 8.51.18 → 8.51.20
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/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +200 -35
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +4 -3
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +13 -3
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +30 -37
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +16 -41
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +2 -2
- package/package.json +1 -1
|
@@ -146,7 +146,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
146
146
|
static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
|
|
147
147
|
static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
|
|
148
148
|
|
|
149
|
-
private final String pluginVersion = "8.51.
|
|
149
|
+
private final String pluginVersion = "8.51.20";
|
|
150
150
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
151
151
|
|
|
152
152
|
private SharedPreferences.Editor editor;
|
|
@@ -14,6 +14,7 @@ import androidx.work.WorkerParameters;
|
|
|
14
14
|
import java.io.*;
|
|
15
15
|
import java.io.FileInputStream;
|
|
16
16
|
import java.net.HttpURLConnection;
|
|
17
|
+
import java.net.MalformedURLException;
|
|
17
18
|
import java.net.URL;
|
|
18
19
|
import java.nio.channels.FileChannel;
|
|
19
20
|
import java.security.MessageDigest;
|
|
@@ -30,6 +31,8 @@ import java.util.concurrent.Future;
|
|
|
30
31
|
import java.util.concurrent.TimeUnit;
|
|
31
32
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
32
33
|
import java.util.concurrent.atomic.AtomicLong;
|
|
34
|
+
import java.util.function.BooleanSupplier;
|
|
35
|
+
import java.util.function.LongConsumer;
|
|
33
36
|
import okhttp3.Call;
|
|
34
37
|
import okhttp3.Callback;
|
|
35
38
|
import okhttp3.Dispatcher;
|
|
@@ -47,6 +50,41 @@ import org.json.JSONObject;
|
|
|
47
50
|
|
|
48
51
|
public class DownloadService extends Worker {
|
|
49
52
|
|
|
53
|
+
static final class DownloadRetryException extends RuntimeException {
|
|
54
|
+
|
|
55
|
+
DownloadRetryException(String message) {
|
|
56
|
+
super(message);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
DownloadRetryException(String message, Throwable cause) {
|
|
60
|
+
super(message, cause);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static final class ZipWritePlan {
|
|
65
|
+
|
|
66
|
+
final int statusCode;
|
|
67
|
+
final long writeOffset;
|
|
68
|
+
|
|
69
|
+
ZipWritePlan(int statusCode, long writeOffset) {
|
|
70
|
+
this.statusCode = statusCode;
|
|
71
|
+
this.writeOffset = writeOffset;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static final class ContentRangeInfo {
|
|
76
|
+
|
|
77
|
+
final long start;
|
|
78
|
+
final long end;
|
|
79
|
+
final long total;
|
|
80
|
+
|
|
81
|
+
ContentRangeInfo(long start, long end, long total) {
|
|
82
|
+
this.start = start;
|
|
83
|
+
this.end = end;
|
|
84
|
+
this.total = total;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
50
88
|
private static Logger logger;
|
|
51
89
|
|
|
52
90
|
public static void setLogger(Logger loggerInstance) {
|
|
@@ -389,6 +427,11 @@ public class DownloadService extends Worker {
|
|
|
389
427
|
handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum);
|
|
390
428
|
return createSuccessResult(dest, version, sessionKey, checksum, false);
|
|
391
429
|
}
|
|
430
|
+
} catch (DownloadRetryException e) {
|
|
431
|
+
if (logger != null) {
|
|
432
|
+
logger.debug("Download will retry: " + e.getMessage());
|
|
433
|
+
}
|
|
434
|
+
return Result.retry();
|
|
392
435
|
} catch (Exception e) {
|
|
393
436
|
return createFailureResult(e.getMessage());
|
|
394
437
|
}
|
|
@@ -652,7 +695,6 @@ public class DownloadService extends Worker {
|
|
|
652
695
|
|
|
653
696
|
HttpURLConnection httpConn = null;
|
|
654
697
|
InputStream inputStream = null;
|
|
655
|
-
FileOutputStream outputStream = null;
|
|
656
698
|
BufferedReader reader = null;
|
|
657
699
|
BufferedWriter writer = null;
|
|
658
700
|
|
|
@@ -689,6 +731,10 @@ public class DownloadService extends Worker {
|
|
|
689
731
|
clearDownloadData(documentsDir, id);
|
|
690
732
|
}
|
|
691
733
|
|
|
734
|
+
if (isStopped()) {
|
|
735
|
+
throw new DownloadRetryException("download_stopped");
|
|
736
|
+
}
|
|
737
|
+
|
|
692
738
|
if (downloadedBytes > 0) {
|
|
693
739
|
httpConn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-");
|
|
694
740
|
}
|
|
@@ -696,7 +742,11 @@ public class DownloadService extends Worker {
|
|
|
696
742
|
int responseCode = httpConn.getResponseCode();
|
|
697
743
|
|
|
698
744
|
if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL) {
|
|
699
|
-
|
|
745
|
+
ZipWritePlan writePlan = planZipResumeWrite(responseCode, downloadedBytes, httpConn.getHeaderField("Content-Range"));
|
|
746
|
+
long responseBodyLength = httpConn.getContentLength();
|
|
747
|
+
long contentLength = shouldAppendHttpBody(writePlan.statusCode, writePlan.writeOffset)
|
|
748
|
+
? (responseBodyLength >= 0 ? responseBodyLength + writePlan.writeOffset : -1)
|
|
749
|
+
: responseBodyLength;
|
|
700
750
|
|
|
701
751
|
// Check if we have enough space for the actual file
|
|
702
752
|
if (contentLength > 0 && availableSpace < contentLength * 2) {
|
|
@@ -705,44 +755,36 @@ public class DownloadService extends Worker {
|
|
|
705
755
|
|
|
706
756
|
try {
|
|
707
757
|
inputStream = httpConn.getInputStream();
|
|
708
|
-
outputStream = new FileOutputStream(tempFile, downloadedBytes > 0);
|
|
709
758
|
|
|
710
|
-
if (downloadedBytes == 0) {
|
|
759
|
+
if (writePlan.writeOffset == 0 && downloadedBytes == 0) {
|
|
711
760
|
writer = new BufferedWriter(new FileWriter(infoFile));
|
|
712
761
|
writer.write(String.valueOf(version));
|
|
713
762
|
writer.close();
|
|
714
763
|
writer = null;
|
|
715
764
|
}
|
|
716
765
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
// Flush every 1MB to ensure progress is saved
|
|
726
|
-
if (downloadedBytes % (1024 * 1024) == 0) {
|
|
727
|
-
outputStream.flush();
|
|
766
|
+
final int[] lastNotifiedPercent = { 0 };
|
|
767
|
+
final long totalContentLength = contentLength;
|
|
768
|
+
String contentRangeHeader = httpConn.getHeaderField("Content-Range");
|
|
769
|
+
writeHttpBody(tempFile, inputStream, writePlan.statusCode, writePlan.writeOffset, this::isStopped, (written) -> {
|
|
770
|
+
int percent = calcTotalPercent(written, totalContentLength);
|
|
771
|
+
if (percent >= lastNotifiedPercent[0] + 10) {
|
|
772
|
+
lastNotifiedPercent[0] = (percent / 10) * 10;
|
|
773
|
+
setProgress(lastNotifiedPercent[0]);
|
|
728
774
|
}
|
|
729
|
-
|
|
730
|
-
// Computing percentage
|
|
731
|
-
int percent = calcTotalPercent(downloadedBytes, contentLength);
|
|
732
|
-
if (percent >= lastNotifiedPercent + 10) {
|
|
733
|
-
lastNotifiedPercent = (percent / 10) * 10;
|
|
734
|
-
setProgress(lastNotifiedPercent);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
// Final flush
|
|
739
|
-
outputStream.flush();
|
|
740
|
-
outputStream.close();
|
|
741
|
-
outputStream = null;
|
|
775
|
+
});
|
|
742
776
|
|
|
743
777
|
inputStream.close();
|
|
744
778
|
inputStream = null;
|
|
745
779
|
|
|
780
|
+
validateZipDownloadComplete(
|
|
781
|
+
tempFile,
|
|
782
|
+
writePlan.statusCode,
|
|
783
|
+
contentRangeHeader,
|
|
784
|
+
writePlan.writeOffset,
|
|
785
|
+
responseBodyLength
|
|
786
|
+
);
|
|
787
|
+
|
|
746
788
|
// Rename the temp file with the final name (dest)
|
|
747
789
|
if (!tempFile.renameTo(new File(documentsDir, dest))) {
|
|
748
790
|
throw new RuntimeException("Failed to rename temp file to final destination");
|
|
@@ -756,13 +798,12 @@ public class DownloadService extends Worker {
|
|
|
756
798
|
// Try to free some memory
|
|
757
799
|
System.gc();
|
|
758
800
|
throw new RuntimeException("low_mem_fail");
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
try {
|
|
763
|
-
outputStream.close();
|
|
764
|
-
} catch (Exception ignored) {}
|
|
801
|
+
} catch (IOException e) {
|
|
802
|
+
if ("download_stopped".equals(e.getMessage())) {
|
|
803
|
+
throw new DownloadRetryException("download_stopped", e);
|
|
765
804
|
}
|
|
805
|
+
throw new DownloadRetryException(e.getMessage(), e);
|
|
806
|
+
} finally {
|
|
766
807
|
if (inputStream != null) {
|
|
767
808
|
try {
|
|
768
809
|
inputStream.close();
|
|
@@ -774,10 +815,14 @@ public class DownloadService extends Worker {
|
|
|
774
815
|
} catch (Exception ignored) {}
|
|
775
816
|
}
|
|
776
817
|
}
|
|
818
|
+
} else if (isRetryableHttpStatus(responseCode)) {
|
|
819
|
+
throw new DownloadRetryException("HTTP error: " + responseCode);
|
|
777
820
|
} else {
|
|
778
821
|
infoFile.delete();
|
|
779
822
|
throw new RuntimeException("HTTP error: " + responseCode);
|
|
780
823
|
}
|
|
824
|
+
} catch (DownloadRetryException e) {
|
|
825
|
+
throw e;
|
|
781
826
|
} catch (OutOfMemoryError e) {
|
|
782
827
|
logger.error("Critical memory error: " + e.getMessage());
|
|
783
828
|
System.gc(); // Suggest garbage collection
|
|
@@ -785,6 +830,12 @@ public class DownloadService extends Worker {
|
|
|
785
830
|
} catch (SecurityException e) {
|
|
786
831
|
logger.error("Security error during download: " + e.getMessage());
|
|
787
832
|
throw new RuntimeException("security_error: " + e.getMessage());
|
|
833
|
+
} catch (MalformedURLException e) {
|
|
834
|
+
logger.error("Invalid download URL: " + e.getMessage());
|
|
835
|
+
throw new RuntimeException("invalid_url: " + e.getMessage());
|
|
836
|
+
} catch (IOException e) {
|
|
837
|
+
logger.error("Download error: " + e.getMessage());
|
|
838
|
+
throw new DownloadRetryException(e.getMessage(), e);
|
|
788
839
|
} catch (Exception e) {
|
|
789
840
|
logger.error("Download error: " + e.getMessage());
|
|
790
841
|
throw new RuntimeException(e.getMessage());
|
|
@@ -798,6 +849,93 @@ public class DownloadService extends Worker {
|
|
|
798
849
|
}
|
|
799
850
|
}
|
|
800
851
|
|
|
852
|
+
static boolean isRetryableHttpStatus(int responseCode) {
|
|
853
|
+
return responseCode >= 500 || responseCode == 408 || responseCode == 429;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
static long parseContentRangeStart(String contentRange) {
|
|
857
|
+
ContentRangeInfo range = parseContentRange(contentRange);
|
|
858
|
+
return range == null ? -1 : range.start;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
static ContentRangeInfo parseContentRange(String contentRange) {
|
|
862
|
+
if (contentRange == null || contentRange.isEmpty()) {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
String trimmed = contentRange.trim();
|
|
866
|
+
if (!trimmed.startsWith("bytes ")) {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
int slash = trimmed.indexOf('/');
|
|
870
|
+
if (slash < 0) {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
int dash = trimmed.indexOf('-', 6);
|
|
874
|
+
if (dash < 0 || dash >= slash) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
long start = Long.parseLong(trimmed.substring(6, dash).trim());
|
|
879
|
+
long end = Long.parseLong(trimmed.substring(dash + 1, slash).trim());
|
|
880
|
+
String totalPart = trimmed.substring(slash + 1).trim();
|
|
881
|
+
long total = "*".equals(totalPart) ? -1 : Long.parseLong(totalPart);
|
|
882
|
+
if (end < start) {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
return new ContentRangeInfo(start, end, total);
|
|
886
|
+
} catch (NumberFormatException e) {
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
static void validateZipDownloadComplete(
|
|
892
|
+
File tempFile,
|
|
893
|
+
int responseCode,
|
|
894
|
+
String contentRangeHeader,
|
|
895
|
+
long writeOffset,
|
|
896
|
+
long responseBodyLength
|
|
897
|
+
) {
|
|
898
|
+
long fileLen = tempFile.length();
|
|
899
|
+
if (responseCode == HttpURLConnection.HTTP_PARTIAL) {
|
|
900
|
+
ContentRangeInfo range = parseContentRange(contentRangeHeader);
|
|
901
|
+
if (range == null || range.total < 0) {
|
|
902
|
+
throw new DownloadRetryException("unknown_content_range_total");
|
|
903
|
+
}
|
|
904
|
+
if (range.start != writeOffset) {
|
|
905
|
+
throw new DownloadRetryException("content_range_mismatch");
|
|
906
|
+
}
|
|
907
|
+
long advertisedSpan = range.end - range.start + 1;
|
|
908
|
+
long receivedSpan = fileLen - writeOffset;
|
|
909
|
+
if (receivedSpan != advertisedSpan) {
|
|
910
|
+
throw new DownloadRetryException("incomplete_content_range");
|
|
911
|
+
}
|
|
912
|
+
if (fileLen != range.total) {
|
|
913
|
+
throw new DownloadRetryException("incomplete_download");
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (responseBodyLength >= 0 && fileLen != responseBodyLength) {
|
|
918
|
+
throw new DownloadRetryException("incomplete_download");
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
static ZipWritePlan planZipResumeWrite(int responseCode, long downloadedBytes, String contentRange) {
|
|
923
|
+
if (responseCode == HttpURLConnection.HTTP_PARTIAL) {
|
|
924
|
+
long rangeStart = parseContentRangeStart(contentRange);
|
|
925
|
+
if (rangeStart == downloadedBytes) {
|
|
926
|
+
return new ZipWritePlan(HttpURLConnection.HTTP_PARTIAL, downloadedBytes);
|
|
927
|
+
}
|
|
928
|
+
if (rangeStart == 0) {
|
|
929
|
+
return new ZipWritePlan(HttpURLConnection.HTTP_OK, 0);
|
|
930
|
+
}
|
|
931
|
+
throw new DownloadRetryException("invalid_content_range");
|
|
932
|
+
}
|
|
933
|
+
if (responseCode == HttpURLConnection.HTTP_OK && downloadedBytes > 0) {
|
|
934
|
+
return new ZipWritePlan(HttpURLConnection.HTTP_OK, 0);
|
|
935
|
+
}
|
|
936
|
+
return new ZipWritePlan(responseCode, downloadedBytes);
|
|
937
|
+
}
|
|
938
|
+
|
|
801
939
|
private void clearDownloadData(String docDir, String id) {
|
|
802
940
|
File tempFile = new File(docDir, "temp_" + id + ".tmp");
|
|
803
941
|
File infoFile = new File(docDir, "update_" + id + ".dat");
|
|
@@ -995,17 +1133,44 @@ public class DownloadService extends Worker {
|
|
|
995
1133
|
}
|
|
996
1134
|
|
|
997
1135
|
static void writeHttpBody(File dest, InputStream body, int statusCode, long existingBytes) throws IOException {
|
|
1136
|
+
writeHttpBody(dest, body, statusCode, existingBytes, null, null);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
static void writeHttpBody(
|
|
1140
|
+
File dest,
|
|
1141
|
+
InputStream body,
|
|
1142
|
+
int statusCode,
|
|
1143
|
+
long existingBytes,
|
|
1144
|
+
BooleanSupplier shouldStop,
|
|
1145
|
+
LongConsumer onProgress
|
|
1146
|
+
) throws IOException {
|
|
1147
|
+
if (shouldStop != null && shouldStop.getAsBoolean()) {
|
|
1148
|
+
throw new IOException("download_stopped");
|
|
1149
|
+
}
|
|
998
1150
|
boolean append = shouldAppendHttpBody(statusCode, existingBytes);
|
|
999
1151
|
byte[] buffer = new byte[CryptoCipher.ioBufferBytes()];
|
|
1000
1152
|
try (FileOutputStream fos = new FileOutputStream(dest, append)) {
|
|
1001
1153
|
int n;
|
|
1002
1154
|
long written = append ? existingBytes : 0;
|
|
1003
|
-
while (
|
|
1155
|
+
while (true) {
|
|
1156
|
+
if (shouldStop != null && shouldStop.getAsBoolean()) {
|
|
1157
|
+
throw new IOException("download_stopped");
|
|
1158
|
+
}
|
|
1159
|
+
n = body.read(buffer);
|
|
1160
|
+
if (shouldStop != null && shouldStop.getAsBoolean()) {
|
|
1161
|
+
throw new IOException("download_stopped");
|
|
1162
|
+
}
|
|
1163
|
+
if (n == -1) {
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1004
1166
|
fos.write(buffer, 0, n);
|
|
1005
1167
|
written += n;
|
|
1006
1168
|
if (written % (1024 * 1024) == 0) {
|
|
1007
1169
|
fos.flush();
|
|
1008
1170
|
}
|
|
1171
|
+
if (onProgress != null) {
|
|
1172
|
+
onProgress.accept(written);
|
|
1173
|
+
}
|
|
1009
1174
|
}
|
|
1010
1175
|
fos.flush();
|
|
1011
1176
|
}
|
|
@@ -96,7 +96,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
96
96
|
deinit {
|
|
97
97
|
implementation.shutdown()
|
|
98
98
|
}
|
|
99
|
-
private let pluginVersion: String = "8.51.
|
|
99
|
+
private let pluginVersion: String = "8.51.20"
|
|
100
100
|
private let launchStartedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
101
101
|
static let updateUrlDefault = "https://plugin.capgo.app/updates"
|
|
102
102
|
static let statsUrlDefault = "https://plugin.capgo.app/stats"
|
|
@@ -3555,7 +3555,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3555
3555
|
}
|
|
3556
3556
|
|
|
3557
3557
|
private func makeSplashscreenCall(callbackId: String, options: [String: Any], methodName: String) -> CAPPluginCall {
|
|
3558
|
-
CAPPluginCall(callbackId: callbackId, options: options, success: { [weak self] (_, _) in
|
|
3558
|
+
CAPPluginCall(callbackId: callbackId, methodName: methodName, options: options, success: { [weak self] (_, _) in
|
|
3559
3559
|
guard let self = self else { return }
|
|
3560
3560
|
self.logger.info(self.splashscreenCompletedMessage(methodName: methodName))
|
|
3561
3561
|
}, error: { [weak self] (_) in
|
|
@@ -3846,7 +3846,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
3846
3846
|
return
|
|
3847
3847
|
}
|
|
3848
3848
|
|
|
3849
|
-
let scheduleTimeout = {
|
|
3849
|
+
let scheduleTimeout = { [weak self] in
|
|
3850
|
+
guard let self = self else { return }
|
|
3850
3851
|
self.autoSplashscreenTimeoutWorkItem?.cancel()
|
|
3851
3852
|
|
|
3852
3853
|
let workItem = DispatchWorkItem { [weak self] in
|
|
@@ -172,7 +172,15 @@ import UIKit
|
|
|
172
172
|
return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorTimedOut
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
// lazy var is not thread-safe; concurrent manifest downloads can race first access when statsUrl is empty.
|
|
176
|
+
private var _alamofireSession: Session?
|
|
177
|
+
private let alamofireSessionLock = NSLock()
|
|
178
|
+
private var alamofireSession: Session {
|
|
179
|
+
alamofireSessionLock.lock()
|
|
180
|
+
defer { alamofireSessionLock.unlock() }
|
|
181
|
+
if let session = _alamofireSession {
|
|
182
|
+
return session
|
|
183
|
+
}
|
|
176
184
|
let configuration = URLSessionConfiguration.ephemeral
|
|
177
185
|
configuration.httpAdditionalHeaders = ["User-Agent": self.userAgent]
|
|
178
186
|
configuration.httpCookieStorage = nil
|
|
@@ -180,8 +188,10 @@ import UIKit
|
|
|
180
188
|
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
|
181
189
|
configuration.urlCache = nil
|
|
182
190
|
configuration.httpMaximumConnectionsPerHost = Self.manifestMaxConcurrentFiles
|
|
183
|
-
|
|
184
|
-
|
|
191
|
+
let session = Session(configuration: configuration)
|
|
192
|
+
_alamofireSession = session
|
|
193
|
+
return session
|
|
194
|
+
}
|
|
185
195
|
private let networkResponseQueue = DispatchQueue(label: "ee.forgr.capacitor-updater.network-response", qos: .utility)
|
|
186
196
|
|
|
187
197
|
public var notifyDownloadRaw: (String, Int, Bool, BundleInfo?) -> Void = { _, _, _, _ in }
|
|
@@ -161,49 +161,43 @@ public struct CryptoCipher {
|
|
|
161
161
|
let bufferSize = checksumBufferBytes()
|
|
162
162
|
var sha256 = SHA256()
|
|
163
163
|
|
|
164
|
+
let fileHandle: FileHandle
|
|
164
165
|
do {
|
|
165
|
-
|
|
166
|
+
fileHandle = try FileHandle(forReadingFrom: filePath)
|
|
167
|
+
} catch {
|
|
168
|
+
logger.error("Cannot open file for checksum calculation")
|
|
169
|
+
logger.debug("Path: \(filePath.path), Error: \(error)")
|
|
170
|
+
return ""
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
defer {
|
|
166
174
|
do {
|
|
167
|
-
|
|
175
|
+
try fileHandle.close()
|
|
168
176
|
} catch {
|
|
169
|
-
logger.error("
|
|
170
|
-
logger.debug("
|
|
171
|
-
return ""
|
|
177
|
+
logger.error("Error closing file during checksum")
|
|
178
|
+
logger.debug("Error: \(error)")
|
|
172
179
|
}
|
|
180
|
+
}
|
|
173
181
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
while autoreleasepool(invoking: {
|
|
183
|
+
let fileData: Data
|
|
184
|
+
do {
|
|
185
|
+
fileData = try fileHandle.read(upToCount: bufferSize) ?? Data()
|
|
186
|
+
} catch {
|
|
187
|
+
logger.error("Error reading file during checksum")
|
|
188
|
+
logger.debug("Error: \(error)")
|
|
189
|
+
return false
|
|
181
190
|
}
|
|
182
191
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return false
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if fileData.count > 0 {
|
|
194
|
-
sha256.update(data: fileData)
|
|
195
|
-
return true // Continue
|
|
196
|
-
} else {
|
|
197
|
-
return false // End of file
|
|
198
|
-
}
|
|
199
|
-
}) {}
|
|
192
|
+
if fileData.count > 0 {
|
|
193
|
+
sha256.update(data: fileData)
|
|
194
|
+
return true // Continue
|
|
195
|
+
} else {
|
|
196
|
+
return false // End of file
|
|
197
|
+
}
|
|
198
|
+
}) {}
|
|
200
199
|
|
|
201
|
-
|
|
202
|
-
} catch {
|
|
203
|
-
logger.error("Cannot calculate checksum")
|
|
204
|
-
logger.debug("Path: \(filePath.path), Error: \(error)")
|
|
205
|
-
return ""
|
|
206
|
-
}
|
|
200
|
+
return hexString(from: sha256)
|
|
207
201
|
}
|
|
208
202
|
|
|
209
203
|
final class RunningChecksum {
|
|
@@ -222,8 +216,7 @@ public struct CryptoCipher {
|
|
|
222
216
|
}
|
|
223
217
|
|
|
224
218
|
static func hexString(from sha256: SHA256) -> String {
|
|
225
|
-
|
|
226
|
-
return copy.finalize().compactMap { String(format: "%02x", $0) }.joined()
|
|
219
|
+
return sha256.finalize().compactMap { String(format: "%02x", $0) }.joined()
|
|
227
220
|
}
|
|
228
221
|
|
|
229
222
|
static func shortPathKey(_ fileName: String) -> String {
|
|
@@ -151,59 +151,34 @@ public class DelayUpdateUtils {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
public func setMultiDelay(delayConditions: String) -> Bool {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return true
|
|
159
|
-
} catch {
|
|
160
|
-
logger.error("Failed to delay update, [Error calling 'setMultiDelay()']: \(error)")
|
|
161
|
-
return false
|
|
162
|
-
}
|
|
154
|
+
UserDefaults.standard.set(delayConditions, forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
|
|
155
|
+
UserDefaults.standard.synchronize()
|
|
156
|
+
logger.info("Delay update saved")
|
|
157
|
+
return true
|
|
163
158
|
}
|
|
164
159
|
|
|
165
160
|
public func setBackgroundTimestamp(_ backgroundTimestamp: Int64) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
logger.info("Background timestamp saved")
|
|
170
|
-
} catch {
|
|
171
|
-
logger.error("Failed to save background timestamp, [Error calling 'setBackgroundTimestamp()']: \(error)")
|
|
172
|
-
}
|
|
161
|
+
UserDefaults.standard.set(backgroundTimestamp, forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
|
|
162
|
+
UserDefaults.standard.synchronize()
|
|
163
|
+
logger.info("Background timestamp saved")
|
|
173
164
|
}
|
|
174
165
|
|
|
175
166
|
public func unsetBackgroundTimestamp() {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
logger.info("Background timestamp removed")
|
|
180
|
-
} catch {
|
|
181
|
-
// swiftlint:disable:next line_length
|
|
182
|
-
logger.error("Failed to remove background timestamp, [Error calling 'unsetBackgroundTimestamp()']: \(error)")
|
|
183
|
-
}
|
|
167
|
+
UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
|
|
168
|
+
UserDefaults.standard.synchronize()
|
|
169
|
+
logger.info("Background timestamp removed")
|
|
184
170
|
}
|
|
185
171
|
|
|
186
172
|
private func getBackgroundTimestamp() -> Int64 {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
let timestamp = UserDefaults.standard.object(forKey: key) as? Int64 ?? 0
|
|
190
|
-
return timestamp
|
|
191
|
-
} catch {
|
|
192
|
-
logger.error("Failed to get background timestamp, [Error calling 'getBackgroundTimestamp()']: \(error)")
|
|
193
|
-
return 0
|
|
194
|
-
}
|
|
173
|
+
let key = DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY
|
|
174
|
+
return UserDefaults.standard.object(forKey: key) as? Int64 ?? 0
|
|
195
175
|
}
|
|
196
176
|
|
|
197
177
|
public func cancelDelay(source: String) -> Bool {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return true
|
|
203
|
-
} catch {
|
|
204
|
-
logger.error("Failed to cancel update delay: \(error)")
|
|
205
|
-
return false
|
|
206
|
-
}
|
|
178
|
+
UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
|
|
179
|
+
UserDefaults.standard.synchronize()
|
|
180
|
+
logger.info("All delays canceled from \(source)")
|
|
181
|
+
return true
|
|
207
182
|
}
|
|
208
183
|
|
|
209
184
|
// MARK: - Helper methods
|
|
@@ -792,9 +792,9 @@ extension UIWindow {
|
|
|
792
792
|
message: "Update downloaded! Reload to apply version \(latest.version)?",
|
|
793
793
|
plugin: plugin,
|
|
794
794
|
bridge: bridge,
|
|
795
|
-
onReload: {
|
|
795
|
+
onReload: {
|
|
796
796
|
_ = updater.set(bundle: bundle)
|
|
797
|
-
_ = plugin
|
|
797
|
+
_ = plugin._reload()
|
|
798
798
|
}
|
|
799
799
|
)
|
|
800
800
|
}
|