import type { Context } from "@oak/oak"; import { APIErrorCode } from "../../shared/api.ts"; import { APIException } from "./api-exception.ts"; import { serveFile } from "./serve-file.ts"; import { ALLOWED_IMAGE_MIMES, ATTACHMENTS_DIR, AVATARS_DIR, DUMPS_DIR, MAX_IMAGE_SIZE_BYTES, PLAYLIST_IMAGES_DIR, UPLOADS_DIR, } from "../config.ts"; export { ALLOWED_IMAGE_MIMES, ATTACHMENTS_DIR, AVATARS_DIR, DUMPS_DIR, PLAYLIST_IMAGES_DIR, UPLOADS_DIR, }; /** Detect image MIME type from magic bytes, ignoring the browser-declared type. */ export function detectImageMime(data: Uint8Array): string | null { if (data[0] === 0xFF && data[1] === 0xD8 && data[2] === 0xFF) { return "image/jpeg"; } if ( data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4E && data[3] === 0x47 ) return "image/png"; if ( data[0] === 0x47 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x38 ) return "image/gif"; // RIFF....WEBP if ( data[0] === 0x52 && data[1] === 0x49 && data[2] === 0x46 && data[3] === 0x46 && data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50 ) return "image/webp"; return null; } /** Validates image upload data: checks size and MIME. Returns the detected MIME type or throws APIException. */ export function validateImageUpload(data: Uint8Array): string { if (data.length > MAX_IMAGE_SIZE_BYTES) { throw new APIException( APIErrorCode.BAD_REQUEST, 400, "File too large (max 5 MB)", ); } const mime = detectImageMime(data); if (!mime) { throw new APIException( APIErrorCode.BAD_REQUEST, 400, "File content is not a recognised image (JPEG, PNG, GIF, WebP)", ); } return mime; } export async function serveUploadedFile( ctx: Context, filePath: string, mime: string, cacheMaxAge = 3600, isPrivate = false, ): Promise { ctx.response.headers.set("Content-Disposition", "inline"); ctx.response.headers.set("X-Content-Type-Options", "nosniff"); ctx.response.headers.set("Cross-Origin-Resource-Policy", "cross-origin"); const served = await serveFile(ctx, filePath, { mime, cacheControl: `${isPrivate ? "private" : "public"}, max-age=${cacheMaxAge}`, }); if (!served) ctx.response.status = 404; return served; }