import path from "node:path"; // Backup manifest for the saved-video store (Phase 4 of the video-persistence // feature). When the store is mirrored to a backup destination, a manifest is // written at the destination root recording, for each saved container, its // canonical // location, its size, and a streamed sha256. // The verify step compares this manifest against what's actually on the backup // destination (and the live store) to report drift. // // This module is pure (types + parse + path math) so it's importable from both // client and server; the filesystem/rsync work lives in // common/controller/backupSavedVideos.ts. export const BACKUP_MANIFEST_FILENAME = "backup-manifest.json"; export type BackupEntry = { // The channel slug the container belongs to. slug: string; // The video id within the channel. videoId: string; // Container basename (e.g. "source-media.mp4"). file: string; // Size in bytes at backup time. bytes: number; // sha256 of the container contents, streamed at backup time. sha256: string; }; export type BackupManifest = { // ISO timestamp the backup that produced this manifest completed. backedUpAt: string; // One entry per saved container mirrored to the destination. entries: BackupEntry[]; }; // Canonical relative location of a backed-up container under the destination // root: //. The store mirrors this layout regardless of // which physical root (global or per-channel override) the live copy lives on, // so every saved video lands in one predictable place in the backup. export function backupEntryRelDir(entry: Pick): string { return path.join(entry.slug, entry.videoId); } export function backupEntryRelPath( entry: Pick, ): string { return path.join(entry.slug, entry.videoId, entry.file); } function parseEntry(raw: unknown): BackupEntry | null { if (!raw || typeof raw !== "object") return null; const r = raw as Record; if (typeof r.slug !== "string" || r.slug === "") return null; if (typeof r.videoId !== "string" || r.videoId === "") return null; if (typeof r.file !== "string" || r.file === "") return null; if (typeof r.bytes !== "number" || !Number.isFinite(r.bytes)) return null; if (typeof r.sha256 !== "string" || r.sha256 === "") return null; return { slug: r.slug, videoId: r.videoId, file: r.file, bytes: r.bytes, sha256: r.sha256, }; } export function parseBackupManifest(raw: unknown): BackupManifest | null { if (!raw || typeof raw !== "object") return null; const r = raw as Record; if (!Array.isArray(r.entries)) return null; const entries: BackupEntry[] = []; for (const e of r.entries) { const parsed = parseEntry(e); if (parsed) entries.push(parsed); } return { backedUpAt: typeof r.backedUpAt === "string" ? r.backedUpAt : "", entries, }; }