@capgo/capacitor-updater 6.2.9 → 6.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +62 -31
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +44 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +325 -6
- package/ios/Plugin/CapacitorUpdater.swift +186 -1
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +11 -7
- package/package.json +10 -10
package/android/build.gradle
CHANGED
|
@@ -58,4 +58,5 @@ dependencies {
|
|
|
58
58
|
testImplementation "junit:junit:$junitVersion"
|
|
59
59
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
60
60
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
61
|
+
implementation 'org.brotli:dec:0.1.2'
|
|
61
62
|
}
|
|
@@ -54,6 +54,7 @@ import java.util.zip.CRC32;
|
|
|
54
54
|
import java.util.zip.ZipEntry;
|
|
55
55
|
import java.util.zip.ZipInputStream;
|
|
56
56
|
import javax.crypto.SecretKey;
|
|
57
|
+
import org.json.JSONArray;
|
|
57
58
|
import org.json.JSONException;
|
|
58
59
|
import org.json.JSONObject;
|
|
59
60
|
|
|
@@ -283,6 +284,10 @@ public class CapacitorUpdater {
|
|
|
283
284
|
String sessionKey = bundle.getString(DownloadService.SESSIONKEY);
|
|
284
285
|
String checksum = bundle.getString(DownloadService.CHECKSUM);
|
|
285
286
|
String error = bundle.getString(DownloadService.ERROR);
|
|
287
|
+
boolean isManifest = bundle.getBoolean(
|
|
288
|
+
DownloadService.IS_MANIFEST,
|
|
289
|
+
false
|
|
290
|
+
);
|
|
286
291
|
Log.i(
|
|
287
292
|
CapacitorUpdater.TAG,
|
|
288
293
|
"res " +
|
|
@@ -318,7 +323,8 @@ public class CapacitorUpdater {
|
|
|
318
323
|
version,
|
|
319
324
|
sessionKey,
|
|
320
325
|
checksum,
|
|
321
|
-
true
|
|
326
|
+
true,
|
|
327
|
+
isManifest
|
|
322
328
|
);
|
|
323
329
|
} else {
|
|
324
330
|
Log.i(TAG, "Unknown action " + action);
|
|
@@ -353,35 +359,39 @@ public class CapacitorUpdater {
|
|
|
353
359
|
String version,
|
|
354
360
|
String sessionKey,
|
|
355
361
|
String checksumRes,
|
|
356
|
-
Boolean setNext
|
|
362
|
+
Boolean setNext,
|
|
363
|
+
Boolean isManifest
|
|
357
364
|
) {
|
|
358
365
|
File downloaded = null;
|
|
359
|
-
String checksum;
|
|
366
|
+
String checksum = "";
|
|
360
367
|
|
|
361
368
|
try {
|
|
362
369
|
this.notifyDownload(id, 71);
|
|
363
370
|
downloaded = new File(this.documentsDir, dest);
|
|
364
371
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
this.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
372
|
+
if (!isManifest) {
|
|
373
|
+
String checksumDecrypted = Objects.requireNonNullElse(checksumRes, "");
|
|
374
|
+
if (!this.hasOldPrivateKeyPropertyInConfig && !sessionKey.isEmpty()) {
|
|
375
|
+
this.decryptFileV2(downloaded, sessionKey, version);
|
|
376
|
+
checksumDecrypted = this.decryptChecksum(checksumRes, version);
|
|
377
|
+
checksum = this.calcChecksumV2(downloaded);
|
|
378
|
+
} else {
|
|
379
|
+
this.decryptFile(downloaded, sessionKey, version);
|
|
380
|
+
checksum = this.calcChecksum(downloaded);
|
|
381
|
+
}
|
|
382
|
+
if (
|
|
383
|
+
(!checksumDecrypted.isEmpty() || !this.publicKey.isEmpty()) &&
|
|
384
|
+
!checksumDecrypted.equals(checksum)
|
|
385
|
+
) {
|
|
386
|
+
Log.e(
|
|
387
|
+
CapacitorUpdater.TAG,
|
|
388
|
+
"Error checksum '" + checksumDecrypted + "' '" + checksum + "' '"
|
|
389
|
+
);
|
|
390
|
+
this.sendStats("checksum_fail");
|
|
391
|
+
throw new IOException("Checksum failed: " + id);
|
|
392
|
+
}
|
|
384
393
|
}
|
|
394
|
+
// Remove the decryption for manifest downloads
|
|
385
395
|
} catch (IOException e) {
|
|
386
396
|
final Boolean res = this.delete(id);
|
|
387
397
|
if (!res) {
|
|
@@ -400,11 +410,17 @@ public class CapacitorUpdater {
|
|
|
400
410
|
}
|
|
401
411
|
|
|
402
412
|
try {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
413
|
+
if (!isManifest) {
|
|
414
|
+
final File unzipped = this.unzip(id, downloaded, this.randomString());
|
|
415
|
+
this.notifyDownload(id, 91);
|
|
416
|
+
final String idName = bundleDirectory + "/" + id;
|
|
417
|
+
this.flattenAssets(unzipped, idName);
|
|
418
|
+
} else {
|
|
419
|
+
this.notifyDownload(id, 91);
|
|
420
|
+
final String idName = bundleDirectory + "/" + id;
|
|
421
|
+
this.flattenAssets(downloaded, idName);
|
|
422
|
+
downloaded.delete();
|
|
423
|
+
}
|
|
408
424
|
this.notifyDownload(id, 100);
|
|
409
425
|
this.saveBundleInfo(id, null);
|
|
410
426
|
BundleInfo next = new BundleInfo(
|
|
@@ -447,7 +463,8 @@ public class CapacitorUpdater {
|
|
|
447
463
|
final String version,
|
|
448
464
|
final String sessionKey,
|
|
449
465
|
final String checksum,
|
|
450
|
-
final String dest
|
|
466
|
+
final String dest,
|
|
467
|
+
final JSONArray manifest
|
|
451
468
|
) {
|
|
452
469
|
Intent intent = new Intent(this.activity, DownloadService.class);
|
|
453
470
|
intent.putExtra(DownloadService.URL, url);
|
|
@@ -460,6 +477,9 @@ public class CapacitorUpdater {
|
|
|
460
477
|
intent.putExtra(DownloadService.VERSION, version);
|
|
461
478
|
intent.putExtra(DownloadService.SESSIONKEY, sessionKey);
|
|
462
479
|
intent.putExtra(DownloadService.CHECKSUM, checksum);
|
|
480
|
+
if (manifest != null) {
|
|
481
|
+
intent.putExtra(DownloadService.MANIFEST, manifest.toString());
|
|
482
|
+
}
|
|
463
483
|
this.activity.startService(intent);
|
|
464
484
|
}
|
|
465
485
|
|
|
@@ -702,7 +722,8 @@ public class CapacitorUpdater {
|
|
|
702
722
|
final String url,
|
|
703
723
|
final String version,
|
|
704
724
|
final String sessionKey,
|
|
705
|
-
final String checksum
|
|
725
|
+
final String checksum,
|
|
726
|
+
final JSONArray manifest
|
|
706
727
|
) {
|
|
707
728
|
final String id = this.randomString();
|
|
708
729
|
this.saveBundleInfo(
|
|
@@ -717,13 +738,15 @@ public class CapacitorUpdater {
|
|
|
717
738
|
);
|
|
718
739
|
this.notifyDownload(id, 0);
|
|
719
740
|
this.notifyDownload(id, 5);
|
|
741
|
+
|
|
720
742
|
this.downloadFileBackground(
|
|
721
743
|
id,
|
|
722
744
|
url,
|
|
723
745
|
version,
|
|
724
746
|
sessionKey,
|
|
725
747
|
checksum,
|
|
726
|
-
this.randomString()
|
|
748
|
+
this.randomString(),
|
|
749
|
+
manifest
|
|
727
750
|
);
|
|
728
751
|
}
|
|
729
752
|
|
|
@@ -749,7 +772,15 @@ public class CapacitorUpdater {
|
|
|
749
772
|
final String dest = this.randomString();
|
|
750
773
|
this.downloadFile(id, url, dest);
|
|
751
774
|
final Boolean finished =
|
|
752
|
-
this.finishDownload(
|
|
775
|
+
this.finishDownload(
|
|
776
|
+
id,
|
|
777
|
+
dest,
|
|
778
|
+
version,
|
|
779
|
+
sessionKey,
|
|
780
|
+
checksum,
|
|
781
|
+
false,
|
|
782
|
+
false
|
|
783
|
+
);
|
|
753
784
|
final BundleStatus status = finished
|
|
754
785
|
? BundleStatus.PENDING
|
|
755
786
|
: BundleStatus.ERROR;
|
|
@@ -42,6 +42,7 @@ import java.util.UUID;
|
|
|
42
42
|
import java.util.concurrent.Phaser;
|
|
43
43
|
import java.util.concurrent.TimeUnit;
|
|
44
44
|
import java.util.concurrent.TimeoutException;
|
|
45
|
+
import org.json.JSONArray;
|
|
45
46
|
import org.json.JSONException;
|
|
46
47
|
|
|
47
48
|
@CapacitorPlugin(name = "CapacitorUpdater")
|
|
@@ -53,7 +54,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
53
54
|
private static final String channelUrlDefault =
|
|
54
55
|
"https://api.capgo.app/channel_self";
|
|
55
56
|
|
|
56
|
-
private final String PLUGIN_VERSION = "6.
|
|
57
|
+
private final String PLUGIN_VERSION = "6.3.3";
|
|
57
58
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
58
59
|
|
|
59
60
|
private SharedPreferences.Editor editor;
|
|
@@ -79,6 +80,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
79
80
|
// private static final CountDownLatch semaphoreReady = new CountDownLatch(1);
|
|
80
81
|
private static final Phaser semaphoreReady = new Phaser(1);
|
|
81
82
|
|
|
83
|
+
private int lastNotifiedStatPercent = 0;
|
|
84
|
+
|
|
82
85
|
public Thread startNewThread(final Runnable function, Number waitTime) {
|
|
83
86
|
Thread bgTask = new Thread(() -> {
|
|
84
87
|
try {
|
|
@@ -351,6 +354,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
351
354
|
final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
|
|
352
355
|
ret.put("bundle", bundleInfo.toJSON());
|
|
353
356
|
this.notifyListeners("download", ret);
|
|
357
|
+
|
|
354
358
|
if (percent == 100) {
|
|
355
359
|
final JSObject retDownloadComplete = new JSObject(
|
|
356
360
|
ret,
|
|
@@ -361,11 +365,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
361
365
|
"download_complete",
|
|
362
366
|
bundleInfo.getVersionName()
|
|
363
367
|
);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
368
|
+
lastNotifiedStatPercent = 100;
|
|
369
|
+
} else {
|
|
370
|
+
int currentStatPercent = (percent / 10) * 10; // Round down to nearest 10
|
|
371
|
+
if (currentStatPercent > lastNotifiedStatPercent) {
|
|
372
|
+
this.implementation.sendStats(
|
|
373
|
+
"download_" + currentStatPercent,
|
|
374
|
+
bundleInfo.getVersionName()
|
|
375
|
+
);
|
|
376
|
+
lastNotifiedStatPercent = currentStatPercent;
|
|
377
|
+
}
|
|
369
378
|
}
|
|
370
379
|
} catch (final Exception e) {
|
|
371
380
|
Log.e(CapacitorUpdater.TAG, "Could not notify listeners", e);
|
|
@@ -508,7 +517,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
508
517
|
}
|
|
509
518
|
call.resolve(res);
|
|
510
519
|
}
|
|
511
|
-
})
|
|
520
|
+
})
|
|
521
|
+
);
|
|
512
522
|
} catch (final Exception e) {
|
|
513
523
|
Log.e(CapacitorUpdater.TAG, "Failed to unsetChannel: ", e);
|
|
514
524
|
call.reject("Failed to unsetChannel: ", e);
|
|
@@ -550,7 +560,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
550
560
|
}
|
|
551
561
|
call.resolve(res);
|
|
552
562
|
}
|
|
553
|
-
})
|
|
563
|
+
})
|
|
564
|
+
);
|
|
554
565
|
} catch (final Exception e) {
|
|
555
566
|
Log.e(CapacitorUpdater.TAG, "Failed to setChannel: " + channel, e);
|
|
556
567
|
call.reject("Failed to setChannel: " + channel, e);
|
|
@@ -568,7 +579,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
568
579
|
} else {
|
|
569
580
|
call.resolve(res);
|
|
570
581
|
}
|
|
571
|
-
})
|
|
582
|
+
})
|
|
583
|
+
);
|
|
572
584
|
} catch (final Exception e) {
|
|
573
585
|
Log.e(CapacitorUpdater.TAG, "Failed to getChannel", e);
|
|
574
586
|
call.reject("Failed to getChannel", e);
|
|
@@ -787,7 +799,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
787
799
|
}
|
|
788
800
|
call.resolve(ret);
|
|
789
801
|
}
|
|
790
|
-
)
|
|
802
|
+
)
|
|
803
|
+
);
|
|
791
804
|
}
|
|
792
805
|
|
|
793
806
|
private boolean _reset(final Boolean toLastSuccessful) {
|
|
@@ -1298,12 +1311,27 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1298
1311
|
final String checksum = res.has("checksum")
|
|
1299
1312
|
? res.getString("checksum")
|
|
1300
1313
|
: "";
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1314
|
+
|
|
1315
|
+
if (res.has("manifest")) {
|
|
1316
|
+
// Handle manifest-based download
|
|
1317
|
+
JSONArray manifest = res.getJSONArray("manifest");
|
|
1318
|
+
CapacitorUpdaterPlugin.this.implementation.downloadBackground(
|
|
1319
|
+
url,
|
|
1320
|
+
latestVersionName,
|
|
1321
|
+
sessionKey,
|
|
1322
|
+
checksum,
|
|
1323
|
+
manifest
|
|
1324
|
+
);
|
|
1325
|
+
} else {
|
|
1326
|
+
// Handle single file download (existing code)
|
|
1327
|
+
CapacitorUpdaterPlugin.this.implementation.downloadBackground(
|
|
1328
|
+
url,
|
|
1329
|
+
latestVersionName,
|
|
1330
|
+
sessionKey,
|
|
1331
|
+
checksum,
|
|
1332
|
+
null
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1307
1335
|
} catch (final Exception e) {
|
|
1308
1336
|
Log.e(CapacitorUpdater.TAG, "error downloading file", e);
|
|
1309
1337
|
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
@@ -7,12 +7,30 @@ package ee.forgr.capacitor_updater;
|
|
|
7
7
|
|
|
8
8
|
import android.app.IntentService;
|
|
9
9
|
import android.content.Intent;
|
|
10
|
+
import android.util.Log;
|
|
11
|
+
import com.android.volley.DefaultRetryPolicy;
|
|
12
|
+
import com.android.volley.NetworkResponse;
|
|
13
|
+
import com.android.volley.Request;
|
|
14
|
+
import com.android.volley.Response;
|
|
15
|
+
import com.android.volley.toolbox.HttpHeaderParser;
|
|
16
|
+
import com.android.volley.toolbox.Volley;
|
|
10
17
|
import java.io.*;
|
|
18
|
+
import java.io.FileInputStream;
|
|
11
19
|
import java.net.HttpURLConnection;
|
|
12
20
|
import java.net.URL;
|
|
13
|
-
import java.net.URLConnection;
|
|
14
21
|
import java.nio.channels.FileChannel;
|
|
15
|
-
import java.
|
|
22
|
+
import java.security.MessageDigest;
|
|
23
|
+
import java.util.ArrayList;
|
|
24
|
+
import java.util.List;
|
|
25
|
+
import java.util.concurrent.CompletableFuture;
|
|
26
|
+
import java.util.concurrent.ExecutorService;
|
|
27
|
+
import java.util.concurrent.Executors;
|
|
28
|
+
import java.util.concurrent.Future;
|
|
29
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
30
|
+
import java.util.concurrent.atomic.AtomicLong;
|
|
31
|
+
import org.brotli.dec.BrotliInputStream;
|
|
32
|
+
import org.json.JSONArray;
|
|
33
|
+
import org.json.JSONObject;
|
|
16
34
|
|
|
17
35
|
public class DownloadService extends IntentService {
|
|
18
36
|
|
|
@@ -27,6 +45,8 @@ public class DownloadService extends IntentService {
|
|
|
27
45
|
public static final String CHECKSUM = "checksum";
|
|
28
46
|
public static final String NOTIFICATION = "service receiver";
|
|
29
47
|
public static final String PERCENTDOWNLOAD = "percent receiver";
|
|
48
|
+
public static final String IS_MANIFEST = "is_manifest";
|
|
49
|
+
public static final String MANIFEST = "manifest";
|
|
30
50
|
private static final String UPDATE_FILE = "update.dat";
|
|
31
51
|
|
|
32
52
|
public DownloadService() {
|
|
@@ -53,7 +73,159 @@ public class DownloadService extends IntentService {
|
|
|
53
73
|
String version = intent.getStringExtra(VERSION);
|
|
54
74
|
String sessionKey = intent.getStringExtra(SESSIONKEY);
|
|
55
75
|
String checksum = intent.getStringExtra(CHECKSUM);
|
|
76
|
+
String manifestString = intent.getStringExtra(MANIFEST);
|
|
56
77
|
|
|
78
|
+
Log.d("DownloadService", "onHandleIntent" + manifestString);
|
|
79
|
+
if (manifestString != null) {
|
|
80
|
+
handleManifestDownload(
|
|
81
|
+
id,
|
|
82
|
+
documentsDir,
|
|
83
|
+
dest,
|
|
84
|
+
version,
|
|
85
|
+
sessionKey,
|
|
86
|
+
manifestString
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
handleSingleFileDownload(
|
|
90
|
+
url,
|
|
91
|
+
id,
|
|
92
|
+
documentsDir,
|
|
93
|
+
dest,
|
|
94
|
+
version,
|
|
95
|
+
sessionKey,
|
|
96
|
+
checksum
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private void handleManifestDownload(
|
|
102
|
+
String id,
|
|
103
|
+
String documentsDir,
|
|
104
|
+
String dest,
|
|
105
|
+
String version,
|
|
106
|
+
String sessionKey,
|
|
107
|
+
String manifestString
|
|
108
|
+
) {
|
|
109
|
+
try {
|
|
110
|
+
Log.d("DownloadService", "handleManifestDownload");
|
|
111
|
+
JSONArray manifest = new JSONArray(manifestString);
|
|
112
|
+
File destFolder = new File(documentsDir, dest);
|
|
113
|
+
File cacheFolder = new File(
|
|
114
|
+
getApplicationContext().getCacheDir(),
|
|
115
|
+
"capgo_downloads"
|
|
116
|
+
);
|
|
117
|
+
// Ensure directories are created
|
|
118
|
+
if (!destFolder.exists() && !destFolder.mkdirs()) {
|
|
119
|
+
throw new IOException(
|
|
120
|
+
"Failed to create destination directory: " +
|
|
121
|
+
destFolder.getAbsolutePath()
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (!cacheFolder.exists() && !cacheFolder.mkdirs()) {
|
|
125
|
+
throw new IOException(
|
|
126
|
+
"Failed to create cache directory: " + cacheFolder.getAbsolutePath()
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
int totalFiles = manifest.length();
|
|
131
|
+
final AtomicLong completedFiles = new AtomicLong(0);
|
|
132
|
+
final AtomicBoolean hasError = new AtomicBoolean(false);
|
|
133
|
+
|
|
134
|
+
// Use more threads for I/O-bound operations
|
|
135
|
+
int threadCount = Math.min(
|
|
136
|
+
Runtime.getRuntime().availableProcessors() * 2,
|
|
137
|
+
32
|
|
138
|
+
);
|
|
139
|
+
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
|
140
|
+
CompletableFuture<Void>[] futures = new CompletableFuture[totalFiles];
|
|
141
|
+
|
|
142
|
+
for (int i = 0; i < totalFiles; i++) {
|
|
143
|
+
JSONObject entry = manifest.getJSONObject(i);
|
|
144
|
+
String fileName = entry.getString("file_name");
|
|
145
|
+
String fileHash = entry.getString("file_hash");
|
|
146
|
+
String downloadUrl = entry.getString("download_url");
|
|
147
|
+
|
|
148
|
+
File targetFile = new File(destFolder, fileName);
|
|
149
|
+
File cacheFile = new File(
|
|
150
|
+
cacheFolder,
|
|
151
|
+
fileHash + "_" + new File(fileName).getName()
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Ensure parent directories of the target file exist
|
|
155
|
+
if (
|
|
156
|
+
!targetFile.getParentFile().exists() &&
|
|
157
|
+
!targetFile.getParentFile().mkdirs()
|
|
158
|
+
) {
|
|
159
|
+
throw new IOException(
|
|
160
|
+
"Failed to create parent directory for: " +
|
|
161
|
+
targetFile.getAbsolutePath()
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
futures[i] = CompletableFuture.runAsync(
|
|
166
|
+
() -> {
|
|
167
|
+
try {
|
|
168
|
+
if (cacheFile.exists()) {
|
|
169
|
+
if (verifyChecksum(cacheFile, fileHash)) {
|
|
170
|
+
copyFile(cacheFile, targetFile);
|
|
171
|
+
Log.d("DownloadService", "already cached " + fileName);
|
|
172
|
+
} else {
|
|
173
|
+
cacheFile.delete();
|
|
174
|
+
downloadAndVerify(
|
|
175
|
+
downloadUrl,
|
|
176
|
+
targetFile,
|
|
177
|
+
cacheFile,
|
|
178
|
+
fileHash,
|
|
179
|
+
id
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
downloadAndVerify(
|
|
184
|
+
downloadUrl,
|
|
185
|
+
targetFile,
|
|
186
|
+
cacheFile,
|
|
187
|
+
fileHash,
|
|
188
|
+
id
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
long completed = completedFiles.incrementAndGet();
|
|
193
|
+
int percent = calcTotalPercent(completed, totalFiles);
|
|
194
|
+
notifyDownload(id, percent);
|
|
195
|
+
} catch (Exception e) {
|
|
196
|
+
Log.e("DownloadService", "Error processing file: " + fileName, e);
|
|
197
|
+
hasError.set(true);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
executor
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Wait for all downloads to complete
|
|
205
|
+
CompletableFuture.allOf(futures).join();
|
|
206
|
+
|
|
207
|
+
executor.shutdown();
|
|
208
|
+
|
|
209
|
+
if (hasError.get()) {
|
|
210
|
+
throw new IOException("One or more files failed to download");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
publishResults(dest, id, version, "", sessionKey, "", true);
|
|
214
|
+
} catch (Exception e) {
|
|
215
|
+
Log.e("DownloadService", "Error in handleManifestDownload", e);
|
|
216
|
+
publishResults("", id, version, "", sessionKey, e.getMessage(), true);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private void handleSingleFileDownload(
|
|
221
|
+
String url,
|
|
222
|
+
String id,
|
|
223
|
+
String documentsDir,
|
|
224
|
+
String dest,
|
|
225
|
+
String version,
|
|
226
|
+
String sessionKey,
|
|
227
|
+
String checksum
|
|
228
|
+
) {
|
|
57
229
|
File target = new File(documentsDir, dest);
|
|
58
230
|
File infoFile = new File(documentsDir, UPDATE_FILE); // The file where the download progress (how much byte
|
|
59
231
|
// downloaded) is stored
|
|
@@ -138,14 +310,22 @@ public class DownloadService extends IntentService {
|
|
|
138
310
|
// Rename the temp file with the final name (dest)
|
|
139
311
|
tempFile.renameTo(new File(documentsDir, dest));
|
|
140
312
|
infoFile.delete();
|
|
141
|
-
publishResults(dest, id, version, checksum, sessionKey, "");
|
|
313
|
+
publishResults(dest, id, version, checksum, sessionKey, "", false);
|
|
142
314
|
} else {
|
|
143
315
|
infoFile.delete();
|
|
144
316
|
}
|
|
145
317
|
httpConn.disconnect();
|
|
146
318
|
} catch (OutOfMemoryError e) {
|
|
147
319
|
e.printStackTrace();
|
|
148
|
-
publishResults(
|
|
320
|
+
publishResults(
|
|
321
|
+
"",
|
|
322
|
+
id,
|
|
323
|
+
version,
|
|
324
|
+
checksum,
|
|
325
|
+
sessionKey,
|
|
326
|
+
"low_mem_fail",
|
|
327
|
+
false
|
|
328
|
+
);
|
|
149
329
|
} catch (Exception e) {
|
|
150
330
|
e.printStackTrace();
|
|
151
331
|
publishResults(
|
|
@@ -154,7 +334,8 @@ public class DownloadService extends IntentService {
|
|
|
154
334
|
version,
|
|
155
335
|
checksum,
|
|
156
336
|
sessionKey,
|
|
157
|
-
e.getLocalizedMessage()
|
|
337
|
+
e.getLocalizedMessage(),
|
|
338
|
+
false
|
|
158
339
|
);
|
|
159
340
|
}
|
|
160
341
|
}
|
|
@@ -186,7 +367,8 @@ public class DownloadService extends IntentService {
|
|
|
186
367
|
String version,
|
|
187
368
|
String checksum,
|
|
188
369
|
String sessionKey,
|
|
189
|
-
String error
|
|
370
|
+
String error,
|
|
371
|
+
boolean isManifest
|
|
190
372
|
) {
|
|
191
373
|
Intent intent = new Intent(NOTIFICATION);
|
|
192
374
|
intent.setPackage(getPackageName());
|
|
@@ -198,6 +380,143 @@ public class DownloadService extends IntentService {
|
|
|
198
380
|
intent.putExtra(VERSION, version);
|
|
199
381
|
intent.putExtra(SESSIONKEY, sessionKey);
|
|
200
382
|
intent.putExtra(CHECKSUM, checksum);
|
|
383
|
+
intent.putExtra(IS_MANIFEST, isManifest);
|
|
201
384
|
sendBroadcast(intent);
|
|
202
385
|
}
|
|
386
|
+
|
|
387
|
+
// Helper methods
|
|
388
|
+
|
|
389
|
+
private void copyFile(File source, File dest) throws IOException {
|
|
390
|
+
try (
|
|
391
|
+
FileInputStream inStream = new FileInputStream(source);
|
|
392
|
+
FileOutputStream outStream = new FileOutputStream(dest);
|
|
393
|
+
FileChannel inChannel = inStream.getChannel();
|
|
394
|
+
FileChannel outChannel = outStream.getChannel()
|
|
395
|
+
) {
|
|
396
|
+
inChannel.transferTo(0, inChannel.size(), outChannel);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private void downloadAndVerify(
|
|
401
|
+
String downloadUrl,
|
|
402
|
+
File targetFile,
|
|
403
|
+
File cacheFile,
|
|
404
|
+
String expectedHash,
|
|
405
|
+
String id
|
|
406
|
+
) throws Exception {
|
|
407
|
+
Log.d("DownloadService", "downloadAndVerify " + downloadUrl);
|
|
408
|
+
URL url = new URL(downloadUrl);
|
|
409
|
+
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
410
|
+
connection.setRequestMethod("GET");
|
|
411
|
+
|
|
412
|
+
// Create a temporary file for the compressed data
|
|
413
|
+
File compressedFile = new File(
|
|
414
|
+
getApplicationContext().getCacheDir(),
|
|
415
|
+
"temp_" + targetFile.getName() + ".br"
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
try (
|
|
419
|
+
InputStream inputStream = connection.getInputStream();
|
|
420
|
+
FileOutputStream compressedFos = new FileOutputStream(compressedFile)
|
|
421
|
+
) {
|
|
422
|
+
byte[] buffer = new byte[8192];
|
|
423
|
+
int bytesRead;
|
|
424
|
+
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
425
|
+
compressedFos.write(buffer, 0, bytesRead);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Decompress the file
|
|
430
|
+
try (
|
|
431
|
+
FileInputStream fis = new FileInputStream(compressedFile);
|
|
432
|
+
BrotliInputStream brotliInputStream = new BrotliInputStream(fis);
|
|
433
|
+
FileOutputStream fos = new FileOutputStream(targetFile)
|
|
434
|
+
) {
|
|
435
|
+
byte[] buffer = new byte[8192];
|
|
436
|
+
int len;
|
|
437
|
+
while ((len = brotliInputStream.read(buffer)) != -1) {
|
|
438
|
+
fos.write(buffer, 0, len);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Delete the compressed file
|
|
443
|
+
compressedFile.delete();
|
|
444
|
+
|
|
445
|
+
// Verify checksum
|
|
446
|
+
String actualHash = calculateFileHash(targetFile);
|
|
447
|
+
if (actualHash.equals(expectedHash)) {
|
|
448
|
+
// Copy the downloaded file to cache if checksum is correct
|
|
449
|
+
copyFile(targetFile, cacheFile);
|
|
450
|
+
Log.d("DownloadService", "copied to cache " + targetFile.getName());
|
|
451
|
+
} else {
|
|
452
|
+
targetFile.delete();
|
|
453
|
+
throw new IOException(
|
|
454
|
+
"Checksum verification failed for " +
|
|
455
|
+
targetFile.getName() +
|
|
456
|
+
" " +
|
|
457
|
+
expectedHash +
|
|
458
|
+
" " +
|
|
459
|
+
actualHash
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Custom request for handling input stream
|
|
465
|
+
private class InputStreamVolleyRequest extends Request<byte[]> {
|
|
466
|
+
|
|
467
|
+
private final Response.Listener<byte[]> mListener;
|
|
468
|
+
|
|
469
|
+
public InputStreamVolleyRequest(
|
|
470
|
+
int method,
|
|
471
|
+
String mUrl,
|
|
472
|
+
Response.Listener<byte[]> listener,
|
|
473
|
+
Response.ErrorListener errorListener
|
|
474
|
+
) {
|
|
475
|
+
super(method, mUrl, errorListener);
|
|
476
|
+
mListener = listener;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
@Override
|
|
480
|
+
protected void deliverResponse(byte[] response) {
|
|
481
|
+
mListener.onResponse(response);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@Override
|
|
485
|
+
protected Response<byte[]> parseNetworkResponse(NetworkResponse response) {
|
|
486
|
+
byte[] responseData = response.data;
|
|
487
|
+
return Response.success(
|
|
488
|
+
responseData,
|
|
489
|
+
HttpHeaderParser.parseCacheHeaders(response)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private boolean verifyChecksum(File file, String expectedHash) {
|
|
495
|
+
try {
|
|
496
|
+
String actualHash = calculateFileHash(file);
|
|
497
|
+
return actualHash.equals(expectedHash);
|
|
498
|
+
} catch (Exception e) {
|
|
499
|
+
e.printStackTrace();
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private String calculateFileHash(File file) throws Exception {
|
|
505
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
506
|
+
FileInputStream fis = new FileInputStream(file);
|
|
507
|
+
byte[] byteArray = new byte[1024];
|
|
508
|
+
int bytesCount = 0;
|
|
509
|
+
|
|
510
|
+
while ((bytesCount = fis.read(byteArray)) != -1) {
|
|
511
|
+
digest.update(byteArray, 0, bytesCount);
|
|
512
|
+
}
|
|
513
|
+
fis.close();
|
|
514
|
+
|
|
515
|
+
byte[] bytes = digest.digest();
|
|
516
|
+
StringBuilder sb = new StringBuilder();
|
|
517
|
+
for (byte aByte : bytes) {
|
|
518
|
+
sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
|
|
519
|
+
}
|
|
520
|
+
return sb.toString();
|
|
521
|
+
}
|
|
203
522
|
}
|
|
@@ -9,6 +9,7 @@ import SSZipArchive
|
|
|
9
9
|
import Alamofire
|
|
10
10
|
import zlib
|
|
11
11
|
import CryptoKit
|
|
12
|
+
import Compression
|
|
12
13
|
|
|
13
14
|
extension Collection {
|
|
14
15
|
subscript(safe index: Index) -> Element? {
|
|
@@ -88,6 +89,13 @@ struct InfoObject: Codable {
|
|
|
88
89
|
var channel: String?
|
|
89
90
|
var defaultChannel: String?
|
|
90
91
|
}
|
|
92
|
+
|
|
93
|
+
public struct ManifestEntry: Codable {
|
|
94
|
+
let file_name: String?
|
|
95
|
+
let file_hash: String?
|
|
96
|
+
let download_url: String?
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
struct AppVersionDec: Decodable {
|
|
92
100
|
let version: String?
|
|
93
101
|
let checksum: String?
|
|
@@ -97,7 +105,9 @@ struct AppVersionDec: Decodable {
|
|
|
97
105
|
let session_key: String?
|
|
98
106
|
let major: Bool?
|
|
99
107
|
let data: [String: String]?
|
|
108
|
+
let manifest: [ManifestEntry]?
|
|
100
109
|
}
|
|
110
|
+
|
|
101
111
|
public class AppVersion: NSObject {
|
|
102
112
|
var version: String = ""
|
|
103
113
|
var checksum: String = ""
|
|
@@ -107,6 +117,7 @@ public class AppVersion: NSObject {
|
|
|
107
117
|
var sessionKey: String?
|
|
108
118
|
var major: Bool?
|
|
109
119
|
var data: [String: String]?
|
|
120
|
+
var manifest: [ManifestEntry]?
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
extension AppVersion {
|
|
@@ -246,6 +257,9 @@ extension CustomError: LocalizedError {
|
|
|
246
257
|
private let NEXT_VERSION: String = "nextVersion"
|
|
247
258
|
private var unzipPercent = 0
|
|
248
259
|
|
|
260
|
+
// Add this line to declare cacheFolder
|
|
261
|
+
private let cacheFolder: URL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("capgo_downloads")
|
|
262
|
+
|
|
249
263
|
public let TAG: String = "✨ Capacitor-updater:"
|
|
250
264
|
public let CAP_SERVER_PATH: String = "serverBasePath"
|
|
251
265
|
public var versionBuild: String = ""
|
|
@@ -582,6 +596,9 @@ extension CustomError: LocalizedError {
|
|
|
582
596
|
if let data = response.value?.data {
|
|
583
597
|
latest.data = data
|
|
584
598
|
}
|
|
599
|
+
if let manifest = response.value?.manifest {
|
|
600
|
+
latest.manifest = manifest
|
|
601
|
+
}
|
|
585
602
|
case let .failure(error):
|
|
586
603
|
print("\(self.TAG) Error getting Latest", response.value ?? "", error )
|
|
587
604
|
latest.message = "Error getting Latest \(String(describing: response.value))"
|
|
@@ -685,6 +702,174 @@ extension CustomError: LocalizedError {
|
|
|
685
702
|
}
|
|
686
703
|
}
|
|
687
704
|
|
|
705
|
+
public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String) throws -> BundleInfo {
|
|
706
|
+
let id = self.randomString(length: 10)
|
|
707
|
+
print("\(self.TAG) downloadManifest start \(id)")
|
|
708
|
+
let destFolder = self.getBundleDirectory(id: id)
|
|
709
|
+
|
|
710
|
+
try FileManager.default.createDirectory(at: cacheFolder, withIntermediateDirectories: true, attributes: nil)
|
|
711
|
+
try FileManager.default.createDirectory(at: destFolder, withIntermediateDirectories: true, attributes: nil)
|
|
712
|
+
|
|
713
|
+
// Create and save BundleInfo before starting the download process
|
|
714
|
+
let bundleInfo = BundleInfo(id: id, version: version, status: BundleStatus.DOWNLOADING, downloaded: Date(), checksum: "")
|
|
715
|
+
self.saveBundleInfo(id: id, bundle: bundleInfo)
|
|
716
|
+
|
|
717
|
+
// Notify the start of the download process
|
|
718
|
+
self.notifyDownload(id: id, percent: 0, ignoreMultipleOfTen: true)
|
|
719
|
+
|
|
720
|
+
let dispatchGroup = DispatchGroup()
|
|
721
|
+
var downloadError: Error?
|
|
722
|
+
|
|
723
|
+
let totalFiles = manifest.count
|
|
724
|
+
var completedFiles = 0
|
|
725
|
+
|
|
726
|
+
for entry in manifest {
|
|
727
|
+
guard let fileName = entry.file_name,
|
|
728
|
+
let fileHash = entry.file_hash,
|
|
729
|
+
let downloadUrl = entry.download_url else {
|
|
730
|
+
continue
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
let fileNameWithoutPath = (fileName as NSString).lastPathComponent
|
|
734
|
+
let cacheFileName = "\(fileHash)_\(fileNameWithoutPath)"
|
|
735
|
+
let cacheFilePath = cacheFolder.appendingPathComponent(cacheFileName)
|
|
736
|
+
let destFilePath = destFolder.appendingPathComponent(fileName)
|
|
737
|
+
|
|
738
|
+
// Create necessary subdirectories in the destination folder
|
|
739
|
+
try FileManager.default.createDirectory(at: destFilePath.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
|
|
740
|
+
|
|
741
|
+
dispatchGroup.enter()
|
|
742
|
+
|
|
743
|
+
if FileManager.default.fileExists(atPath: cacheFilePath.path) {
|
|
744
|
+
// File exists in cache, copy to destination
|
|
745
|
+
do {
|
|
746
|
+
try FileManager.default.copyItem(at: cacheFilePath, to: destFilePath)
|
|
747
|
+
print("\(self.TAG) downloadManifest \(fileName) copy from cache \(id)")
|
|
748
|
+
completedFiles += 1
|
|
749
|
+
self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
|
|
750
|
+
dispatchGroup.leave()
|
|
751
|
+
} catch {
|
|
752
|
+
downloadError = error
|
|
753
|
+
print("\(self.TAG) downloadManifest \(fileName) cache error \(id): \(error)")
|
|
754
|
+
dispatchGroup.leave()
|
|
755
|
+
}
|
|
756
|
+
} else {
|
|
757
|
+
// File not in cache, download, decompress, and save to both cache and destination
|
|
758
|
+
AF.download(downloadUrl).responseData { response in
|
|
759
|
+
defer { dispatchGroup.leave() }
|
|
760
|
+
|
|
761
|
+
switch response.result {
|
|
762
|
+
case .success(let data):
|
|
763
|
+
do {
|
|
764
|
+
// Decompress the Brotli data
|
|
765
|
+
guard let decompressedData = self.decompressBrotli(data: data) else {
|
|
766
|
+
throw NSError(domain: "BrotliDecompressionError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decompress Brotli data"])
|
|
767
|
+
}
|
|
768
|
+
// Save decompressed data to cache
|
|
769
|
+
try decompressedData.write(to: cacheFilePath)
|
|
770
|
+
// Save decompressed data to destination
|
|
771
|
+
try decompressedData.write(to: destFilePath)
|
|
772
|
+
|
|
773
|
+
completedFiles += 1
|
|
774
|
+
self.notifyDownload(id: id, percent: self.calcTotalPercent(percent: Int((Double(completedFiles) / Double(totalFiles)) * 100), min: 10, max: 70))
|
|
775
|
+
print("\(self.TAG) downloadManifest \(id) \(fileName) downloaded, decompressed, and cached")
|
|
776
|
+
} catch {
|
|
777
|
+
downloadError = error
|
|
778
|
+
print("\(self.TAG) downloadManifest \(id) \(fileName) error: \(error)")
|
|
779
|
+
}
|
|
780
|
+
case .failure(let error):
|
|
781
|
+
downloadError = error
|
|
782
|
+
print("\(self.TAG) downloadManifest \(id) \(fileName) download error: \(error)")
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
dispatchGroup.wait()
|
|
789
|
+
|
|
790
|
+
if let error = downloadError {
|
|
791
|
+
// Update bundle status to ERROR if download failed
|
|
792
|
+
let errorBundle = bundleInfo.setStatus(status: BundleStatus.ERROR.localizedString)
|
|
793
|
+
self.saveBundleInfo(id: id, bundle: errorBundle)
|
|
794
|
+
throw error
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Update bundle status to PENDING after successful download
|
|
798
|
+
let updatedBundle = bundleInfo.setStatus(status: BundleStatus.PENDING.localizedString)
|
|
799
|
+
self.saveBundleInfo(id: id, bundle: updatedBundle)
|
|
800
|
+
|
|
801
|
+
print("\(self.TAG) downloadManifest done \(id)")
|
|
802
|
+
return updatedBundle
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
private func decompressBrotli(data: Data) -> Data? {
|
|
806
|
+
let outputBufferSize = 65536
|
|
807
|
+
var outputBuffer = [UInt8](repeating: 0, count: outputBufferSize)
|
|
808
|
+
var decompressedData = Data()
|
|
809
|
+
|
|
810
|
+
let streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
|
|
811
|
+
var status = compression_stream_init(streamPointer, COMPRESSION_STREAM_DECODE, COMPRESSION_BROTLI)
|
|
812
|
+
guard status != COMPRESSION_STATUS_ERROR else {
|
|
813
|
+
print("\(self.TAG) Unable to initialize the decompression stream.")
|
|
814
|
+
return nil
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
defer {
|
|
818
|
+
compression_stream_destroy(streamPointer)
|
|
819
|
+
streamPointer.deallocate()
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
streamPointer.pointee.src_size = 0
|
|
823
|
+
streamPointer.pointee.dst_ptr = UnsafeMutablePointer<UInt8>(&outputBuffer)
|
|
824
|
+
streamPointer.pointee.dst_size = outputBufferSize
|
|
825
|
+
|
|
826
|
+
let input = data
|
|
827
|
+
|
|
828
|
+
while true {
|
|
829
|
+
if streamPointer.pointee.src_size == 0 {
|
|
830
|
+
streamPointer.pointee.src_size = input.count
|
|
831
|
+
input.withUnsafeBytes { rawBufferPointer in
|
|
832
|
+
if let baseAddress = rawBufferPointer.baseAddress {
|
|
833
|
+
streamPointer.pointee.src_ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
|
|
834
|
+
} else {
|
|
835
|
+
print("\(self.TAG) Error: Unable to get base address of input data")
|
|
836
|
+
status = COMPRESSION_STATUS_ERROR
|
|
837
|
+
return
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if status == COMPRESSION_STATUS_ERROR {
|
|
843
|
+
break
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
status = compression_stream_process(streamPointer, 0)
|
|
847
|
+
|
|
848
|
+
let have = outputBufferSize - streamPointer.pointee.dst_size
|
|
849
|
+
if have > 0 {
|
|
850
|
+
decompressedData.append(outputBuffer, count: have)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if status == COMPRESSION_STATUS_END {
|
|
854
|
+
break
|
|
855
|
+
} else if status == COMPRESSION_STATUS_ERROR {
|
|
856
|
+
print("\(self.TAG) Error during Brotli decompression")
|
|
857
|
+
return nil
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if streamPointer.pointee.dst_size == 0 {
|
|
861
|
+
streamPointer.pointee.dst_ptr = UnsafeMutablePointer<UInt8>(&outputBuffer)
|
|
862
|
+
streamPointer.pointee.dst_size = outputBufferSize
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if input.count == 0 {
|
|
866
|
+
break
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return status == COMPRESSION_STATUS_END ? decompressedData : nil
|
|
871
|
+
}
|
|
872
|
+
|
|
688
873
|
public func download(url: URL, version: String, sessionKey: String) throws -> BundleInfo {
|
|
689
874
|
let id: String = self.randomString(length: 10)
|
|
690
875
|
let semaphore = DispatchSemaphore(value: 0)
|
|
@@ -1170,7 +1355,7 @@ extension CustomError: LocalizedError {
|
|
|
1170
1355
|
}
|
|
1171
1356
|
semaphore.signal()
|
|
1172
1357
|
}
|
|
1173
|
-
semaphore.
|
|
1358
|
+
semaphore.signal()
|
|
1174
1359
|
}
|
|
1175
1360
|
operationQueue.addOperation(operation)
|
|
1176
1361
|
|
|
@@ -14,8 +14,8 @@ import Version
|
|
|
14
14
|
*/
|
|
15
15
|
@objc(CapacitorUpdaterPlugin)
|
|
16
16
|
public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
17
|
-
public let identifier = "CapacitorUpdaterPlugin"
|
|
18
|
-
public let jsName = "CapacitorUpdater"
|
|
17
|
+
public let identifier = "CapacitorUpdaterPlugin"
|
|
18
|
+
public let jsName = "CapacitorUpdater"
|
|
19
19
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
20
20
|
CAPPluginMethod(name: "download", returnType: CAPPluginReturnPromise),
|
|
21
21
|
CAPPluginMethod(name: "setUpdateUrl", returnType: CAPPluginReturnPromise),
|
|
@@ -40,10 +40,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
40
40
|
CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise),
|
|
41
41
|
CAPPluginMethod(name: "next", returnType: CAPPluginReturnPromise),
|
|
42
42
|
CAPPluginMethod(name: "isAutoUpdateEnabled", returnType: CAPPluginReturnPromise),
|
|
43
|
-
CAPPluginMethod(name: "getBuiltinVersion", returnType: CAPPluginReturnPromise)
|
|
44
|
-
]
|
|
43
|
+
CAPPluginMethod(name: "getBuiltinVersion", returnType: CAPPluginReturnPromise)
|
|
44
|
+
]
|
|
45
45
|
public var implementation = CapacitorUpdater()
|
|
46
|
-
private let PLUGIN_VERSION: String = "6.
|
|
46
|
+
private let PLUGIN_VERSION: String = "6.3.3"
|
|
47
47
|
static let updateUrlDefault = "https://api.capgo.app/updates"
|
|
48
48
|
static let statsUrlDefault = "https://api.capgo.app/stats"
|
|
49
49
|
static let channelUrlDefault = "https://api.capgo.app/channel_self"
|
|
@@ -757,7 +757,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
757
757
|
print("\(self.implementation.TAG) Failed to delete failed bundle: \(nextImpl!.toString())")
|
|
758
758
|
}
|
|
759
759
|
}
|
|
760
|
-
|
|
760
|
+
if res.manifest != nil {
|
|
761
|
+
nextImpl = try self.implementation.downloadManifest(manifest: res.manifest!, version: latestVersionName, sessionKey: sessionKey)
|
|
762
|
+
} else {
|
|
763
|
+
nextImpl = try self.implementation.download(url: downloadUrl, version: latestVersionName, sessionKey: sessionKey)
|
|
764
|
+
}
|
|
761
765
|
}
|
|
762
766
|
guard let next = nextImpl else {
|
|
763
767
|
print("\(self.implementation.TAG) Error downloading file")
|
|
@@ -772,7 +776,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
772
776
|
if !self.implementation.hasOldPrivateKeyPropertyInConfig {
|
|
773
777
|
res.checksum = try self.implementation.decryptChecksum(checksum: res.checksum, version: latestVersionName)
|
|
774
778
|
}
|
|
775
|
-
if res.checksum != "" && next.getChecksum() != res.checksum {
|
|
779
|
+
if res.checksum != "" && next.getChecksum() != res.checksum && res.manifest == nil {
|
|
776
780
|
print("\(self.implementation.TAG) Error checksum", next.getChecksum(), res.checksum)
|
|
777
781
|
self.implementation.sendStats(action: "checksum_fail", versionName: next.getVersionName())
|
|
778
782
|
let id = next.getId()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-updater",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.3",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Live update for capacitor apps",
|
|
6
6
|
"main": "dist/plugin.cjs.js",
|
|
@@ -59,22 +59,22 @@
|
|
|
59
59
|
"@capacitor/android": "^6.0.0",
|
|
60
60
|
"@capacitor/cli": "^6.0.0",
|
|
61
61
|
"@capacitor/core": "^6.0.0",
|
|
62
|
-
"@capacitor/docgen": "^0.
|
|
62
|
+
"@capacitor/docgen": "^0.3.0",
|
|
63
63
|
"@capacitor/ios": "^6.0.0",
|
|
64
64
|
"@ionic/eslint-config": "^0.4.0",
|
|
65
65
|
"@ionic/prettier-config": "^4.0.0",
|
|
66
66
|
"@ionic/swiftlint-config": "^1.1.2",
|
|
67
|
-
"@types/node": "^
|
|
67
|
+
"@types/node": "^22.7.5",
|
|
68
68
|
"@typescript-eslint/eslint-plugin": "^7.11.0",
|
|
69
69
|
"@typescript-eslint/parser": "^7.11.0",
|
|
70
70
|
"eslint": "^8.57.0",
|
|
71
|
-
"eslint-plugin-import": "^2.
|
|
72
|
-
"prettier": "^3.
|
|
73
|
-
"prettier-plugin-java": "^2.6.
|
|
74
|
-
"rimraf": "^
|
|
75
|
-
"rollup": "^4.
|
|
76
|
-
"swiftlint": "^
|
|
77
|
-
"typescript": "^5.
|
|
71
|
+
"eslint-plugin-import": "^2.31.0",
|
|
72
|
+
"prettier": "^3.3.3",
|
|
73
|
+
"prettier-plugin-java": "^2.6.4",
|
|
74
|
+
"rimraf": "^6.0.1",
|
|
75
|
+
"rollup": "^4.24.0",
|
|
76
|
+
"swiftlint": "^2.0.0",
|
|
77
|
+
"typescript": "^5.6.3"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
80
|
"@capacitor/core": "^6.0.0"
|