feat: Implement file upload and management system with database integration

This commit is contained in:
2025-07-30 11:37:25 +02:00
parent 07b4af5f24
commit 639a7b7eab
9 changed files with 778 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
import { NextResponse } from "next/server";
import { unlink } from "fs/promises";
import path from "path";
import db from "@/lib/db";
export async function DELETE(request, { params }) {
try {
const fileId = params.fileId;
// Get file info from database
const file = db.prepare(`
SELECT * FROM file_attachments WHERE file_id = ?
`).get(parseInt(fileId));
if (!file) {
return NextResponse.json(
{ error: "File not found" },
{ status: 404 }
);
}
// Delete physical file
try {
const fullPath = path.join(process.cwd(), "public", file.file_path);
await unlink(fullPath);
} catch (fileError) {
console.warn("Could not delete physical file:", fileError.message);
// Continue with database deletion even if file doesn't exist
}
// Delete from database
const result = db.prepare(`
DELETE FROM file_attachments WHERE file_id = ?
`).run(parseInt(fileId));
if (result.changes === 0) {
return NextResponse.json(
{ error: "File not found" },
{ status: 404 }
);
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error deleting file:", error);
return NextResponse.json(
{ error: "Failed to delete file" },
{ status: 500 }
);
}
}
export async function GET(request, { params }) {
try {
const fileId = params.fileId;
// Get file info from database
const file = db.prepare(`
SELECT * FROM file_attachments WHERE file_id = ?
`).get(parseInt(fileId));
if (!file) {
return NextResponse.json(
{ error: "File not found" },
{ status: 404 }
);
}
return NextResponse.json(file);
} catch (error) {
console.error("Error fetching file:", error);
return NextResponse.json(
{ error: "Failed to fetch file" },
{ status: 500 }
);
}
}

162
src/app/api/files/route.js Normal file
View File

@@ -0,0 +1,162 @@
import { NextRequest, NextResponse } from "next/server";
import { writeFile, mkdir } from "fs/promises";
import { existsSync } from "fs";
import path from "path";
import db from "@/lib/db";
import { auditLog } from "@/lib/middleware/auditLog";
const UPLOAD_DIR = path.join(process.cwd(), "public", "uploads");
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const ALLOWED_TYPES = [
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"image/jpeg",
"image/png",
"image/gif",
"text/plain"
];
export async function POST(request) {
try {
const formData = await request.formData();
const file = formData.get("file");
const entityType = formData.get("entityType");
const entityId = formData.get("entityId");
const description = formData.get("description") || "";
if (!file || !entityType || !entityId) {
return NextResponse.json(
{ error: "File, entityType, and entityId are required" },
{ status: 400 }
);
}
// Validate entity type
if (!["contract", "project", "task"].includes(entityType)) {
return NextResponse.json(
{ error: "Invalid entity type" },
{ status: 400 }
);
}
// Validate file
if (file.size > MAX_FILE_SIZE) {
return NextResponse.json(
{ error: "File size too large (max 10MB)" },
{ status: 400 }
);
}
if (!ALLOWED_TYPES.includes(file.type)) {
return NextResponse.json(
{ error: "File type not allowed" },
{ status: 400 }
);
}
// Create upload directory structure
const entityDir = path.join(UPLOAD_DIR, entityType + "s", entityId);
if (!existsSync(entityDir)) {
await mkdir(entityDir, { recursive: true });
}
// Generate unique filename
const timestamp = Date.now();
const sanitizedOriginalName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_");
const storedFilename = `${timestamp}_${sanitizedOriginalName}`;
const filePath = path.join(entityDir, storedFilename);
const relativePath = `/uploads/${entityType}s/${entityId}/${storedFilename}`;
// Save file
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
await writeFile(filePath, buffer);
// Save to database
const stmt = db.prepare(`
INSERT INTO file_attachments (
entity_type, entity_id, original_filename, stored_filename,
file_path, file_size, mime_type, description, uploaded_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
entityType,
parseInt(entityId),
file.name,
storedFilename,
relativePath,
file.size,
file.type,
description,
null // TODO: Get from session when auth is implemented
);
const newFile = {
file_id: result.lastInsertRowid,
entity_type: entityType,
entity_id: parseInt(entityId),
original_filename: file.name,
stored_filename: storedFilename,
file_path: relativePath,
file_size: file.size,
mime_type: file.type,
description: description,
upload_date: new Date().toISOString()
};
return NextResponse.json(newFile, { status: 201 });
} catch (error) {
console.error("File upload error:", error);
return NextResponse.json(
{ error: "Failed to upload file" },
{ status: 500 }
);
}
}
export async function GET(request) {
try {
const { searchParams } = new URL(request.url);
const entityType = searchParams.get("entityType");
const entityId = searchParams.get("entityId");
if (!entityType || !entityId) {
return NextResponse.json(
{ error: "entityType and entityId are required" },
{ status: 400 }
);
}
const files = db.prepare(`
SELECT
file_id,
entity_type,
entity_id,
original_filename,
stored_filename,
file_path,
file_size,
mime_type,
description,
upload_date,
uploaded_by
FROM file_attachments
WHERE entity_type = ? AND entity_id = ?
ORDER BY upload_date DESC
`).all(entityType, parseInt(entityId));
return NextResponse.json(files);
} catch (error) {
console.error("Error fetching files:", error);
return NextResponse.json(
{ error: "Failed to fetch files" },
{ status: 500 }
);
}
}