198 lines
9.1 KiB
Swift
198 lines
9.1 KiB
Swift
import Foundation
|
|
import Localization
|
|
|
|
/// 清理规则。目录清单参考了 Mole 的清理范围(仅参考范围,代码为独立实现)。
|
|
public enum RuleCatalog {
|
|
public static func rules() -> [CleanRule] {
|
|
[userCaches, logs] + browsers.map(browserRule) + [xcodeDerivedData, simulatorCaches, npmCache, xcodeArchives,
|
|
incompleteDownloads, installers, trash]
|
|
}
|
|
|
|
// MARK: 系统
|
|
|
|
/// 由其他规则单独处理,或清理后会影响系统功能的缓存目录
|
|
static let excludedCacheNames: Set<String> = [
|
|
"Google", "Firefox", "Microsoft Edge", "BraveSoftware", "Arc",
|
|
"com.google.Chrome", "com.microsoft.edgemac", "com.brave.Browser", "company.thebrowser.Browser",
|
|
"org.mozilla.firefox", "com.apple.Safari",
|
|
]
|
|
|
|
static let userCaches = CleanRule(
|
|
id: "system.caches", category: .system, title: tr("应用缓存"),
|
|
detail: tr("~/Library/Caches 中各应用的缓存,删除后会按需重建"), symbol: "archivebox",
|
|
checksOwnerApp: true
|
|
) { env in
|
|
try children(of: env.home + "/Library/Caches").filter { url in
|
|
let name = url.lastPathComponent
|
|
return !name.hasPrefix("com.apple.") && !excludedCacheNames.contains(name)
|
|
}
|
|
}
|
|
|
|
static let logs = CleanRule(
|
|
id: "system.logs", category: .system, title: tr("应用日志与崩溃报告"),
|
|
detail: tr("~/Library/Logs 中的日志和诊断报告"), symbol: "doc.text",
|
|
checksOwnerApp: true
|
|
) { env in
|
|
try children(of: env.home + "/Library/Logs")
|
|
}
|
|
|
|
// MARK: 浏览器
|
|
|
|
struct Browser: Sendable {
|
|
let id: String
|
|
let name: String
|
|
let bundleID: String
|
|
/// ~/Library/Caches 下的整个缓存目录
|
|
let cacheDirectories: [String]
|
|
/// ~/Library/Application Support 下的用户数据目录(其中的配置目录含 Code Cache 等)
|
|
let userDataDirectory: String?
|
|
}
|
|
|
|
static let browsers: [Browser] = [
|
|
Browser(id: "chrome", name: "Chrome", bundleID: "com.google.Chrome",
|
|
cacheDirectories: ["Google/Chrome"], userDataDirectory: "Google/Chrome"),
|
|
Browser(id: "edge", name: "Edge", bundleID: "com.microsoft.edgemac",
|
|
cacheDirectories: ["Microsoft Edge"], userDataDirectory: "Microsoft Edge"),
|
|
Browser(id: "brave", name: "Brave", bundleID: "com.brave.Browser",
|
|
cacheDirectories: ["BraveSoftware/Brave-Browser"], userDataDirectory: "BraveSoftware/Brave-Browser"),
|
|
Browser(id: "arc", name: "Arc", bundleID: "company.thebrowser.Browser",
|
|
cacheDirectories: ["Arc", "company.thebrowser.Browser"], userDataDirectory: "Arc/User Data"),
|
|
Browser(id: "firefox", name: "Firefox", bundleID: "org.mozilla.firefox",
|
|
cacheDirectories: [], userDataDirectory: nil),
|
|
Browser(id: "safari", name: "Safari", bundleID: "com.apple.Safari",
|
|
cacheDirectories: ["com.apple.Safari"], userDataDirectory: nil),
|
|
]
|
|
|
|
/// Chromium 配置目录内可安全清理的缓存子目录
|
|
static let chromiumProfileCaches = ["Code Cache", "GPUCache", "Service Worker/CacheStorage"]
|
|
|
|
static func browserRule(_ browser: Browser) -> CleanRule {
|
|
CleanRule(
|
|
id: "browser.\(browser.id)", category: .browser, title: tr("\(browser.name) 缓存"),
|
|
detail: tr("网页缓存与脚本缓存,不涉及 Cookie、历史记录和密码"), symbol: "globe",
|
|
blockingApps: [(browser.bundleID, browser.name)], minimumAge: 0
|
|
) { env in
|
|
let fileManager = FileManager.default
|
|
var urls = browser.cacheDirectories
|
|
.map { URL(fileURLWithPath: env.home + "/Library/Caches/" + $0) }
|
|
// 浏览器缓存目录本身属于 ~/Library/Caches 的子目录,逐个校验可读性
|
|
urls = try urls.filter { url in
|
|
guard fileManager.fileExists(atPath: url.path) else { return false }
|
|
_ = try children(of: url.path)
|
|
return true
|
|
}
|
|
|
|
if browser.id == "firefox" {
|
|
let profiles = (try? children(of: env.home + "/Library/Caches/Firefox/Profiles")) ?? []
|
|
urls += profiles.map { $0.appendingPathComponent("cache2") }
|
|
.filter { fileManager.fileExists(atPath: $0.path) }
|
|
}
|
|
|
|
if let userData = browser.userDataDirectory {
|
|
let base = env.home + "/Library/Application Support/" + userData
|
|
let profiles = ((try? children(of: base)) ?? []).filter(isChromiumProfile)
|
|
for profile in profiles {
|
|
urls += chromiumProfileCaches
|
|
.map { profile.appendingPathComponent($0) }
|
|
.filter { fileManager.fileExists(atPath: $0.path) }
|
|
}
|
|
}
|
|
return urls
|
|
}
|
|
}
|
|
|
|
static func isChromiumProfile(_ url: URL) -> Bool {
|
|
let name = url.lastPathComponent
|
|
guard name == "Default" || name.hasPrefix("Profile ") || name == "Guest Profile" else { return false }
|
|
return FileManager.default.fileExists(atPath: url.appendingPathComponent("Preferences").path)
|
|
}
|
|
|
|
// MARK: 开发者
|
|
|
|
static let xcodeDerivedData = CleanRule(
|
|
id: "developer.derivedData", category: .developer, title: tr("Xcode 编译缓存"),
|
|
detail: tr("DerivedData,下次编译时自动重建"), symbol: "hammer",
|
|
blockingApps: [("com.apple.dt.Xcode", "Xcode")]
|
|
) { env in
|
|
try childrenIfExists(of: env.home + "/Library/Developer/Xcode/DerivedData")
|
|
}
|
|
|
|
static let simulatorCaches = CleanRule(
|
|
id: "developer.simulatorCaches", category: .developer, title: tr("模拟器缓存"),
|
|
detail: tr("CoreSimulator 的动态库与运行时缓存"), symbol: "iphone",
|
|
blockingApps: [("com.apple.iphonesimulator", tr("模拟器"))]
|
|
) { env in
|
|
try childrenIfExists(of: env.home + "/Library/Developer/CoreSimulator/Caches")
|
|
}
|
|
|
|
static let npmCache = CleanRule(
|
|
id: "developer.npm", category: .developer, title: tr("npm 缓存"),
|
|
detail: tr("~/.npm/_cacache,安装依赖时自动重新下载"), symbol: "shippingbox"
|
|
) { env in
|
|
let path = env.home + "/.npm/_cacache"
|
|
return FileManager.default.fileExists(atPath: path) ? [URL(fileURLWithPath: path)] : []
|
|
}
|
|
|
|
static let xcodeArchives = CleanRule(
|
|
id: "developer.archives", category: .developer, title: tr("Xcode 归档"),
|
|
detail: tr("打包生成的 .xcarchive,删除后无法重新符号化旧版本崩溃日志"), symbol: "archivebox.fill",
|
|
selectedByDefault: false, policy: .trash, minimumAge: 0
|
|
) { env in
|
|
try childrenIfExists(of: env.home + "/Library/Developer/Xcode/Archives")
|
|
}
|
|
|
|
// MARK: 下载
|
|
|
|
static let incompleteDownloads = CleanRule(
|
|
id: "downloads.incomplete", category: .downloads, title: tr("未完成的下载"),
|
|
detail: tr("超过 1 天未更新的 .crdownload / .part / .download"), symbol: "arrow.down.circle",
|
|
policy: .trash, minimumAge: 86_400
|
|
) { env in
|
|
try childrenIfExists(of: env.home + "/Downloads")
|
|
.filter { ["crdownload", "part", "download"].contains($0.pathExtension.lowercased()) }
|
|
}
|
|
|
|
static let installers = CleanRule(
|
|
id: "downloads.installers", category: .downloads, title: tr("安装包"),
|
|
detail: tr("下载目录中的 .dmg / .pkg / .xip / .iso,移到废纸篓"), symbol: "shippingbox.fill",
|
|
selectedByDefault: false, policy: .trash, minimumAge: 0
|
|
) { env in
|
|
try childrenIfExists(of: env.home + "/Downloads")
|
|
.filter { ["dmg", "pkg", "xip", "iso"].contains($0.pathExtension.lowercased()) }
|
|
}
|
|
|
|
// MARK: 废纸篓
|
|
|
|
static let trash = CleanRule(
|
|
id: "trash", category: .trash, title: tr("清空废纸篓"),
|
|
detail: tr("永久删除废纸篓中的内容,无法恢复"), symbol: "trash",
|
|
selectedByDefault: false, minimumAge: 0
|
|
) { env in
|
|
try childrenIfExists(of: env.home + "/.Trash")
|
|
}
|
|
|
|
// MARK: 工具
|
|
|
|
/// 列出目录的直接子项(不含隐藏文件);无权限时抛出 CleanerAccessError
|
|
static func children(of path: String) throws -> [URL] {
|
|
do {
|
|
return try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: path),
|
|
includingPropertiesForKeys: nil,
|
|
options: [.skipsHiddenFiles])
|
|
} catch let error as CocoaError where error.code == .fileReadNoPermission {
|
|
throw CleanerAccessError.permissionDenied
|
|
} catch let error as NSError where error.domain == NSPOSIXErrorDomain && error.code == Int(EPERM) {
|
|
throw CleanerAccessError.permissionDenied
|
|
}
|
|
}
|
|
|
|
static func childrenIfExists(of path: String) throws -> [URL] {
|
|
guard FileManager.default.fileExists(atPath: path) else { return [] }
|
|
return try children(of: path)
|
|
}
|
|
}
|
|
|
|
public enum CleanerAccessError: Error, Sendable {
|
|
case permissionDenied
|
|
}
|