import Foundation import UIKit enum FileVaultError: Error { case readFailed case writeFailed case removeFailed case decodeFailed } /// `@unchecked Sendable`:rootURL 是 let,方法只 I/O 到沙盒目录(线程安全), /// 可被任意 actor / Task 跨边界访问。 /// `nonisolated(unsafe) shared`:见 ModelStore 同款注释。 final class FileVault: @unchecked Sendable { nonisolated(unsafe) static let shared: FileVault = { do { let appSupport = try FileManager.default.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) let vaultURL = appSupport.appendingPathComponent("Vault", isDirectory: true) return try FileVault(rootURL: vaultURL) } catch { fatalError("FileVault.shared init failed: \(error)") } }() let rootURL: URL init(rootURL: URL) throws { self.rootURL = rootURL try FileManager.default.createDirectory( at: rootURL, withIntermediateDirectories: true, attributes: [.protectionKey: FileProtectionType.complete] ) } struct SavedAsset { let relativePath: String let bytes: Int } // MARK: - Path Safety private func resolveSafePath(_ relativePath: String) throws -> URL { guard !relativePath.contains("/"), !relativePath.contains(".."), !relativePath.isEmpty else { throw FileVaultError.readFailed } let url = rootURL.appendingPathComponent(relativePath) guard url.path.hasPrefix(rootURL.path) else { throw FileVaultError.readFailed } return url } // MARK: - Public API func writeJPEG(_ image: UIImage, quality: CGFloat = 0.85) throws -> SavedAsset { guard let data = image.jpegData(compressionQuality: quality) else { throw FileVaultError.writeFailed } let filename = "\(UUID().uuidString).jpg" let url = rootURL.appendingPathComponent(filename) try data.write(to: url, options: [.atomic, .completeFileProtection]) return SavedAsset(relativePath: filename, bytes: data.count) } func loadImage(relativePath: String) throws -> UIImage { let url = try resolveSafePath(relativePath) let data: Data do { data = try Data(contentsOf: url) } catch { throw FileVaultError.readFailed } guard let image = UIImage(data: data) else { throw FileVaultError.decodeFailed } return image } func remove(relativePath: String) throws { let url = try resolveSafePath(relativePath) do { try FileManager.default.removeItem(at: url) } catch { throw FileVaultError.removeFailed } } func wipe() throws { let fm = FileManager.default let contents = try fm.contentsOfDirectory(at: rootURL, includingPropertiesForKeys: nil) for url in contents { try fm.removeItem(at: url) } } }