93 lines
2.5 KiB
TypeScript
93 lines
2.5 KiB
TypeScript
import { randomUUID } from "crypto";
|
|
import { mkdir, rm, writeFile } from "fs/promises";
|
|
import path from "path";
|
|
|
|
export const MEDIA_UPLOAD_ROOT = path.join(process.cwd(), "public", "uploads", "media");
|
|
export const MAX_MEDIA_FILE_SIZE = 5 * 1024 * 1024;
|
|
|
|
const MIME_EXTENSIONS: Record<string, string> = {
|
|
"image/x-icon": ".ico",
|
|
"image/vnd.microsoft.icon": ".ico",
|
|
"image/jpeg": ".jpg",
|
|
"image/png": ".png",
|
|
"image/webp": ".webp",
|
|
"image/svg+xml": ".svg",
|
|
"application/pdf": ".pdf",
|
|
};
|
|
|
|
export function sanitizeBaseName(value: string) {
|
|
return value
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
.slice(0, 60);
|
|
}
|
|
|
|
export function getExtensionForMimeType(mimeType: string) {
|
|
return MIME_EXTENSIONS[mimeType] ?? null;
|
|
}
|
|
|
|
export function isManagedMediaFilePath(filePath: string | null | undefined) {
|
|
return typeof filePath === "string" && filePath.startsWith("/uploads/media/");
|
|
}
|
|
|
|
export function resolveMediaUploadPath(filePath: string) {
|
|
if (!isManagedMediaFilePath(filePath)) {
|
|
throw new Error("Only managed media uploads can be resolved.");
|
|
}
|
|
|
|
const relativePath = filePath.replace("/uploads/media/", "");
|
|
const absolutePath = path.resolve(MEDIA_UPLOAD_ROOT, relativePath);
|
|
|
|
if (!absolutePath.startsWith(MEDIA_UPLOAD_ROOT)) {
|
|
throw new Error("Resolved media upload path escapes the upload root.");
|
|
}
|
|
|
|
return absolutePath;
|
|
}
|
|
|
|
export async function removeManagedMediaFile(filePath: string | null | undefined) {
|
|
if (!isManagedMediaFilePath(filePath)) {
|
|
return false;
|
|
}
|
|
|
|
const managedFilePath = filePath as string;
|
|
|
|
await rm(resolveMediaUploadPath(managedFilePath), {
|
|
force: true,
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
export async function saveMediaUpload(file: File, folder: string) {
|
|
if (!file || file.size === 0) {
|
|
return null;
|
|
}
|
|
|
|
const extension = getExtensionForMimeType(file.type);
|
|
|
|
if (!extension) {
|
|
throw new Error("Unsupported file type.");
|
|
}
|
|
|
|
if (file.size > MAX_MEDIA_FILE_SIZE) {
|
|
throw new Error("File is too large.");
|
|
}
|
|
|
|
const safeBaseName = sanitizeBaseName(file.name.replace(/\.[^.]+$/, "")) || "asset";
|
|
const finalName = `${Date.now()}-${safeBaseName}-${randomUUID().slice(0, 8)}${extension}`;
|
|
const targetDir = path.join(MEDIA_UPLOAD_ROOT, folder);
|
|
const targetPath = path.join(targetDir, finalName);
|
|
|
|
await mkdir(targetDir, { recursive: true });
|
|
await writeFile(targetPath, Buffer.from(await file.arrayBuffer()));
|
|
|
|
return {
|
|
url: `/uploads/media/${folder}/${finalName}`,
|
|
fileName: finalName,
|
|
mimeType: file.type,
|
|
size: file.size,
|
|
};
|
|
}
|