项目文件夹

文件
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

302 行
10 KiB
Swift

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
import Foundation
import MacToolsPluginKit
typealias DiskCleanScanProgressHandler = @Sendable (DiskCleanScanLogMessage) async -> Void
protocol DiskCleanScanning: Sendable {
func scan(
choices: Set<DiskCleanChoice>,
progress: DiskCleanScanProgressHandler
) async throws -> DiskCleanScanResult
}
extension DiskCleanScanning {
func scan(choices: Set<DiskCleanChoice>) async throws -> DiskCleanScanResult {
try await scan(choices: choices, progress: { _ in })
}
}
protocol DiskCleanProcessInspecting: Sendable {
func runningProcessName(from names: [String]) -> String?
}
struct LocalDiskCleanProcessInspector: DiskCleanProcessInspecting {
func runningProcessName(from names: [String]) -> String? {
for name in names where isProcessRunning(name) {
return name
}
return nil
}
private func isProcessRunning(_ name: String) -> Bool {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/pgrep")
process.arguments = ["-x", name]
do {
try process.run()
process.waitUntilExit()
return process.terminationStatus == 0
} catch {
return false
}
}
}
struct DiskCleanScanner: DiskCleanScanning {
private struct ExpandedItem {
let rule: DiskCleanRule
let item: DiskCleanFileItem
}
let ruleCatalog: DiskCleanRuleCatalog
let fileSystem: DiskCleanFileSystemProviding
let safetyPolicy: DiskCleanSafetyPolicy
let processInspector: DiskCleanProcessInspecting
let now: @Sendable () -> Date
let localization: PluginLocalization
init(
ruleCatalog: DiskCleanRuleCatalog = .moleFirstVersion,
fileSystem: DiskCleanFileSystemProviding = LocalDiskCleanFileSystem(),
safetyPolicy: DiskCleanSafetyPolicy = DiskCleanSafetyPolicy(),
processInspector: DiskCleanProcessInspecting = LocalDiskCleanProcessInspector(),
now: @escaping @Sendable () -> Date = Date.init,
localization: PluginLocalization = PluginLocalization(bundle: .main)
) {
self.ruleCatalog = ruleCatalog
self.fileSystem = fileSystem
self.safetyPolicy = safetyPolicy
self.processInspector = processInspector
self.now = now
self.localization = localization
}
func scan(
choices: Set<DiskCleanChoice>,
progress: DiskCleanScanProgressHandler
) async throws -> DiskCleanScanResult {
var expandedItems: [ExpandedItem] = []
var currentChoice: DiskCleanChoice?
for rule in ruleCatalog.rules where choices.contains(rule.choice) {
try Task.checkCancellation()
if currentChoice != rule.choice {
currentChoice = rule.choice
await progress(
DiskCleanScanLogMessage(
text: localization.format(
"scanLog.scope",
defaultValue: "扫描范围:%@",
rule.choice.title(localization: localization)
),
tone: .info
)
)
}
await progress(
DiskCleanScanLogMessage(
text: localization.format("scanLog.expandRule", defaultValue: "展开规则:%@", rule.title),
tone: .info
)
)
for target in rule.targets {
try Task.checkCancellation()
let items = try expand(target)
await progress(
DiskCleanScanLogMessage(
text: localization.format(
"scanLog.matched",
defaultValue: "匹配 %d 项:%@",
items.count,
rule.title
),
tone: items.isEmpty ? .info : .success
)
)
expandedItems += items.map { ExpandedItem(rule: rule, item: $0) }
}
}
let firstItemByPath = Dictionary(
expandedItems.map { ($0.item.path, $0) },
uniquingKeysWith: { first, _ in first }
)
let deduplicatedPaths = fileSystem.deduplicatedParentChildPaths(expandedItems.map(\.item.path))
var candidates: [DiskCleanCandidate] = []
for path in deduplicatedPaths {
try Task.checkCancellation()
guard let expandedItem = firstItemByPath[path] else {
continue
}
let candidate = makeCandidate(from: expandedItem)
candidates.append(candidate)
await progress(logMessage(for: candidate))
}
await progress(
DiskCleanScanLogMessage(
text: localization.format(
"scanLog.completed",
defaultValue: "扫描完成:%d 项,%d 项可清理",
candidates.count,
candidates.filter { $0.safety.isCleanable }.count
),
tone: .success
)
)
return DiskCleanScanResult(
choices: choices,
candidates: candidates,
scannedAt: now()
)
}
private func expand(_ target: DiskCleanRule.Target) throws -> [DiskCleanFileItem] {
switch target {
case let .path(pattern):
return try fileSystem.expandPathPattern(pattern)
case let .dynamic(dynamicRule):
return try expand(dynamicRule)
}
}
private func expand(_ dynamicRule: DiskCleanDynamicRule) throws -> [DiskCleanFileItem] {
var items: [DiskCleanFileItem] = []
for pattern in staticFallbackPatterns(for: dynamicRule) {
items += try fileSystem.expandPathPattern(pattern)
}
return items
}
private func staticFallbackPatterns(for dynamicRule: DiskCleanDynamicRule) -> [String] {
switch dynamicRule {
case .xcodeDerivedData:
return ["~/Library/Developer/Xcode/DerivedData/*"]
case .unavailableSimulators:
return []
case .npmCache:
return [
"~/.npm/_cacache/*",
"~/.npm/_npx/*",
"~/.npm/_logs/*",
"~/.npm/_prebuilds/*"
]
case .pnpmStore:
return ["~/Library/pnpm/store/*"]
case .bunCache:
return ["~/.bun/install/cache/*"]
case .pipCache:
return [
"~/Library/Caches/pip/*",
"~/.cache/pip/*"
]
case .goBuildCache:
return [
"~/Library/Caches/go-build/*",
"~/.cache/go-build/*"
]
case .goModuleCache:
return ["~/go/pkg/mod/*"]
case .miseCache:
return ["~/Library/Caches/mise/*"]
case .jetbrainsToolboxOldVersions:
return []
case .aiAgentOldVersions:
return []
case .serviceWorkerCache:
return [
"~/Library/Application Support/Google/Chrome/*/Service Worker/CacheStorage/*/*",
"~/Library/Application Support/Arc/*/Service Worker/CacheStorage/*/*",
"~/Library/Application Support/BraveSoftware/Brave-Browser/*/Service Worker/CacheStorage/*/*",
"~/Library/Application Support/Vivaldi/*/Service Worker/CacheStorage/*/*",
"~/Library/Application Support/Code/Service Worker/CacheStorage/*/*",
"~/Library/Application Support/Cursor/Service Worker/CacheStorage/*/*"
]
case .oldBrowserVersions:
return []
}
}
private func makeCandidate(from expandedItem: ExpandedItem) -> DiskCleanCandidate {
let rule = expandedItem.rule
let item = expandedItem.item
let safety = safetyStatus(for: rule, item: item)
let size = (try? fileSystem.sizeOfItem(at: item.path)) ?? 0
return DiskCleanCandidate(
id: "\(rule.id)::\(item.path)",
ruleID: rule.id,
choice: rule.choice,
title: rule.title,
path: item.path,
sizeBytes: size,
safety: safety,
risk: rule.risk
)
}
private func logMessage(for candidate: DiskCleanCandidate) -> DiskCleanScanLogMessage {
switch candidate.safety {
case .allowed:
return DiskCleanScanLogMessage(
text: localization.format("scanLog.candidate.allowed", defaultValue: "可清理:%@", candidate.path),
tone: .success
)
case .whitelisted:
return DiskCleanScanLogMessage(
text: localization.format("scanLog.candidate.whitelisted", defaultValue: "白名单保护:%@", candidate.path),
tone: .warning
)
case .protected:
return DiskCleanScanLogMessage(
text: localization.format("scanLog.candidate.protected", defaultValue: "敏感数据保护:%@", candidate.path),
tone: .warning
)
case .invalid:
return DiskCleanScanLogMessage(
text: localization.format("scanLog.candidate.invalid", defaultValue: "路径安全保护:%@", candidate.path),
tone: .warning
)
case .requiresAdmin:
return DiskCleanScanLogMessage(
text: localization.format("scanLog.candidate.requiresAdmin", defaultValue: "需要管理员权限:%@", candidate.path),
tone: .warning
)
case let .inUse(processName):
return DiskCleanScanLogMessage(
text: localization.format(
"scanLog.candidate.inUse",
defaultValue: "正在使用(%@)%@",
processName,
candidate.path
),
tone: .warning
)
}
}
private func safetyStatus(
for rule: DiskCleanRule,
item: DiskCleanFileItem
) -> DiskCleanSafetyStatus {
if let processName = processInspector.runningProcessName(from: rule.skipWhenProcessIsRunning) {
return .inUse(processName: processName)
}
if rule.requiresAdmin {
return .requiresAdmin(reason: "admin privileges required")
}
return safetyPolicy.safetyStatus(
for: item.path,
isSymlink: item.isSymlink,
resolvedSymlinkTarget: item.resolvedSymlinkTarget
)
}
}