import { promises as fs, createReadStream as fsCreateReadStream } from "node:fs"; import path from "node:path"; import type { StorageProvider } from "./types"; const STORAGE_DIR = path.resolve(process.env.STORAGE_DIR ?? "./storage"); const MEDIA_BASE = process.env.MEDIA_PUBLIC_BASE_URL ?? "/media"; // Cover art is the only asset class served publicly by nginx from /media. const PUBLIC_PREFIXES = ["art/"]; function resolveSafe(key: string): string { // Prevent path traversal: the resolved path must stay inside STORAGE_DIR. const full = path.resolve(STORAGE_DIR, key); if (full !== STORAGE_DIR && !full.startsWith(STORAGE_DIR + path.sep)) { throw new Error(`Invalid storage key: ${key}`); } return full; } /** Local-disk storage for the single-VPS Plesk deployment. */ export class LocalStorageProvider implements StorageProvider { async put(key: string, data: Buffer | Uint8Array): Promise { const full = resolveSafe(key); await fs.mkdir(path.dirname(full), { recursive: true }); await fs.writeFile(full, data); } async get(key: string): Promise { return fs.readFile(resolveSafe(key)); } /** * Stream a file (optionally an inclusive byte range) without buffering it all * into memory. Goes through resolveSafe so path-traversal protection holds. */ createReadStream(key: string, range?: { start: number; end: number }): NodeJS.ReadableStream { const full = resolveSafe(key); return range ? fsCreateReadStream(full, { start: range.start, end: range.end }) : fsCreateReadStream(full); } async exists(key: string): Promise { try { await fs.access(resolveSafe(key)); return true; } catch { return false; } } async delete(key: string): Promise { try { await fs.unlink(resolveSafe(key)); } catch (err: unknown) { if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err; } } async size(key: string): Promise { try { const stat = await fs.stat(resolveSafe(key)); return stat.size; } catch { return null; } } publicUrl(key: string): string | null { if (PUBLIC_PREFIXES.some((p) => key.startsWith(p))) { return `${MEDIA_BASE}/${key}`; } return null; } /** Absolute filesystem path for a key — used by the worker (ffmpeg) and the asset route. */ absolutePath(key: string): string { return resolveSafe(key); } }