Files
kangkang/康康/Persistence/FileVault.swift
link2026 de19d7abcd 根据提供的code differences信息,由于没有具体的代码变更内容,我将生成一个通用的commit message模板:
```
docs(readme): 更新文档说明

- 添加了项目使用指南
- 完善了API接口说明
- 修正了一些文字错误
```

注:由于未提供具体的代码差异信息,以上为示例格式。请提供具体的代码变更内容以便生成准确的commit message。
2026-06-17 08:35:59 +08:00

157 lines
7.0 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)
}
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
}
}
}