import path from "node:path"; import fs from "fs-extra"; import type { Paths } from "../lib/paths"; import { isDoNotClean } from "../lib/doNotClean-server"; import { readChannelConfig } from "./channels"; import { computeKeptVideoIds } from "./keptVideos"; import { dropSavedVideo, loadSavedVideo } from "../lib/savedVideo-server"; const { pathExists, readdir } = fs; export type PruneSavedVideosResult = { // Dirs carrying a saved-video pointer that were considered. scanned: number; // Window-persisted containers dropped (rolled out, unpinned). pruned: number; bytesFreed: number; }; // Retention prune of the saved-video store. A persisted source video is removed // from the store only when ALL of these hold: // - it was persisted by the rolling keep-latest rule (pointer keepReason // "keep-latest") — NOT a manual archive ("override") or a pin ("pin"), // - its id has rolled OUT of the channel's current keep-latest window, and // - it is not pinned via do-not-clean (a kept video later deleted-from-source // is pinned and must survive forever). // This is what bounds the store's growth as new uploads displace old ones, and it // is the Phase-3 resolution of the Phase-2 "source media not auto-pruned" caveat. // Manually-archived and pinned containers are never auto-evicted. export async function pruneSavedVideos({ channelSlug, paths, onLog, signal, }: { channelSlug: string; paths: Paths; onLog?: (msg: string) => void; signal?: AbortSignal; }): Promise { const log = onLog ?? ((m: string) => console.log(m)); const dataDir = path.join(paths.channelsDir, channelSlug, "data"); if (!(await pathExists(dataDir))) { return { scanned: 0, pruned: 0, bytesFreed: 0 }; } const config = await readChannelConfig(paths, channelSlug); const keptIds = await computeKeptVideoIds({ paths, channelSlug, keepLatest: config?.keepLatest ?? 0, }); const dirs = await readdir(dataDir); let scanned = 0; let pruned = 0; let bytesFreed = 0; for (const id of dirs) { if (signal?.aborted) break; const videoDir = path.join(dataDir, id); const pointer = await loadSavedVideo(videoDir); if (!pointer) continue; scanned++; // Only the rolling keep-latest containers are eligible for auto-eviction. if (pointer.keepReason !== "keep-latest") continue; if (keptIds.has(id)) continue; // still inside the window if (await isDoNotClean(videoDir)) continue; // pinned -> keep forever const bytes = await dropSavedVideo(videoDir); bytesFreed += bytes; pruned++; log( `Pruned saved video ${id} (rolled out of keep-latest window; freed ${bytes} bytes)`, ); } if (pruned > 0) { log( `Pruned ${pruned} saved video(s) from the store (freed ${bytesFreed} bytes).`, ); } return { scanned, pruned, bytesFreed }; }