secretive/Sources/Packages/Sources/Brief/Updater.swift

182 lines
4.9 KiB
Swift
Raw Normal View History

2020-03-15 08:01:40 +00:00
import Foundation
import Combine
2020-03-22 01:43:26 +00:00
public protocol UpdaterProtocol: ObservableObject {
2020-03-15 08:01:40 +00:00
var update: Release? { get }
var testBuild: Bool { get }
2020-03-15 08:01:40 +00:00
}
2020-03-22 01:43:26 +00:00
public class Updater: ObservableObject, UpdaterProtocol {
2020-03-15 08:01:40 +00:00
2020-03-22 01:43:26 +00:00
@Published public var update: Release?
public let testBuild: Bool
2020-03-15 08:01:40 +00:00
2021-01-18 08:49:26 +00:00
private let osVersion: SemVer
2021-09-23 04:10:04 +00:00
private let currentVersion: SemVer
2021-01-18 08:49:26 +00:00
2021-09-23 04:10:04 +00:00
public init(checkOnLaunch: Bool, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion), currentVersion: SemVer = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")) {
2021-01-18 08:49:26 +00:00
self.osVersion = osVersion
2021-09-23 04:10:04 +00:00
self.currentVersion = currentVersion
testBuild = currentVersion == SemVer("0.0.0")
2020-09-22 06:12:50 +00:00
if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
checkForUpdates()
}
2020-03-15 08:01:40 +00:00
let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in
self.checkForUpdates()
}
timer.tolerance = 60*60
}
2020-03-22 01:43:26 +00:00
public func checkForUpdates() {
2020-03-15 08:01:40 +00:00
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
guard let data = data else { return }
2021-01-18 08:49:26 +00:00
guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
self.evaluate(releases: releases)
2020-03-15 08:01:40 +00:00
}.resume()
}
public func ignore(release: Release) {
guard !release.critical else { return }
defaults.set(true, forKey: release.name)
DispatchQueue.main.async {
self.update = nil
}
}
}
extension Updater {
2021-01-18 08:49:26 +00:00
func evaluate(releases: [Release]) {
guard let release = releases
.sorted()
.reversed()
.filter({ !$0.prerelease })
.first(where: { $0.minimumOSVersion <= osVersion }) else { return }
guard !userIgnored(release: release) else { return }
2020-09-22 06:12:50 +00:00
guard !release.prerelease else { return }
2020-09-22 07:17:22 +00:00
let latestVersion = SemVer(release.name)
if latestVersion > currentVersion {
DispatchQueue.main.async {
self.update = release
2020-03-15 08:01:40 +00:00
}
}
}
func userIgnored(release: Release) -> Bool {
guard !release.critical else { return false }
return defaults.bool(forKey: release.name)
}
var defaults: UserDefaults {
UserDefaults(suiteName: "com.maxgoedjen.Secretive.updater.ignorelist")!
}
2020-03-15 08:01:40 +00:00
}
2021-01-18 08:49:26 +00:00
public struct SemVer {
2020-09-22 07:17:22 +00:00
let versionNumbers: [Int]
2021-01-18 08:49:26 +00:00
public init(_ version: String) {
2020-09-22 07:17:22 +00:00
// 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
}
2021-01-18 08:49:26 +00:00
public init(_ version: OperatingSystemVersion) {
versionNumbers = [version.majorVersion, version.minorVersion, version.patchVersion]
}
2020-09-22 07:17:22 +00:00
}
extension SemVer: Comparable {
2021-01-18 08:49:26 +00:00
public static func < (lhs: SemVer, rhs: SemVer) -> Bool {
2020-09-22 07:17:22 +00:00
for (latest, current) in zip(lhs.versionNumbers, rhs.versionNumbers) {
if latest < current {
return true
} else if latest > current {
return false
}
}
return false
}
}
2020-03-15 08:01:40 +00:00
extension Updater {
enum Constants {
2021-01-18 08:49:26 +00:00
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases")!
2020-03-15 08:01:40 +00:00
}
}
2020-03-22 01:43:26 +00:00
public struct Release: Codable {
public let name: String
2020-09-22 06:12:50 +00:00
public let prerelease: Bool
2020-03-22 01:43:26 +00:00
public let html_url: URL
public let body: String
2020-09-22 06:12:50 +00:00
public init(name: String, prerelease: Bool, html_url: URL, body: String) {
2020-03-22 01:43:26 +00:00
self.name = name
2020-09-22 06:12:50 +00:00
self.prerelease = prerelease
2020-03-22 01:43:26 +00:00
self.html_url = html_url
self.body = body
}
2020-03-15 08:01:40 +00:00
}
2020-09-22 06:12:50 +00:00
extension Release: Identifiable {
public var id: String {
html_url.absoluteString
}
}
2020-03-15 08:01:40 +00:00
2021-01-18 08:49:26 +00:00
extension Release: Comparable {
public static func < (lhs: Release, rhs: Release) -> Bool {
lhs.version < rhs.version
}
}
2020-03-15 08:01:40 +00:00
extension Release {
2020-03-22 01:43:26 +00:00
public var critical: Bool {
2020-09-22 06:12:50 +00:00
body.contains(Constants.securityContent)
2020-03-15 08:01:40 +00:00
}
2021-01-18 08:49:26 +00:00
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]))
}
2020-03-15 08:01:40 +00:00
}
extension Release {
enum Constants {
static let securityContent = "Critical Security Update"
}
}