Feature custom clear cache (#64)

* Add support for manual cache removal

* Format sorting closure
This commit is contained in:
Nindi Gill 2023-06-24 19:06:55 +10:00 committed by GitHub
parent 706049becc
commit f215031df1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 234 additions and 28 deletions

View file

@ -138,6 +138,8 @@
575812C22A380B5E00425BAF /* InstallerVolume.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812C12A380B5E00425BAF /* InstallerVolume.swift */; }; 575812C22A380B5E00425BAF /* InstallerVolume.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812C12A380B5E00425BAF /* InstallerVolume.swift */; };
575812C42A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812C32A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift */; }; 575812C42A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812C32A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift */; };
575812C62A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812C52A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift */; }; 575812C62A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575812C52A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift */; };
577267AC2A4568CD00434B2C /* SettingsInstallerCacheAlertType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 577267AB2A4568CD00434B2C /* SettingsInstallerCacheAlertType.swift */; };
577267AE2A45734700434B2C /* SettingsInstallersCacheTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 577267AD2A45734700434B2C /* SettingsInstallersCacheTableView.swift */; };
5795700B2A31B06F004C7051 /* ButtonStyle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5795700A2A31B06F004C7051 /* ButtonStyle+Extension.swift */; }; 5795700B2A31B06F004C7051 /* ButtonStyle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5795700A2A31B06F004C7051 /* ButtonStyle+Extension.swift */; };
5795700D2A31B081004C7051 /* MistActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5795700C2A31B081004C7051 /* MistActionButtonStyle.swift */; }; 5795700D2A31B081004C7051 /* MistActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5795700C2A31B081004C7051 /* MistActionButtonStyle.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -288,6 +290,8 @@
575812C12A380B5E00425BAF /* InstallerVolume.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerVolume.swift; sourceTree = "<group>"; }; 575812C12A380B5E00425BAF /* InstallerVolume.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerVolume.swift; sourceTree = "<group>"; };
575812C32A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerVolumeSelectionInformationView.swift; sourceTree = "<group>"; }; 575812C32A3821A900425BAF /* InstallerVolumeSelectionInformationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerVolumeSelectionInformationView.swift; sourceTree = "<group>"; };
575812C52A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerVolumeSelectionPickerView.swift; sourceTree = "<group>"; }; 575812C52A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallerVolumeSelectionPickerView.swift; sourceTree = "<group>"; };
577267AB2A4568CD00434B2C /* SettingsInstallerCacheAlertType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInstallerCacheAlertType.swift; sourceTree = "<group>"; };
577267AD2A45734700434B2C /* SettingsInstallersCacheTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInstallersCacheTableView.swift; sourceTree = "<group>"; };
5795700A2A31B06F004C7051 /* ButtonStyle+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ButtonStyle+Extension.swift"; sourceTree = "<group>"; }; 5795700A2A31B06F004C7051 /* ButtonStyle+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ButtonStyle+Extension.swift"; sourceTree = "<group>"; };
5795700C2A31B081004C7051 /* MistActionButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MistActionButtonStyle.swift; sourceTree = "<group>"; }; 5795700C2A31B081004C7051 /* MistActionButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MistActionButtonStyle.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -436,6 +440,7 @@
390451D32856F74B00E0B563 /* Package.swift */, 390451D32856F74B00E0B563 /* Package.swift */,
3935F48F286976D000760AB0 /* ProgressAlertType.swift */, 3935F48F286976D000760AB0 /* ProgressAlertType.swift */,
393F35BB28641181005B7165 /* RefreshState.swift */, 393F35BB28641181005B7165 /* RefreshState.swift */,
577267AB2A4568CD00434B2C /* SettingsInstallerCacheAlertType.swift */,
); );
path = Model; path = Model;
sourceTree = "<group>"; sourceTree = "<group>";
@ -556,6 +561,7 @@
39FF05F52859850F00A86670 /* SettingsFirmwaresView.swift */, 39FF05F52859850F00A86670 /* SettingsFirmwaresView.swift */,
39252A78285A85AF00956C74 /* SettingsInstallersView.swift */, 39252A78285A85AF00956C74 /* SettingsInstallersView.swift */,
39252AB8285C7BC700956C74 /* SettingsInstallersCacheView.swift */, 39252AB8285C7BC700956C74 /* SettingsInstallersCacheView.swift */,
577267AD2A45734700434B2C /* SettingsInstallersCacheTableView.swift */,
39252ABA285C7D3800956C74 /* SettingsInstallersCatalogsView.swift */, 39252ABA285C7D3800956C74 /* SettingsInstallersCatalogsView.swift */,
39FF05F72859851800A86670 /* SettingsApplicationsView.swift */, 39FF05F72859851800A86670 /* SettingsApplicationsView.swift */,
39252A7A285AC50400956C74 /* SettingsDiskImagesView.swift */, 39252A7A285AC50400956C74 /* SettingsDiskImagesView.swift */,
@ -766,6 +772,7 @@
files = ( files = (
39FF05F02859848500A86670 /* SettingsView.swift in Sources */, 39FF05F02859848500A86670 /* SettingsView.swift in Sources */,
39CF562A2861E1CB006FB5D2 /* DirectoryRemover.swift in Sources */, 39CF562A2861E1CB006FB5D2 /* DirectoryRemover.swift in Sources */,
577267AE2A45734700434B2C /* SettingsInstallersCacheTableView.swift in Sources */,
575812C62A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift in Sources */, 575812C62A38296A00425BAF /* InstallerVolumeSelectionPickerView.swift in Sources */,
398734C828601FFC00B4C357 /* FileMover.swift in Sources */, 398734C828601FFC00B4C357 /* FileMover.swift in Sources */,
39CF55AF2861582F006FB5D2 /* AuthorizationError+Extension.swift in Sources */, 39CF55AF2861582F006FB5D2 /* AuthorizationError+Extension.swift in Sources */,
@ -790,6 +797,7 @@
39252AC3285CA5FE00956C74 /* InstallerExportView.swift in Sources */, 39252AC3285CA5FE00956C74 /* InstallerExportView.swift in Sources */,
398734D228603DE700B4C357 /* Array+Extension.swift in Sources */, 398734D228603DE700B4C357 /* Array+Extension.swift in Sources */,
39FF05F82859851800A86670 /* SettingsApplicationsView.swift in Sources */, 39FF05F82859851800A86670 /* SettingsApplicationsView.swift in Sources */,
577267AC2A4568CD00434B2C /* SettingsInstallerCacheAlertType.swift in Sources */,
39CF561D2861C3F5006FB5D2 /* DiskImageCreator.swift in Sources */, 39CF561D2861C3F5006FB5D2 /* DiskImageCreator.swift in Sources */,
575812BA2A373A4F00425BAF /* FirmwareAlertType.swift in Sources */, 575812BA2A373A4F00425BAF /* FirmwareAlertType.swift in Sources */,
39CF55AD28615530006FB5D2 /* SettingsGeneralHelperView.swift in Sources */, 39CF55AD28615530006FB5D2 /* SettingsGeneralHelperView.swift in Sources */,

View file

@ -0,0 +1,11 @@
//
// SettingsInstallerCacheAlertType.swift
// Mist
//
// Created by Nindi Gill on 23/6/2023.
//
enum SettingsInstallerCacheAlertType: String {
case confirmation = "Confirmation"
case error = "Error"
}

View file

@ -119,7 +119,11 @@ struct RefreshView: View {
} }
} }
firmwares.sort { $0.version == $1.version ? ($0.build.count == $1.build.count ? $0.build > $1.build : $0.build.count > $1.build.count) : $0.version > $1.version } firmwares.sort {
$0.version == $1.version ?
($0.build.count == $1.build.count ? $0.build > $1.build : $0.build.count > $1.build.count) :
$0.version > $1.version
}
return firmwares return firmwares
} }

