3.8.7 - almost there

This commit is contained in:
Alin
2024-09-18 17:22:51 -06:00
parent e181415d51
commit ef23d0a724
14 changed files with 864 additions and 517 deletions
+1 -1
View File
@@ -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()
}
}
}
-334
View File
@@ -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)
//}
+484 -146
View File
@@ -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)
// }
//
//}
+36 -1
View File
@@ -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
+206 -3
View File
@@ -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