diff --git a/Packages/XStatsKit/Sources/Cleaner/RuleCatalog.swift b/Packages/XStatsKit/Sources/Cleaner/RuleCatalog.swift index f0ad0d2..e8f9921 100644 --- a/Packages/XStatsKit/Sources/Cleaner/RuleCatalog.swift +++ b/Packages/XStatsKit/Sources/Cleaner/RuleCatalog.swift @@ -6,7 +6,7 @@ public enum RuleCatalog { public static func rules() -> [CleanRule] { [userCaches, logs] + browsers.map(browserRule) + [xcodeDerivedData, simulatorCaches, npmCache, yarnCache, pnpmCache, bunCache, - goCache, rustCache, uvCache, xcodeArchives, + goCache, rustCache, uvCache, homebrewCache, xcodeArchives, incompleteDownloads, installers, trash] } @@ -16,7 +16,7 @@ public enum RuleCatalog { static let excludedCacheNames: Set = [ "Google", "Firefox", "Microsoft Edge", "BraveSoftware", "Arc", "com.google.Chrome", "com.microsoft.edgemac", "com.brave.Browser", "company.thebrowser.Browser", - "org.mozilla.firefox", "com.apple.Safari", + "org.mozilla.firefox", "com.apple.Safari", "Homebrew", ] static let userCaches = CleanRule( @@ -215,6 +215,30 @@ public enum RuleCatalog { try childrenIfExists(of: env.home + "/.cache/uv") } + static let homebrewCache = CleanRule( + id: "developer.homebrew", category: .developer, title: tr("Homebrew 下载缓存"), + detail: tr("超过 1 天的 Homebrew 下载文件,不影响已安装软件"), symbol: "shippingbox", + selectedByDefault: false, minimumAge: 86_400 + ) { env in + // 仅识别 Homebrew 已完成的内容寻址下载;同目录中的临时文件和其他缓存不属于本规则。 + let cacheDirectory = URL(fileURLWithPath: env.home + "/Library/Caches/Homebrew") + let downloads = cacheDirectory.appendingPathComponent("downloads") + guard FileManager.default.fileExists(atPath: downloads.path) else { return [] } + guard try cacheDirectory.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink != true, + try downloads.resourceValues(forKeys: [.isSymbolicLinkKey]).isSymbolicLink != true else { + return [] + } + return try children(of: downloads.path).filter { url in + let name = url.lastPathComponent + guard !name.hasSuffix(".incomplete"), + name.range(of: #"^[0-9a-f]{64}--.+$"#, options: .regularExpression) != nil, + let values = try? url.resourceValues(forKeys: [.isRegularFileKey, .isSymbolicLinkKey]) else { + return false + } + return values.isRegularFile == true && values.isSymbolicLink != true + } + } + static let xcodeArchives = CleanRule( id: "developer.archives", category: .developer, title: tr("Xcode 归档"), detail: tr("打包生成的 .xcarchive,删除后无法重新符号化旧版本崩溃日志"), symbol: "archivebox.fill", diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv index cddd19b..c311279 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ar.tsv @@ -116,6 +116,8 @@ uv 缓存 ذاكرة uv المؤقتة Yarn 缓存 ذاكرة Yarn المؤقتة pnpm 缓存 ذاكرة pnpm المؤقتة Bun 缓存 ذاكرة Bun المؤقتة +Homebrew 下载缓存 ذاكرة تنزيلات Homebrew المؤقتة +超过 1 天的 Homebrew 下载文件,不影响已安装软件 ملفات تنزيل Homebrew الأقدم من يوم واحد؛ لا تتأثر البرامج المثبتة npm 下载缓存,由 npm 自行清理 ذاكرة تنزيل npm المؤقتة، ينظفها npm بنفسه Yarn 全局与离线镜像缓存,由 Yarn 自行清理 ذاكرة Yarn العامة ومرآة العمل دون اتصال، ينظفها Yarn بنفسه pnpm 内容寻址存储,仅清理未被引用的包 مخزن pnpm المعنون بالمحتوى؛ تزال الحزم غير المشار إليها فقط diff --git a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv index 2bb6ded..ae36ba3 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/de.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/de.tsv @@ -116,6 +116,8 @@ uv 缓存 uv-Cache Yarn 缓存 Yarn-Cache pnpm 缓存 pnpm-Cache Bun 缓存 Bun-Cache +Homebrew 下载缓存 Homebrew-Download-Cache +超过 1 天的 Homebrew 下载文件,不影响已安装软件 Homebrew-Downloads, die älter als einen Tag sind; installierte Software bleibt erhalten npm 下载缓存,由 npm 自行清理 npm-Download-Cache; wird von npm selbst bereinigt Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Globaler Yarn- und Offline-Mirror-Cache; wird von Yarn selbst bereinigt pnpm 内容寻址存储,仅清理未被引用的包 Inhaltsadressierter pnpm-Speicher; nur nicht referenzierte Pakete werden entfernt diff --git a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv index 8372208..48b1e09 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/es.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/es.tsv @@ -116,6 +116,8 @@ uv 缓存 Caché de uv Yarn 缓存 Caché de Yarn pnpm 缓存 Caché de pnpm Bun 缓存 Caché de Bun +Homebrew 下载缓存 Caché de descargas de Homebrew +超过 1 天的 Homebrew 下载文件,不影响已安装软件 Descargas de Homebrew de más de un día; el software instalado no se verá afectado npm 下载缓存,由 npm 自行清理 Caché de descargas de npm, limpiada por npm Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Caché global y espejo sin conexión de Yarn, limpiadas por Yarn pnpm 内容寻址存储,仅清理未被引用的包 Almacén direccionado por contenido de pnpm; solo se eliminan paquetes sin referencias diff --git a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv index b0529a1..7a9b1e5 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/fr.tsv @@ -116,6 +116,8 @@ uv 缓存 Cache uv Yarn 缓存 Cache Yarn pnpm 缓存 Cache pnpm Bun 缓存 Cache Bun +Homebrew 下载缓存 Cache des téléchargements Homebrew +超过 1 天的 Homebrew 下载文件,不影响已安装软件 Téléchargements Homebrew datant de plus d’un jour ; les logiciels installés ne sont pas affectés npm 下载缓存,由 npm 自行清理 Cache de téléchargement npm, nettoyé par npm Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Caches global et miroir hors ligne de Yarn, nettoyés par Yarn pnpm 内容寻址存储,仅清理未被引用的包 Stockage adressé par contenu de pnpm ; seuls les paquets non référencés sont supprimés diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv index 3a98df5..3b5d3ca 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ja.tsv @@ -116,6 +116,8 @@ uv 缓存 uvキャッシュ Yarn 缓存 Yarnキャッシュ pnpm 缓存 pnpmキャッシュ Bun 缓存 Bunキャッシュ +Homebrew 下载缓存 Homebrewダウンロードキャッシュ +超过 1 天的 Homebrew 下载文件,不影响已安装软件 1日以上前のHomebrewダウンロードファイル。インストール済みのソフトウェアには影響しません npm 下载缓存,由 npm 自行清理 npmのダウンロードキャッシュ。npmでクリーンアップします Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarnのグローバルキャッシュとオフラインミラー。Yarnでクリーンアップします pnpm 内容寻址存储,仅清理未被引用的包 pnpmのコンテンツアドレスストア。参照されていないパッケージのみ削除します diff --git a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv index 3ed5e6a..39b6602 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/ko.tsv @@ -116,6 +116,8 @@ uv 缓存 uv 캐시 Yarn 缓存 Yarn 캐시 pnpm 缓存 pnpm 캐시 Bun 缓存 Bun 캐시 +Homebrew 下载缓存 Homebrew 다운로드 캐시 +超过 1 天的 Homebrew 下载文件,不影响已安装软件 1일 이상 지난 Homebrew 다운로드 파일만 정리하며 설치된 소프트웨어에는 영향을 주지 않습니다 npm 下载缓存,由 npm 自行清理 npm 다운로드 캐시이며 npm이 직접 정리합니다 Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarn 전역 및 오프라인 미러 캐시이며 Yarn이 직접 정리합니다 pnpm 内容寻址存储,仅清理未被引用的包 pnpm 콘텐츠 주소 지정 저장소이며 참조되지 않은 패키지만 정리합니다 diff --git a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv index 5f31b61..c3c71dc 100644 --- a/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv +++ b/Packages/XStatsKit/Sources/Localization/Resources/zh-Hant.tsv @@ -117,6 +117,8 @@ uv 缓存 uv 緩存 Yarn 缓存 Yarn 緩存 pnpm 缓存 pnpm 緩存 Bun 缓存 Bun 緩存 +Homebrew 下载缓存 Homebrew 下載緩存 +超过 1 天的 Homebrew 下载文件,不影响已安装软件 超過 1 天的 Homebrew 下載檔案,不影響已安裝軟件 npm 下载缓存,由 npm 自行清理 npm 下載緩存,由 npm 自行清理 Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarn 全局與離線鏡像緩存,由 Yarn 自行清理 pnpm 内容寻址存储,仅清理未被引用的包 pnpm 內容尋址存儲,僅清理未被引用的包 diff --git a/Packages/XStatsKit/Sources/Localization/Translations.swift b/Packages/XStatsKit/Sources/Localization/Translations.swift index cd06db8..d574d57 100644 --- a/Packages/XStatsKit/Sources/Localization/Translations.swift +++ b/Packages/XStatsKit/Sources/Localization/Translations.swift @@ -241,6 +241,8 @@ uv 缓存 uv Cache Yarn 缓存 Yarn Cache pnpm 缓存 pnpm Cache Bun 缓存 Bun Cache +Homebrew 下载缓存 Homebrew Download Cache +超过 1 天的 Homebrew 下载文件,不影响已安装软件 Homebrew downloads older than one day; installed software is unaffected npm 下载缓存,由 npm 自行清理 npm download cache; cleaned by npm Yarn 全局与离线镜像缓存,由 Yarn 自行清理 Yarn global and offline mirror caches; cleaned by Yarn pnpm 内容寻址存储,仅清理未被引用的包 pnpm content-addressable store; only unreferenced packages are removed diff --git a/Packages/XStatsKit/Sources/XStatsUI/State/CleanerController.swift b/Packages/XStatsKit/Sources/XStatsUI/State/CleanerController.swift index ca30f8c..8dcca4c 100644 --- a/Packages/XStatsKit/Sources/XStatsUI/State/CleanerController.swift +++ b/Packages/XStatsKit/Sources/XStatsUI/State/CleanerController.swift @@ -66,11 +66,11 @@ public final class CleanerController { task = Task { let result = await CleanEngine.clean(scans, selected: selection, preferTrash: preferTrash, environment: environment) report = result - phase = .finished // 清理后重新计算剩余可清理空间 let rescanned = await CleanEngine.scan(rules, environment: Self.environment()) self.scans = rescanned lastScan = Date() + phase = .finished } } diff --git a/Packages/XStatsKit/Tests/CleanerTests/CleanerTests.swift b/Packages/XStatsKit/Tests/CleanerTests/CleanerTests.swift index 3fae451..0ff5e9c 100644 --- a/Packages/XStatsKit/Tests/CleanerTests/CleanerTests.swift +++ b/Packages/XStatsKit/Tests/CleanerTests/CleanerTests.swift @@ -122,12 +122,55 @@ struct DeveloperCacheRuleTests { let rules = Dictionary(uniqueKeysWithValues: RuleCatalog.rules().map { ($0.id, $0) }) for id in ["developer.npm", "developer.yarn", "developer.pnpm", "developer.bun", - "developer.go", "developer.rust", "developer.uv"] { + "developer.go", "developer.rust", "developer.uv", "developer.homebrew"] { #expect(rules[id]?.category == .developer) #expect(rules[id]?.selectedByDefault == false) } } + @Test func homebrewRuleOnlyCleansCompletedOlderDownloads() async throws { + let home = try FakeHome() + defer { home.remove() } + let hash = String(repeating: "a", count: 64) + let oldDownload = try home.file("Library/Caches/Homebrew/downloads/\(hash)--wget-1.0.tar.gz", age: 172_800) + let recentDownload = try home.file("Library/Caches/Homebrew/downloads/\(hash)--curl-1.0.tar.gz", age: 60) + let incomplete = try home.file("Library/Caches/Homebrew/downloads/\(hash)--git-1.0.tar.gz.incomplete", age: 172_800) + let unrelated = try home.file("Library/Caches/Homebrew/downloads/notes.txt", age: 172_800) + let backup = try home.file("Library/Caches/Homebrew/Backup/important.txt", age: 172_800) + let environment = home.environment() + + let generic = try RuleCatalog.userCaches.locate(environment) + #expect(!generic.contains { $0.lastPathComponent == "Homebrew" }) + + let scans = await CleanEngine.scan([RuleCatalog.homebrewCache], environment: environment) + let scan = try #require(scans.first) + #expect(scan.items.map { $0.url.resolvingSymlinksInPath() } == [oldDownload.resolvingSymlinksInPath()]) + + let report = await CleanEngine.clean(scans, selected: [RuleCatalog.homebrewCache.id], + preferTrash: false, environment: environment, log: nil) + #expect(report.removedCount == 1) + #expect(!FileManager.default.fileExists(atPath: oldDownload.path)) + for retained in [recentDownload, incomplete, unrelated, backup] { + #expect(FileManager.default.fileExists(atPath: retained.path)) + } + } + + @Test func homebrewRuleRejectsLinkedDownloadDirectory() async throws { + let home = try FakeHome() + defer { home.remove() } + let hash = String(repeating: "b", count: 64) + let backup = try home.file("Library/Caches/Homebrew/Backup/\(hash)--archive.tar.gz", age: 172_800) + let downloads = home.root.appendingPathComponent("Library/Caches/Homebrew/downloads") + try FileManager.default.createSymbolicLink(at: downloads, withDestinationURL: backup.deletingLastPathComponent()) + + let environment = home.environment() + #expect(try RuleCatalog.homebrewCache.locate(environment).isEmpty) + let scans = await CleanEngine.scan([RuleCatalog.homebrewCache], environment: environment) + + #expect(scans.first?.items.isEmpty == true) + #expect(FileManager.default.fileExists(atPath: backup.path)) + } + @Test func packageManagerCachesUseTheirBrandLogos() { let rules = Dictionary(uniqueKeysWithValues: RuleCatalog.rules().map { ($0.id, $0) }) let expected = [