-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUpdateChecker.swift
More file actions
174 lines (147 loc) · 7.17 KB
/
UpdateChecker.swift
File metadata and controls
174 lines (147 loc) · 7.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import Foundation
import Cocoa
class UpdateChecker {
static let githubOwner = "proverbiallemon"
static let githubRepo = "CrusherControl"
static func checkForUpdates(silent: Bool = true) {
guard let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
return
}
let urlString = "https://api.github.com/repos/\(githubOwner)/\(githubRepo)/releases/latest"
guard let url = URL(string: urlString) else { return }
var request = URLRequest(url: url)
request.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
if !silent {
DispatchQueue.main.async {
showAlert(title: "Update Check Failed", message: "Could not connect to GitHub.")
}
}
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let tagName = json["tag_name"] as? String,
let assets = json["assets"] as? [[String: Any]],
let htmlUrl = json["html_url"] as? String {
// Remove 'v' prefix from tag if present
let latestVersion = tagName.hasPrefix("v") ? String(tagName.dropFirst()) : tagName
if isNewerVersion(latestVersion, than: currentVersion) {
DispatchQueue.main.async {
promptForUpdate(
currentVersion: currentVersion,
newVersion: latestVersion,
releaseUrl: htmlUrl,
assets: assets
)
}
} else if !silent {
DispatchQueue.main.async {
showAlert(title: "No Updates", message: "You're running the latest version (\(currentVersion)).")
}
}
}
} catch {
if !silent {
DispatchQueue.main.async {
showAlert(title: "Update Check Failed", message: "Could not parse response.")
}
}
}
}.resume()
}
private static func isNewerVersion(_ new: String, than current: String) -> Bool {
let newParts = new.split(separator: ".").compactMap { Int($0) }
let currentParts = current.split(separator: ".").compactMap { Int($0) }
for i in 0..<max(newParts.count, currentParts.count) {
let newPart = i < newParts.count ? newParts[i] : 0
let currentPart = i < currentParts.count ? currentParts[i] : 0
if newPart > currentPart { return true }
if newPart < currentPart { return false }
}
return false
}
private static func promptForUpdate(currentVersion: String, newVersion: String, releaseUrl: String, assets: [[String: Any]]) {
let alert = NSAlert()
alert.messageText = "Update Available"
alert.informativeText = "A new version of Crusher Control is available.\n\nCurrent: v\(currentVersion)\nLatest: v\(newVersion)\n\nWould you like to download it?"
alert.alertStyle = .informational
alert.addButton(withTitle: "Download")
alert.addButton(withTitle: "Later")
if alert.runModal() == .alertFirstButtonReturn {
// Find the zip asset
if let zipAsset = assets.first(where: { ($0["name"] as? String)?.hasSuffix(".zip") == true }),
let downloadUrl = zipAsset["browser_download_url"] as? String,
let url = URL(string: downloadUrl) {
downloadAndInstall(from: url)
} else if let url = URL(string: releaseUrl) {
// Fallback to opening release page
NSWorkspace.shared.open(url)
}
}
}
private static func downloadAndInstall(from url: URL) {
let downloadTask = URLSession.shared.downloadTask(with: url) { tempUrl, response, error in
guard let tempUrl = tempUrl, error == nil else {
DispatchQueue.main.async {
showAlert(title: "Download Failed", message: "Could not download the update.")
}
return
}
DispatchQueue.main.async {
installUpdate(from: tempUrl)
}
}
downloadTask.resume()
showAlert(title: "Downloading...", message: "The update is being downloaded. The app will restart when ready.")
}
private static func installUpdate(from zipUrl: URL) {
let fileManager = FileManager.default
let tempDir = fileManager.temporaryDirectory.appendingPathComponent("CrusherControlUpdate")
do {
// Clean up any previous temp directory
if fileManager.fileExists(atPath: tempDir.path) {
try fileManager.removeItem(at: tempDir)
}
try fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true)
// Unzip
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/unzip")
process.arguments = ["-o", zipUrl.path, "-d", tempDir.path]
try process.run()
process.waitUntilExit()
// Find the .app in the unzipped contents
let contents = try fileManager.contentsOfDirectory(at: tempDir, includingPropertiesForKeys: nil)
guard let newApp = contents.first(where: { $0.pathExtension == "app" }) else {
showAlert(title: "Install Failed", message: "Could not find app in downloaded archive.")
return
}
// Get current app location
guard let currentAppUrl = Bundle.main.bundleURL as URL? else {
showAlert(title: "Install Failed", message: "Could not determine current app location.")
return
}
// Replace app (move current to trash, move new to location)
let trashedUrl = try fileManager.url(for: .trashDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("CrusherControl-old-\(Date().timeIntervalSince1970).app")
try fileManager.moveItem(at: currentAppUrl, to: trashedUrl)
try fileManager.moveItem(at: newApp, to: currentAppUrl)
// Relaunch
let task = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/open")
task.arguments = [currentAppUrl.path]
try task.run()
NSApp.terminate(nil)
} catch {
showAlert(title: "Install Failed", message: "Error: \(error.localizedDescription)")
}
}
private static func showAlert(title: String, message: String) {
let alert = NSAlert()
alert.messageText = title
alert.informativeText = message
alert.alertStyle = .informational
alert.runModal()
}
}