```
refactor: 重命名项目名称从"体己"到"康康" 将整个项目的目录结构从"体己"重命名为"康康",包括所有源代码文件、 资源文件、测试文件以及Xcode项目配置文件。此更改涉及项目中所有的 文件路径和应用入口点(App/TijiApp.swift → App/KangkangApp.swift)。 ```
This commit is contained in:
101
康康/Persistence/FileVault.swift
Normal file
101
康康/Persistence/FileVault.swift
Normal file
@@ -0,0 +1,101 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user