@capgo/capacitor-updater 8.51.0 → 8.51.2
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/README.md +50 -47
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +7 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +1 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +251 -85
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +289 -60
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +1 -1
- package/dist/docs.json +26 -2
- package/dist/esm/definitions.d.ts +23 -7
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +5 -1
- package/ios/Sources/CapacitorUpdaterPlugin/BundleStatus.swift +3 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +609 -69
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +187 -34
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +8 -0
- package/package.json +1 -1
|
@@ -22,8 +22,11 @@ import UIKit
|
|
|
22
22
|
private let FALLBACK_VERSION: String = "pastVersion"
|
|
23
23
|
private let NEXT_VERSION: String = "nextVersion"
|
|
24
24
|
private let PREVIEW_FALLBACK_VERSION: String = "previewFallbackVersion"
|
|
25
|
+
private let PENDING_DELETE_IDS: String = "pendingDeleteIds"
|
|
25
26
|
private var unzipPercent = 0
|
|
26
27
|
private let TEMP_UNZIP_PREFIX: String = "capgo_unzip_"
|
|
28
|
+
private let deletePaceSeconds: TimeInterval = 0.075
|
|
29
|
+
private let deleteLock = NSLock()
|
|
27
30
|
|
|
28
31
|
// Add this line to declare cacheFolder
|
|
29
32
|
private let cacheFolder: URL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("capgo_downloads")
|
|
@@ -34,6 +37,8 @@ import UIKit
|
|
|
34
37
|
public var pluginVersion: String = ""
|
|
35
38
|
public var timeout: Double = 20
|
|
36
39
|
public var statsUrl: String = ""
|
|
40
|
+
/// Optional gate run before any download touches disk (e.g. wait for launch cleanup).
|
|
41
|
+
public var beforeDownload: (() -> Void)?
|
|
37
42
|
public var channelUrl: String = ""
|
|
38
43
|
public var defaultChannel: String = ""
|
|
39
44
|
public var appId: String = ""
|
|
@@ -1052,7 +1057,12 @@ import UIKit
|
|
|
1052
1057
|
return json
|
|
1053
1058
|
}
|
|
1054
1059
|
|
|
1060
|
+
private func runBeforeDownload() {
|
|
1061
|
+
beforeDownload?()
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1055
1064
|
public func downloadManifest(manifest: [ManifestEntry], version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
1065
|
+
self.runBeforeDownload()
|
|
1056
1066
|
let id = self.randomString(length: 10)
|
|
1057
1067
|
logger.info("downloadManifest start \(id)")
|
|
1058
1068
|
let destFolder = self.getBundleDirectory(id: id)
|
|
@@ -1535,6 +1545,7 @@ import UIKit
|
|
|
1535
1545
|
}
|
|
1536
1546
|
|
|
1537
1547
|
public func download(url: URL, version: String, sessionKey: String, link: String? = nil, comment: String? = nil) throws -> BundleInfo {
|
|
1548
|
+
self.runBeforeDownload()
|
|
1538
1549
|
let id: String = self.randomString(length: 10)
|
|
1539
1550
|
// Each download uses its own temp files keyed by bundle ID to prevent collisions
|
|
1540
1551
|
if version != getLocalUpdateVersion(for: id) {
|
|
@@ -1811,6 +1822,9 @@ import UIKit
|
|
|
1811
1822
|
}
|
|
1812
1823
|
|
|
1813
1824
|
public func delete(id: String, removeInfo: Bool) -> Bool {
|
|
1825
|
+
self.deleteLock.lock()
|
|
1826
|
+
defer { self.deleteLock.unlock() }
|
|
1827
|
+
|
|
1814
1828
|
let deleted: BundleInfo = self.getBundleInfo(id: id)
|
|
1815
1829
|
if deleted.isBuiltin() || self.getCurrentBundleId() == id {
|
|
1816
1830
|
logger.info("Cannot delete current or builtin bundle")
|
|
@@ -1821,6 +1835,7 @@ import UIKit
|
|
|
1821
1835
|
if let previewFallback = self.getPreviewFallbackBundle(),
|
|
1822
1836
|
!previewFallback.isDeleted(),
|
|
1823
1837
|
!previewFallback.isErrorStatus(),
|
|
1838
|
+
!previewFallback.isDeleting(),
|
|
1824
1839
|
previewFallback.getId() == id {
|
|
1825
1840
|
logger.info("Cannot delete the preview fallback bundle")
|
|
1826
1841
|
logger.debug("Bundle ID: \(id)")
|
|
@@ -1831,6 +1846,7 @@ import UIKit
|
|
|
1831
1846
|
if let next = self.getNextBundle(),
|
|
1832
1847
|
!next.isDeleted() &&
|
|
1833
1848
|
!next.isErrorStatus() &&
|
|
1849
|
+
!next.isDeleting() &&
|
|
1834
1850
|
next.getId() == id {
|
|
1835
1851
|
logger.info("Cannot delete the next bundle")
|
|
1836
1852
|
logger.debug("Bundle ID: \(id)")
|
|
@@ -1838,24 +1854,55 @@ import UIKit
|
|
|
1838
1854
|
}
|
|
1839
1855
|
|
|
1840
1856
|
let destPersist: URL = libraryDir.appendingPathComponent(bundleDirectory).appendingPathComponent(id)
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
logger.error("
|
|
1845
|
-
logger.debug("
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1857
|
+
let hadRegistry = self.hasStoredBundleInfo(id: id)
|
|
1858
|
+
let hadFolder = FileManager.default.fileExists(atPath: destPersist.path)
|
|
1859
|
+
if !hadRegistry && !hadFolder {
|
|
1860
|
+
logger.error("Cannot delete unknown bundle")
|
|
1861
|
+
logger.debug("Bundle ID: \(id)")
|
|
1862
|
+
return false
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// Persist DELETING before touching disk so kill/OOM can resume on next launch.
|
|
1866
|
+
if !deleted.isDeleting() {
|
|
1867
|
+
if !self.saveBundleInfo(id: id, bundle: deleted.setStatus(status: BundleStatus.DELETING.storedValue)) {
|
|
1868
|
+
logger.error("Failed to persist DELETING marker, aborting disk delete")
|
|
1869
|
+
logger.debug("Bundle ID: \(id)")
|
|
1870
|
+
return false
|
|
1849
1871
|
}
|
|
1850
|
-
|
|
1872
|
+
UserDefaults.standard.synchronize()
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
if FileManager.default.fileExists(atPath: destPersist.path) {
|
|
1876
|
+
do {
|
|
1877
|
+
try FileManager.default.removeItem(atPath: destPersist.path)
|
|
1878
|
+
} catch {
|
|
1879
|
+
logger.error("Bundle folder not removed, will retry later")
|
|
1880
|
+
logger.debug("Path: \(destPersist.path), Error: \(error.localizedDescription)")
|
|
1881
|
+
return false
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Only drop registry after the folder is confirmed gone.
|
|
1886
|
+
if FileManager.default.fileExists(atPath: destPersist.path) {
|
|
1887
|
+
logger.error("Bundle folder still present after delete, will retry later")
|
|
1888
|
+
logger.debug("Bundle ID: \(id)")
|
|
1851
1889
|
return false
|
|
1852
1890
|
}
|
|
1891
|
+
|
|
1892
|
+
let finalized: Bool
|
|
1853
1893
|
if removeInfo {
|
|
1854
|
-
self.
|
|
1894
|
+
finalized = self.saveBundleInfo(id: id, bundle: nil)
|
|
1855
1895
|
} else {
|
|
1856
|
-
self.saveBundleInfo(id: id, bundle: deleted.setStatus(status: BundleStatus.DELETED.storedValue))
|
|
1896
|
+
finalized = self.saveBundleInfo(id: id, bundle: deleted.setStatus(status: BundleStatus.DELETED.storedValue))
|
|
1857
1897
|
}
|
|
1858
|
-
|
|
1898
|
+
guard finalized else {
|
|
1899
|
+
logger.error("Failed to finalize delete registry update, will retry later")
|
|
1900
|
+
logger.debug("Bundle ID: \(id)")
|
|
1901
|
+
return false
|
|
1902
|
+
}
|
|
1903
|
+
UserDefaults.standard.synchronize()
|
|
1904
|
+
self.dequeuePendingDelete(id: id)
|
|
1905
|
+
logger.info("Bundle deleted and confirmed gone")
|
|
1859
1906
|
logger.debug("Version: \(deleted.getVersionName())")
|
|
1860
1907
|
self.sendStats(action: "delete", versionName: deleted.getVersionName())
|
|
1861
1908
|
return true
|
|
@@ -1865,6 +1912,47 @@ import UIKit
|
|
|
1865
1912
|
return self.delete(id: id, removeInfo: true)
|
|
1866
1913
|
}
|
|
1867
1914
|
|
|
1915
|
+
/// Resume incomplete deletes one-by-one. Safe across app kill / OOM because
|
|
1916
|
+
/// delete() marks DELETING before disk work and only clears registry after confirm.
|
|
1917
|
+
public func drainPendingDeletes() {
|
|
1918
|
+
var pendingIds = Set(self.list(raw: true).filter { $0.isDeleting() }.map { $0.getId() }.filter { !$0.isEmpty })
|
|
1919
|
+
pendingIds.formUnion(self.getPendingDeleteIds())
|
|
1920
|
+
for id in pendingIds {
|
|
1921
|
+
logger.info("Resuming pending delete for bundle: \(id)")
|
|
1922
|
+
if self.delete(id: id, removeInfo: true) {
|
|
1923
|
+
self.dequeuePendingDelete(id: id)
|
|
1924
|
+
}
|
|
1925
|
+
Thread.sleep(forTimeInterval: self.deletePaceSeconds)
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
private func getPendingDeleteIds() -> Set<String> {
|
|
1930
|
+
guard let raw = UserDefaults.standard.string(forKey: self.PENDING_DELETE_IDS), !raw.isEmpty else {
|
|
1931
|
+
return []
|
|
1932
|
+
}
|
|
1933
|
+
return Set(raw.split(separator: ",").map { String($0) }.filter { !$0.isEmpty })
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
private func enqueuePendingDelete(id: String) {
|
|
1937
|
+
guard !id.isEmpty else { return }
|
|
1938
|
+
var ids = self.getPendingDeleteIds()
|
|
1939
|
+
guard ids.insert(id).inserted else { return }
|
|
1940
|
+
UserDefaults.standard.set(ids.sorted().joined(separator: ","), forKey: self.PENDING_DELETE_IDS)
|
|
1941
|
+
UserDefaults.standard.synchronize()
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
private func dequeuePendingDelete(id: String) {
|
|
1945
|
+
guard !id.isEmpty else { return }
|
|
1946
|
+
var ids = self.getPendingDeleteIds()
|
|
1947
|
+
guard ids.remove(id) != nil else { return }
|
|
1948
|
+
if ids.isEmpty {
|
|
1949
|
+
UserDefaults.standard.removeObject(forKey: self.PENDING_DELETE_IDS)
|
|
1950
|
+
} else {
|
|
1951
|
+
UserDefaults.standard.set(ids.sorted().joined(separator: ","), forKey: self.PENDING_DELETE_IDS)
|
|
1952
|
+
}
|
|
1953
|
+
UserDefaults.standard.synchronize()
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1868
1956
|
public func cleanupDeltaCache() {
|
|
1869
1957
|
cleanupDeltaCache(threadToCheck: nil)
|
|
1870
1958
|
}
|
|
@@ -1924,6 +2012,11 @@ import UIKit
|
|
|
1924
2012
|
|
|
1925
2013
|
do {
|
|
1926
2014
|
try fileManager.removeItem(at: url)
|
|
2015
|
+
if fileManager.fileExists(atPath: url.path) {
|
|
2016
|
+
logger.error("Orphan bundle directory still present after delete")
|
|
2017
|
+
logger.debug("Bundle ID: \(id)")
|
|
2018
|
+
continue
|
|
2019
|
+
}
|
|
1927
2020
|
self.removeBundleInfo(id: id)
|
|
1928
2021
|
logger.info("Deleted orphan bundle directory")
|
|
1929
2022
|
logger.debug("Bundle ID: \(id)")
|
|
@@ -1938,6 +2031,40 @@ import UIKit
|
|
|
1938
2031
|
}
|
|
1939
2032
|
}
|
|
1940
2033
|
|
|
2034
|
+
public func allowedBundleIdsForCleanup() -> Set<String> {
|
|
2035
|
+
var allowedIds = Set(self.list(raw: true).compactMap { info -> String? in
|
|
2036
|
+
let id = info.getId()
|
|
2037
|
+
// DELETED tombstones must not protect leftover folders.
|
|
2038
|
+
// DELETING stays protected so drainPendingDeletes owns the removal.
|
|
2039
|
+
if id.isEmpty || info.isDeleted() {
|
|
2040
|
+
return nil
|
|
2041
|
+
}
|
|
2042
|
+
return id
|
|
2043
|
+
})
|
|
2044
|
+
let currentId = self.getCurrentBundleId()
|
|
2045
|
+
if !currentId.isEmpty {
|
|
2046
|
+
allowedIds.insert(currentId)
|
|
2047
|
+
}
|
|
2048
|
+
let fallback = self.getFallbackBundle()
|
|
2049
|
+
let fallbackId = fallback.getId()
|
|
2050
|
+
if !fallbackId.isEmpty && !fallback.isDeleting() {
|
|
2051
|
+
allowedIds.insert(fallbackId)
|
|
2052
|
+
}
|
|
2053
|
+
if let next = self.getNextBundle() {
|
|
2054
|
+
let nextId = next.getId()
|
|
2055
|
+
if !nextId.isEmpty && !next.isDeleting() {
|
|
2056
|
+
allowedIds.insert(nextId)
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
if let previewFallback = self.getPreviewFallbackBundle() {
|
|
2060
|
+
let previewId = previewFallback.getId()
|
|
2061
|
+
if !previewId.isEmpty && !previewFallback.isDeleting() {
|
|
2062
|
+
allowedIds.insert(previewId)
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return allowedIds
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1941
2068
|
public func cleanupOrphanedTempFolders(threadToCheck: Thread?) {
|
|
1942
2069
|
let fileManager = FileManager.default
|
|
1943
2070
|
|
|
@@ -2079,7 +2206,8 @@ import UIKit
|
|
|
2079
2206
|
destPersist.isDirectory &&
|
|
2080
2207
|
!indexPersist.isDirectory &&
|
|
2081
2208
|
indexPersist.exist &&
|
|
2082
|
-
!bundleIndo.isDeleted()
|
|
2209
|
+
!bundleIndo.isDeleted() &&
|
|
2210
|
+
!bundleIndo.isDeleting() {
|
|
2083
2211
|
return true
|
|
2084
2212
|
}
|
|
2085
2213
|
return false
|
|
@@ -2169,17 +2297,39 @@ import UIKit
|
|
|
2169
2297
|
let fallbackIsPreviewFallback = previewFallback?.getId() == fallback.getId()
|
|
2170
2298
|
logger.info("Fallback bundle is: \(fallback.toString())")
|
|
2171
2299
|
logger.info("Version successfully loaded: \(bundle.toString())")
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2300
|
+
let previousFallbackId = fallback.getId()
|
|
2301
|
+
let nextBundle = self.getNextBundle()
|
|
2302
|
+
let previousIsNext = nextBundle?.getId() == previousFallbackId &&
|
|
2303
|
+
!(nextBundle?.isDeleted() ?? true) &&
|
|
2304
|
+
!(nextBundle?.isErrorStatus() ?? true) &&
|
|
2305
|
+
!(nextBundle?.isDeleting() ?? true)
|
|
2306
|
+
let shouldDeletePrevious = autoDeletePrevious &&
|
|
2307
|
+
!fallback.isBuiltin() &&
|
|
2308
|
+
previousFallbackId != bundle.getId() &&
|
|
2309
|
+
!fallbackIsPreviewFallback &&
|
|
2310
|
+
!previousIsNext
|
|
2311
|
+
if shouldDeletePrevious {
|
|
2312
|
+
// Mark durable intent before fallback switch so a kill mid-flight still retries.
|
|
2313
|
+
if !self.saveBundleInfo(id: previousFallbackId, bundle: fallback.setStatus(status: BundleStatus.DELETING.storedValue)) {
|
|
2314
|
+
self.logger.error("Failed to persist DELETING for previous bundle; queueing durable retry")
|
|
2315
|
+
self.logger.debug("Bundle ID: \(previousFallbackId)")
|
|
2316
|
+
self.enqueuePendingDelete(id: previousFallbackId)
|
|
2180
2317
|
}
|
|
2318
|
+
UserDefaults.standard.synchronize()
|
|
2181
2319
|
}
|
|
2182
2320
|
self.setFallbackBundle(fallback: bundle)
|
|
2321
|
+
if shouldDeletePrevious {
|
|
2322
|
+
DispatchQueue.global(qos: .utility).async {
|
|
2323
|
+
let res = self.delete(id: previousFallbackId)
|
|
2324
|
+
if res {
|
|
2325
|
+
self.logger.info("Deleted previous bundle")
|
|
2326
|
+
self.logger.debug("Bundle ID: \(previousFallbackId)")
|
|
2327
|
+
} else {
|
|
2328
|
+
self.logger.info("Previous bundle delete incomplete, will retry")
|
|
2329
|
+
self.logger.debug("Bundle ID: \(previousFallbackId)")
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2183
2333
|
}
|
|
2184
2334
|
|
|
2185
2335
|
public func setError(bundle: BundleInfo) {
|
|
@@ -2289,7 +2439,8 @@ import UIKit
|
|
|
2289
2439
|
self.logger.info("Public channel requested, channel override removed")
|
|
2290
2440
|
|
|
2291
2441
|
setChannel.status = responseValue.status ?? "ok"
|
|
2292
|
-
setChannel.message = responseValue.message
|
|
2442
|
+
setChannel.message = responseValue.message
|
|
2443
|
+
?? "Public channel requested, channel override removed. Device will use public channel automatically."
|
|
2293
2444
|
} else {
|
|
2294
2445
|
self.defaultChannel = channel
|
|
2295
2446
|
UserDefaults.standard.set(channel, forKey: defaultChannelKey)
|
|
@@ -2304,7 +2455,6 @@ import UIKit
|
|
|
2304
2455
|
|
|
2305
2456
|
func getChannel(defaultChannelKey: String? = nil) -> GetChannel {
|
|
2306
2457
|
let getChannel: GetChannel = GetChannel()
|
|
2307
|
-
|
|
2308
2458
|
// Check if rate limit was exceeded
|
|
2309
2459
|
if CapgoUpdater.rateLimitExceeded {
|
|
2310
2460
|
logger.debug("Skipping getChannel due to rate limit (429). Requests will resume after app restart.")
|
|
@@ -2680,23 +2830,26 @@ import UIKit
|
|
|
2680
2830
|
self.saveBundleInfo(id: id, bundle: nil)
|
|
2681
2831
|
}
|
|
2682
2832
|
|
|
2683
|
-
|
|
2833
|
+
@discardableResult
|
|
2834
|
+
public func saveBundleInfo(id: String, bundle: BundleInfo?) -> Bool {
|
|
2684
2835
|
if bundle != nil && (bundle!.isBuiltin() || bundle!.isUnknown()) {
|
|
2685
2836
|
logger.info("Not saving info for bundle [\(id)] \(bundle?.toString() ?? "")")
|
|
2686
|
-
return
|
|
2837
|
+
return false
|
|
2687
2838
|
}
|
|
2688
2839
|
if bundle == nil {
|
|
2689
2840
|
logger.info("Removing info for bundle [\(id)]")
|
|
2690
2841
|
UserDefaults.standard.removeObject(forKey: "\(id)\(self.INFO_SUFFIX)")
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2842
|
+
return true
|
|
2843
|
+
}
|
|
2844
|
+
let update = bundle!.setId(id: id)
|
|
2845
|
+
logger.info("Storing info for bundle [\(id)] \(update.toString())")
|
|
2846
|
+
do {
|
|
2847
|
+
try UserDefaults.standard.setObj(update, forKey: "\(id)\(self.INFO_SUFFIX)")
|
|
2848
|
+
return true
|
|
2849
|
+
} catch {
|
|
2850
|
+
logger.error("Failed to save bundle info")
|
|
2851
|
+
logger.debug("Bundle ID: \(id), Error: \(error.localizedDescription)")
|
|
2852
|
+
return false
|
|
2700
2853
|
}
|
|
2701
2854
|
}
|
|
2702
2855
|
|
|
@@ -668,6 +668,14 @@ extension UIWindow {
|
|
|
668
668
|
}
|
|
669
669
|
return
|
|
670
670
|
}
|
|
671
|
+
guard plugin.persistDefaultChannelStateFromDefaults() else {
|
|
672
|
+
DispatchQueue.main.async {
|
|
673
|
+
progressAlert.dismiss(animated: true) {
|
|
674
|
+
self.showError(message: "Channel set to \(name), but local persistence failed.", plugin: plugin)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return
|
|
678
|
+
}
|
|
671
679
|
|
|
672
680
|
// Update progress message
|
|
673
681
|
DispatchQueue.main.async {
|