81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
import { db } from "../../../db/index";
|
|
import { uploads, courses } from "../../../db/schema";
|
|
import { eq } from "drizzle-orm";
|
|
import { randomUUID } from "crypto";
|
|
import { writeFile, mkdir } from "fs/promises";
|
|
import { resolve } from "path";
|
|
import { parsePdf } from "../../../utils/parsePdf";
|
|
import { detectUploadType } from "../../../utils/detectUploadType";
|
|
|
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const id = getRouterParam(event, "id")!;
|
|
|
|
if (!UUID_RE.test(id)) {
|
|
throw createError({ statusCode: 400, message: "Invalid course id" });
|
|
}
|
|
|
|
const course = await db.query.courses.findFirst({ where: eq(courses.id, id) });
|
|
if (!course) throw createError({ statusCode: 404, message: "Course not found" });
|
|
|
|
const formData = await readFormData(event);
|
|
const file = formData.get("file") as File | null;
|
|
|
|
if (!file) {
|
|
throw createError({ statusCode: 400, message: "file is required" });
|
|
}
|
|
|
|
// 50mb limit
|
|
if (file.size > 50 * 1024 * 1024) {
|
|
throw createError({ statusCode: 400, message: "File exceeds 50MB limit" });
|
|
}
|
|
|
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
|
|
// check pdf magic bytes: %PDF = 0x25 0x50 0x44 0x46
|
|
if (buffer[0] !== 0x25 || buffer[1] !== 0x50 || buffer[2] !== 0x44 || buffer[3] !== 0x46) {
|
|
throw createError({ statusCode: 400, message: "File does not appear to be a valid PDF" });
|
|
}
|
|
|
|
const uploadDir = resolve(process.cwd(), "uploads", id);
|
|
await mkdir(uploadDir, { recursive: true });
|
|
|
|
const uploadId = randomUUID();
|
|
const safeFilename = `${uploadId}-${file.name.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
|
|
// safeFilename shouldnt be just dots/underscores
|
|
if (/^[._]+$/.test(safeFilename.replace(uploadId + "-", ""))) {
|
|
throw createError({ statusCode: 400, message: "Invalid filename" });
|
|
}
|
|
|
|
const storedPath = resolve(uploadDir, safeFilename);
|
|
await writeFile(storedPath, buffer);
|
|
|
|
let extractedText: string | null = null;
|
|
let pdfWarning: string | undefined;
|
|
|
|
try {
|
|
extractedText = await parsePdf(buffer);
|
|
} catch (err: any) {
|
|
console.error(`[upload] PDF text extraction failed for file size ${buffer.length}: ${err?.message ?? err}`);
|
|
pdfWarning = "PDF text extraction failed";
|
|
}
|
|
|
|
const detectedType = await detectUploadType(file.name, extractedText ?? "");
|
|
|
|
await db.insert(uploads).values({
|
|
id: uploadId,
|
|
courseId: id,
|
|
filename: file.name,
|
|
type: detectedType,
|
|
storedPath,
|
|
extractedText,
|
|
});
|
|
|
|
if (pdfWarning) {
|
|
return { uploadId, filename: file.name, type: detectedType, warning: pdfWarning };
|
|
}
|
|
|
|
return { uploadId, filename: file.name, type: detectedType };
|
|
});
|