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 `.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: "'\\''") + "'" } }