Skip to content

Commit b5545bd

Browse files
committed
fixed updater, now zips to a dedicated folder
1 parent bea64da commit b5545bd

2 files changed

Lines changed: 43 additions & 32 deletions

File tree

NumWorksMac/Core/Updaters/AppUpdater.swift

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ final class AppUpdater: ObservableObject {
1919
@Published var phase: Phase = .idle
2020

2121
private var panel: NSPanel?
22+
/// Kept so Retry can run after a failed attempt (phase does not carry URL).
23+
private var lastUpdateURL: URL?
2224

2325
private let fileManager = FileManager.default
2426

2527
func presentUpdate(remoteURL: URL, remoteVersion: String) {
2628
Task { @MainActor in
2729
let notes = (try? await AppUpdateChecker.fetchLatestReleaseNotes()) ?? ""
30+
lastUpdateURL = remoteURL
2831
phase = .updateAvailable(version: remoteVersion, url: remoteURL, releaseNotes: notes)
2932
showPanel()
3033
}
@@ -33,19 +36,33 @@ final class AppUpdater: ObservableObject {
3336
func dismiss() {
3437
closePanel()
3538
phase = .idle
39+
lastUpdateURL = nil
40+
NotificationCenter.default.post(name: .appUpdateFlowDidFinish, object: nil)
41+
}
42+
43+
func retry() {
44+
guard let url = lastUpdateURL else { return }
45+
Task { await downloadAndInstall(remoteURL: url) }
3646
}
3747

3848
func downloadAndInstall(remoteURL: URL) async {
49+
if case .downloading = phase { return }
3950
phase = .downloading
51+
lastUpdateURL = remoteURL
52+
53+
guard let downloads = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
54+
phase = .failed("Downloads folder unavailable.")
55+
return
56+
}
57+
58+
let extractDir = downloads.appendingPathComponent("NumWorksUpdate", isDirectory: true)
4059

4160
do {
4261
let (tmpURL, response) = try await URLSession.shared.download(from: remoteURL)
4362

44-
let downloads = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask).first!
45-
4663
var filename = response.suggestedFilename ?? remoteURL.lastPathComponent
4764
if filename.isEmpty { filename = "NumWorks.zip" }
48-
if filename.lowercased().hasSuffix(".zip") == false {
65+
if !filename.lowercased().hasSuffix(".zip") {
4966
filename += ".zip"
5067
}
5168

@@ -54,32 +71,25 @@ final class AppUpdater: ObservableObject {
5471
try? fileManager.removeItem(at: zipURL)
5572
try fileManager.moveItem(at: tmpURL, to: zipURL)
5673

57-
if zipURL.pathExtension.lowercased() != "zip" {
58-
throw NSError(domain: "AppUpdater", code: 2)
59-
}
74+
try? fileManager.removeItem(at: extractDir)
75+
try fileManager.createDirectory(at: extractDir, withIntermediateDirectories: true)
6076

61-
let unzipStart = Date()
62-
try unzip(zipURL: zipURL, to: downloads)
77+
try unzip(zipURL: zipURL, to: extractDir)
6378
try? fileManager.removeItem(at: zipURL)
6479

65-
let extractedApp = try findNewestExtractedApp(in: downloads, since: unzipStart)
80+
let extractedApp = try findExtractedApp(in: extractDir)
6681
let targetApp = downloads.appendingPathComponent("NumWorks.app")
6782

6883
if extractedApp.standardizedFileURL != targetApp.standardizedFileURL {
6984
try? fileManager.removeItem(at: targetApp)
7085
try fileManager.moveItem(at: extractedApp, to: targetApp)
71-
} else {
72-
// If it already extracted as NumWorks.app, make sure it replaces any previous copy.
73-
// (At this point, targetApp is the extracted app.)
7486
}
7587

88+
try? fileManager.removeItem(at: extractDir)
7689
phase = .readyToOpen
7790
} catch {
78-
// Best effort cleanup
79-
if let downloads = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask).first {
80-
let fallback = downloads.appendingPathComponent("NumWorks.zip")
81-
try? fileManager.removeItem(at: fallback)
82-
}
91+
try? fileManager.removeItem(at: extractDir)
92+
try? fileManager.removeItem(at: downloads.appendingPathComponent("NumWorks.zip"))
8393
phase = .failed(error.localizedDescription)
8494
}
8595
}
@@ -98,31 +108,23 @@ final class AppUpdater: ObservableObject {
98108
NSApp.terminate(nil)
99109
}
100110

101-
private func findNewestExtractedApp(in downloads: URL, since: Date) throws -> URL {
111+
/// Finds the first .app bundle under the given directory (e.g. our dedicated extract folder).
112+
/// Unzip often restores timestamps from the zip, so we do not filter by date.
113+
private func findExtractedApp(in directory: URL) throws -> URL {
102114
let fm = fileManager
103-
let keys: Set<URLResourceKey> = [.contentModificationDateKey, .isDirectoryKey]
115+
let keys: Set<URLResourceKey> = [.isDirectoryKey]
104116

105-
guard let e = fm.enumerator(at: downloads, includingPropertiesForKeys: Array(keys), options: [.skipsHiddenFiles]) else {
117+
guard let e = fm.enumerator(at: directory, includingPropertiesForKeys: Array(keys), options: [.skipsHiddenFiles]) else {
106118
throw NSError(domain: "AppUpdater", code: 3)
107119
}
108120

109-
var bestURL: URL?
110-
var bestDate: Date = since
111-
112121
for case let url as URL in e {
113-
if url.pathExtension.lowercased() != "app" { continue }
114-
122+
guard url.pathExtension.lowercased() == "app" else { continue }
115123
let rv = try? url.resourceValues(forKeys: keys)
116124
guard rv?.isDirectory == true else { continue }
117-
118-
let d = rv?.contentModificationDate ?? .distantPast
119-
if d >= bestDate {
120-
bestDate = d
121-
bestURL = url
122-
}
125+
return url
123126
}
124127

125-
if let bestURL { return bestURL }
126128
throw NSError(domain: "AppUpdater", code: 4)
127129
}
128130

NumWorksMac/UI/Updates/AppUpdateView.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ struct AppUpdateView: View {
6464
.font(.headline)
6565
Text(message)
6666
.foregroundColor(.red)
67+
68+
HStack {
69+
Button("Later") {
70+
updater.dismiss()
71+
}
72+
Button("Retry") {
73+
updater.retry()
74+
}
75+
}
6776
}
6877
}
6978
.padding(24)

0 commit comments

Comments
 (0)