62 lines
2.9 KiB
Swift
62 lines
2.9 KiB
Swift
import Foundation
|
|
import CryptoKit
|
|
|
|
/// Persists `.meta` payloads. The plum-local cache (keyed by file path) is the
|
|
/// always-on source of truth; mirroring `filename.ext.meta` next to the file on
|
|
/// black is opportunistic and guarded — it never blocks the UI and tolerates an
|
|
/// unreachable host (black is non-ECC ZFS; the cache is what we rely on).
|
|
public enum MetaWriter {
|
|
public static func metaDir() -> URL {
|
|
FileManager.default.homeDirectoryForCurrentUser
|
|
.appendingPathComponent(".local/state/tv-anarchy/meta")
|
|
}
|
|
|
|
/// Cache key is the sha256 of the CANONICAL (black-side) path, so lookups hit
|
|
/// regardless of whether the caller holds a legacy plum mount path or the
|
|
/// black-side form the scanner now emits.
|
|
public static func cacheURL(forPath path: String) -> URL {
|
|
let digest = SHA256.hash(data: Data(MediaPaths.toRemote(path).utf8))
|
|
let hex = digest.map { String(format: "%02x", $0) }.joined()
|
|
return metaDir().appendingPathComponent("\(hex).json")
|
|
}
|
|
|
|
@discardableResult
|
|
public static func writeCache(_ meta: MediaMeta) -> Bool {
|
|
let url = cacheURL(forPath: meta.path)
|
|
try? FileManager.default.createDirectory(at: metaDir(), withIntermediateDirectories: true)
|
|
let enc = JSONEncoder()
|
|
enc.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
|
|
enc.dateEncodingStrategy = .iso8601
|
|
guard let data = try? enc.encode(meta) else { return false }
|
|
return (try? data.write(to: url, options: .atomic)) != nil
|
|
}
|
|
|
|
public static func loadCache(forPath path: String) -> MediaMeta? {
|
|
guard let data = try? Data(contentsOf: cacheURL(forPath: path)) else { return nil }
|
|
let dec = JSONDecoder()
|
|
dec.dateDecodingStrategy = .iso8601
|
|
return try? dec.decode(MediaMeta.self, from: data)
|
|
}
|
|
|
|
/// Best-effort: write `<remotePath>.meta` on black via ssh (base64 to dodge
|
|
/// quoting/stdin issues). Returns false on any failure — callers ignore it.
|
|
/// `remotePath` must be a black-side path (e.g. under /bigdisk/_/media).
|
|
@discardableResult
|
|
public static func mirrorToBlack(_ meta: MediaMeta, remotePath: String, endpoint: String) -> Bool {
|
|
let enc = JSONEncoder()
|
|
enc.outputFormatting = [.withoutEscapingSlashes]
|
|
enc.dateEncodingStrategy = .iso8601
|
|
guard let data = try? enc.encode(meta) else { return false }
|
|
let b64 = data.base64EncodedString()
|
|
let remote = remotePath + ".meta"
|
|
// printf the base64 locally, pipe over ssh, decode into the sidecar.
|
|
let command = "printf %s \(shq(b64)) | /usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=5 "
|
|
+ "\(shq(endpoint)) \(shq("base64 -d > " + shq(remote)))"
|
|
let r = ProcessRunner.runShell(command, timeout: 20)
|
|
return r.ok
|
|
}
|
|
|
|
private static func shq(_ s: String) -> String {
|
|
"'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'"
|
|
}
|
|
}
|