@capgo/capacitor-updater 8.51.13 → 8.51.14
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/BundleInfo.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +26 -14
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +43 -12
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +8 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +8 -8
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +5 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +27 -7
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +18 -3
- package/package.json +1 -1
|
@@ -99,7 +99,7 @@ public class BundleInfo {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
public boolean isDownloaded() {
|
|
102
|
-
return
|
|
102
|
+
return !this.isBuiltin() && this.downloaded != null && !this.downloaded.isEmpty() && !this.isDeleted() && !this.isDeleting();
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
public String getDownloaded() {
|
|
@@ -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.14";
|
|
150
150
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
151
151
|
|
|
152
152
|
private SharedPreferences.Editor editor;
|
|
@@ -250,6 +250,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
250
250
|
private volatile long webViewPageStartedAtMs = 0;
|
|
251
251
|
private volatile boolean launchStartReported = false;
|
|
252
252
|
private volatile boolean launchReadyReported = false;
|
|
253
|
+
private volatile boolean launchTimeoutReported = false;
|
|
254
|
+
private final Object launchReportLock = new Object();
|
|
253
255
|
private FrameLayout splashscreenLoaderOverlay;
|
|
254
256
|
private Runnable splashscreenTimeoutRunnable;
|
|
255
257
|
private FrameLayout previewTransitionLoaderOverlay;
|
|
@@ -1751,16 +1753,18 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1751
1753
|
}
|
|
1752
1754
|
|
|
1753
1755
|
private void reportAppLaunchReady(final BundleInfo bundle) {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1756
|
+
synchronized (this.launchReportLock) {
|
|
1757
|
+
if (
|
|
1758
|
+
this.implementation == null ||
|
|
1759
|
+
this.implementation.statsUrl == null ||
|
|
1760
|
+
this.implementation.statsUrl.isEmpty() ||
|
|
1761
|
+
this.launchReadyReported ||
|
|
1762
|
+
this.launchTimeoutReported
|
|
1763
|
+
) {
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
this.launchReadyReported = true;
|
|
1761
1767
|
}
|
|
1762
|
-
|
|
1763
|
-
this.launchReadyReported = true;
|
|
1764
1768
|
final Map<String, String> metadata = new HashMap<>();
|
|
1765
1769
|
metadata.put("duration_ms", Long.toString(Math.max(0, System.currentTimeMillis() - this.launchStartedAtMs)));
|
|
1766
1770
|
metadata.put("launch_started_at", Long.toString(this.launchStartedAtMs));
|
|
@@ -1769,14 +1773,22 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1769
1773
|
}
|
|
1770
1774
|
|
|
1771
1775
|
private void reportAppLaunchTimeout(final BundleInfo bundle) {
|
|
1772
|
-
|
|
1773
|
-
|
|
1776
|
+
synchronized (this.launchReportLock) {
|
|
1777
|
+
if (
|
|
1778
|
+
this.implementation == null ||
|
|
1779
|
+
this.implementation.statsUrl == null ||
|
|
1780
|
+
this.implementation.statsUrl.isEmpty() ||
|
|
1781
|
+
this.launchReadyReported ||
|
|
1782
|
+
this.launchTimeoutReported
|
|
1783
|
+
) {
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
this.launchTimeoutReported = true;
|
|
1774
1787
|
}
|
|
1775
|
-
|
|
1776
1788
|
final Map<String, String> metadata = new HashMap<>();
|
|
1777
1789
|
metadata.put("duration_ms", Long.toString(Math.max(0, System.currentTimeMillis() - this.launchStartedAtMs)));
|
|
1778
1790
|
metadata.put("launch_started_at", Long.toString(this.launchStartedAtMs));
|
|
1779
|
-
metadata.put("timeout_ms", Long.toString(this.
|
|
1791
|
+
metadata.put("timeout_ms", Long.toString(this.resolveAppReadyCheckTimeoutMs()));
|
|
1780
1792
|
metadata.put("source", "app_ready_timeout");
|
|
1781
1793
|
this.implementation.sendStats("app_launch_timeout", bundle == null ? "" : bundle.getVersionName(), "", metadata);
|
|
1782
1794
|
}
|
|
@@ -2854,7 +2854,17 @@ public class CapgoUpdater {
|
|
|
2854
2854
|
|
|
2855
2855
|
public void restorePendingStats() {
|
|
2856
2856
|
File file = pendingStatsFile();
|
|
2857
|
-
if (file == null
|
|
2857
|
+
if (file == null) {
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2860
|
+
File backup = new File(file.getAbsolutePath() + ".bak");
|
|
2861
|
+
if (!file.exists() && backup.exists() && !backup.renameTo(file)) {
|
|
2862
|
+
if (logger != null) {
|
|
2863
|
+
logger.error("Failed to restore stats backup");
|
|
2864
|
+
}
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
if (!file.exists()) {
|
|
2858
2868
|
return;
|
|
2859
2869
|
}
|
|
2860
2870
|
try {
|
|
@@ -2958,18 +2968,39 @@ public class CapgoUpdater {
|
|
|
2958
2968
|
|
|
2959
2969
|
private static void writeFileAtomically(final File file, final byte[] bytes) throws IOException {
|
|
2960
2970
|
final File tmp = new File(file.getAbsolutePath() + ".tmp");
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
out
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2971
|
+
File backup = null;
|
|
2972
|
+
try {
|
|
2973
|
+
try (FileOutputStream out = new FileOutputStream(tmp)) {
|
|
2974
|
+
out.write(bytes);
|
|
2975
|
+
out.flush();
|
|
2976
|
+
}
|
|
2977
|
+
if (tmp.renameTo(file)) {
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
if (file.exists()) {
|
|
2981
|
+
backup = new File(file.getAbsolutePath() + ".bak");
|
|
2982
|
+
if (backup.exists() && !backup.delete()) {
|
|
2983
|
+
throw new IOException("Failed to replace " + file.getAbsolutePath());
|
|
2984
|
+
}
|
|
2985
|
+
if (!file.renameTo(backup)) {
|
|
2986
|
+
throw new IOException("Failed to replace " + file.getAbsolutePath());
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
if (tmp.renameTo(file)) {
|
|
2990
|
+
if (backup != null && backup.exists() && !backup.delete()) {
|
|
2991
|
+
backup.deleteOnExit();
|
|
2992
|
+
}
|
|
2993
|
+
backup = null;
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2972
2996
|
throw new IOException("Failed to persist " + file.getAbsolutePath());
|
|
2997
|
+
} finally {
|
|
2998
|
+
if (backup != null && !file.exists()) {
|
|
2999
|
+
backup.renameTo(file);
|
|
3000
|
+
}
|
|
3001
|
+
if (tmp.exists() && !tmp.delete()) {
|
|
3002
|
+
tmp.deleteOnExit();
|
|
3003
|
+
}
|
|
2973
3004
|
}
|
|
2974
3005
|
}
|
|
2975
3006
|
|
|
@@ -169,7 +169,11 @@ public class CryptoCipher {
|
|
|
169
169
|
}
|
|
170
170
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
171
171
|
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getEncoded(), "AES"), new IvParameterSpec(iv));
|
|
172
|
-
File
|
|
172
|
+
File parent = file.getAbsoluteFile().getParentFile();
|
|
173
|
+
if (parent == null) {
|
|
174
|
+
throw new IOException("Cannot create temp file for " + file.getAbsolutePath());
|
|
175
|
+
}
|
|
176
|
+
File tempFile = File.createTempFile("capgo-aes-", ".tmp", parent);
|
|
173
177
|
try {
|
|
174
178
|
byte[] inBuf = new byte[ioBufferBytes()];
|
|
175
179
|
// Reuse one output buffer. cipher.update(in) allocates a new byte[] per chunk.
|
|
@@ -187,6 +191,9 @@ public class CryptoCipher {
|
|
|
187
191
|
fos.write(outBuf, 0, last);
|
|
188
192
|
}
|
|
189
193
|
}
|
|
194
|
+
if (tempFile.length() == 0) {
|
|
195
|
+
throw new IOException("Empty decrypted data");
|
|
196
|
+
}
|
|
190
197
|
replaceFile(tempFile, file);
|
|
191
198
|
tempFile = null;
|
|
192
199
|
} finally {
|
|
@@ -76,8 +76,10 @@ public class DownloadService extends Worker {
|
|
|
76
76
|
public static final String DEFAULT_CHANNEL = "default_channel";
|
|
77
77
|
public static final String IS_PROD = "is_prod";
|
|
78
78
|
public static final String IS_EMULATOR = "is_emulator";
|
|
79
|
-
// HTTP + decode share one pool. Cap by CPU: 8 on 4 cores, 16 on 8 cores
|
|
79
|
+
// HTTP + decode share one pool. Cap per host by CPU: 8 on 4 cores, 16 on 8 cores.
|
|
80
|
+
// Keep the global cap at 64 so API calls on another host are not starved by manifest downloads.
|
|
80
81
|
private static final int MANIFEST_MAX_CONCURRENT_FILES = manifestMaxConcurrentFiles();
|
|
82
|
+
private static final int SHARED_MAX_REQUESTS = 64;
|
|
81
83
|
private static final String UPDATE_FILE = "update.dat";
|
|
82
84
|
|
|
83
85
|
// Shared OkHttpClient to prevent resource leaks
|
|
@@ -89,7 +91,7 @@ public class DownloadService extends Worker {
|
|
|
89
91
|
// Initialize shared client with User-Agent interceptor
|
|
90
92
|
static {
|
|
91
93
|
Dispatcher dispatcher = new Dispatcher();
|
|
92
|
-
dispatcher.setMaxRequests(
|
|
94
|
+
dispatcher.setMaxRequests(SHARED_MAX_REQUESTS);
|
|
93
95
|
dispatcher.setMaxRequestsPerHost(MANIFEST_MAX_CONCURRENT_FILES);
|
|
94
96
|
sharedClient = new OkHttpClient.Builder()
|
|
95
97
|
.dispatcher(dispatcher)
|
|
@@ -905,12 +907,9 @@ public class DownloadService extends Worker {
|
|
|
905
907
|
} catch (IOException e) {
|
|
906
908
|
String msg = e.getMessage();
|
|
907
909
|
if (msg != null && msg.contains("Checksum verification failed")) {
|
|
908
|
-
if (finalTargetFile.exists() && !finalTargetFile.delete()) {
|
|
909
|
-
logger.debug("Failed to delete dest after checksum mismatch");
|
|
910
|
-
}
|
|
911
910
|
sendStatsAsync("download_manifest_checksum_fail", getInputData().getString(VERSION) + ":" + finalTargetFile.getName());
|
|
912
911
|
keepPartial = false;
|
|
913
|
-
} else if (isBrotli) {
|
|
912
|
+
} else if (isBrotli && msg != null && msg.toLowerCase(java.util.Locale.US).contains("brotli")) {
|
|
914
913
|
sendStatsAsync("download_manifest_brotli_fail", getInputData().getString(VERSION) + ":" + finalTargetFile.getName());
|
|
915
914
|
keepPartial = false;
|
|
916
915
|
}
|
|
@@ -947,7 +946,8 @@ public class DownloadService extends Worker {
|
|
|
947
946
|
if (CapgoUpdater.isSafeCacheHash(hash) && hash.length() == 64) {
|
|
948
947
|
return new File(cacheDir, "partial_" + hash + "_" + token + ".tmp");
|
|
949
948
|
}
|
|
950
|
-
|
|
949
|
+
String digest = CryptoCipher.shortPathKey((hash == null ? "" : hash) + "\0" + (fileName == null ? "" : fileName));
|
|
950
|
+
return new File(cacheDir, "partial_" + digest + "_" + token + ".tmp");
|
|
951
951
|
}
|
|
952
952
|
|
|
953
953
|
static boolean shouldAppendHttpBody(int statusCode, long existingBytes) {
|
|
@@ -1062,7 +1062,7 @@ public class DownloadService extends Worker {
|
|
|
1062
1062
|
}
|
|
1063
1063
|
}
|
|
1064
1064
|
logger.error("Error: Raw data (" + fileName + "): " + hexDump);
|
|
1065
|
-
throw e;
|
|
1065
|
+
throw new IOException("Brotli process failed for " + fileName + ": " + e.getMessage(), e);
|
|
1066
1066
|
}
|
|
1067
1067
|
}
|
|
1068
1068
|
|
|
@@ -238,6 +238,9 @@ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetect
|
|
|
238
238
|
|
|
239
239
|
private void setPreviewMenuButtonsEnabled(List<Button> buttons, boolean enabled) {
|
|
240
240
|
for (Button button : buttons) {
|
|
241
|
+
if (!enabled && "Close menu".equals(button.getText().toString())) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
241
244
|
button.setEnabled(enabled);
|
|
242
245
|
}
|
|
243
246
|
}
|
|
@@ -180,6 +180,11 @@ public struct AES128Key {
|
|
|
180
180
|
}
|
|
181
181
|
try output.close()
|
|
182
182
|
|
|
183
|
+
let decryptedSize = (try fileManager.attributesOfItem(atPath: tempURL.path)[.size] as? NSNumber)?.uint64Value ?? 0
|
|
184
|
+
if decryptedSize == 0 {
|
|
185
|
+
throw NSError(domain: "Empty decrypted data", code: 7, userInfo: nil)
|
|
186
|
+
}
|
|
187
|
+
|
|
183
188
|
do {
|
|
184
189
|
_ = try fileManager.replaceItemAt(destination, withItemAt: tempURL)
|
|
185
190
|
} catch {
|
|
@@ -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.14"
|
|
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"
|
|
@@ -1096,6 +1096,9 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1096
1096
|
}
|
|
1097
1097
|
let res = self.implementation.list()
|
|
1098
1098
|
for version in res {
|
|
1099
|
+
if Thread.current.isCancelled {
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1099
1102
|
self.logger.info("Deleting obsolete bundle: \(version.getId())")
|
|
1100
1103
|
let deleted = self.implementation.delete(id: version.getId())
|
|
1101
1104
|
if !deleted {
|
|
@@ -1103,17 +1106,28 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1103
1106
|
}
|
|
1104
1107
|
Thread.sleep(forTimeInterval: 0.075)
|
|
1105
1108
|
}
|
|
1106
|
-
self.implementation.cleanupDeltaCache()
|
|
1109
|
+
self.implementation.cleanupDeltaCache(threadToCheck: Thread.current)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if Thread.current.isCancelled {
|
|
1113
|
+
return
|
|
1107
1114
|
}
|
|
1108
1115
|
|
|
1109
1116
|
// Resume any DELETING leftovers from prior kills, one-by-one.
|
|
1110
1117
|
self.implementation.drainPendingDeletes()
|
|
1111
1118
|
|
|
1119
|
+
if Thread.current.isCancelled {
|
|
1120
|
+
return
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1112
1123
|
// Always sweep orphan directories so incomplete prior cleanups (or failed deletes)
|
|
1113
1124
|
// cannot leave hundreds of MB behind across launches.
|
|
1114
1125
|
let allowedIds = self.implementation.allowedBundleIdsForCleanup()
|
|
1115
|
-
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds)
|
|
1116
|
-
|
|
1126
|
+
self.implementation.cleanupDownloadDirectories(allowedIds: allowedIds, threadToCheck: Thread.current)
|
|
1127
|
+
if Thread.current.isCancelled {
|
|
1128
|
+
return
|
|
1129
|
+
}
|
|
1130
|
+
self.implementation.cleanupOrphanedTempFolders(threadToCheck: Thread.current)
|
|
1117
1131
|
|
|
1118
1132
|
if self.defaultChannelCleanupMustRetry {
|
|
1119
1133
|
self.logger.warn("Keeping the previous native build version so default channel cleanup retries")
|
|
@@ -1131,7 +1145,13 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
1131
1145
|
}
|
|
1132
1146
|
|
|
1133
1147
|
logger.info("Waiting for cleanup to complete before starting download...")
|
|
1134
|
-
cleanupGroup.wait()
|
|
1148
|
+
let result = cleanupGroup.wait(timeout: .now() + .seconds(60))
|
|
1149
|
+
if result == .timedOut {
|
|
1150
|
+
logger.warn("Cleanup wait timed out after 60s, cancelling leftover cleanup")
|
|
1151
|
+
cleanupThread?.cancel()
|
|
1152
|
+
_ = cleanupGroup.wait(timeout: .now() + .seconds(60))
|
|
1153
|
+
return
|
|
1154
|
+
}
|
|
1135
1155
|
logger.info("Cleanup finished, proceeding with download")
|
|
1136
1156
|
}
|
|
1137
1157
|
|
|
@@ -2749,7 +2769,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2749
2769
|
guard self.previewSessionEnabled else {
|
|
2750
2770
|
return
|
|
2751
2771
|
}
|
|
2752
|
-
if let topVC = UIApplication.topViewController(),
|
|
2772
|
+
if let topVC = UIApplication.topViewController(self.bridge?.viewController),
|
|
2753
2773
|
topVC.isKind(of: UIAlertController.self) {
|
|
2754
2774
|
self.previewSessionAlertPending = true
|
|
2755
2775
|
UserDefaults.standard.set(true, forKey: self.previewSessionAlertPendingDefaultsKey)
|
|
@@ -2763,7 +2783,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
2763
2783
|
preferredStyle: .alert
|
|
2764
2784
|
)
|
|
2765
2785
|
alert.addAction(UIAlertAction(title: "Got it", style: .default))
|
|
2766
|
-
if let topVC = UIApplication.topViewController() {
|
|
2786
|
+
if let topVC = UIApplication.topViewController(self.bridge?.viewController) {
|
|
2767
2787
|
topVC.present(alert, animated: true)
|
|
2768
2788
|
} else {
|
|
2769
2789
|
self.previewSessionAlertPending = true
|
|
@@ -318,7 +318,8 @@ import UIKit
|
|
|
318
318
|
if isSafeCacheHash(hash) && hash.count == 64 {
|
|
319
319
|
return cacheFolder.appendingPathComponent("partial_\(hash)_\(token).tmp")
|
|
320
320
|
}
|
|
321
|
-
|
|
321
|
+
let digest = CryptoCipher.shortPathKey("\(hash)|\(fileName)")
|
|
322
|
+
return cacheFolder.appendingPathComponent("partial_\(digest)_\(token).tmp")
|
|
322
323
|
}
|
|
323
324
|
|
|
324
325
|
private func cleanupOldManifestPartials() {
|
|
@@ -2254,6 +2255,7 @@ import UIKit
|
|
|
2254
2255
|
if !hadRegistry && !hadFolder {
|
|
2255
2256
|
logger.error("Cannot delete unknown bundle")
|
|
2256
2257
|
logger.debug("Bundle ID: \(id)")
|
|
2258
|
+
self.dequeuePendingDelete(id: id)
|
|
2257
2259
|
return false
|
|
2258
2260
|
}
|
|
2259
2261
|
|
|
@@ -2313,6 +2315,10 @@ import UIKit
|
|
|
2313
2315
|
var pendingIds = Set(self.list(raw: true).filter { $0.isDeleting() }.map { $0.getId() }.filter { !$0.isEmpty })
|
|
2314
2316
|
pendingIds.formUnion(self.getPendingDeleteIds())
|
|
2315
2317
|
for id in pendingIds {
|
|
2318
|
+
if Thread.current.isCancelled {
|
|
2319
|
+
logger.warn("drainPendingDeletes was cancelled")
|
|
2320
|
+
return
|
|
2321
|
+
}
|
|
2316
2322
|
logger.info("Resuming pending delete for bundle: \(id)")
|
|
2317
2323
|
if self.delete(id: id, removeInfo: true) {
|
|
2318
2324
|
self.dequeuePendingDelete(id: id)
|
|
@@ -2499,11 +2505,16 @@ import UIKit
|
|
|
2499
2505
|
logger.debug("Error: \(error.localizedDescription)")
|
|
2500
2506
|
}
|
|
2501
2507
|
|
|
2508
|
+
if let thread = threadToCheck, thread.isCancelled {
|
|
2509
|
+
logger.warn("cleanupOrphanedTempFolders was cancelled")
|
|
2510
|
+
return
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2502
2513
|
// Also cleanup old download temp files (package_*.tmp and update_*.dat)
|
|
2503
|
-
cleanupOldDownloadTempFiles()
|
|
2514
|
+
cleanupOldDownloadTempFiles(threadToCheck: threadToCheck)
|
|
2504
2515
|
}
|
|
2505
2516
|
|
|
2506
|
-
private func cleanupOldDownloadTempFiles() {
|
|
2517
|
+
private func cleanupOldDownloadTempFiles(threadToCheck: Thread? = nil) {
|
|
2507
2518
|
let fileManager = FileManager.default
|
|
2508
2519
|
guard let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
|
2509
2520
|
return
|
|
@@ -2514,6 +2525,10 @@ import UIKit
|
|
|
2514
2525
|
let oneHourAgo = Date().addingTimeInterval(-3600)
|
|
2515
2526
|
|
|
2516
2527
|
for url in contents {
|
|
2528
|
+
if let thread = threadToCheck, thread.isCancelled {
|
|
2529
|
+
logger.warn("cleanupOldDownloadTempFiles was cancelled")
|
|
2530
|
+
return
|
|
2531
|
+
}
|
|
2517
2532
|
let fileName = url.lastPathComponent
|
|
2518
2533
|
// Only cleanup package_*.tmp and update_*.dat files
|
|
2519
2534
|
let isDownloadTemp = (fileName.hasPrefix("package_") && fileName.hasSuffix(".tmp")) ||
|