Initial commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import "server-only";
|
||||
import path from "node:path";
|
||||
import { getConfiguredVideoRoots } from "./index";
|
||||
import { getAppSetting } from "@/lib/db/appSettings";
|
||||
import { isManualSubtitlePath } from "./manualSubtitles";
|
||||
|
||||
/**
|
||||
* In-process set of subtitle paths the user picked via /api/pick-file
|
||||
* during this session. Covers the case where someone browses a .srt
|
||||
* sitting outside any indexed video root — the OS picker IS the
|
||||
* authorization. Entries time out after TTL_MS to bound how long an
|
||||
* old picked path remains servable.
|
||||
*/
|
||||
const TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||
const trusted = new Map<string, number>();
|
||||
|
||||
function pruneExpired(now: number): void {
|
||||
for (const [k, expiresAt] of trusted) {
|
||||
if (expiresAt <= now) trusted.delete(k);
|
||||
}
|
||||
}
|
||||
|
||||
function normalize(p: string): string {
|
||||
// Path keys use the resolved + lowercased form on Windows so case
|
||||
// differences don't bypass the guard. POSIX is case-sensitive so we
|
||||
// keep original case there.
|
||||
const resolved = path.resolve(p);
|
||||
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
||||
}
|
||||
|
||||
export function trustSubtitlePath(abs: string): void {
|
||||
pruneExpired(Date.now());
|
||||
trusted.set(normalize(abs), Date.now() + TTL_MS);
|
||||
}
|
||||
|
||||
export function isSessionTrustedSubtitlePath(abs: string): boolean {
|
||||
const now = Date.now();
|
||||
pruneExpired(now);
|
||||
const key = normalize(abs);
|
||||
const exp = trusted.get(key);
|
||||
if (exp == null) return false;
|
||||
if (exp <= now) {
|
||||
trusted.delete(key);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isInside(child: string, parent: string): boolean {
|
||||
const c = process.platform === "win32" ? path.resolve(child).toLowerCase() : path.resolve(child);
|
||||
const p = process.platform === "win32" ? path.resolve(parent).toLowerCase() : path.resolve(parent);
|
||||
if (!p) return false;
|
||||
if (c === p) return true;
|
||||
const sep = path.sep;
|
||||
return c.startsWith(p.endsWith(sep) ? p : p + sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if `abs` resolves under one of:
|
||||
* - a configured video root,
|
||||
* - a configured subtitleExtraPaths entry,
|
||||
* - the implicit data/generated-subtitles/ root (WhisperJAV output),
|
||||
* - a session-trusted pick-file path (exact match, not prefix),
|
||||
* - a path persisted in the manual_subtitles table (user explicitly
|
||||
* Browse'd it during a previous session).
|
||||
*/
|
||||
export function isAllowedSubtitlePath(abs: string): boolean {
|
||||
const resolved = path.resolve(abs);
|
||||
for (const root of getConfiguredVideoRoots()) {
|
||||
if (root && isInside(resolved, root)) return true;
|
||||
}
|
||||
const subRoots = getAppSetting("subtitleExtraPaths") ?? [];
|
||||
for (const root of subRoots) {
|
||||
if (root && isInside(resolved, root)) return true;
|
||||
}
|
||||
const generatedRoot = path.join(process.cwd(), "data", "generated-subtitles");
|
||||
if (isInside(resolved, generatedRoot)) return true;
|
||||
if (isSessionTrustedSubtitlePath(resolved)) return true;
|
||||
if (isManualSubtitlePath(resolved)) return true;
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user