Initial commit: PodcastYes — AI podcast platform
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { promises as fs } 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<void> {
|
||||
const full = resolveSafe(key);
|
||||
await fs.mkdir(path.dirname(full), { recursive: true });
|
||||
await fs.writeFile(full, data);
|
||||
}
|
||||
|
||||
async get(key: string): Promise<Buffer> {
|
||||
return fs.readFile(resolveSafe(key));
|
||||
}
|
||||
|
||||
async exists(key: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(resolveSafe(key));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
try {
|
||||
await fs.unlink(resolveSafe(key));
|
||||
} catch (err: unknown) {
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async size(key: string): Promise<number | null> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user