mirror of
https://github.com/exituser/Pearcleaner.git
synced 2026-09-17 09:38:51 +00:00
3.8.7 - almost there
This commit is contained in:
@@ -65,7 +65,7 @@ struct AppCommands: Commands {
|
||||
undoTrash(appState: appState) {
|
||||
reloadAppsList(appState: appState, fsm: fsm)
|
||||
for app in appState.trashedFiles {
|
||||
AppPathFinder(appInfo: app, appState: appState, locations: locations, undo: true).findPaths()
|
||||
AppPathFinder(appInfo: app, locations: locations, appState: appState, undo: true).findPaths()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
////
|
||||
//// AppPathsFetch-Async.swift
|
||||
//// Pearcleaner
|
||||
////
|
||||
//// Created by Alin Lupascu on 6/27/24.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import AppKit
|
||||
//import SwiftUI
|
||||
//
|
||||
//actor PathFinderState {
|
||||
// var fileSize: [URL: Int64] = [:]
|
||||
// var fileIconData: [URL: Data?] = [:]
|
||||
//
|
||||
// func setFileDetails(for path: URL, size: Int64, icon: Data?) {
|
||||
// fileSize[path] = size
|
||||
// fileIconData[path] = icon
|
||||
// }
|
||||
//
|
||||
// func getFileSize() -> [URL: Int64] {
|
||||
// return fileSize
|
||||
// }
|
||||
//
|
||||
// func getFileIconData() -> [URL: Data?] {
|
||||
// return fileIconData
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//class AppPathFinderAsync {
|
||||
// private var appInfo: AppInfo
|
||||
// private var appState: AppState
|
||||
// private var locations: Locations
|
||||
// private var backgroundRun: Bool
|
||||
// private var reverseAddon: Bool
|
||||
// private var undo: Bool
|
||||
// private var completion: () -> Void = {}
|
||||
// private var collection: [URL] = []
|
||||
// private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
|
||||
// private var state = PathFinderState() // Actor instance
|
||||
//
|
||||
// init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, reverseAddon: Bool = false, undo: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
// self.appInfo = appInfo
|
||||
// self.appState = appState
|
||||
// self.locations = locations
|
||||
// self.backgroundRun = backgroundRun
|
||||
// self.reverseAddon = reverseAddon
|
||||
// self.undo = undo
|
||||
// self.completion = completion
|
||||
// }
|
||||
//
|
||||
// func findPaths() async {
|
||||
// await initialURLProcessing()
|
||||
// await withTaskGroup(of: Void.self) { group in
|
||||
// for location in self.locations.apps.paths {
|
||||
// group.addTask {
|
||||
// await self.processDirectoryLocation(location)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// await withTaskGroup(of: Void.self) { group in
|
||||
// for location in self.locations.apps.paths {
|
||||
// group.addTask {
|
||||
// await self.processFileLocation(location)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// await finalizeCollection()
|
||||
// }
|
||||
//
|
||||
// private func initialURLProcessing() async {
|
||||
// if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
|
||||
// let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
|
||||
// collectionAccessQueue.sync {
|
||||
// self.collection.append(modifiedUrl)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func processDirectoryLocation(_ location: String) async {
|
||||
// do {
|
||||
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
|
||||
// for item in contents {
|
||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
//
|
||||
// var isDirectory: ObjCBool = false
|
||||
// if FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory), isDirectory.boolValue {
|
||||
// if shouldSkipItem(itemL, at: itemURL) {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// collectionAccessQueue.sync {
|
||||
// let alreadyIncluded = self.collection.contains { existingURL in
|
||||
// itemURL.path.hasPrefix(existingURL.path)
|
||||
// }
|
||||
//
|
||||
// if !alreadyIncluded && specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||
// self.collection.append(itemURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// print("Error processing directory location: \(location), error: \(error)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func processFileLocation(_ location: String) async {
|
||||
// do {
|
||||
// let contents = try FileManager.default.contentsOfDirectory(atPath: location)
|
||||
// for item in contents {
|
||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
//
|
||||
// if FileManager.default.fileExists(atPath: itemURL.path),
|
||||
// !shouldSkipItem(itemL, at: itemURL),
|
||||
// specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||
// collectionAccessQueue.sync {
|
||||
// self.collection.append(itemURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// print("Error processing file location: \(location), error: \(error)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
|
||||
// var containsItem = false
|
||||
// collectionAccessQueue.sync {
|
||||
// containsItem = self.collection.contains(itemURL)
|
||||
// }
|
||||
// if containsItem || !isSupportedFileType(at: itemURL.path) {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// for skipCondition in skipConditions {
|
||||
// if itemL.hasPrefix(skipCondition.skipPrefix) {
|
||||
// let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
||||
// if !isAllowed {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// private func specificCondition(itemL: String, itemURL: URL) -> Bool {
|
||||
// let bundleIdentifierL = self.appInfo.bundleIdentifier.pearFormat()
|
||||
// let bundleComponents = self.appInfo.bundleIdentifier.components(separatedBy: ".").compactMap { $0 != "-" ? $0.lowercased() : nil }
|
||||
// let bundle = bundleComponents.suffix(2).joined()
|
||||
// let nameL = self.appInfo.appName.pearFormat()
|
||||
// let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
||||
//
|
||||
// for condition in conditions {
|
||||
// if bundleIdentifierL.contains(condition.bundle_id) {
|
||||
// let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
|
||||
// let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
|
||||
//
|
||||
// if hasExcludeKeyword {
|
||||
// return false
|
||||
// }
|
||||
// if hasIncludeKeyword {
|
||||
// if !condition.exclude.contains(where: itemL.contains) {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if self.appInfo.webApp {
|
||||
// return itemL.contains(bundleIdentifierL)
|
||||
// }
|
||||
//
|
||||
// return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP))
|
||||
// }
|
||||
//
|
||||
// func getAllContainers(bundleURL: URL) async -> [URL] {
|
||||
// await withCheckedContinuation { continuation in
|
||||
// var containers: [URL] = []
|
||||
//
|
||||
// let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
|
||||
//
|
||||
// guard let containerBundleIdentifier = bundleIdentifier else {
|
||||
// printOS("Get Containers: No bundle identifier found for the given bundle URL.")
|
||||
// continuation.resume(returning: containers)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
|
||||
// if FileManager.default.fileExists(atPath: groupContainer.path) {
|
||||
// containers.append(groupContainer)
|
||||
// }
|
||||
// } else {
|
||||
// printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
|
||||
// }
|
||||
//
|
||||
// let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
|
||||
//
|
||||
// do {
|
||||
// let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
//
|
||||
// let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
|
||||
//
|
||||
// for directory in containerDirectories {
|
||||
// let directoryName = directory.lastPathComponent
|
||||
//
|
||||
// if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
|
||||
// let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
|
||||
// if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
|
||||
// if applicationBundleID == self.appInfo.bundleIdentifier {
|
||||
// containers.append(directory)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// printOS("Error accessing the Containers directory: \(error)")
|
||||
// }
|
||||
//
|
||||
// continuation.resume(returning: containers)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleOutliers(include: Bool = true) -> [URL] {
|
||||
// var outliers: [URL] = []
|
||||
// let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
//
|
||||
// let matchingConditions = conditions.filter { condition in
|
||||
// bundleIdentifier.contains(condition.bundle_id)
|
||||
// }
|
||||
//
|
||||
// for condition in matchingConditions {
|
||||
// if include {
|
||||
// if let forceIncludes = condition.includeForce {
|
||||
// for path in forceIncludes {
|
||||
// outliers.append(path)
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// if let excludeForce = condition.excludeForce {
|
||||
// for path in excludeForce {
|
||||
// outliers.append(path)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return outliers
|
||||
// }
|
||||
//
|
||||
// private func finalizeCollection() async {
|
||||
// let allContainers = await getAllContainers(bundleURL: self.appInfo.path)
|
||||
// let outliers = handleOutliers()
|
||||
// let outliersEx = handleOutliers(include: false)
|
||||
// var tempCollection: [URL] = []
|
||||
// collectionAccessQueue.sync {
|
||||
// tempCollection = self.collection
|
||||
// }
|
||||
// tempCollection.append(contentsOf: allContainers)
|
||||
// tempCollection.append(contentsOf: outliers)
|
||||
//
|
||||
// let excludePaths = outliersEx.map { $0.path }
|
||||
// tempCollection.removeAll { url in
|
||||
// excludePaths.contains(url.path)
|
||||
// }
|
||||
//
|
||||
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
|
||||
// var filteredCollection: [URL] = []
|
||||
// var previousUrl: URL?
|
||||
// for url in sortedCollection {
|
||||
// if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
|
||||
// continue
|
||||
// }
|
||||
// filteredCollection.append(url)
|
||||
// previousUrl = url
|
||||
// }
|
||||
//
|
||||
// await handlePostProcessing(sortedCollection: filteredCollection)
|
||||
// }
|
||||
//
|
||||
// private func handlePostProcessing(sortedCollection: [URL]) async {
|
||||
// for path in sortedCollection {
|
||||
// let size = totalSizeOnDisk(for: path)
|
||||
// if let icon = getIconForFileOrFolderNS(atPath: path) {
|
||||
// let iconData = serializeImage(icon)
|
||||
// await state.setFileDetails(for: path, size: size.real, icon: iconData)
|
||||
// } else {
|
||||
// await state.setFileDetails(for: path, size: size.real, icon: nil)
|
||||
// }
|
||||
// }
|
||||
// await updateAppState(with: sortedCollection)
|
||||
// }
|
||||
//
|
||||
// private func updateAppState(with sortedCollection: [URL]) async {
|
||||
// let fileSize = await self.state.getFileSize()
|
||||
// let fileIconData = await self.state.getFileIconData()
|
||||
// let fileIcons = fileIconData.mapValues { deserializeImage($0) }
|
||||
// let arch = checkAppBundleArchitecture(at: self.appInfo.path.path)
|
||||
//
|
||||
// self.appInfo.fileSize = fileSize
|
||||
// self.appInfo.fileIcon = fileIcons
|
||||
// self.appInfo.arch = arch
|
||||
//
|
||||
// await MainActor.run {
|
||||
// if !self.backgroundRun {
|
||||
// self.appState.appInfo = self.appInfo
|
||||
// if !self.undo {
|
||||
// self.appState.selectedItems = Set(sortedCollection)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if self.reverseAddon {
|
||||
// self.appState.appInfoStore.append(self.appInfo)
|
||||
// }
|
||||
//
|
||||
// self.completion()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//func serializeImage(_ image: NSImage?) -> Data? {
|
||||
// guard let image = image else { return nil }
|
||||
// guard let tiffData = image.tiffRepresentation else { return nil }
|
||||
// let bitmapImage = NSBitmapImageRep(data: tiffData)
|
||||
// return bitmapImage?.representation(using: .png, properties: [:])
|
||||
//}
|
||||
//
|
||||
//func deserializeImage(_ data: Data?) -> NSImage? {
|
||||
// guard let data = data else { return nil }
|
||||
// return NSImage(data: data)
|
||||
//}
|
||||
@@ -11,42 +11,28 @@ import SwiftUI
|
||||
import AlinFoundation
|
||||
|
||||
class AppPathFinder {
|
||||
// Shared properties
|
||||
private var appInfo: AppInfo
|
||||
private var appState: AppState
|
||||
private var locations: Locations
|
||||
private var backgroundRun: Bool
|
||||
private var undo: Bool
|
||||
private var completion: () -> Void = {}
|
||||
private var collection: [URL] = []
|
||||
private var containerCollection: [URL] = []
|
||||
private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess")
|
||||
|
||||
init(appInfo: AppInfo = .empty, appState: AppState, locations: Locations, backgroundRun: Bool = false, undo: Bool = false, completion: @escaping () -> Void = {}) {
|
||||
// GUI-specific properties (can be nil for CLI)
|
||||
private var appState: AppState?
|
||||
private var undo: Bool = false
|
||||
private var completion: (() -> Void)?
|
||||
|
||||
// Initializer for both CLI and GUI
|
||||
init(appInfo: AppInfo, locations: Locations, appState: AppState? = nil, undo: Bool = false, completion: (() -> Void)? = nil) {
|
||||
self.appInfo = appInfo
|
||||
self.appState = appState
|
||||
self.locations = locations
|
||||
self.backgroundRun = backgroundRun
|
||||
self.appState = appState
|
||||
self.undo = undo
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func findPaths() {
|
||||
Task(priority: .background) {
|
||||
if self.appInfo.webApp {
|
||||
containerCollection = self.getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
self.finalizeCollection()
|
||||
} else {
|
||||
containerCollection = self.getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
self.collectDirectories()
|
||||
self.collectFiles()
|
||||
self.finalizeCollection()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Shared Methods
|
||||
private func initialURLProcessing() {
|
||||
if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
|
||||
let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
|
||||
@@ -54,18 +40,55 @@ class AppPathFinder {
|
||||
}
|
||||
}
|
||||
|
||||
private func collectDirectories() {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
private func getAllContainers(bundleURL: URL) -> [URL] {
|
||||
var containers: [URL] = []
|
||||
|
||||
for location in self.locations.apps.paths {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.processDirectoryLocation(location)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
// Extract bundle identifier from bundleURL
|
||||
let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
|
||||
|
||||
// Ensure the bundleIdentifier is not nil
|
||||
guard let containerBundleIdentifier = bundleIdentifier else {
|
||||
printOS("Get Containers: No bundle identifier found for the given bundle URL.")
|
||||
return containers // Returns whatever was found so far, possibly empty
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
// Get the regular container URL for the extracted bundle identifier
|
||||
if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
|
||||
if FileManager.default.fileExists(atPath: groupContainer.path) {
|
||||
containers.append(groupContainer)
|
||||
}
|
||||
} else {
|
||||
printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
|
||||
}
|
||||
|
||||
let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
|
||||
|
||||
do {
|
||||
let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
|
||||
// Define a regular expression to match UUID format
|
||||
let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
|
||||
|
||||
for directory in containerDirectories {
|
||||
let directoryName = directory.lastPathComponent
|
||||
|
||||
// Check if the directory name matches the UUID pattern
|
||||
if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
|
||||
// Attempt to read the metadata plist file
|
||||
let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
|
||||
if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
|
||||
if applicationBundleID == self.appInfo.bundleIdentifier {
|
||||
containers.append(directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
printOS("Error accessing the Containers directory: \(error)")
|
||||
}
|
||||
|
||||
// Return all found containers
|
||||
return containers
|
||||
}
|
||||
|
||||
private func processDirectoryLocation(_ location: String) {
|
||||
@@ -95,20 +118,6 @@ class AppPathFinder {
|
||||
}
|
||||
}
|
||||
|
||||
private func collectFiles() {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for location in self.locations.apps.paths {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.processFileLocation(location)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
}
|
||||
|
||||
private func processFileLocation(_ location: String) {
|
||||
if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) {
|
||||
for item in contents {
|
||||
@@ -126,6 +135,114 @@ class AppPathFinder {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleOutliers(include: Bool = true) -> [URL] {
|
||||
var outliers: [URL] = []
|
||||
let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
|
||||
// Find conditions that match the current app's bundle identifier
|
||||
let matchingConditions = conditions.filter { condition in
|
||||
bundleIdentifier.contains(condition.bundle_id)
|
||||
}
|
||||
|
||||
|
||||
for condition in matchingConditions {
|
||||
if include {
|
||||
// Handle includeForce
|
||||
if let forceIncludes = condition.includeForce {
|
||||
for path in forceIncludes {
|
||||
outliers.append(path)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle excludeForce
|
||||
if let excludeForce = condition.excludeForce {
|
||||
for path in excludeForce {
|
||||
outliers.append(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return outliers
|
||||
}
|
||||
|
||||
|
||||
// MARK: - findPaths Methods
|
||||
func findPaths() {
|
||||
Task(priority: .background) {
|
||||
if self.appInfo.webApp {
|
||||
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
self.finalizeCollection()
|
||||
} else {
|
||||
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
self.collectDirectories()
|
||||
self.collectFiles()
|
||||
self.finalizeCollection()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func findPathsCLI() -> Set<URL> {
|
||||
if self.appInfo.webApp {
|
||||
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
return finalizeCollectionCLI() // Return the collected paths
|
||||
} else {
|
||||
containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
self.initialURLProcessing()
|
||||
self.collectDirectoriesCLI() // Synchronous version
|
||||
self.collectFilesCLI() // Synchronous version
|
||||
return finalizeCollectionCLI() // Return the collected paths
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Unique Methods
|
||||
|
||||
|
||||
private func collectDirectories() {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for location in self.locations.apps.paths {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.processDirectoryLocation(location)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
}
|
||||
|
||||
private func collectFiles() {
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for location in self.locations.apps.paths {
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
self.processFileLocation(location)
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
}
|
||||
|
||||
|
||||
private func collectDirectoriesCLI() {
|
||||
for location in self.locations.apps.paths {
|
||||
processDirectoryLocation(location)
|
||||
}
|
||||
}
|
||||
|
||||
private func collectFilesCLI() {
|
||||
for location in self.locations.apps.paths {
|
||||
processFileLocation(location)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
|
||||
@@ -185,93 +302,9 @@ class AppPathFinder {
|
||||
}
|
||||
|
||||
|
||||
private func getAllContainers(bundleURL: URL) -> [URL] {
|
||||
var containers: [URL] = []
|
||||
|
||||
// Extract bundle identifier from bundleURL
|
||||
let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
|
||||
|
||||
// Ensure the bundleIdentifier is not nil
|
||||
guard let containerBundleIdentifier = bundleIdentifier else {
|
||||
printOS("Get Containers: No bundle identifier found for the given bundle URL.")
|
||||
return containers // Returns whatever was found so far, possibly empty
|
||||
}
|
||||
|
||||
// Get the regular container URL for the extracted bundle identifier
|
||||
if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
|
||||
if FileManager.default.fileExists(atPath: groupContainer.path) {
|
||||
containers.append(groupContainer)
|
||||
}
|
||||
} else {
|
||||
printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
|
||||
}
|
||||
|
||||
let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
|
||||
|
||||
do {
|
||||
let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
|
||||
// Define a regular expression to match UUID format
|
||||
let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
|
||||
|
||||
for directory in containerDirectories {
|
||||
let directoryName = directory.lastPathComponent
|
||||
|
||||
// Check if the directory name matches the UUID pattern
|
||||
if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
|
||||
// Attempt to read the metadata plist file
|
||||
let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
|
||||
if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
|
||||
if applicationBundleID == self.appInfo.bundleIdentifier {
|
||||
containers.append(directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
printOS("Error accessing the Containers directory: \(error)")
|
||||
}
|
||||
|
||||
// Return all found containers
|
||||
return containers
|
||||
}
|
||||
|
||||
|
||||
private func handleOutliers(include: Bool = true) -> [URL] {
|
||||
var outliers: [URL] = []
|
||||
let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
|
||||
// Find conditions that match the current app's bundle identifier
|
||||
let matchingConditions = conditions.filter { condition in
|
||||
bundleIdentifier.contains(condition.bundle_id)
|
||||
}
|
||||
|
||||
|
||||
for condition in matchingConditions {
|
||||
if include {
|
||||
// Handle includeForce
|
||||
if let forceIncludes = condition.includeForce {
|
||||
for path in forceIncludes {
|
||||
outliers.append(path)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle excludeForce
|
||||
if let excludeForce = condition.excludeForce {
|
||||
for path in excludeForce {
|
||||
outliers.append(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return outliers
|
||||
}
|
||||
|
||||
private func finalizeCollection() {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
// let allContainers = self.getAllContainers(bundleURL: self.appInfo.path)
|
||||
let outliers = self.handleOutliers()
|
||||
let outliersEx = self.handleOutliers(include: false)
|
||||
var tempCollection: [URL] = []
|
||||
@@ -302,10 +335,48 @@ class AppPathFinder {
|
||||
}
|
||||
|
||||
self.handlePostProcessing(sortedCollection: filteredCollection)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func finalizeCollectionCLI() -> Set<URL> {
|
||||
let outliers = handleOutliers()
|
||||
let outliersEx = handleOutliers(include: false)
|
||||
var tempCollection: [URL] = []
|
||||
self.collectionAccessQueue.sync {
|
||||
tempCollection = self.collection
|
||||
}
|
||||
tempCollection.append(contentsOf: self.containerCollection)
|
||||
tempCollection.append(contentsOf: outliers)
|
||||
|
||||
// Remove excluded paths
|
||||
let excludePaths = outliersEx.map { $0.path }
|
||||
tempCollection.removeAll { url in
|
||||
excludePaths.contains(url.path)
|
||||
}
|
||||
|
||||
// Sort and filter subdirectories
|
||||
let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
|
||||
var filteredCollection: [URL] = []
|
||||
var previousUrl: URL?
|
||||
|
||||
for url in sortedCollection {
|
||||
if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
|
||||
continue
|
||||
}
|
||||
filteredCollection.append(url)
|
||||
previousUrl = url
|
||||
}
|
||||
|
||||
// Remove Trash paths if necessary
|
||||
if filteredCollection.count == 1, let firstURL = filteredCollection.first, firstURL.path.contains(".Trash") {
|
||||
filteredCollection.removeAll()
|
||||
}
|
||||
|
||||
return Set(filteredCollection)
|
||||
}
|
||||
|
||||
private func handlePostProcessing(sortedCollection: [URL]) {
|
||||
// Calculate file details (sizes and icons), update app state, and call completion
|
||||
var fileSize: [URL: Int64] = [:]
|
||||
@@ -321,11 +392,12 @@ class AppPathFinder {
|
||||
|
||||
let arch = checkAppBundleArchitecture(at: self.appInfo.path.path)
|
||||
|
||||
var updatedCollection = sortedCollection
|
||||
if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") {
|
||||
updatedCollection.removeAll()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
var updatedCollection = sortedCollection
|
||||
if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") {
|
||||
updatedCollection.removeAll()
|
||||
}
|
||||
|
||||
// Update appInfo and appState with the new values
|
||||
self.appInfo.fileSize = fileSize
|
||||
@@ -333,19 +405,285 @@ class AppPathFinder {
|
||||
self.appInfo.fileIcon = fileIcon
|
||||
self.appInfo.arch = arch
|
||||
|
||||
if !self.backgroundRun {
|
||||
self.appState.appInfo = self.appInfo
|
||||
if !self.undo {
|
||||
self.appState.selectedItems = Set(updatedCollection)
|
||||
}
|
||||
self.appState?.appInfo = self.appInfo
|
||||
if !self.undo {
|
||||
self.appState?.selectedItems = Set(updatedCollection)
|
||||
}
|
||||
|
||||
// Append object to store if running reverse search with empty store
|
||||
// if self.reverseAddon {
|
||||
// self.appState.appInfoStore.append(self.appInfo)
|
||||
// }
|
||||
|
||||
self.completion()
|
||||
self.completion?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// CLI // =========================================================================================================================
|
||||
|
||||
|
||||
//class AppPathFinderCLI {
|
||||
// private var appInfo: AppInfo
|
||||
// private var locations: Locations
|
||||
// private var collection: [URL] = []
|
||||
// private var containerCollection: [URL] = []
|
||||
// private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinderCLI.collectionAccess")
|
||||
//
|
||||
// init(appInfo: AppInfo = .empty, locations: Locations) {
|
||||
// self.appInfo = appInfo
|
||||
// self.locations = locations
|
||||
// }
|
||||
//
|
||||
// // Make this function synchronous for the CLI
|
||||
// func findPaths() -> Set<URL> {
|
||||
// if self.appInfo.webApp {
|
||||
// containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
// self.initialURLProcessing()
|
||||
// return finalizeCollection() // Return the collected paths
|
||||
// } else {
|
||||
// containerCollection = getAllContainers(bundleURL: self.appInfo.path)
|
||||
// self.initialURLProcessing()
|
||||
// self.collectDirectories() // Synchronous version
|
||||
// self.collectFiles() // Synchronous version
|
||||
// return finalizeCollection() // Return the collected paths
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func initialURLProcessing() {
|
||||
// if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") {
|
||||
// let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url
|
||||
// self.collection.append(modifiedUrl)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func shouldSkipItem(_ itemL: String, at itemURL: URL) -> Bool {
|
||||
// var containsItem = false
|
||||
// collectionAccessQueue.sync {
|
||||
// containsItem = self.collection.contains(itemURL)
|
||||
// }
|
||||
// if containsItem || !isSupportedFileType(at: itemURL.path) {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// for skipCondition in skipConditions {
|
||||
// if itemL.hasPrefix(skipCondition.skipPrefix) {
|
||||
// let isAllowed = skipCondition.allowPrefixes.contains(where: itemL.hasPrefix)
|
||||
// if !isAllowed {
|
||||
// return true // Skip because it starts with a base prefix but is not in the allowed list
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// private func processDirectoryLocation(_ location: String) {
|
||||
// if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) {
|
||||
// for item in contents {
|
||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
//
|
||||
// var isDirectory: ObjCBool = false
|
||||
// if FileManager.default.fileExists(atPath: itemURL.path, isDirectory: &isDirectory), isDirectory.boolValue {
|
||||
// // Perform the check to skip the item if needed
|
||||
// if shouldSkipItem(itemL, at: itemURL) {
|
||||
// continue
|
||||
// }
|
||||
//
|
||||
// collectionAccessQueue.sync {
|
||||
// let alreadyIncluded = self.collection.contains { existingURL in
|
||||
// itemURL.path.hasPrefix(existingURL.path)
|
||||
// }
|
||||
//
|
||||
// if !alreadyIncluded && specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||
// self.collection.append(itemURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func specificCondition(itemL: String, itemURL: URL) -> Bool {
|
||||
// let bundleIdentifierL = self.appInfo.bundleIdentifier.pearFormat()
|
||||
// let bundleComponents = self.appInfo.bundleIdentifier.components(separatedBy: ".").compactMap { $0 != "-" ? $0.lowercased() : nil }
|
||||
// let bundle = bundleComponents.suffix(2).joined()
|
||||
// let nameL = self.appInfo.appName.pearFormat()
|
||||
// let nameLFiltered = nameL.filter { $0.isLetter }
|
||||
//
|
||||
// let nameP = self.appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "")
|
||||
//
|
||||
// for condition in conditions {
|
||||
// if bundleIdentifierL.contains(condition.bundle_id) {
|
||||
// // Exclude keywords
|
||||
// let hasExcludeKeyword = condition.exclude.contains(where: itemL.contains)
|
||||
// if hasExcludeKeyword {
|
||||
// return false
|
||||
// }
|
||||
// // Include keywords
|
||||
// let hasIncludeKeyword = condition.include.contains(where: itemL.contains)
|
||||
// if hasIncludeKeyword {
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
// if self.appInfo.webApp {
|
||||
// return itemL.contains(bundleIdentifierL)
|
||||
// }
|
||||
//
|
||||
// return itemL.contains(bundleIdentifierL) || itemL.contains(bundle) || (nameL.count > 3 && itemL.contains(nameL)) || (nameP.count > 3 && itemL.contains(nameP) || (nameLFiltered.count > 3 && itemL.contains(nameLFiltered)))
|
||||
//
|
||||
// }
|
||||
//
|
||||
// // Make collectDirectories synchronous
|
||||
// private func collectDirectories() {
|
||||
// for location in self.locations.apps.paths {
|
||||
// self.processDirectoryLocation(location) // Synchronous call
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func processFileLocation(_ location: String) {
|
||||
// if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) {
|
||||
// for item in contents {
|
||||
// let itemURL = URL(fileURLWithPath: location).appendingPathComponent(item)
|
||||
// let itemL = item.replacingOccurrences(of: ".", with: "").replacingOccurrences(of: " ", with: "").lowercased()
|
||||
//
|
||||
// if FileManager.default.fileExists(atPath: itemURL.path),
|
||||
// !shouldSkipItem(itemL, at: itemURL),
|
||||
// specificCondition(itemL: itemL, itemURL: itemURL) {
|
||||
// collectionAccessQueue.sync {
|
||||
// self.collection.append(itemURL)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func getAllContainers(bundleURL: URL) -> [URL] {
|
||||
// var containers: [URL] = []
|
||||
//
|
||||
// // Extract bundle identifier from bundleURL
|
||||
// let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier
|
||||
//
|
||||
// // Ensure the bundleIdentifier is not nil
|
||||
// guard let containerBundleIdentifier = bundleIdentifier else {
|
||||
// printOS("Get Containers: No bundle identifier found for the given bundle URL.")
|
||||
// return containers // Returns whatever was found so far, possibly empty
|
||||
// }
|
||||
//
|
||||
// // Get the regular container URL for the extracted bundle identifier
|
||||
// if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) {
|
||||
// if FileManager.default.fileExists(atPath: groupContainer.path) {
|
||||
// containers.append(groupContainer)
|
||||
// }
|
||||
// } else {
|
||||
// printOS("Get Containers: Failed to retrieve container URL for bundle identifier: \(containerBundleIdentifier)")
|
||||
// }
|
||||
//
|
||||
// let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers")
|
||||
//
|
||||
// do {
|
||||
// let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath!, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
//
|
||||
// // Define a regular expression to match UUID format
|
||||
// let uuidRegex = try NSRegularExpression(pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive)
|
||||
//
|
||||
// for directory in containerDirectories {
|
||||
// let directoryName = directory.lastPathComponent
|
||||
//
|
||||
// // Check if the directory name matches the UUID pattern
|
||||
// if uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil {
|
||||
// // Attempt to read the metadata plist file
|
||||
// let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist")
|
||||
// if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String {
|
||||
// if applicationBundleID == self.appInfo.bundleIdentifier {
|
||||
// containers.append(directory)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// printOS("Error accessing the Containers directory: \(error)")
|
||||
// }
|
||||
//
|
||||
// // Return all found containers
|
||||
// return containers
|
||||
// }
|
||||
//
|
||||
// // Make collectFiles synchronous
|
||||
// private func collectFiles() {
|
||||
// for location in self.locations.apps.paths {
|
||||
// self.processFileLocation(location) // Synchronous call
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleOutliers(include: Bool = true) -> [URL] {
|
||||
// var outliers: [URL] = []
|
||||
// let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat()
|
||||
//
|
||||
// // Find conditions that match the current app's bundle identifier
|
||||
// let matchingConditions = conditions.filter { condition in
|
||||
// bundleIdentifier.contains(condition.bundle_id)
|
||||
// }
|
||||
//
|
||||
//
|
||||
// for condition in matchingConditions {
|
||||
// if include {
|
||||
// // Handle includeForce
|
||||
// if let forceIncludes = condition.includeForce {
|
||||
// for path in forceIncludes {
|
||||
// outliers.append(path)
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // Handle excludeForce
|
||||
// if let excludeForce = condition.excludeForce {
|
||||
// for path in excludeForce {
|
||||
// outliers.append(path)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// return outliers
|
||||
// }
|
||||
//
|
||||
// // No need to dispatch asynchronously here
|
||||
// private func finalizeCollection() -> Set<URL> {
|
||||
// let outliers = self.handleOutliers()
|
||||
// let outliersEx = self.handleOutliers(include: false)
|
||||
// var tempCollection: [URL] = []
|
||||
// self.collectionAccessQueue.sync {
|
||||
// tempCollection = self.collection
|
||||
// }
|
||||
// tempCollection.append(contentsOf: self.containerCollection)
|
||||
// tempCollection.append(contentsOf: outliers)
|
||||
//
|
||||
// let excludePaths = outliersEx.map { $0.path }
|
||||
// tempCollection.removeAll { url in
|
||||
// excludePaths.contains(url.path)
|
||||
// }
|
||||
//
|
||||
// let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path })
|
||||
// var filteredCollection: [URL] = []
|
||||
// var previousUrl: URL?
|
||||
//
|
||||
// for url in sortedCollection {
|
||||
// if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") {
|
||||
// continue
|
||||
// }
|
||||
// filteredCollection.append(url)
|
||||
// previousUrl = url
|
||||
// }
|
||||
//
|
||||
// // Handle Trash directory case
|
||||
// var updatedCollection = filteredCollection
|
||||
// if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") {
|
||||
// // If only one item exists and it's in the Trash, remove it
|
||||
// updatedCollection.removeAll()
|
||||
// }
|
||||
//
|
||||
// return Set(updatedCollection)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@@ -145,7 +145,7 @@ func showAppInFiles(appInfo: AppInfo, appState: AppState, locations: Locations,
|
||||
appState.showProgress = true
|
||||
|
||||
// Initialize the path finder and execute its search.
|
||||
AppPathFinder(appInfo: appInfo, appState: appState, locations: locations) {
|
||||
AppPathFinder(appInfo: appInfo, locations: locations, appState: appState) {
|
||||
updateOnMain {
|
||||
// Update the progress indicator on the main thread once the search completes.
|
||||
appState.showProgress = false
|
||||
@@ -210,7 +210,42 @@ func moveFilesToTrash(appState: AppState, at fileURLs: [URL], completion: @escap
|
||||
|
||||
}
|
||||
|
||||
func moveFilesToTrashCLI(at fileURLs: [URL]) -> Bool {
|
||||
// Stop Sentinel FileWatcher momentarily to ignore .app bundle being sent to Trash
|
||||
sendStopNotificationFW()
|
||||
|
||||
// Create the AppleScript for moving files to the Trash
|
||||
let posixFiles = fileURLs.map { item in
|
||||
return "POSIX file \"\(item.path)\"" + (item == fileURLs.last ? "" : ", ")}.joined()
|
||||
|
||||
let scriptSource = """
|
||||
tell application \"Finder\" to delete { \(posixFiles) }
|
||||
"""
|
||||
|
||||
var error: NSDictionary?
|
||||
if let scriptObject = NSAppleScript(source: scriptSource) {
|
||||
let output: NSAppleEventDescriptor = scriptObject.executeAndReturnError(&error)
|
||||
|
||||
// Handle any AppleScript errors
|
||||
if let error = error {
|
||||
print("Trash Error: \(error)") // Synchronous error reporting
|
||||
return false // Indicate failure
|
||||
}
|
||||
|
||||
// Check if output is null, indicating the user canceled the operation
|
||||
if output.descriptorType == typeNull {
|
||||
print("Trash Error: operation canceled by the user") // Synchronous cancellation reporting
|
||||
return false // Indicate failure due to cancellation
|
||||
}
|
||||
|
||||
// Process output if it exists
|
||||
if let outputString = output.stringValue {
|
||||
print("Trash: \(outputString)")
|
||||
}
|
||||
}
|
||||
|
||||
return true // Indicate success
|
||||
}
|
||||
|
||||
|
||||
// Undo trash action
|
||||
|
||||
@@ -56,13 +56,215 @@ func findAndSetWindowFrame(named titles: [String], windowSettings: WindowSetting
|
||||
}
|
||||
|
||||
|
||||
// Process CLI // ========================================================================================================
|
||||
func processCLI(arguments: [String], appState: AppState, locations: Locations) {
|
||||
let options = Array(arguments.dropFirst()) // Remove the first argument (binary path)
|
||||
|
||||
// Private function to list files for uninstall, using the provided path
|
||||
func listFiles(at path: String) {
|
||||
// Convert the provided string path to a URL
|
||||
let url = URL(fileURLWithPath: path)
|
||||
|
||||
// print("[BETA] Pearcleaner CLI | List Files:\n")
|
||||
|
||||
// Fetch the app info and safely unwrap
|
||||
guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else {
|
||||
print("Error: Invalid path or unable to fetch app info at path: \(path)")
|
||||
exit(1) // Exit with non-zero code to indicate failure
|
||||
}
|
||||
|
||||
// Use the AppPathFinderCLI to find paths synchronously
|
||||
let appPathFinder = AppPathFinder(appInfo: appInfo, locations: locations)
|
||||
|
||||
// Call findPaths to get the Set of URLs
|
||||
let foundPaths = appPathFinder.findPathsCLI()
|
||||
|
||||
// Print each path in the Set to the console
|
||||
for path in foundPaths {
|
||||
print(path.path)
|
||||
}
|
||||
}
|
||||
|
||||
// Private function to uninstall the application bundle at a given path
|
||||
func uninstallApp(at path: String) {
|
||||
// Convert the provided string path to a URL
|
||||
let url = URL(fileURLWithPath: path)
|
||||
print("[BETA] Pearcleaner CLI | Uninstall Application:\n")
|
||||
|
||||
// Fetch the app info and safely unwrap
|
||||
guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else {
|
||||
print("Error: Invalid path or unable to fetch app info at path: \(path)")
|
||||
exit(1) // Exit with non-zero code to indicate failure
|
||||
}
|
||||
|
||||
killApp(appId: appInfo.bundleIdentifier) {
|
||||
let success = moveFilesToTrashCLI(at: [appInfo.path])
|
||||
if success {
|
||||
print("Application moved to the trash successfully.")
|
||||
exit(0)
|
||||
} else {
|
||||
print("Failed to move application to trash.")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private function to uninstall the application and all related files at a given path
|
||||
func uninstallAll(at path: String) {
|
||||
// Convert the provided string path to a URL
|
||||
let url = URL(fileURLWithPath: path)
|
||||
print("[BETA] Pearcleaner CLI | Uninstall Application + Related Files:\n")
|
||||
|
||||
// Fetch the app info and safely unwrap
|
||||
guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else {
|
||||
print("Error: Invalid path or unable to fetch app info at path: \(path)")
|
||||
exit(1) // Exit with non-zero code to indicate failure
|
||||
}
|
||||
|
||||
// Use the AppPathFinderCLI to find paths synchronously
|
||||
let appPathFinder = AppPathFinder(appInfo: appInfo, locations: locations)
|
||||
|
||||
// Call findPaths to get the Set of URLs
|
||||
let foundPaths = appPathFinder.findPathsCLI()
|
||||
|
||||
killApp(appId: appInfo.bundleIdentifier) {
|
||||
let success = moveFilesToTrashCLI(at: Array(foundPaths))
|
||||
if success {
|
||||
print("The following files have been moved to the trash successfully:\n")
|
||||
// Print each path in the Set to the console
|
||||
for path in foundPaths {
|
||||
print(path.path)
|
||||
}
|
||||
exit(0)
|
||||
} else {
|
||||
print("Failed to move application and related files to trash.")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle help option (-h or --help)
|
||||
if options.contains("-h") || options.contains("--help") {
|
||||
displayHelp()
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Handle --list or -l option with a path argument
|
||||
if let listIndex = options.firstIndex(where: { $0 == "--list" || $0 == "-l" }), listIndex + 1 < options.count {
|
||||
let path = options[listIndex + 1] // Path provided after --list or -l
|
||||
listFiles(at: path)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Handle --uninstall or -u option with a path argument
|
||||
if let uninstallIndex = options.firstIndex(where: { $0 == "--uninstall" || $0 == "-u" }), uninstallIndex + 1 < options.count {
|
||||
let path = options[uninstallIndex + 1] // Path provided after --uninstall or -u
|
||||
uninstallApp(at: path)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Handle --uninstall-all or -ua option with a path argument
|
||||
if let uninstallAllIndex = options.firstIndex(where: { $0 == "--uninstall-all" || $0 == "-ua" }), uninstallAllIndex + 1 < options.count {
|
||||
let path = options[uninstallAllIndex + 1] // Path provided after --uninstall-all or -ua
|
||||
uninstallAll(at: path)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// If no valid option was provided, show the help menu by default
|
||||
displayHelp()
|
||||
exit(0)
|
||||
}
|
||||
|
||||
// Private function to display help message
|
||||
func displayHelp() {
|
||||
print("""
|
||||
|
||||
[BETA] Pearcleaner CLI | Usage:
|
||||
|
||||
--list <path>, -l <path> Find application files available for uninstall at the specified path
|
||||
--uninstall <path>, -u <path> Remove only the application bundle at the specified path
|
||||
--uninstall-all <path>, -ua <path> Remove the application bundle and all related files at the specified path
|
||||
--help, -h Show this help message
|
||||
|
||||
""")
|
||||
}
|
||||
|
||||
|
||||
// Check if pearcleaner symlink exists
|
||||
func checkCLISymlink() -> Bool {
|
||||
let filePath = "/usr/local/bin/pearcleaner"
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// Check if the file exists at the given path
|
||||
return fileManager.fileExists(atPath: filePath)
|
||||
}
|
||||
|
||||
// Install/uninstall symlink for CLI
|
||||
func manageSymlink(install: Bool) {
|
||||
// Get the current running application's bundle binary path
|
||||
guard let appPath = Bundle.main.executablePath else {
|
||||
printOS("Error: Unable to get the executable path.")
|
||||
return
|
||||
}
|
||||
|
||||
// Path where the symlink should be created
|
||||
let symlinkPath = "/usr/local/bin/pearcleaner"
|
||||
|
||||
// Check if the symlink already exists
|
||||
let symlinkExists = checkCLISymlink()
|
||||
|
||||
// If we are installing the symlink and it already exists, skip creating it
|
||||
if install && symlinkExists {
|
||||
printOS("Symlink already exists at \(symlinkPath). No action needed.")
|
||||
return
|
||||
}
|
||||
|
||||
// If we are uninstalling the symlink and it doesn't exist, skip removing it
|
||||
if !install && !symlinkExists {
|
||||
printOS("Symlink does not exist at \(symlinkPath). No action needed.")
|
||||
return
|
||||
}
|
||||
|
||||
// Create AppleScript commands for installing or uninstalling the symlink
|
||||
let script: String
|
||||
|
||||
if install {
|
||||
// AppleScript to create a symlink with admin privileges
|
||||
script = """
|
||||
do shell script "ln -s '\(appPath)' '\(symlinkPath)'" with administrator privileges
|
||||
"""
|
||||
} else {
|
||||
// AppleScript to remove the symlink with admin privileges
|
||||
script = """
|
||||
do shell script "rm '\(symlinkPath)'" with administrator privileges
|
||||
"""
|
||||
}
|
||||
|
||||
// Execute the AppleScript
|
||||
var error: NSDictionary?
|
||||
if let scriptObject = NSAppleScript(source: script) {
|
||||
scriptObject.executeAndReturnError(&error)
|
||||
|
||||
if let error = error {
|
||||
printOS("AppleScript Error: \(error)")
|
||||
} else {
|
||||
if install {
|
||||
printOS("Symlink created successfully at \(symlinkPath).")
|
||||
} else {
|
||||
printOS("Symlink removed successfully from \(symlinkPath).")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printOS("Error: Unable to create the AppleScript object.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Brew cleanup
|
||||
func caskCleanup(app: String) {
|
||||
Task(priority: .high) {
|
||||
let formattedApp = app.lowercased().replacingOccurrences(of: " ", with: "-")
|
||||
print(formattedApp)
|
||||
|
||||
#if arch(x86_64)
|
||||
let cmd = "/usr/local/bin/brew"
|
||||
@@ -85,6 +287,8 @@ func caskCleanup(app: String) {
|
||||
end tell
|
||||
"""
|
||||
|
||||
print(script)
|
||||
|
||||
var error: NSDictionary?
|
||||
if let appleScript = NSAppleScript(source: script) {
|
||||
appleScript.executeAndReturnError(&error)
|
||||
@@ -98,7 +302,6 @@ func caskCleanup(app: String) {
|
||||
|
||||
|
||||
|
||||
|
||||
// Print list of files locally
|
||||
func saveURLsToFile(urls: Set<URL>, appState: AppState, copy: Bool = false) {
|
||||
|
||||
@@ -426,7 +629,7 @@ func uninstallPearcleaner(appState: AppState, locations: Locations) {
|
||||
let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL)
|
||||
|
||||
// Find application files for Pearcleaner
|
||||
AppPathFinder(appInfo: appInfo!, appState: appState, locations: locations, completion: {
|
||||
AppPathFinder(appInfo: appInfo!, locations: locations, appState: appState, completion: {
|
||||
// Kill Pearcleaner and tell Finder to trash the files
|
||||
let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") }
|
||||
let posixFiles = selectedItemsArray.map { item in
|
||||
|
||||
Reference in New Issue
Block a user