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