import Foundation /// Fetches the library index that black maintains out-of-band (`build_index.sh` /// writes a flat `size⇥mtime⇥path` TSV), so a "scan" on plum is one instant SSH /// `cat` instead of a minutes-long NFS walk. The walk is disk-bound on black's /// ZFS pool (~3.5 min) regardless of where it runs, so it must NOT happen in the /// app's hot path — black builds the index on a finished-download trigger; plum /// just reads it (and falls back to a live walk when black is unreachable). public enum LibraryIndex { /// Matches the transmission bridge default + the ControlMaster it keeps warm. static var host: String { ProcessInfo.processInfo.environment["BLACK_SSH_HOST"] ?? "lilith@10.0.0.11" } static let indexPath = "/bigdisk/_/media/_tools/index.tsv" static let builder = "/bigdisk/_/media/_tools/build_index.sh" private static let control = [ "-o", "ControlPath=/tmp/tva-cm-%r@%h:%p", "-o", "ConnectTimeout=12", "-o", "BatchMode=yes", ] /// The prebuilt index TSV, or nil when black is unreachable or it hasn't been /// built yet (caller falls back to a local walk). public static func fetch() -> String? { let r = ProcessRunner.run("/usr/bin/ssh", control + [host, "cat \(indexPath)"]) guard r.ok, !r.stdout.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil } return r.stdout } /// Kick a background, low-priority (nice/ionice) rebuild on black — fire and /// forget. `addDir` appends one finished-download folder cheaply; nil rebuilds /// the whole index ("overscan"). Returns false if the SSH itself failed. @discardableResult public static func rebuild(addDir: String? = nil) -> Bool { let args = addDir.map { "--add \(shq($0))" } ?? "" // best-effort-LOW I/O (ionice -c2 -n6), NOT idle (-c3): black seeds 200+ // torrents at load ~13, where idle-class I/O is starved to a near-halt // (a full rebuild made ~17% in 15 min). `setsid Int? { let r = ProcessRunner.run("/usr/bin/ssh", control + [host, "wc -l < \(indexPath) 2>/dev/null"]) return r.ok ? Int(r.stdout.trimmingCharacters(in: .whitespacesAndNewlines)) : nil } /// Live full-rebuild progress in ONE round-trip: lines written to the temp file /// so far + whether the build is still running. nil if unreachable. Drives the /// determinate progress bar (lines-so-far / prior `indexCount`). public static func buildStatus() -> (lines: Int, building: Bool)? { let dir = (indexPath as NSString).deletingLastPathComponent let cmd = "L=$(cat \(dir)/index.tsv.tmp.* 2>/dev/null | wc -l); " + "if ls \(dir)/index.tsv.tmp.* >/dev/null 2>&1; then echo \"$L 1\"; else echo \"$L 0\"; fi" let r = ProcessRunner.run("/usr/bin/ssh", control + [host, cmd]) let parts = r.stdout.split(separator: " ") guard r.ok, parts.count == 2, let lines = Int(parts[0]) else { return nil } return (lines, parts[1] == "1") } private static func shq(_ s: String) -> String { "'" + s.replacingOccurrences(of: "'", with: "'\\''") + "'" } }