Files
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

197 lines
6.9 KiB
Swift

import Darwin
import Foundation
struct DiskCleanFileItem: Equatable, Sendable {
let path: String
let isDirectory: Bool
let isSymlink: Bool
let resolvedSymlinkTarget: String?
}
protocol DiskCleanFileSystemProviding: Sendable {
func expandPathPattern(_ pattern: String) throws -> [DiskCleanFileItem]
func itemInfo(at path: String) throws -> DiskCleanFileItem?
func sizeOfItem(at path: String) throws -> Int64
func removeItem(at path: String) throws
func deduplicatedParentChildPaths(_ paths: [String]) -> [String]
}
struct LocalDiskCleanFileSystem: DiskCleanFileSystemProviding, @unchecked Sendable {
private let fileManager: FileManager
private let homeDirectory: String
init(
fileManager: FileManager = .default,
homeDirectory: String = NSHomeDirectory()
) {
self.fileManager = fileManager
self.homeDirectory = Self.normalizeSlashes(homeDirectory)
}
func expandPathPattern(_ pattern: String) throws -> [DiskCleanFileItem] {
let expandedPattern = Self.normalizeSlashes(expandHome(in: pattern))
guard Self.containsGlob(expandedPattern) else {
guard let item = try itemInfo(at: expandedPattern) else { return [] }
return [item]
}
let matchedPaths = try Self.globPaths(matching: expandedPattern)
return try deduplicatedParentChildPaths(matchedPaths)
.compactMap { try itemInfo(at: $0) }
}
func itemInfo(at path: String) throws -> DiskCleanFileItem? {
let expandedPath = Self.normalizeSlashes(expandHome(in: path))
guard fileManager.fileExists(atPath: expandedPath) || isSymlink(at: expandedPath) else {
return nil
}
let attributes = try fileManager.attributesOfItem(atPath: expandedPath)
let fileType = attributes[.type] as? FileAttributeType
let isSymlink = fileType == .typeSymbolicLink
let isDirectory = fileType == .typeDirectory
return DiskCleanFileItem(
path: expandedPath,
isDirectory: isDirectory,
isSymlink: isSymlink,
resolvedSymlinkTarget: isSymlink ? try? fileManager.destinationOfSymbolicLink(atPath: expandedPath) : nil
)
}
func sizeOfItem(at path: String) throws -> Int64 {
let expandedPath = Self.normalizeSlashes(expandHome(in: path))
let attributes = try fileManager.attributesOfItem(atPath: expandedPath)
let fileType = attributes[.type] as? FileAttributeType
if fileType != .typeDirectory || fileType == .typeSymbolicLink {
return Int64((attributes[.size] as? NSNumber)?.int64Value ?? 0)
}
var total: Int64 = 0
if let enumerator = fileManager.enumerator(
at: URL(fileURLWithPath: expandedPath),
includingPropertiesForKeys: [.isDirectoryKey, .isSymbolicLinkKey, .fileSizeKey, .totalFileAllocatedSizeKey],
options: [.skipsPackageDescendants]
) {
for case let url as URL in enumerator {
let itemAttributes = try fileManager.attributesOfItem(atPath: url.path)
guard (itemAttributes[.type] as? FileAttributeType) != .typeDirectory else {
continue
}
total += Int64((itemAttributes[.size] as? NSNumber)?.int64Value ?? 0)
}
}
return total
}
func removeItem(at path: String) throws {
let expandedPath = Self.normalizeSlashes(expandHome(in: path))
guard fileManager.fileExists(atPath: expandedPath) || isSymlink(at: expandedPath) else {
return
}
try fileManager.removeItem(atPath: expandedPath)
}
func deduplicatedParentChildPaths(_ paths: [String]) -> [String] {
let uniqueSortedPaths = Array(Set(paths.map(Self.normalizeSlashes))).sorted()
var kept: [String] = []
var keptPathSet: Set<String> = []
for path in uniqueSortedPaths {
if Self.hasKeptParent(for: path, in: keptPathSet) {
continue
}
kept.append(path)
keptPathSet.insert(path)
}
return kept
}
private func expandHome(in pattern: String) -> String {
if pattern == "~" || pattern == "$HOME" || pattern == "${HOME}" {
return homeDirectory
}
if pattern.hasPrefix("~/") {
return homeDirectory + String(pattern.dropFirst())
}
if pattern.hasPrefix("$HOME/") {
return homeDirectory + String(pattern.dropFirst("$HOME".count))
}
if pattern.hasPrefix("${HOME}/") {
return homeDirectory + String(pattern.dropFirst("${HOME}".count))
}
return pattern
}
private func isSymlink(at path: String) -> Bool {
guard let attributes = try? fileManager.attributesOfItem(atPath: path) else {
return false
}
return (attributes[.type] as? FileAttributeType) == .typeSymbolicLink
}
private static func containsGlob(_ pattern: String) -> Bool {
pattern.contains { "*?[".contains($0) }
}
private static func globPaths(matching pattern: String) throws -> [String] {
var globResult = glob_t()
defer { globfree(&globResult) }
let status = pattern.withCString { glob($0, 0, nil, &globResult) }
if status == GLOB_NOMATCH {
return []
}
guard status == 0 else {
throw DiskCleanFileSystemError.globFailed(pattern: pattern, status: status)
}
guard let pathVector = globResult.gl_pathv else {
return []
}
return (0..<Int(globResult.gl_pathc)).compactMap { index in
guard let pathPointer = pathVector[index] else { return nil }
return normalizeSlashes(String(cString: pathPointer))
}
}
private static func hasKeptParent(for path: String, in keptPathSet: Set<String>) -> Bool {
var currentPath = path
while let slashIndex = currentPath.lastIndex(of: "/") {
currentPath = String(currentPath[..<slashIndex])
if currentPath.isEmpty {
currentPath = "/"
}
if keptPathSet.contains(currentPath) {
return true
}
if currentPath == "/" {
break
}
}
return false
}
private enum DiskCleanFileSystemError: LocalizedError {
case globFailed(pattern: String, status: Int32)
var errorDescription: String? {
switch self {
case let .globFailed(pattern, status):
return "Failed to expand path pattern \(pattern) (glob status \(status))"
}
}
}
private static func normalizeSlashes(_ path: String) -> String {
var normalized = path
while normalized.contains("//") {
normalized = normalized.replacingOccurrences(of: "//", with: "/")
}
return normalized
}
}