feat: Implement file upload and management system with database integration
This commit is contained in:
79
src/app/api/files/[fileId]/route.js
Normal file
79
src/app/api/files/[fileId]/route.js
Normal 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
162
src/app/api/files/route.js
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user