Files

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
}