|
|
|
@ -1,22 +1,24 @@ |
|
|
|
|
import Foundation |
|
|
|
|
import Combine |
|
|
|
|
|
|
|
|
|
public protocol UpdaterProtocol: ObservableObject { |
|
|
|
|
|
|
|
|
|
var update: Release? { get } |
|
|
|
|
var testBuild: Bool { get } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version. |
|
|
|
|
public class Updater: ObservableObject, UpdaterProtocol { |
|
|
|
|
|
|
|
|
|
@Published public var update: Release? |
|
|
|
|
|
|
|
|
|
public let testBuild: Bool |
|
|
|
|
|
|
|
|
|
/// The current OS version. |
|
|
|
|
private let osVersion: SemVer |
|
|
|
|
/// The current version of the app that is running. |
|
|
|
|
private let currentVersion: SemVer |
|
|
|
|
|
|
|
|
|
public init(checkOnLaunch: Bool, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) { |
|
|
|
|
/// Initializes an Updater. |
|
|
|
|
/// - Parameters: |
|
|
|
|
/// - checkOnLaunch: A boolean describing whether the Updater should check for available updates on launch. |
|
|
|
|
/// - checkFrequency: The interval at which the Updater should check for updates. Subject to a tolerance of 1 hour. |
|
|
|
|
/// - osVersion: The current OS version. |
|
|
|
|
/// - currentVersion: The current version of the app that is running. |
|
|
|
|
public init(checkOnLaunch: Bool, checkFrequency: TimeInterval = Measurement(value: 24, unit: UnitDuration.hours).converted(to: .seconds).value, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) { |
|
|
|
|
self.osVersion = osVersion |
|
|
|
|
self.currentVersion = currentVersion |
|
|
|
|
testBuild = currentVersion == SemVer("0.0.0") |
|
|
|
@ -24,12 +26,13 @@ public class Updater: ObservableObject, UpdaterProtocol { |
|
|
|
|
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet. |
|
|
|
|
checkForUpdates() |
|
|
|
|
} |
|
|
|
|
let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in |
|
|
|
|
let timer = Timer.scheduledTimer(withTimeInterval: checkFrequency, repeats: true) { _ in |
|
|
|
|
self.checkForUpdates() |
|
|
|
|
} |
|
|
|
|
timer.tolerance = 60*60 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Manually trigger an update check. |
|
|
|
|
public func checkForUpdates() { |
|
|
|
|
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in |
|
|
|
|
guard let data = data else { return } |
|
|
|
@ -38,6 +41,8 @@ public class Updater: ObservableObject, UpdaterProtocol { |
|
|
|
|
}.resume() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release. |
|
|
|
|
/// - Parameter release: The release to ignore. |
|
|
|
|
public func ignore(release: Release) { |
|
|
|
|
guard !release.critical else { return } |
|
|
|
|
defaults.set(true, forKey: release.name) |
|
|
|
@ -50,6 +55,8 @@ public class Updater: ObservableObject, UpdaterProtocol { |
|
|
|
|
|
|
|
|
|
extension Updater { |
|
|
|
|
|
|
|
|
|
/// Evaluates the available downloadable releases, and selects the newest non-prerelease release that the user is able to run. |
|
|
|
|
/// - Parameter releases: An array of ``Release`` objects. |
|
|
|
|
func evaluate(releases: [Release]) { |
|
|
|
|
guard let release = releases |
|
|
|
|
.sorted() |
|
|
|
@ -66,49 +73,18 @@ extension Updater { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Checks whether the user has ignored a release. |
|
|
|
|
/// - Parameter release: The release to check. |
|
|
|
|
/// - Returns: A boolean describing whether the user has ignored the release. Will always be false if the release is critical. |
|
|
|
|
func userIgnored(release: Release) -> Bool { |
|
|
|
|
guard !release.critical else { return false } |
|
|
|
|
return defaults.bool(forKey: release.name) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// The user defaults used to store user ignore state. |
|
|
|
|
var defaults: UserDefaults { |
|
|
|
|
UserDefaults(suiteName: "com.maxgoedjen.Secretive.updater.ignorelist")! |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public struct SemVer { |
|
|
|
|
|
|
|
|
|
let versionNumbers: [Int] |
|
|
|
|
|
|
|
|
|
public init(_ version: String) { |
|
|
|
|
// Betas have the format 1.2.3_beta1 |
|
|
|
|
let strippedBeta = version.split(separator: "_").first! |
|
|
|
|
var split = strippedBeta.split(separator: ".").compactMap { Int($0) } |
|
|
|
|
while split.count < 3 { |
|
|
|
|
split.append(0) |
|
|
|
|
} |
|
|
|
|
versionNumbers = split |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public init(_ version: OperatingSystemVersion) { |
|
|
|
|
versionNumbers = [version.majorVersion, version.minorVersion, version.patchVersion] |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
extension SemVer: Comparable { |
|
|
|
|
|
|
|
|
|
public static func < (lhs: SemVer, rhs: SemVer) -> Bool { |
|
|
|
|
for (latest, current) in zip(lhs.versionNumbers, rhs.versionNumbers) { |
|
|
|
|
if latest < current { |
|
|
|
|
return true |
|
|
|
|
} else if latest > current { |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -119,63 +95,3 @@ extension Updater { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public struct Release: Codable { |
|
|
|
|
|
|
|
|
|
public let name: String |
|
|
|
|
public let prerelease: Bool |
|
|
|
|
public let html_url: URL |
|
|
|
|
public let body: String |
|
|
|
|
|
|
|
|
|
public init(name: String, prerelease: Bool, html_url: URL, body: String) { |
|
|
|
|
self.name = name |
|
|
|
|
self.prerelease = prerelease |
|
|
|
|
self.html_url = html_url |
|
|
|
|
self.body = body |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
extension Release: Identifiable { |
|
|
|
|
|
|
|
|
|
public var id: String { |
|
|
|
|
html_url.absoluteString |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
extension Release: Comparable { |
|
|
|
|
|
|
|
|
|
public static func < (lhs: Release, rhs: Release) -> Bool { |
|
|
|
|
lhs.version < rhs.version |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
extension Release { |
|
|
|
|
|
|
|
|
|
public var critical: Bool { |
|
|
|
|
body.contains(Constants.securityContent) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public var version: SemVer { |
|
|
|
|
SemVer(name) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public var minimumOSVersion: SemVer { |
|
|
|
|
guard let range = body.range(of: "Minimum macOS Version"), |
|
|
|
|
let numberStart = body.rangeOfCharacter(from: CharacterSet.decimalDigits, options: [], range: range.upperBound..<body.endIndex) else { return SemVer("11.0.0") } |
|
|
|
|
let numbersEnd = body.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines, options: [], range: numberStart.upperBound..<body.endIndex)?.lowerBound ?? body.endIndex |
|
|
|
|
let version = numberStart.lowerBound..<numbersEnd |
|
|
|
|
return SemVer(String(body[version])) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
extension Release { |
|
|
|
|
|
|
|
|
|
enum Constants { |
|
|
|
|
static let securityContent = "Critical Security Update" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|