import { NextRequest, NextResponse } from "next/server"; import { getIronSession } from "iron-session"; import { SessionData, sessionOptions } from "@/lib/session"; import { prisma } from "@/lib/prisma"; import { saveChunk, assembleChunks, deleteFile } from "@/lib/storage"; import { logActivity } from "@/lib/activity"; import { cleanupOrphanChunks, getSpaceIdForFolder } from "@/lib/trash"; import { notifySharedSpaceUpload } from "@/lib/notifications"; import mime from "mime-types"; export async function POST(request: NextRequest) { const response = NextResponse.next(); const session = await getIronSession(request, response, sessionOptions); const formData = await request.formData(); const chunk = formData.get("chunk") as File | null; const uploadId = formData.get("uploadId") as string; const chunkIndex = parseInt(formData.get("chunkIndex") as string); const totalChunks = parseInt(formData.get("totalChunks") as string); const fileName = formData.get("fileName") as string; const folderId = formData.get("folderId") as string | null; const declaredSize = parseInt((formData.get("fileSize") as string) || "0"); if (!chunk || !uploadId || !/^[A-Za-z0-9_-]{6,80}$/.test(uploadId) || isNaN(chunkIndex) || isNaN(totalChunks) || !fileName) { return NextResponse.json({ error: "Missing chunk data" }, { status: 400 }); } if (/[\\/]/.test(fileName)) return NextResponse.json({ error: "Nom de fichier invalide" }, { status: 400 }); const maxSize = parseInt(process.env.MAX_FILE_SIZE || "104857600"); if (declaredSize > maxSize) { return NextResponse.json({ error: `Fichier trop volumineux (max ${Math.round(maxSize / 1024 / 1024)} Mo)` }, { status: 413 }); } const targetFolder = folderId && folderId !== "root" ? folderId : null; if (targetFolder && chunkIndex === 0) { const exists = await prisma.folder.findUnique({ where: { id: targetFolder }, select: { id: true } }); if (!exists) return NextResponse.json({ error: "Dossier de destination introuvable" }, { status: 404 }); } const buffer = Buffer.from(await chunk.arrayBuffer()); await saveChunk(buffer, uploadId, chunkIndex); // If this is the last chunk, assemble if (chunkIndex === totalChunks - 1) { const { storagePath, size } = await assembleChunks(uploadId, totalChunks, fileName); if (size > maxSize) { await deleteFile(storagePath); return NextResponse.json({ error: `Fichier trop volumineux (max ${Math.round(maxSize / 1024 / 1024)} Mo)` }, { status: 413 }); } // Quota de l'espace partagé (hérité via l'arborescence) const spaceId = await getSpaceIdForFolder(targetFolder); if (spaceId) { const space = await prisma.sharedSpace.findUnique({ where: { id: spaceId } }); if (space) { const agg = await prisma.file.aggregate({ _sum: { size: true }, where: { deletedAt: null, folder: { sharedSpaceId: spaceId } } }); if (Number(agg._sum.size || 0) + size > Number(space.quotaBytes)) { await deleteFile(storagePath); return NextResponse.json({ error: "Quota de l'espace partagé dépassé" }, { status: 413 }); } } } const mimeType = mime.lookup(fileName) || "application/octet-stream"; const dbFile = await prisma.file.create({ data: { name: fileName, storagePath, size: BigInt(size), mimeType, folderId: targetFolder, userId: session.userId ?? null, isEncrypted: true, }, }); await logActivity("UPLOAD", dbFile.id, `Uploaded ${fileName} (chunked)`); if (spaceId && session.userId) notifySharedSpaceUpload(spaceId, session.userId, fileName, dbFile.id).catch(() => {}); cleanupOrphanChunks().catch(() => {}); return NextResponse.json( { ...dbFile, size: Number(dbFile.size), complete: true }, { status: 201 } ); } return NextResponse.json({ chunkIndex, received: true }); }