Add fingerprint cache
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import Foundation
|
||||
import Vision
|
||||
|
||||
/// 1ファイル分のキャッシュ。ファイルサイズと更新日時が一致するときだけ再利用する。
|
||||
struct FingerprintRecord: Codable, Sendable {
|
||||
var fileSize: Int64
|
||||
var modificationDate: Date
|
||||
var basics: ImageBasics?
|
||||
var featurePrint: FeaturePrintObservation?
|
||||
|
||||
func matches(_ file: ScannedFile) -> Bool {
|
||||
fileSize == file.fileSize
|
||||
&& abs(modificationDate.timeIntervalSince(file.modificationDate)) < 1
|
||||
}
|
||||
}
|
||||
|
||||
/// dHash と feature print をディスクに保存し、同じフォルダの再スキャンを速くする。
|
||||
///
|
||||
/// feature print は1枚あたり数 KB あるので、JSON ではなくバイナリ plist で持つ。
|
||||
actor FingerprintCache {
|
||||
private var records: [String: FingerprintRecord] = [:]
|
||||
private var isDirty = false
|
||||
private let fileURL: URL?
|
||||
|
||||
init(fileURL: URL? = FingerprintCache.defaultFileURL()) {
|
||||
self.fileURL = fileURL
|
||||
self.records = Self.loadRecords(from: fileURL)
|
||||
}
|
||||
|
||||
static func defaultFileURL() -> URL? {
|
||||
guard let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
||||
else { return nil }
|
||||
let directory = caches.appendingPathComponent("Photochk", isDirectory: true)
|
||||
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
return directory.appendingPathComponent("fingerprints.plist")
|
||||
}
|
||||
|
||||
// MARK: - 読み出し
|
||||
|
||||
func basics(for files: [ScannedFile]) -> [URL: ImageBasics] {
|
||||
var found: [URL: ImageBasics] = [:]
|
||||
for file in files {
|
||||
if let record = records[file.url.path], record.matches(file), let basics = record.basics {
|
||||
found[file.url] = basics
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func featurePrints(for files: [ScannedFile]) -> [URL: FeaturePrintObservation] {
|
||||
var found: [URL: FeaturePrintObservation] = [:]
|
||||
for file in files {
|
||||
if let record = records[file.url.path], record.matches(file),
|
||||
let print = record.featurePrint {
|
||||
found[file.url] = print
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// MARK: - 書き込み
|
||||
|
||||
func store(basics: [URL: ImageBasics], for files: [ScannedFile]) {
|
||||
for file in files {
|
||||
guard let value = basics[file.url] else { continue }
|
||||
update(file) { $0.basics = value }
|
||||
}
|
||||
}
|
||||
|
||||
func store(featurePrints: [URL: FeaturePrintObservation], for files: [ScannedFile]) {
|
||||
for file in files {
|
||||
guard let value = featurePrints[file.url] else { continue }
|
||||
update(file) { $0.featurePrint = value }
|
||||
}
|
||||
}
|
||||
|
||||
private func update(_ file: ScannedFile, _ body: (inout FingerprintRecord) -> Void) {
|
||||
var record = records[file.url.path] ?? FingerprintRecord(
|
||||
fileSize: file.fileSize,
|
||||
modificationDate: file.modificationDate
|
||||
)
|
||||
if !record.matches(file) {
|
||||
// ファイルが変わっていたら作り直す
|
||||
record = FingerprintRecord(
|
||||
fileSize: file.fileSize,
|
||||
modificationDate: file.modificationDate
|
||||
)
|
||||
}
|
||||
body(&record)
|
||||
records[file.url.path] = record
|
||||
isDirty = true
|
||||
}
|
||||
|
||||
/// 消えたファイルのエントリを捨てる。スキャンのたびに肥大化するのを防ぐ。
|
||||
func prune(keeping files: [ScannedFile]) {
|
||||
let alive = Set(files.map(\.url.path))
|
||||
let before = records.count
|
||||
records = records.filter { alive.contains($0.key) }
|
||||
if records.count != before { isDirty = true }
|
||||
}
|
||||
|
||||
// MARK: - 永続化
|
||||
|
||||
func save() {
|
||||
guard isDirty, let fileURL else { return }
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .binary
|
||||
guard let data = try? encoder.encode(records) else { return }
|
||||
try? data.write(to: fileURL, options: .atomic)
|
||||
isDirty = false
|
||||
}
|
||||
|
||||
private static func loadRecords(from fileURL: URL?) -> [String: FingerprintRecord] {
|
||||
guard let fileURL, let data = try? Data(contentsOf: fileURL) else { return [:] }
|
||||
// 形式が変わっていたら黙って捨てる。キャッシュなので作り直せばよい。
|
||||
return (try? PropertyListDecoder().decode([String: FingerprintRecord].self, from: data)) ?? [:]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user