Files
Pearcleaner/Pearcleaner/Logic/Utilities.swift
T
2023-11-12 20:59:30 -07:00

510 lines
18 KiB
Swift

//
// Utilities.swift
// Pearcleaner
//
// Created by Alin Lupascu on 11/3/23.
//
import Foundation
import SwiftUI
// Make updates on main thread
func updateOnMain(_ updates: @escaping () -> Void) {
DispatchQueue.main.async {
updates()
}
}
// Execute functions on background thread
func updateOnBackground(_ updates: @escaping () -> Void) {
DispatchQueue.global(qos: .background).async {
updates()
}
}
// Check for access to Full Disk access
func checkAndRequestFullDiskAccess(appState: AppState, skipAlert: Bool = false) -> Bool {
@AppStorage("settings.permissions.disk") var diskP: Bool = false
let fileURL = URL(fileURLWithPath: "\(home)/Library/Application Support/com.apple.TCC/TCC.db")
let accessStatus = FileManager.default.isWritableFile(atPath: fileURL.path)
if accessStatus {
diskP = true
_ = checkAndRequestAccessibilityAccess(appState: appState)
return true
} else {
diskP = false
if !skipAlert {
appState.alertType = .diskAccess
appState.showAlert = true
}
return false
}
}
// Check for access to System Events
func checkAndRequestAccessibilityAccess(appState: AppState) -> Bool {
@AppStorage("settings.permissions.events") var diskE: Bool = false
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
let accessEnabled = AXIsProcessTrustedWithOptions(options)
if accessEnabled {
diskE = true
return accessEnabled
} else {
diskE = false
return false
}
}
// Check if symlink
func isSymlink(atPath path: URL) -> Bool {
do {
let _ = try path.checkResourceIsReachable()
let resourceValues = try path.resourceValues(forKeys: [.isSymbolicLinkKey])
return resourceValues.isSymbolicLink == true
} catch {
return false
}
}
// Open trash folder
func openTrash() {
if let trashURL = try? FileManager.default.url(for: .trashDirectory, in: .userDomainMask, appropriateFor: nil, create: false) {
NSWorkspace.shared.open(trashURL)
}
}
// Check if restricted app
func isRestricted(atPath path: URL) -> Bool {
if path.path.contains("Safari") || path.path.contains(Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "") {
return true
} else {
return false
}
}
// Convert icon to png so colors render correctly
func convertICNSToPNG(icon: NSImage, size: NSSize) -> NSImage? {
// Resize the icon to the specified size
let resizedIcon = NSImage(size: size)
resizedIcon.lockFocus()
icon.draw(in: NSRect(x: 0, y: 0, width: size.width, height: size.height))
resizedIcon.unlockFocus()
// Convert the resized icon to PNG format
if let resizedImageData = resizedIcon.tiffRepresentation,
let resizedBitmap = NSBitmapImageRep(data: resizedImageData),
let pngData = resizedBitmap.representation(using: .png, properties: [:]) {
return NSImage(data: pngData)
}
return nil
}
// Get icon for files and folders
func getIconForFileOrFolder(atPath path: URL) -> Image? {
let icon = NSWorkspace.shared.icon(forFile: path.path)
let nsImage = icon
return Image(nsImage: nsImage)
}
// Relaunch app
func relaunchApp(afterDelay seconds: TimeInterval = 0.5) -> Never {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", "sleep \(seconds); open \"\(Bundle.main.bundlePath)\""]
task.launch()
NSApp.terminate(nil)
exit(0)
}
// --- Extend Int to convert hours to seconds ---
extension Int {
var daysToSeconds: Double {
return Double(self) * 24 * 60 * 60
}
}
// --- Extend String to remove periods, spaces and lowercase the string
extension String {
func pearFormat() -> String {
return self.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
}
}
// --- Trash Relationship ---
extension FileManager {
public func isInTrash(_ file: URL) -> Bool {
var relationship: URLRelationship = .other
try? getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file)
return relationship == .contains
}
}
// --- Gradient ---
func schemeGradient(for colorScheme: ColorScheme) -> LinearGradient {
return LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing)
// switch colorScheme {
// case .light:
// return LinearGradient(gradient: Gradient(colors: [Color("AccentColor")]), startPoint: .leading, endPoint: .trailing)
// case .dark:
// return LinearGradient(gradient: Gradient(colors: [.pink, .orange]), startPoint: .leading, endPoint: .trailing)
// @unknown default:
// return LinearGradient(gradient: Gradient(colors: [Color("AccentColor")]), startPoint: .leading, endPoint: .trailing)
// }
}
// Get total size of folders and files using DU cli command
func totalSizeOnDisk(for paths: [URL]) -> String? {
var totalSize = 0
let process = Process()
process.launchPath = "/usr/bin/du"
process.arguments = ["-sk"] + paths.map { (url: URL) -> String in
return url.path
}
let pipe = Pipe()
process.standardOutput = pipe
try? process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let output = String(data: data, encoding: .utf8) {
let lines = output.components(separatedBy: .newlines)
for line in lines {
let components = line.components(separatedBy: "\t")
if let sizeString = components.first, let size = Int(sizeString) {
totalSize += size * 1024 // Convert the size from kilobytes to bytes
}
}
}
let byteCountFormatter = ByteCountFormatter()
byteCountFormatter.countStyle = .file
byteCountFormatter.allowedUnits = [.useAll]
return byteCountFormatter.string(fromByteCount: Int64(totalSize))
}
func totalSizeOnDisk(for path: URL) -> String? {
return totalSizeOnDisk(for: [path])
}
// Get total size of folders and files using straight swift
extension URL {
func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
return try [self].totalAllocatedSize(includingSubfolders: includingSubfolders)
}
func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
return try [self].totalSizeOnDisk(includingSubfolders: includingSubfolders)
}
}
extension Array where Element == URL {
func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
var totalSize = 0
for path in self {
// if path.absoluteString.contains(".app") {
let resourceValues = try path.resourceValues(forKeys: [.isDirectoryKey, .isPackageKey, .totalFileAllocatedSizeKey])
if resourceValues.isDirectory == true || path.pathExtension == "app" {
if includingSubfolders {
let filePaths = FileManager.default.subpaths(atPath: path.path) ?? []
for filePath in filePaths {
let fileUrl = path.appendingPathComponent(filePath)
let fileAttributes = try FileManager.default.attributesOfItem(atPath: fileUrl.path)
let fileSize = fileAttributes[.size] as? Int64 ?? 0
totalSize += Int(fileSize)
}
}
} else {
totalSize += resourceValues.totalFileAllocatedSize ?? 0
}
// }
}
return totalSize
}
func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
if let totalSize = try self.totalAllocatedSize(includingSubfolders: includingSubfolders) {
let byteCountFormatter = ByteCountFormatter()
byteCountFormatter.countStyle = .file
byteCountFormatter.allowedUnits = [.useBytes, .useKB, .useMB, .useTB]
return byteCountFormatter.string(fromByteCount: Int64(totalSize))
}
return nil
}
}
// Alerts
func presentAlert(appState: AppState) -> Alert {
switch appState.alertType {
case .update:
return Alert(title: Text("Update Available 🥳"), message: Text("You may choose to install the update now, otherwise you may start the update later from Settings"), primaryButton: .default(Text("Install")) {
downloadUpdate(appState: appState)
appState.alertType = .off
}, secondaryButton: .cancel())
case .no_update:
return Alert(title: Text("No Updates 😌"), message: Text("The application is on the latest release available"), primaryButton: .cancel(Text("Okay")), secondaryButton: .default(Text("Force Update")) {
downloadUpdate(appState: appState)
appState.alertType = .off
})
case .diskAccess:
return Alert(title: Text("Permissions"), message: Text("This application requires Full Disk and Accessibility permissions. Once you grant full disk, don't restart the app yet. Wait a second for a second prompt for accessibility and then restart"), primaryButton: .default(Text("Allow in Settings")) {
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") {
NSWorkspace.shared.open(url)
}
appState.alertType = .off
}, secondaryButton: .cancel(Text("Later")))
// case .eventsAccess:
// return Alert(title: Text("Accessibility Permission"), message: Text("This application requires Accessibility permission"), primaryButton: .default(Text("Allow in Settings")) {
// if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
// NSWorkspace.shared.open(url)
// }
// appState.alertType = .off
// }, secondaryButton: .cancel(Text("Later")))
case .restartApp:
return Alert(title: Text("Update Completed!"), message: Text("The application has been updated to the latest version, would you like to restart now?"), primaryButton: .default(Text("Restart")), secondaryButton: .cancel(Text("Later")) {
appState.alertType = .off
relaunchApp(afterDelay: 1)
})
case .off:
return Alert(title: Text(""))
}
}
// --- Create Application Support folder if it doesn't exist ---
func ensureApplicationSupportFolderExists(appState: AppState) {
let fileManager = FileManager.default
let supportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("Pearcleaner")
// Check to make sure Application Support/Support Admin folder exists
if !fileManager.fileExists(atPath: supportURL.path) {
try! fileManager.createDirectory(at: supportURL, withIntermediateDirectories: true)
print("Created Application Support/Pearcleaner folder")
}
}
func launchctl(load: Bool) {
let cmd = load ? "load" : "unload"
if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerSentinel", ofType: "plist") {
var plistContent = try! String(contentsOfFile: plistPath)
let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerSentinel")
// Replace the placeholder with the actual executable path
plistContent = plistContent.replacingOccurrences(of: "__EXECUTABLE_PATH__", with: executableURL.path)
// Create a temporary plist file with the updated content
let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerSentinel.plist")
do {
try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
} catch {
print("Error writing the temporary plist file: \(error)")
return
}
let task = Process()
task.launchPath = "/bin/launchctl"
task.arguments = [cmd, "-w", temporaryPlistURL.path]
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
// if let output = String(data: data, encoding: .utf8) {
// print("Output: \(output)")
// }
}
}
//func unloadAgent() {
// if let plistPath = Bundle.main.path(forResource: "com.alienator88.PearcleanerMonitor", ofType: "plist") {
// var plistContent = try! String(contentsOfFile: plistPath)
// let executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/PearcleanerMonitor")
//
// // Replace the placeholder with the actual executable path
// plistContent = plistContent.replacingOccurrences(of: "__EXECUTABLE_PATH__", with: executableURL.path)
//
// // Create a temporary plist file with the updated content
// let temporaryPlistURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("com.alienator88.PearcleanerMonitor.plist")
//
// do {
// try plistContent.write(to: temporaryPlistURL, atomically: false, encoding: .utf8)
// } catch {
// print("Error writing the temporary plist file: \(error)")
// return
// }
// let task = Process()
// task.launchPath = "/bin/launchctl"
// task.arguments = ["unload", "-w", temporaryPlistURL.path]
//
// let pipe = Pipe()
// task.standardOutput = pipe
// task.standardError = pipe
//
// task.launch()
//
//// let data = pipe.fileHandleForReading.readDataToEndOfFile()
//// if let output = String(data: data, encoding: .utf8) {
//// print("Output: \(output)")
//// }
// }
//}
//import FileWatcher
//
//// MARK: https://github.com/eonist/FileWatcher
//
//func fileW() {
// let filewatcher = FileWatcher([NSString(string: "~/.Trash").expandingTildeInPath])
// filewatcher.queue = DispatchQueue.global()
// filewatcher.callback = { event in
// print("Something happened here: " + event.path)
// }
//
// filewatcher.start()
//}
//extension Array where Element == URL {
// // DU shell way of getting sizes
// func totalAllocatedSize2(includingSubfolders: Bool = false) throws -> Int? {
// var totalSize = 0
//
// for path in self {
// let process = Process()
// process.launchPath = "/usr/bin/du"
// process.arguments = ["-sk", path.path]
//
// let pipe = Pipe()
// process.standardOutput = pipe
//
// try process.run()
// process.waitUntilExit()
//
// let data = pipe.fileHandleForReading.readDataToEndOfFile()
// if let output = String(data: data, encoding: .utf8) {
// let sizeString = output.components(separatedBy: "\t").first ?? ""
// if let size = Int(sizeString) {
// totalSize += size * 1024
// }
// }
// }
// return totalSize
// }
//
// func totalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
// var totalSize = 0
//
// for path in self {
// if includingSubfolders {
// guard
// let urls = FileManager.default.enumerator(at: path, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { return nil }
// let pathSize = try urls.lazy.reduce(0) {
// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) + $0
// }
// totalSize += pathSize
// } else {
// let pathSize = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil).lazy.reduce(0) {
// (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
// .totalFileAllocatedSize ?? 0) + $0
// }
// totalSize += pathSize
// }
// }
//
// return totalSize
// }
//
// func totalSizeOnDisk(includingSubfolders: Bool = false) throws -> String? {
// if let totalSize = try self.totalAllocatedSize(includingSubfolders: includingSubfolders) {
// let byteCountFormatter = ByteCountFormatter()
// byteCountFormatter.countStyle = .file
// byteCountFormatter.allowedUnits = [.useBytes, .useKB, .useMB, .useTB]
// return byteCountFormatter.string(fromByteCount: Int64(totalSize))
// }
// return nil
// }
//}
//func isSymbolicLink(atPath path: URL) -> Bool {
//// let fileManager = FileManager.default
//
// do {
//// let path = "/Applications/Safari.app"
//// let url = URL(fileURLWithPath: path)
// let destinationPath = path.resolvingSymlinksInPath().path
// print("The symlink at \(path) points to \(destinationPath)")
// return true
// } catch {
// print("An error occurred: \(error)")
// return false
// }
// return false
//// var isDirectory = false
//// let exists = FileManager.default.fileExists(atPath: path, isDirectory: isDirectory)
//// if exists && isDirectory {
//// do {
//// let _ = try FileManager.default.destinationOfSymbolicLink(atPath: path)
//// return true
//// } catch {
//// return false
//// }
//// }
//// return false
//}