View file

@ -0,0 +1,49 @@
//
// SettingsInstallersCacheTableView.swift
// Mist
//
// Created by Nindi Gill on 23/6/2023.
//
import SwiftUI
struct SettingsInstallersCacheTableView: View {
var installers: [Installer]
@Binding var selectedInstallerId: String?
var cacheDownloads: Bool
private let height: CGFloat = 126
private let width: CGFloat = 150
private let length: CGFloat = 16
var body: some View {
Table(installers, selection: $selectedInstallerId) {
TableColumn("") { installer in
ScaledImage(name: "Application - \(installer.version.isEmpty ? "macOS" : installer.imageName)", length: length)
}
.width(length)
TableColumn("Release") { installer in
Text(installer.version.isEmpty ? installer.id : installer.name)
}
.width(width)
TableColumn("Version") { installer in
Text(installer.version.isEmpty ? "Unknown" : installer.version)
}
TableColumn("Build") { installer in
Text(installer.build.isEmpty ? "Unknown" : installer.build)
}
TableColumn("Size") { installer in
Text(installer.size.bytesString())
}
}
.tableStyle(.bordered)
.frame(minHeight: height, maxHeight: height)
.disabled(!cacheDownloads)
.opacity(cacheDownloads ? 1 : 0.5)
}
}
struct SettingsInstallersCacheTableView_Previews: PreviewProvider {
static var previews: some View {
SettingsInstallersCacheTableView(installers: [.example], selectedInstallerId: .constant(nil), cacheDownloads: true)
}
}

