Files
gerbeur/api/lib/upload.ts

83 lines
2.2 KiB
TypeScript

import type { Context } from "@oak/oak";
import { APIErrorCode, APIException } from "../model/interfaces.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,
): Promise<boolean> {
let data: Uint8Array;
try {
data = await Deno.readFile(filePath);
} catch {
ctx.response.status = 404;
return false;
}
ctx.response.headers.set("Content-Type", mime);
ctx.response.headers.set("Content-Disposition", "inline");
ctx.response.headers.set("Cache-Control", `public, max-age=${cacheMaxAge}`);
ctx.response.headers.set("Cross-Origin-Resource-Policy", "cross-origin");
ctx.response.body = data;
return true;
}