v3: performance pass, bundle size pass, i18n pass, docker pass

This commit is contained in:
khannurien
2026-04-08 13:19:39 +00:00
parent 20b9bfe7b4
commit 1321e374bf
21 changed files with 502 additions and 301 deletions

View File

@@ -32,6 +32,7 @@ export const DUMPS_DIR = `${UPLOADS_DIR}/dumps`;
export const AVATARS_DIR = `${UPLOADS_DIR}/avatars`;
export const PLAYLIST_IMAGES_DIR = `${UPLOADS_DIR}/playlist-images`;
export const ATTACHMENTS_DIR = `${UPLOADS_DIR}/attachments`;
export const THUMBNAILS_DIR = `${UPLOADS_DIR}/thumbnails`;
export const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB
export const ALLOWED_IMAGE_MIMES = new Set([

View File

@@ -4,6 +4,7 @@ import { oakCors } from "@tajpouria/cors";
import attachmentsRouter from "./routes/attachments.ts";
import dumpsRouter from "./routes/dumps.ts";
import filesRouter from "./routes/files.ts";
import thumbnailsRouter from "./routes/thumbnails.ts";
import usersRouter from "./routes/users.ts";
import avatarsRouter from "./routes/avatars.ts";
import wsRouter from "./routes/ws.ts";
@@ -44,6 +45,10 @@ app.use(
filesRouter.routes(),
filesRouter.allowedMethods(),
);
app.use(
thumbnailsRouter.routes(),
thumbnailsRouter.allowedMethods(),
);
app.use(
attachmentsRouter.routes(),
attachmentsRouter.allowedMethods(),

View File

@@ -19,6 +19,11 @@ import {
export const db = new DatabaseSync(DB_PATH);
db.exec("PRAGMA foreign_keys = ON;");
// Purge expired/used password reset tokens on startup
db.prepare(
`DELETE FROM password_reset_tokens WHERE expires_at < datetime('now') OR used_at IS NOT NULL;`,
).run();
// Purge expired unused invites on startup
db.prepare(
`DELETE FROM invites WHERE used_at IS NULL AND created_at < datetime('now', '-${UNUSED_INVITES_RETENTION_DAYS} days');`,

110
api/routes/thumbnails.ts Normal file
View File

@@ -0,0 +1,110 @@
import { Router } from "@oak/oak";
import { APIErrorCode, APIException } from "../model/interfaces.ts";
import { getDump } from "../services/dump-service.ts";
import { DUMPS_DIR, THUMBNAILS_DIR } from "../config.ts";
const router = new Router({ prefix: "/api/thumbnails" });
async function ffmpegAvailable(): Promise<boolean> {
try {
const cmd = new Deno.Command("ffmpeg", {
args: ["-version"],
stderr: "null",
stdout: "null",
});
const { success } = await cmd.output();
return success;
} catch {
return false;
}
}
async function generateThumbnail(
srcPath: string,
outPath: string,
): Promise<void> {
await Deno.mkdir(THUMBNAILS_DIR, { recursive: true });
const cmd = new Deno.Command("ffmpeg", {
args: [
"-ss",
"00:00:01",
"-i",
srcPath,
"-frames:v",
"1",
"-vf",
"scale=320:-1",
"-f",
"image2",
"-y",
outPath,
],
stdout: "null",
stderr: "null",
});
const { success } = await cmd.output();
if (!success) throw new Error("ffmpeg failed");
}
router.get("/:dumpId", async (ctx) => {
const { dumpId } = ctx.params;
if (!/^[0-9a-f-]{36}$/.test(dumpId)) {
throw new APIException(APIErrorCode.BAD_REQUEST, 400, "Invalid dump ID");
}
const dump = getDump(dumpId);
if (dump.kind !== "file" || !dump.fileMime?.startsWith("video/")) {
throw new APIException(
APIErrorCode.NOT_FOUND,
404,
"No video for this dump",
);
}
const thumbPath = `${THUMBNAILS_DIR}/${dumpId}.jpg`;
// Serve cached thumbnail if it exists
try {
const data = await Deno.readFile(thumbPath);
ctx.response.headers.set("Content-Type", "image/jpeg");
ctx.response.headers.set(
"Cache-Control",
"public, max-age=31536000, immutable",
);
ctx.response.body = data;
return;
} catch {
// Not cached yet — generate it
}
if (!await ffmpegAvailable()) {
throw new APIException(
APIErrorCode.NOT_FOUND,
404,
"Thumbnail generation unavailable",
);
}
const srcPath = `${DUMPS_DIR}/${dumpId}`;
try {
await generateThumbnail(srcPath, thumbPath);
} catch {
throw new APIException(
APIErrorCode.NOT_FOUND,
404,
"Could not generate thumbnail",
);
}
const data = await Deno.readFile(thumbPath);
ctx.response.headers.set("Content-Type", "image/jpeg");
ctx.response.headers.set(
"Cache-Control",
"public, max-age=31536000, immutable",
);
ctx.response.body = data;
});
export default router;