View file

@ -11,48 +11,78 @@ struct SettingsInstallersCacheView: View {
@Binding var cacheDownloads: Bool @Binding var cacheDownloads: Bool
@Binding var cacheDirectory: String @Binding var cacheDirectory: String
@State private var cacheSize: UInt64 = 0 @State private var cacheSize: UInt64 = 0
@State private var buttonClicked: Bool = false
@State private var openPanel: NSOpenPanel = NSOpenPanel() @State private var openPanel: NSOpenPanel = NSOpenPanel()
@State private var installers: [Installer] = []
@State private var selectedInstallerId: String?
@State private var showAlert: Bool = false
@State private var alertType: SettingsInstallerCacheAlertType = .confirmation
private let padding: CGFloat = 5
private var removalMessage: String {
guard let installer: Installer = installers.first(where: { $0.id == selectedInstallerId }) else {
return ""
}
return "Removing '\(installer.version.isEmpty ? installer.id : "\(installer.name) \(installer.version) (\(installer.build))")' will free up \(installer.size.bytesString())."
}
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack(alignment: .firstTextBaseline) { HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Toggle(isOn: $cacheDownloads) { Toggle(isOn: $cacheDownloads) { Text("Cache downloads") }
Text("Cache downloads")
}
FooterText("Speed up future operations by caching a local copy of macOS Installer files.") FooterText("Speed up future operations by caching a local copy of macOS Installer files.")
} }
Spacer() Spacer()
Button("Select...") { Button("Select...") { selectCacheDirectory() }
selectCacheDirectory()
}
.disabled(!cacheDownloads) .disabled(!cacheDownloads)
} }
PathControl(path: $cacheDirectory) PathControl(path: $cacheDirectory)
.disabled(true) .disabled(true)
.opacity(cacheDownloads ? 1 : 0.5)
SettingsInstallersCacheTableView(installers: installers, selectedInstallerId: $selectedInstallerId, cacheDownloads: cacheDownloads)
.padding(.bottom, padding)
HStack(alignment: .firstTextBaseline) { HStack(alignment: .firstTextBaseline) {
FooterText("Cache directory currently contains \(cacheSize.bytesString()) of data.") FooterText("Cache directory currently contains \(cacheSize.bytesString()) of data.")
Spacer() Spacer()
Button("Empty Cache...") { Button("Show in Finder") {
buttonClicked.toggle() showInFinder()
} }
.disabled(cacheSize == 0) .disabled(!cacheDownloads || selectedInstallerId == nil)
Button("Remove...") {
alertType = .confirmation
showAlert = true
}
.disabled(!cacheDownloads || selectedInstallerId == nil)
} }
} }
.onAppear { .onAppear {
retrieveCacheSize() retrieveCache()
} }
.onChange(of: cacheDirectory) { _ in .onChange(of: cacheDirectory) { _ in
retrieveCacheSize() retrieveCache()
} }
.alert(isPresented: $buttonClicked) { .alert(isPresented: $showAlert) {
Alert( switch alertType {
title: Text("Empty Cache Directory?"), case .confirmation:
message: Text("Emptying the cache directory will free up \(cacheSize.bytesString())."), return Alert(
title: Text("Remove Cached Installer?"),
message: Text(removalMessage),
primaryButton: .cancel(), primaryButton: .cancel(),
secondaryButton: .destructive(Text("Empty")) { emptyCache() ; retrieveCacheSize() } secondaryButton: .destructive(Text("Remove")) {
Task {
await emptyCache(for: selectedInstallerId)
retrieveCache()
}
}
) )
case .error:
return Alert(
title: Text("An error has occured!"),
message: Text("There was an error removing the cached Installer directory. Show in Finder to remove manually."),
primaryButton: .default(Text("OK")) { },
secondaryButton: .default(Text("Show in Finder")) { showInFinder() }
)
}
} }
} }
@ -75,7 +105,7 @@ struct SettingsInstallersCacheView: View {
cacheDirectory = url.path cacheDirectory = url.path
} }
private func retrieveCacheSize() { private func retrieveCache() {
let url: URL = URL(fileURLWithPath: cacheDirectory) let url: URL = URL(fileURLWithPath: cacheDirectory)
var isDirectory: ObjCBool = false var isDirectory: ObjCBool = false
@ -86,22 +116,126 @@ struct SettingsInstallersCacheView: View {
} }
cacheSize = try FileManager.default.sizeOfDirectory(at: url) cacheSize = try FileManager.default.sizeOfDirectory(at: url)
let ids: [String] = try FileManager.default.contentsOfDirectory(atPath: url.path)
var installers: [Installer] = []
for id in ids {
let url: URL = url.appendingPathComponent(id)
guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory),
isDirectory.boolValue,
let installer: Installer = installer(for: url) else {
continue
}
installers.append(installer)
}
self.installers = installers.sorted {
$0.version == $1.version ?
($0.build.count == $1.build.count ? $0.build > $1.build : $0.build.count > $1.build.count) :
$0.version.compare($1.version, options: .numeric) == .orderedDescending
}
selectedInstallerId = nil
} catch { } catch {
print(error.localizedDescription) print(error.localizedDescription)
} }
} }
private func emptyCache() { private func installer(for url: URL) -> Installer? {
let id: String = url.lastPathComponent
do { do {
let paths: [String] = try FileManager.default.contentsOfDirectory(atPath: cacheDirectory) if let installer: Installer = Installer.legacyInstallers.first(where: { $0.id == id }) {
return installer
} else {
let distributionURL: URL = url.appendingPathComponent("\(id).English.dist")
let string: String = try String(contentsOf: distributionURL)
for path in paths { if let version: String = versionFromDistribution(string),
let url: URL = URL(fileURLWithPath: cacheDirectory + "/" + path) let build: String = buildFromDistribution(string) {
try FileManager.default.removeItem(at: url) let size: UInt64 = try FileManager.default.sizeOfDirectory(at: url)
return Installer(
id: id,
version: version,
build: build,
date: "",
distributionURL: "",
distributionSize: 0,
packages: [Package(url: "", size: Int(size), integrityDataURL: nil, integrityDataSize: nil)],
boardIDs: [],
deviceIDs: [],
unsupportedModelIdentifiers: []
)
}
} }
} catch { } catch {
print(error.localizedDescription) // do nothing
}
do {
let size: UInt64 = try FileManager.default.sizeOfDirectory(at: url)
return Installer(
id: id,
version: "",
build: "",
date: "",
distributionURL: "",
distributionSize: 0,
packages: [Package(url: "", size: Int(size), integrityDataURL: nil, integrityDataSize: nil)],
boardIDs: [],
deviceIDs: [],
unsupportedModelIdentifiers: []
)
} catch {
return nil
}
}
private func versionFromDistribution(_ string: String) -> String? {
guard string.contains("<key>VERSION</key>") else {
return nil
}
return string.replacingOccurrences(of: "^[\\s\\S]*<key>VERSION<\\/key>\\s*<string>", with: "", options: .regularExpression)
.replacingOccurrences(of: "<\\/string>[\\s\\S]*$", with: "", options: .regularExpression)
}
private func buildFromDistribution(_ string: String) -> String? {
guard string.contains("<key>BUILD</key>") else {
return nil
}
return string.replacingOccurrences(of: "^[\\s\\S]*<key>BUILD<\\/key>\\s*<string>", with: "", options: .regularExpression)
.replacingOccurrences(of: "<\\/string>[\\s\\S]*$", with: "", options: .regularExpression)
}
private func showInFinder() {
guard let id: String = selectedInstallerId else {
return
}
let url: URL = URL(fileURLWithPath: "\(cacheDirectory)/\(id)")
NSWorkspace.shared.open(url)
}
private func emptyCache(for id: String?) async {
guard let id: String = id else {
return
}
let url: URL = URL(fileURLWithPath: "\(cacheDirectory)/\(id)")
do {
try await DirectoryRemover.remove(url)
} catch {
alertType = .error
showAlert = true
} }
} }
} }