@capgo/capacitor-updater 8.42.2 → 8.42.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.
@@ -84,7 +84,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
84
84
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
85
85
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
86
86
 
87
- private final String pluginVersion = "8.42.2";
87
+ private final String pluginVersion = "8.42.4";
88
88
  private static final String DELAY_CONDITION_PREFERENCES = "";
89
89
 
90
90
  private SharedPreferences.Editor editor;
@@ -261,13 +261,90 @@ public class CapgoUpdater {
261
261
  }
262
262
  if (entries.length == 1 && !"index.html".equals(entries[0])) {
263
263
  final File child = new File(sourceFile, entries[0]);
264
- child.renameTo(destinationFile);
264
+ if (!child.renameTo(destinationFile)) {
265
+ throw new IOException("Failed to move bundle contents: " + child.getPath() + " -> " + destinationFile.getPath());
266
+ }
265
267
  } else {
266
- sourceFile.renameTo(destinationFile);
268
+ if (!sourceFile.renameTo(destinationFile)) {
269
+ throw new IOException("Failed to move bundle contents: " + sourceFile.getPath() + " -> " + destinationFile.getPath());
270
+ }
267
271
  }
268
272
  sourceFile.delete();
269
273
  }
270
274
 
275
+ private void cacheBundleFilesAsync(final String id) {
276
+ io.execute(() -> cacheBundleFiles(id));
277
+ }
278
+
279
+ private void cacheBundleFiles(final String id) {
280
+ if (this.activity == null) {
281
+ logger.debug("Skip delta cache population: activity is null");
282
+ return;
283
+ }
284
+
285
+ final File bundleDir = this.getBundleDirectory(id);
286
+ if (!bundleDir.exists()) {
287
+ logger.debug("Skip delta cache population: bundle dir missing");
288
+ return;
289
+ }
290
+
291
+ final File cacheDir = new File(this.activity.getCacheDir(), "capgo_downloads");
292
+ if (cacheDir.exists() && !cacheDir.isDirectory()) {
293
+ logger.debug("Skip delta cache population: cache dir is not a directory");
294
+ return;
295
+ }
296
+ if (!cacheDir.exists() && !cacheDir.mkdirs()) {
297
+ logger.debug("Skip delta cache population: failed to create cache dir");
298
+ return;
299
+ }
300
+
301
+ final List<File> files = new ArrayList<>();
302
+ collectFiles(bundleDir, files);
303
+ for (File file : files) {
304
+ final String checksum = CryptoCipher.calcChecksum(file);
305
+ if (checksum.isEmpty()) {
306
+ continue;
307
+ }
308
+ final String cacheName = checksum + "_" + file.getName();
309
+ final File cacheFile = new File(cacheDir, cacheName);
310
+ if (cacheFile.exists()) {
311
+ continue;
312
+ }
313
+ try {
314
+ copyFile(file, cacheFile);
315
+ } catch (IOException e) {
316
+ logger.debug("Delta cache copy failed: " + file.getPath());
317
+ }
318
+ }
319
+ }
320
+
321
+ private void collectFiles(final File dir, final List<File> files) {
322
+ final File[] entries = dir.listFiles();
323
+ if (entries == null) {
324
+ return;
325
+ }
326
+ for (File entry : entries) {
327
+ if (!this.filter.accept(dir, entry.getName())) {
328
+ continue;
329
+ }
330
+ if (entry.isDirectory()) {
331
+ collectFiles(entry, files);
332
+ } else if (entry.isFile()) {
333
+ files.add(entry);
334
+ }
335
+ }
336
+ }
337
+
338
+ private void copyFile(final File source, final File dest) throws IOException {
339
+ try (final FileInputStream input = new FileInputStream(source); final FileOutputStream output = new FileOutputStream(dest)) {
340
+ final byte[] buffer = new byte[1024 * 1024];
341
+ int length;
342
+ while ((length = input.read(buffer)) != -1) {
343
+ output.write(buffer, 0, length);
344
+ }
345
+ }
346
+ }
347
+
271
348
  private void observeWorkProgress(Context context, String id) {
272
349
  if (!(context instanceof LifecycleOwner)) {
273
350
  logger.error("Context is not a LifecycleOwner, cannot observe work progress");
@@ -482,6 +559,7 @@ public class CapgoUpdater {
482
559
  this.notifyDownload(id, 91);
483
560
  final String idName = bundleDirectory + "/" + id;
484
561
  this.flattenAssets(extractedDir, idName);
562
+ this.cacheBundleFilesAsync(id);
485
563
  } else {
486
564
  this.notifyDownload(id, 91);
487
565
  final String idName = bundleDirectory + "/" + id;
@@ -308,7 +308,9 @@ public class DownloadService extends Worker {
308
308
  String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
309
309
 
310
310
  File targetFile = new File(destFolder, targetFileName);
311
- File cacheFile = new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName());
311
+ String cacheBaseName = new File(isBrotli ? targetFileName : fileName).getName();
312
+ File cacheFile = new File(cacheFolder, finalFileHash + "_" + cacheBaseName);
313
+ final File legacyCacheFile = isBrotli ? new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName()) : null;
312
314
  File builtinFile = new File(builtinFolder, fileName);
313
315
 
314
316
  // Ensure parent directories of the target file exist
@@ -324,7 +326,10 @@ public class DownloadService extends Worker {
324
326
  if (builtinFile.exists() && verifyChecksum(builtinFile, finalFileHash)) {
325
327
  copyFile(builtinFile, targetFile);
326
328
  logger.debug("using builtin file " + fileName);
327
- } else if (tryCopyFromCache(cacheFile, targetFile, finalFileHash)) {
329
+ } else if (
330
+ tryCopyFromCache(cacheFile, targetFile, finalFileHash) ||
331
+ (legacyCacheFile != null && tryCopyFromCache(legacyCacheFile, targetFile, finalFileHash))
332
+ ) {
328
333
  logger.debug("already cached " + fileName);
329
334
  } else {
330
335
  downloadAndVerify(downloadUrl, targetFile, cacheFile, finalFileHash, sessionKey, publicKey, finalIsBrotli);
@@ -71,7 +71,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
71
71
  CAPPluginMethod(name: "completeFlexibleUpdate", returnType: CAPPluginReturnPromise)
72
72
  ]
73
73
  public var implementation = CapgoUpdater()
74
- private let pluginVersion: String = "8.42.2"
74
+ private let pluginVersion: String = "8.42.4"
75
75
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
76
76
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
77
77
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
@@ -383,6 +383,56 @@ import UIKit
383
383
  }
384
384
  }
385
385
 
386
+ private func populateDeltaCacheAsync(for id: String) {
387
+ DispatchQueue.global(qos: .utility).async { [weak self] in
388
+ self?.populateDeltaCache(for: id)
389
+ }
390
+ }
391
+
392
+ private func populateDeltaCache(for id: String) {
393
+ let bundleDir = self.getBundleDirectory(id: id)
394
+ let fileManager = FileManager.default
395
+
396
+ guard fileManager.fileExists(atPath: bundleDir.path) else {
397
+ logger.debug("Skip delta cache population: bundle dir missing")
398
+ return
399
+ }
400
+
401
+ do {
402
+ try fileManager.createDirectory(at: cacheFolder, withIntermediateDirectories: true, attributes: nil)
403
+ } catch {
404
+ logger.debug("Skip delta cache population: failed to create cache dir")
405
+ return
406
+ }
407
+
408
+ guard let enumerator = fileManager.enumerator(at: bundleDir, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles]) else {
409
+ return
410
+ }
411
+
412
+ for case let fileURL as URL in enumerator {
413
+ let resourceValues = try? fileURL.resourceValues(forKeys: [.isDirectoryKey])
414
+ if resourceValues?.isDirectory == true {
415
+ continue
416
+ }
417
+
418
+ let checksum = CryptoCipher.calcChecksum(filePath: fileURL)
419
+ if checksum.isEmpty {
420
+ continue
421
+ }
422
+
423
+ let cacheFile = cacheFolder.appendingPathComponent("\(checksum)_\(fileURL.lastPathComponent)")
424
+ if fileManager.fileExists(atPath: cacheFile.path) {
425
+ continue
426
+ }
427
+
428
+ do {
429
+ try fileManager.copyItem(at: fileURL, to: cacheFile)
430
+ } catch {
431
+ logger.debug("Delta cache copy failed: \(fileURL.path)")
432
+ }
433
+ }
434
+ }
435
+
386
436
  private func createInfoObject() -> InfoObject {
387
437
  return InfoObject(
388
438
  platform: "ios",
@@ -549,10 +599,11 @@ import UIKit
549
599
 
550
600
  let finalFileHash = fileHash
551
601
  let fileNameWithoutPath = (fileName as NSString).lastPathComponent
552
- let cacheFileName = "\(finalFileHash)_\(fileNameWithoutPath)"
553
- let cacheFilePath = cacheFolder.appendingPathComponent(cacheFileName)
554
-
555
602
  let isBrotli = fileName.hasSuffix(".br")
603
+ let cacheBaseName = isBrotli ? String(fileNameWithoutPath.dropLast(3)) : fileNameWithoutPath
604
+ let cacheFilePath = cacheFolder.appendingPathComponent("\(finalFileHash)_\(cacheBaseName)")
605
+ let legacyCacheFilePath: URL? = isBrotli ? cacheFolder.appendingPathComponent("\(finalFileHash)_\(fileNameWithoutPath)") : nil
606
+
556
607
  let destFileName = isBrotli ? String(fileName.dropLast(3)) : fileName
557
608
  let destFilePath = destFolder.appendingPathComponent(destFileName)
558
609
  let builtinFilePath = builtinFolder.appendingPathComponent(fileName)
@@ -571,7 +622,9 @@ import UIKit
571
622
  self.logger.info("downloadManifest \(fileName) using builtin file \(id)")
572
623
  }
573
624
  // Try cache
574
- else if self.tryCopyFromCache(from: cacheFilePath, to: destFilePath, expectedHash: finalFileHash) {
625
+ else if
626
+ self.tryCopyFromCache(from: cacheFilePath, to: destFilePath, expectedHash: finalFileHash) ||
627
+ (legacyCacheFilePath != nil && self.tryCopyFromCache(from: legacyCacheFilePath!, to: destFilePath, expectedHash: finalFileHash)) {
575
628
  self.logger.info("downloadManifest \(fileName) copy from cache \(id)")
576
629
  }
577
630
  // Download
@@ -985,6 +1038,7 @@ import UIKit
985
1038
  CryptoCipher.logChecksumInfo(label: "Calculated bundle checksum", hexChecksum: checksum)
986
1039
  logger.info("Downloading: 80% (unzipping)")
987
1040
  try self.saveDownloaded(sourceZip: finalPath, id: id, base: self.libraryDir.appendingPathComponent(self.bundleDirectory), notify: true)
1041
+ self.populateDeltaCacheAsync(for: id)
988
1042
 
989
1043
  } catch {
990
1044
  logger.error("Failed to unzip file")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "8.42.2",
3
+ "version": "8.42.4",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",