204 lines
5.7 KiB
TypeScript
204 lines
5.7 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { getDb } from "@tline/db";
|
|
import { presignGetObjectUrl } from "@tline/minio";
|
|
import { pickLegacyKeyForRequest, pickVariantKey } from "./variant";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
const paramsSchema = z.object({
|
|
id: z.string().uuid()
|
|
});
|
|
|
|
const legacyVariantSchema = z.enum(["original", "thumb_small", "thumb_med", "poster"]);
|
|
const kindSchema = z.enum(["original", "thumb", "poster", "video_mp4"]);
|
|
const sizeSchema = z.coerce.number().int().positive();
|
|
const videoMp4DefaultSize = 720;
|
|
const legacyVariantMap = {
|
|
original: { kind: "original" as const },
|
|
thumb_small: { kind: "thumb" as const, size: 256 },
|
|
thumb_med: { kind: "thumb" as const, size: 768 },
|
|
poster: { kind: "poster" as const, size: 256 },
|
|
};
|
|
|
|
export async function GET(
|
|
request: Request,
|
|
context: { params: Promise<{ id: string }> }
|
|
): Promise<Response> {
|
|
const rawParams = await context.params;
|
|
const paramsParsed = paramsSchema.safeParse(rawParams);
|
|
if (!paramsParsed.success) {
|
|
return Response.json(
|
|
{ error: "invalid_params", issues: paramsParsed.error.issues },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
const params = paramsParsed.data;
|
|
|
|
const url = new URL(request.url);
|
|
const kindParam = url.searchParams.get("kind");
|
|
const sizeParam = url.searchParams.get("size");
|
|
const legacyVariantParam = url.searchParams.get("variant");
|
|
const endpointParam = url.searchParams.get("endpoint");
|
|
|
|
let requestedKind: z.infer<typeof kindSchema> = "original";
|
|
let requestedSize: number | null = null;
|
|
let legacyVariant: z.infer<typeof legacyVariantSchema> | null = null;
|
|
let endpointOverride: "lan" | "tailnet" | undefined;
|
|
|
|
if (kindParam) {
|
|
const kindParsed = kindSchema.safeParse(kindParam);
|
|
if (!kindParsed.success) {
|
|
return Response.json(
|
|
{ error: "invalid_query", issues: kindParsed.error.issues },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
requestedKind = kindParsed.data;
|
|
if (requestedKind !== "original") {
|
|
if (requestedKind === "video_mp4" && !sizeParam) {
|
|
requestedSize = videoMp4DefaultSize;
|
|
} else {
|
|
const sizeParsed = sizeSchema.safeParse(sizeParam);
|
|
if (!sizeParsed.success) {
|
|
return Response.json(
|
|
{ error: "invalid_query", issues: sizeParsed.error.issues },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
requestedSize = sizeParsed.data;
|
|
}
|
|
}
|
|
} else if (legacyVariantParam) {
|
|
const legacyParsed = legacyVariantSchema.safeParse(legacyVariantParam);
|
|
if (!legacyParsed.success) {
|
|
return Response.json(
|
|
{ error: "invalid_query", issues: legacyParsed.error.issues },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
legacyVariant = legacyParsed.data;
|
|
const mapped = legacyVariantMap[legacyVariant];
|
|
requestedKind = mapped.kind;
|
|
requestedSize = "size" in mapped ? mapped.size : null;
|
|
}
|
|
|
|
if (endpointParam) {
|
|
if (endpointParam !== "lan" && endpointParam !== "tailnet") {
|
|
return Response.json(
|
|
{
|
|
error: "invalid_query",
|
|
issues: [
|
|
{
|
|
code: "custom",
|
|
message: "endpoint must be lan or tailnet",
|
|
path: ["endpoint"],
|
|
},
|
|
],
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
endpointOverride = endpointParam;
|
|
}
|
|
|
|
const db = getDb();
|
|
const rows = await db<
|
|
{
|
|
bucket: string;
|
|
active_key: string;
|
|
thumb_small_key: string | null;
|
|
thumb_med_key: string | null;
|
|
poster_key: string | null;
|
|
mime_type: string;
|
|
}[]
|
|
>`
|
|
select bucket, active_key, thumb_small_key, thumb_med_key, poster_key, mime_type
|
|
from assets
|
|
where id = ${params.id}
|
|
limit 1
|
|
`;
|
|
|
|
const variants = await db<
|
|
{
|
|
kind: string;
|
|
size: number;
|
|
key: string;
|
|
mime_type: string;
|
|
width: number | null;
|
|
height: number | null;
|
|
}[]
|
|
>`
|
|
select kind, size, key, mime_type, width, height
|
|
from asset_variants
|
|
where asset_id = ${params.id}
|
|
`;
|
|
|
|
const asset = rows[0];
|
|
if (!asset) {
|
|
return Response.json({ error: "not_found" }, { status: 404 });
|
|
}
|
|
|
|
const legacyKey = legacyVariant
|
|
? pickLegacyKeyForRequest(
|
|
{ asset },
|
|
{ kind: requestedKind, size: requestedSize ?? 0 },
|
|
)
|
|
: requestedSize !== null
|
|
? pickLegacyKeyForRequest(
|
|
{ asset },
|
|
{ kind: requestedKind, size: requestedSize },
|
|
)
|
|
: null;
|
|
|
|
const key =
|
|
requestedKind === "original"
|
|
? asset.active_key
|
|
: requestedSize !== null
|
|
? pickVariantKey(
|
|
{ variants },
|
|
{ kind: requestedKind, size: requestedSize },
|
|
) ?? legacyKey
|
|
: null;
|
|
|
|
if (!key) {
|
|
return Response.json(
|
|
{ error: "variant_not_available", kind: requestedKind, size: requestedSize },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Hint the browser; especially helpful for Range playback.
|
|
const matchedVariant =
|
|
requestedKind === "original" || requestedSize === null
|
|
? null
|
|
: variants.find(
|
|
(item) => item.kind === requestedKind && item.size === requestedSize,
|
|
) ?? null;
|
|
const responseContentType =
|
|
requestedKind === "original"
|
|
? asset.mime_type
|
|
: matchedVariant?.mime_type ??
|
|
(requestedKind === "video_mp4" ? "video/mp4" : "image/jpeg");
|
|
|
|
const responseContentDisposition =
|
|
(requestedKind === "original" && asset.mime_type.startsWith("video/")) ||
|
|
requestedKind === "video_mp4"
|
|
? "inline"
|
|
: undefined;
|
|
|
|
const signed = await presignGetObjectUrl({
|
|
bucket: asset.bucket,
|
|
key,
|
|
responseContentType,
|
|
responseContentDisposition,
|
|
endpoint: endpointOverride,
|
|
});
|
|
|
|
return Response.json(signed, {
|
|
headers: {
|
|
"Cache-Control": "no-store"
|
|
}
|
|
});
|
|
}
|