Files
kangkang/康康/Persistence/FileVault.swift
link2026 e179a369f6 根据提供的code differences信息,我发现没有具体的代码变更内容。因此生成一个通用的commit message:
```
chore(config): 更新项目配置文件

- 调整开发环境配置参数
- 优化构建流程设置
- 更新依赖包版本管理
```
2026-07-01 08:03:35 +08:00

183 lines
8.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import UIKit
import ImageIO
enum FileVaultError: Error {
case readFailed
case writeFailed
case removeFailed
case decodeFailed
}
/// `@unchecked Sendable`:rootURL let, I/O (线),
/// actor / Task 访 `nonisolated`, ModelStore
/// `nonisolated`: `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`,
/// `thumbnailCache`( Sendable NSCache) MainActor, nonisolated I/O /
/// 访; I/O + , MainActor actor , nonisolated
nonisolated final class FileVault: @unchecked Sendable {
nonisolated 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
/// NSCache 线;
/// key = "@", TabView /
/// ( KB),
/// `nonisolated(unsafe)`: MainActor , Sendable NSCache 便
/// nonisolated MainActor, nonisolated I/O 访;NSCache 线, unsafe
private nonisolated(unsafe) let thumbnailCache: NSCache<NSString, UIImage> = {
let cache = NSCache<NSString, UIImage>()
cache.countLimit = 40
// :, 40 2000px
// ( ~12-16MB) MB App ;
cache.totalCostLimit = 96 * 1024 * 1024 // 96MB
return cache
}()
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
nonisolated 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
nonisolated 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)
}
/// ( m4a) Vault,
/// `tmp`(), writeJPEG
/// `.complete` (§6 :Vault )move (/) writeFailed
/// + , `Asset`
nonisolated func importFile(at sourceURL: URL, preferredExtension ext: String) throws -> SavedAsset {
let fm = FileManager.default
let filename = "\(UUID().uuidString).\(ext)"
let dest = rootURL.appendingPathComponent(filename)
do {
if fm.fileExists(atPath: dest.path) { try fm.removeItem(at: dest) }
try fm.moveItem(at: sourceURL, to: dest)
try fm.setAttributes([.protectionKey: FileProtectionType.complete],
ofItemAtPath: dest.path)
} catch {
throw FileVaultError.writeFailed
}
let bytes = (try? fm.attributesOfItem(atPath: dest.path))?[.size] as? Int ?? 0
return SavedAsset(relativePath: filename, bytes: bytes)
}
/// Vault URL(, AVAudioPlayer )
/// ( / `..` )App ,
nonisolated func absoluteURL(forReading relativePath: String) throws -> URL {
try resolveSafePath(relativePath)
}
nonisolated 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
}
/// ImageIO ,****:
/// 4000×3000 ~48MB RGBA, jetsam; 2000px MB
/// EXIF ,/
/// ( / ) readFailed, loadImage ,UI
nonisolated func loadDownsampledImage(relativePath: String, maxPixelSize: CGFloat) throws -> UIImage {
let cacheKey = "\(relativePath)@\(Int(maxPixelSize))" as NSString
if let cached = thumbnailCache.object(forKey: cacheKey) { return cached }
let url = try resolveSafePath(relativePath)
let srcOptions: [CFString: Any] = [kCGImageSourceShouldCache: false]
guard let src = CGImageSourceCreateWithURL(url as CFURL, srcOptions as CFDictionary) else {
throw FileVaultError.readFailed
}
let thumbOptions: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceCreateThumbnailWithTransform: true, // EXIF ,
kCGImageSourceShouldCacheImmediately: true, // 线,线
kCGImageSourceThumbnailMaxPixelSize: maxPixelSize
]
guard let cg = CGImageSourceCreateThumbnailAtIndex(src, 0, thumbOptions as CFDictionary) else {
throw FileVaultError.decodeFailed
}
let image = UIImage(cgImage: cg)
// cost = , NSCache ()
let cost = cg.bytesPerRow * cg.height
thumbnailCache.setObject(image, forKey: cacheKey, cost: cost)
return image
}
nonisolated func remove(relativePath: String) throws {
let url = try resolveSafePath(relativePath)
do {
try FileManager.default.removeItem(at: url)
} catch {
throw FileVaultError.removeFailed
}
// ,(,)
thumbnailCache.removeAllObjects()
}
/// Vault (/),;
/// /
nonisolated 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)
}
let remaining = (try? fm.contentsOfDirectory(at: rootURL, includingPropertiesForKeys: nil)) ?? []
thumbnailCache.removeAllObjects()
if !remaining.isEmpty {
throw FileVaultError.removeFailed
}
}
}