@capgo/capacitor-updater 6.1.2 → 6.1.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/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +109 -51
- package/ios/Plugin/CapacitorUpdater.swift +236 -101
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +7 -4
- package/package.json +1 -1
|
@@ -56,7 +56,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
56
56
|
private static final String channelUrlDefault =
|
|
57
57
|
"https://api.capgo.app/channel_self";
|
|
58
58
|
|
|
59
|
-
private final String PLUGIN_VERSION = "6.1.
|
|
59
|
+
private final String PLUGIN_VERSION = "6.1.4";
|
|
60
60
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
61
61
|
|
|
62
62
|
private SharedPreferences.Editor editor;
|
|
@@ -3,17 +3,15 @@
|
|
|
3
3
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
4
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
6
|
package ee.forgr.capacitor_updater;
|
|
8
7
|
|
|
9
8
|
import android.app.IntentService;
|
|
10
9
|
import android.content.Intent;
|
|
11
|
-
import java.io
|
|
12
|
-
import java.
|
|
13
|
-
import java.io.FileOutputStream;
|
|
14
|
-
import java.io.InputStream;
|
|
10
|
+
import java.io.*;
|
|
11
|
+
import java.net.HttpURLConnection;
|
|
15
12
|
import java.net.URL;
|
|
16
13
|
import java.net.URLConnection;
|
|
14
|
+
import java.nio.channels.FileChannel;
|
|
17
15
|
import java.util.Objects;
|
|
18
16
|
|
|
19
17
|
public class DownloadService extends IntentService {
|
|
@@ -30,20 +28,22 @@ public class DownloadService extends IntentService {
|
|
|
30
28
|
public static final String SIGNATURE = "signature";
|
|
31
29
|
public static final String NOTIFICATION = "service receiver";
|
|
32
30
|
public static final String PERCENTDOWNLOAD = "percent receiver";
|
|
31
|
+
private static final String UPDATE_FILE = "update.dat";
|
|
33
32
|
|
|
34
33
|
public DownloadService() {
|
|
35
34
|
super("Background DownloadService");
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
private int calcTotalPercent(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
private int calcTotalPercent(long downloadedBytes, long contentLength) {
|
|
38
|
+
if (contentLength <= 0) {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
int percent = (int) (((double) downloadedBytes / contentLength) * 100);
|
|
42
|
+
percent = Math.max(10, percent);
|
|
43
|
+
percent = Math.min(70, percent);
|
|
44
|
+
return percent;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
// Will be called asynchronously by OS.
|
|
47
47
|
@Override
|
|
48
48
|
protected void onHandleIntent(Intent intent) {
|
|
49
49
|
assert intent != null;
|
|
@@ -56,47 +56,95 @@ public class DownloadService extends IntentService {
|
|
|
56
56
|
String checksum = intent.getStringExtra(CHECKSUM);
|
|
57
57
|
String signature = intent.getStringExtra(SIGNATURE);
|
|
58
58
|
|
|
59
|
+
File target = new File(documentsDir, dest);
|
|
60
|
+
File infoFile = new File(documentsDir, UPDATE_FILE); // The file where the download progress (how much byte
|
|
61
|
+
// downloaded) is stored
|
|
62
|
+
File tempFile = new File(documentsDir, "temp" + ".tmp"); // Temp file, where the downloaded data is stored
|
|
59
63
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
URL u = new URL(url);
|
|
65
|
+
HttpURLConnection httpConn = (HttpURLConnection) u.openConnection();
|
|
66
|
+
|
|
67
|
+
// Reading progress file (if exist)
|
|
68
|
+
long downloadedBytes = 0;
|
|
69
|
+
|
|
70
|
+
if (infoFile.exists() && tempFile.exists()) {
|
|
71
|
+
try (
|
|
72
|
+
BufferedReader reader = new BufferedReader(new FileReader(infoFile))
|
|
73
|
+
) {
|
|
74
|
+
String updateVersion = reader.readLine();
|
|
75
|
+
if (!updateVersion.equals(version)) {
|
|
76
|
+
clearDownloadData(documentsDir);
|
|
77
|
+
downloadedBytes = 0;
|
|
78
|
+
} else {
|
|
79
|
+
downloadedBytes = tempFile.length();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
clearDownloadData(documentsDir);
|
|
84
|
+
downloadedBytes = 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (downloadedBytes > 0) {
|
|
88
|
+
httpConn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
int responseCode = httpConn.getResponseCode();
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
if (
|
|
94
|
+
responseCode == HttpURLConnection.HTTP_OK ||
|
|
95
|
+
responseCode == HttpURLConnection.HTTP_PARTIAL
|
|
66
96
|
) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
while ((length = dis.read(buffer)) > 0) {
|
|
81
|
-
fos.write(buffer, 0, length);
|
|
82
|
-
final int newPercent = (int) ((bytesRead * 100) / totalLength);
|
|
83
|
-
if (totalLength > 1 && newPercent != percent) {
|
|
84
|
-
percent = newPercent;
|
|
85
|
-
this.notifyDownload(id, this.calcTotalPercent(percent, 10, 70));
|
|
86
|
-
}
|
|
87
|
-
bytesRead += length;
|
|
97
|
+
String contentType = httpConn.getContentType();
|
|
98
|
+
long contentLength = httpConn.getContentLength() + downloadedBytes;
|
|
99
|
+
|
|
100
|
+
InputStream inputStream = httpConn.getInputStream();
|
|
101
|
+
FileOutputStream outputStream = new FileOutputStream(
|
|
102
|
+
tempFile,
|
|
103
|
+
downloadedBytes > 0
|
|
104
|
+
);
|
|
105
|
+
if (downloadedBytes == 0) {
|
|
106
|
+
try (
|
|
107
|
+
BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))
|
|
108
|
+
) {
|
|
109
|
+
writer.write(String.valueOf(version));
|
|
88
110
|
}
|
|
89
|
-
publishResults(
|
|
90
|
-
dest,
|
|
91
|
-
id,
|
|
92
|
-
version,
|
|
93
|
-
checksum,
|
|
94
|
-
sessionKey,
|
|
95
|
-
signature,
|
|
96
|
-
""
|
|
97
|
-
);
|
|
98
111
|
}
|
|
112
|
+
// Updating the info file
|
|
113
|
+
try (
|
|
114
|
+
BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))
|
|
115
|
+
) {
|
|
116
|
+
writer.write(String.valueOf(version));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
int bytesRead = -1;
|
|
120
|
+
byte[] buffer = new byte[4096];
|
|
121
|
+
int lastPercent = 0;
|
|
122
|
+
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
123
|
+
outputStream.write(buffer, 0, bytesRead);
|
|
124
|
+
downloadedBytes += bytesRead;
|
|
125
|
+
// Saving progress (flushing every 100 Ko)
|
|
126
|
+
if (downloadedBytes % 102400 == 0) {
|
|
127
|
+
outputStream.flush();
|
|
128
|
+
}
|
|
129
|
+
// Computing percentage
|
|
130
|
+
int percent = calcTotalPercent(downloadedBytes, contentLength);
|
|
131
|
+
if (percent != lastPercent) {
|
|
132
|
+
notifyDownload(id, percent);
|
|
133
|
+
lastPercent = percent;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
outputStream.close();
|
|
138
|
+
inputStream.close();
|
|
139
|
+
|
|
140
|
+
// Rename the temp file with the final name (dest)
|
|
141
|
+
tempFile.renameTo(new File(documentsDir, dest));
|
|
142
|
+
infoFile.delete();
|
|
143
|
+
publishResults(dest, id, version, checksum, sessionKey, signature, "");
|
|
144
|
+
} else {
|
|
145
|
+
infoFile.delete();
|
|
99
146
|
}
|
|
147
|
+
httpConn.disconnect();
|
|
100
148
|
} catch (OutOfMemoryError e) {
|
|
101
149
|
e.printStackTrace();
|
|
102
150
|
publishResults(
|
|
@@ -122,6 +170,19 @@ public class DownloadService extends IntentService {
|
|
|
122
170
|
}
|
|
123
171
|
}
|
|
124
172
|
|
|
173
|
+
private void clearDownloadData(String docDir) {
|
|
174
|
+
File tempFile = new File(docDir, "temp" + ".tmp");
|
|
175
|
+
File infoFile = new File(docDir, UPDATE_FILE);
|
|
176
|
+
try {
|
|
177
|
+
tempFile.delete();
|
|
178
|
+
infoFile.delete();
|
|
179
|
+
infoFile.createNewFile();
|
|
180
|
+
tempFile.createNewFile();
|
|
181
|
+
} catch (IOException e) {
|
|
182
|
+
e.printStackTrace();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
125
186
|
private void notifyDownload(String id, int percent) {
|
|
126
187
|
Intent intent = new Intent(PERCENTDOWNLOAD);
|
|
127
188
|
intent.setPackage(getPackageName());
|
|
@@ -144,14 +205,11 @@ public class DownloadService extends IntentService {
|
|
|
144
205
|
if (dest != null && !dest.isEmpty()) {
|
|
145
206
|
intent.putExtra(FILEDEST, dest);
|
|
146
207
|
}
|
|
147
|
-
|
|
148
|
-
intent.putExtra(ERROR, error);
|
|
149
|
-
}
|
|
208
|
+
intent.putExtra(ERROR, error);
|
|
150
209
|
intent.putExtra(ID, id);
|
|
151
210
|
intent.putExtra(VERSION, version);
|
|
152
211
|
intent.putExtra(SESSIONKEY, sessionKey);
|
|
153
212
|
intent.putExtra(CHECKSUM, checksum);
|
|
154
|
-
intent.putExtra(ERROR, error);
|
|
155
213
|
intent.putExtra(SIGNATURE, signature);
|
|
156
214
|
sendBroadcast(intent);
|
|
157
215
|
}
|
|
@@ -237,7 +237,7 @@ extension CustomError: LocalizedError {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
@objc public class CapacitorUpdater: NSObject {
|
|
240
|
-
|
|
240
|
+
|
|
241
241
|
private let versionCode: String = Bundle.main.versionCode ?? ""
|
|
242
242
|
private let versionOs = UIDevice.current.systemVersion
|
|
243
243
|
private let libraryDir: URL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!
|
|
@@ -247,7 +247,7 @@ extension CustomError: LocalizedError {
|
|
|
247
247
|
private let FALLBACK_VERSION: String = "pastVersion"
|
|
248
248
|
private let NEXT_VERSION: String = "nextVersion"
|
|
249
249
|
private var unzipPercent = 0
|
|
250
|
-
|
|
250
|
+
|
|
251
251
|
public let TAG: String = "✨ Capacitor-updater:"
|
|
252
252
|
public let CAP_SERVER_PATH: String = "serverBasePath"
|
|
253
253
|
public var versionBuild: String = ""
|
|
@@ -261,7 +261,11 @@ extension CustomError: LocalizedError {
|
|
|
261
261
|
public var deviceID = UIDevice.current.identifierForVendor?.uuidString ?? ""
|
|
262
262
|
public var privateKey: String = ""
|
|
263
263
|
public var signKey: PublicKey?
|
|
264
|
-
|
|
264
|
+
|
|
265
|
+
public var notifyDownloadRaw: (String, Int, Bool) -> Void = { _, _, _ in }
|
|
266
|
+
public func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false) {
|
|
267
|
+
notifyDownloadRaw(id, percent, ignoreMultipleOfTen)
|
|
268
|
+
}
|
|
265
269
|
public var notifyDownload: (String, Int) -> Void = { _, _ in }
|
|
266
270
|
|
|
267
271
|
private func calcTotalPercent(percent: Int, min: Int, max: Int) -> Int {
|
|
@@ -356,34 +360,7 @@ extension CustomError: LocalizedError {
|
|
|
356
360
|
}
|
|
357
361
|
}
|
|
358
362
|
|
|
359
|
-
private func calcChecksum(filePath: URL) -> String {
|
|
360
|
-
let bufferSize = 1024 * 1024 * 5 // 5 MB
|
|
361
|
-
var checksum = uLong(0)
|
|
362
|
-
|
|
363
|
-
do {
|
|
364
|
-
let fileHandle = try FileHandle(forReadingFrom: filePath)
|
|
365
|
-
defer {
|
|
366
|
-
fileHandle.closeFile()
|
|
367
|
-
}
|
|
368
363
|
|
|
369
|
-
while autoreleasepool(invoking: {
|
|
370
|
-
let fileData = fileHandle.readData(ofLength: bufferSize)
|
|
371
|
-
if fileData.count > 0 {
|
|
372
|
-
checksum = fileData.withUnsafeBytes {
|
|
373
|
-
crc32(checksum, $0.bindMemory(to: Bytef.self).baseAddress, uInt(fileData.count))
|
|
374
|
-
}
|
|
375
|
-
return true // Continue
|
|
376
|
-
} else {
|
|
377
|
-
return false // End of file
|
|
378
|
-
}
|
|
379
|
-
}) {}
|
|
380
|
-
|
|
381
|
-
return String(format: "%08X", checksum).lowercased()
|
|
382
|
-
} catch {
|
|
383
|
-
print("\(self.TAG) Cannot calc checksum: \(filePath.path)", error)
|
|
384
|
-
return ""
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
364
|
|
|
388
365
|
private func verifyBundleSignature(version: String, filePath: URL, signature: String?) throws -> Bool {
|
|
389
366
|
if (self.signKey == nil) {
|
|
@@ -447,6 +424,7 @@ extension CustomError: LocalizedError {
|
|
|
447
424
|
}
|
|
448
425
|
|
|
449
426
|
try decryptedData.write(to: filePath)
|
|
427
|
+
|
|
450
428
|
} catch {
|
|
451
429
|
print("\(self.TAG) Cannot decode: \(filePath.path)", error)
|
|
452
430
|
self.sendStats(action: "decrypt_fail", versionName: version)
|
|
@@ -485,7 +463,7 @@ extension CustomError: LocalizedError {
|
|
|
485
463
|
let newPercent = self.calcTotalPercent(percent: Int(Double(entryNumber) / Double(total) * 100), min: 75, max: 81)
|
|
486
464
|
if newPercent != self.unzipPercent {
|
|
487
465
|
self.unzipPercent = newPercent
|
|
488
|
-
self.notifyDownload(id, newPercent)
|
|
466
|
+
self.notifyDownload(id: id, percent: newPercent)
|
|
489
467
|
}
|
|
490
468
|
}
|
|
491
469
|
|
|
@@ -495,7 +473,7 @@ extension CustomError: LocalizedError {
|
|
|
495
473
|
let destUnZip: URL = libraryDir.appendingPathComponent(randomString(length: 10))
|
|
496
474
|
|
|
497
475
|
self.unzipPercent = 0
|
|
498
|
-
self.notifyDownload(id, 75)
|
|
476
|
+
self.notifyDownload(id: id, percent: 75)
|
|
499
477
|
|
|
500
478
|
let semaphore = DispatchSemaphore(value: 0)
|
|
501
479
|
var unzipError: NSError?
|
|
@@ -605,81 +583,107 @@ extension CustomError: LocalizedError {
|
|
|
605
583
|
UserDefaults.standard.synchronize()
|
|
606
584
|
print("\(self.TAG) Current bundle set to: \((bundle ).isEmpty ? BundleInfo.ID_BUILTIN : bundle)")
|
|
607
585
|
}
|
|
586
|
+
private func calcChecksum(filePath: URL) -> String {
|
|
587
|
+
let bufferSize = 1024 * 1024 * 5 // 5 MB
|
|
588
|
+
var checksum = uLong(0)
|
|
608
589
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
590
|
+
do {
|
|
591
|
+
let fileHandle = try FileHandle(forReadingFrom: filePath)
|
|
592
|
+
defer {
|
|
593
|
+
fileHandle.closeFile()
|
|
594
|
+
}
|
|
613
595
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
596
|
+
while autoreleasepool(invoking: {
|
|
597
|
+
let fileData = fileHandle.readData(ofLength: bufferSize)
|
|
598
|
+
if fileData.count > 0 {
|
|
599
|
+
checksum = fileData.withUnsafeBytes {
|
|
600
|
+
crc32(checksum, $0.bindMemory(to: Bytef.self).baseAddress, uInt(fileData.count))
|
|
601
|
+
}
|
|
602
|
+
return true // Continue
|
|
603
|
+
} else {
|
|
604
|
+
return false // End of file
|
|
605
|
+
}
|
|
606
|
+
}) {}
|
|
618
607
|
|
|
619
|
-
return (
|
|
608
|
+
return String(format: "%08X", checksum).lowercased()
|
|
609
|
+
} catch {
|
|
610
|
+
print("\(self.TAG) Cannot get checksum: \(filePath.path)", error)
|
|
611
|
+
return ""
|
|
620
612
|
}
|
|
621
|
-
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private var tempDataPath: URL {
|
|
616
|
+
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("package.tmp")
|
|
617
|
+
}
|
|
622
618
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
619
|
+
private var updateInfo: URL {
|
|
620
|
+
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("update.dat")
|
|
621
|
+
}
|
|
622
|
+
private var tempData = Data()
|
|
623
|
+
public func download(url: URL, version: String, sessionKey: String, signature: String) throws -> BundleInfo {
|
|
624
|
+
let id: String = self.randomString(length: 10)
|
|
625
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
626
|
+
if(version != getLocalUpdateVersion()){
|
|
627
|
+
cleanDlData()
|
|
626
628
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
try self.decryptFile(filePath: fileURL, sessionKey: sessionKey, version: version)
|
|
646
|
-
checksum = self.calcChecksum(filePath: fileURL)
|
|
647
|
-
} catch {
|
|
648
|
-
print("\(self.TAG) downloaded file verification error", error)
|
|
649
|
-
mainError = error as NSError
|
|
650
|
-
|
|
651
|
-
// Cleanup
|
|
652
|
-
do {
|
|
653
|
-
try self.deleteFolder(source: fileURL)
|
|
654
|
-
} catch {
|
|
655
|
-
print("\(self.TAG) Double error, cannot cleanup", error)
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
do {
|
|
660
|
-
if (mainError == nil) {
|
|
661
|
-
try self.saveDownloaded(sourceZip: fileURL, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory), notify: true)
|
|
662
|
-
try self.deleteFolder(source: fileURL)
|
|
663
|
-
self.notifyDownload(id, 100)
|
|
664
|
-
}
|
|
665
|
-
} catch {
|
|
666
|
-
print("\(self.TAG) download unzip error", error)
|
|
667
|
-
mainError = error as NSError
|
|
668
|
-
}
|
|
669
|
-
case let .failure(error):
|
|
670
|
-
print("\(self.TAG) download error", response.value ?? "", error)
|
|
671
|
-
if let afError = error as? AFError,
|
|
672
|
-
case .sessionTaskFailed(let urlError as URLError) = afError,
|
|
673
|
-
urlError.code == .cannotWriteToFile {
|
|
674
|
-
self.sendStats(action: "low_mem_fail", versionName: version)
|
|
675
|
-
}
|
|
676
|
-
mainError = error as NSError
|
|
677
|
-
}
|
|
629
|
+
ensureResumableFilesExist()
|
|
630
|
+
saveDownloadInfo(version)
|
|
631
|
+
var checksum = ""
|
|
632
|
+
var targetSize = -1
|
|
633
|
+
var lastSentProgress = 0
|
|
634
|
+
var totalReceivedBytes: Int64 = loadDownloadProgress() //Retrieving the amount of already downloaded data if exist, defined at 0 otherwise
|
|
635
|
+
let requestHeaders: HTTPHeaders = ["Range": "bytes=\(totalReceivedBytes)-"]
|
|
636
|
+
//Opening connection for streaming the bytes
|
|
637
|
+
if(totalReceivedBytes == 0){
|
|
638
|
+
self.notifyDownload(id: id, percent: 0, ignoreMultipleOfTen: true)
|
|
639
|
+
}
|
|
640
|
+
var mainError: NSError?
|
|
641
|
+
let monitor = ClosureEventMonitor()
|
|
642
|
+
monitor.requestDidCompleteTaskWithError = { (request, task, error) in
|
|
643
|
+
if error != nil {
|
|
644
|
+
print("\(self.TAG) Downloading failed - ClosureEventMonitor activated")
|
|
645
|
+
mainError = error as NSError?
|
|
678
646
|
}
|
|
679
|
-
|
|
680
|
-
|
|
647
|
+
}
|
|
648
|
+
let session = Session(eventMonitors: [monitor])
|
|
649
|
+
|
|
650
|
+
var request = session.streamRequest(url, headers: requestHeaders).validate().onHTTPResponse(perform: { response in
|
|
651
|
+
if let contentLength = response.headers.value(for: "Content-Length") {
|
|
652
|
+
targetSize = (Int(contentLength) ?? -1) + Int(totalReceivedBytes)
|
|
653
|
+
}
|
|
654
|
+
}).responseStream { [weak self] streamResponse in
|
|
655
|
+
guard let self = self else { return }
|
|
656
|
+
switch streamResponse.event {
|
|
657
|
+
case .stream(let result):
|
|
658
|
+
if case .success(let data) = result {
|
|
659
|
+
self.tempData.append(data)
|
|
660
|
+
|
|
661
|
+
self.savePartialData(startingAt: UInt64(totalReceivedBytes)) // Saving the received data in the package.tmp file
|
|
662
|
+
totalReceivedBytes += Int64(data.count)
|
|
663
|
+
|
|
664
|
+
let percent = Int((Double(totalReceivedBytes) / Double(targetSize)) * 100.0)
|
|
665
|
+
|
|
666
|
+
print("\(self.TAG) Downloading: \(percent)%")
|
|
667
|
+
let currentMilestone = (percent / 10) * 10
|
|
668
|
+
if currentMilestone > lastSentProgress && currentMilestone <= 70 {
|
|
669
|
+
for milestone in stride(from: lastSentProgress + 10, through: currentMilestone, by: 10) {
|
|
670
|
+
self.notifyDownload(id: id, percent: milestone, ignoreMultipleOfTen: true)
|
|
671
|
+
}
|
|
672
|
+
lastSentProgress = currentMilestone
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
print("\(self.TAG) Download failed")
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
case .complete(_):
|
|
682
|
+
print("\(self.TAG) Download complete, total received bytes: \(totalReceivedBytes)")
|
|
683
|
+
semaphore.signal()
|
|
684
|
+
}
|
|
685
|
+
}
|
|
681
686
|
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: checksum))
|
|
682
|
-
self.notifyDownload(id, 0)
|
|
683
687
|
let reachabilityManager = NetworkReachabilityManager()
|
|
684
688
|
reachabilityManager?.startListening { status in
|
|
685
689
|
switch status {
|
|
@@ -694,13 +698,144 @@ extension CustomError: LocalizedError {
|
|
|
694
698
|
}
|
|
695
699
|
semaphore.wait()
|
|
696
700
|
reachabilityManager?.stopListening()
|
|
697
|
-
|
|
701
|
+
|
|
702
|
+
if (mainError != nil) {
|
|
703
|
+
print("\(self.TAG) Failed to download: \(String(describing: mainError))")
|
|
704
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
705
|
+
throw mainError!
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let finalPath = tempDataPath.deletingLastPathComponent().appendingPathComponent("\(id)")
|
|
709
|
+
do {
|
|
710
|
+
let valid = try self.verifyBundleSignature(version: version, filePath: tempDataPath, signature: signature)
|
|
711
|
+
if (!valid) {
|
|
712
|
+
print("\(self.TAG) Invalid signature, cannot accept download")
|
|
713
|
+
self.sendStats(action: "invalid_signature", versionName: version)
|
|
714
|
+
throw CustomError.invalidSignature
|
|
715
|
+
} else {
|
|
716
|
+
print("\(self.TAG) Valid signature")
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
try self.decryptFile(filePath: tempDataPath, sessionKey: sessionKey, version: version)
|
|
720
|
+
try FileManager.default.moveItem(at: tempDataPath, to: finalPath)
|
|
721
|
+
} catch {
|
|
722
|
+
print("\(self.TAG) Failed decrypt file or verify signature or move it: \(error)")
|
|
723
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
724
|
+
cleanDlData()
|
|
725
|
+
throw error
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
do {
|
|
729
|
+
checksum = self.calcChecksum(filePath: finalPath)
|
|
730
|
+
try self.saveDownloaded(sourceZip: finalPath, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory), notify: true)
|
|
731
|
+
} catch {
|
|
732
|
+
print("\(self.TAG) Failed to unzip file: \(error)")
|
|
733
|
+
self.saveBundleInfo(id: id, bundle: BundleInfo(id: id, version: version, status: BundleStatus.ERROR, downloaded: Date(), checksum: checksum))
|
|
734
|
+
cleanDlData()
|
|
735
|
+
// todo: cleanup zip attempts
|
|
698
736
|
throw error
|
|
699
737
|
}
|
|
700
|
-
|
|
738
|
+
|
|
739
|
+
let info = BundleInfo(id: id, version: version, status: BundleStatus.PENDING, downloaded: Date(), checksum: checksum)
|
|
701
740
|
self.saveBundleInfo(id: id, bundle: info)
|
|
741
|
+
self.notifyDownload(id: id, percent: 100)
|
|
742
|
+
self.cleanDlData()
|
|
702
743
|
return info
|
|
703
744
|
}
|
|
745
|
+
private func ensureResumableFilesExist() {
|
|
746
|
+
let fileManager = FileManager.default
|
|
747
|
+
if !fileManager.fileExists(atPath: tempDataPath.path) {
|
|
748
|
+
if !fileManager.createFile(atPath: tempDataPath.path, contents: Data()) {
|
|
749
|
+
print("\(self.TAG) Cannot ensure that a file at \(tempDataPath.path) exists")
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if !fileManager.fileExists(atPath: updateInfo.path) {
|
|
754
|
+
if !fileManager.createFile(atPath: updateInfo.path, contents: Data()) {
|
|
755
|
+
print("\(self.TAG) Cannot ensure that a file at \(updateInfo.path) exists")
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private func cleanDlData(){
|
|
761
|
+
// Deleting package.tmp
|
|
762
|
+
let fileManager = FileManager.default
|
|
763
|
+
if fileManager.fileExists(atPath: tempDataPath.path) {
|
|
764
|
+
do {
|
|
765
|
+
try fileManager.removeItem(at: tempDataPath)
|
|
766
|
+
} catch {
|
|
767
|
+
print("\(self.TAG) Could not delete file at \(tempDataPath): \(error)")
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
print("\(self.TAG) \(tempDataPath.lastPathComponent) does not exist")
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Deleting update.dat
|
|
774
|
+
if fileManager.fileExists(atPath: updateInfo.path) {
|
|
775
|
+
do {
|
|
776
|
+
try fileManager.removeItem(at: updateInfo)
|
|
777
|
+
} catch {
|
|
778
|
+
print("\(self.TAG) Could not delete file at \(updateInfo): \(error)")
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
print("\(self.TAG) \(updateInfo.lastPathComponent) does not exist")
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private func savePartialData(startingAt byteOffset: UInt64) {
|
|
786
|
+
let fileManager = FileManager.default
|
|
787
|
+
do {
|
|
788
|
+
// Check if package.tmp exist
|
|
789
|
+
if !fileManager.fileExists(atPath: tempDataPath.path) {
|
|
790
|
+
try self.tempData.write(to: tempDataPath, options: .atomicWrite)
|
|
791
|
+
} else {
|
|
792
|
+
// If yes, it start writing on it
|
|
793
|
+
let fileHandle = try FileHandle(forWritingTo: tempDataPath)
|
|
794
|
+
fileHandle.seek(toFileOffset: byteOffset) // Moving at the specified position to start writing
|
|
795
|
+
fileHandle.write(self.tempData)
|
|
796
|
+
fileHandle.closeFile()
|
|
797
|
+
}
|
|
798
|
+
} catch {
|
|
799
|
+
print("Failed to write data starting at byte \(byteOffset): \(error)")
|
|
800
|
+
}
|
|
801
|
+
self.tempData.removeAll() // Clearing tempData to avoid writing the same data multiple times
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
private func saveDownloadInfo(_ version: String) {
|
|
806
|
+
do {
|
|
807
|
+
try "\(version)".write(to: updateInfo, atomically: true, encoding: .utf8)
|
|
808
|
+
} catch {
|
|
809
|
+
print("\(self.TAG) Failed to save progress: \(error)")
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
private func getLocalUpdateVersion() -> String { //Return the version that was tried to be downloaded on last download attempt
|
|
813
|
+
if !FileManager.default.fileExists(atPath: updateInfo.path) {
|
|
814
|
+
return "nil"
|
|
815
|
+
}
|
|
816
|
+
guard let versionString = try? String(contentsOf: updateInfo),
|
|
817
|
+
let version = Optional(versionString) else {
|
|
818
|
+
return "nil"
|
|
819
|
+
}
|
|
820
|
+
return version
|
|
821
|
+
}
|
|
822
|
+
private func loadDownloadProgress() -> Int64 {
|
|
823
|
+
|
|
824
|
+
let fileManager = FileManager.default
|
|
825
|
+
do {
|
|
826
|
+
let attributes = try fileManager.attributesOfItem(atPath: tempDataPath.path)
|
|
827
|
+
if let fileSize = attributes[.size] as? NSNumber {
|
|
828
|
+
return fileSize.int64Value
|
|
829
|
+
}
|
|
830
|
+
} catch {
|
|
831
|
+
print("\(self.TAG) Could not retrieve already downloaded data size : \(error)")
|
|
832
|
+
}
|
|
833
|
+
return 0
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
|
|
704
839
|
|
|
705
840
|
public func list() -> [BundleInfo] {
|
|
706
841
|
let dest: URL = libraryDir.appendingPathComponent(bundleDirectory)
|
|
@@ -16,7 +16,7 @@ import SwiftyRSA
|
|
|
16
16
|
@objc(CapacitorUpdaterPlugin)
|
|
17
17
|
public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
18
18
|
public var implementation = CapacitorUpdater()
|
|
19
|
-
private let PLUGIN_VERSION: String = "6.1.
|
|
19
|
+
private let PLUGIN_VERSION: String = "6.1.4"
|
|
20
20
|
static let updateUrlDefault = "https://api.capgo.app/updates"
|
|
21
21
|
static let statsUrlDefault = "https://api.capgo.app/stats"
|
|
22
22
|
static let channelUrlDefault = "https://api.capgo.app/channel_self"
|
|
@@ -74,7 +74,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
implementation.privateKey = getConfig().getString("privateKey", self.defaultPrivateKey)!
|
|
77
|
-
implementation.
|
|
77
|
+
implementation.notifyDownloadRaw = notifyDownload
|
|
78
78
|
implementation.PLUGIN_VERSION = self.PLUGIN_VERSION
|
|
79
79
|
let config = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor().legacyConfig
|
|
80
80
|
implementation.appId = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String ?? ""
|
|
@@ -173,13 +173,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
173
173
|
UserDefaults.standard.synchronize()
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
@objc func notifyDownload(id: String, percent: Int) {
|
|
176
|
+
@objc func notifyDownload(id: String, percent: Int, ignoreMultipleOfTen: Bool = false) {
|
|
177
177
|
let bundle = self.implementation.getBundleInfo(id: id)
|
|
178
178
|
self.notifyListeners("download", data: ["percent": percent, "bundle": bundle.toJSON()])
|
|
179
179
|
if percent == 100 {
|
|
180
180
|
self.notifyListeners("downloadComplete", data: ["bundle": bundle.toJSON()])
|
|
181
181
|
self.implementation.sendStats(action: "download_complete", versionName: bundle.getVersionName())
|
|
182
|
-
} else if percent.isMultiple(of: 10) {
|
|
182
|
+
} else if percent.isMultiple(of: 10) || ignoreMultipleOfTen {
|
|
183
183
|
self.implementation.sendStats(action: "download_\(percent)", versionName: bundle.getVersionName())
|
|
184
184
|
}
|
|
185
185
|
}
|
|
@@ -721,6 +721,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin {
|
|
|
721
721
|
print("\(self.implementation.TAG) Failed to delete failed bundle: \(nextImpl!.toString())")
|
|
722
722
|
}
|
|
723
723
|
}
|
|
724
|
+
guard let signature = signature else {
|
|
725
|
+
throw CustomError.signatureNotProvided
|
|
726
|
+
}
|
|
724
727
|
nextImpl = try self.implementation.download(url: downloadUrl, version: latestVersionName, sessionKey: sessionKey, signature: signature)
|
|
725
728
|
}
|
|
726
729
|
guard let next = nextImpl else {
|