Updatweb
This commit is contained in:
@@ -5,6 +5,15 @@ const path = require("path");
|
||||
const fs = require("fs").promises;
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
const { pool } = require("../config/database");
|
||||
const logger = require("../config/logger");
|
||||
const { uploadLimiter } = require("../config/rateLimiter");
|
||||
require("dotenv").config();
|
||||
|
||||
// Allowed file types
|
||||
const ALLOWED_MIME_TYPES = (
|
||||
process.env.ALLOWED_FILE_TYPES || "image/jpeg,image/png,image/gif,image/webp"
|
||||
).split(",");
|
||||
const MAX_FILE_SIZE = parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024; // 5MB default
|
||||
|
||||
// Configure multer for file uploads
|
||||
const storage = multer.diskStorage({
|
||||
@@ -14,17 +23,19 @@ const storage = multer.diskStorage({
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
cb(null, uploadDir);
|
||||
} catch (error) {
|
||||
logger.error("Error creating upload directory:", error);
|
||||
cb(error);
|
||||
}
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
// Generate unique filename
|
||||
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
||||
const ext = path.extname(file.originalname);
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
const name = path
|
||||
.basename(file.originalname, ext)
|
||||
.replace(/[^a-z0-9]/gi, "-")
|
||||
.toLowerCase();
|
||||
.toLowerCase()
|
||||
.substring(0, 50); // Limit filename length
|
||||
cb(null, name + "-" + uniqueSuffix + ext);
|
||||
},
|
||||
});
|
||||
@@ -32,13 +43,37 @@ const storage = multer.diskStorage({
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024, // 5MB limit
|
||||
fileSize: MAX_FILE_SIZE,
|
||||
files: 10, // Max 10 files per request
|
||||
},
|
||||
fileFilter: function (req, file, cb) {
|
||||
// Accept images only
|
||||
if (!file.mimetype.startsWith("image/")) {
|
||||
return cb(new Error("Only image files are allowed!"), false);
|
||||
// Validate MIME type
|
||||
if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) {
|
||||
logger.warn("File upload rejected - invalid type", {
|
||||
mimetype: file.mimetype,
|
||||
userId: req.session?.user?.id,
|
||||
});
|
||||
return cb(
|
||||
new Error(
|
||||
`File type not allowed. Allowed types: ${ALLOWED_MIME_TYPES.join(
|
||||
", "
|
||||
)}`
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// Validate file extension
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
const allowedExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
|
||||
if (!allowedExtensions.includes(ext)) {
|
||||
logger.warn("File upload rejected - invalid extension", {
|
||||
extension: ext,
|
||||
userId: req.session?.user?.id,
|
||||
});
|
||||
return cb(new Error("Invalid file extension"), false);
|
||||
}
|
||||
|
||||
cb(null, true);
|
||||
},
|
||||
});
|
||||
@@ -47,37 +82,72 @@ const upload = multer({
|
||||
router.post(
|
||||
"/upload",
|
||||
requireAuth,
|
||||
uploadLimiter,
|
||||
upload.array("files", 10),
|
||||
async (req, res) => {
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "No files uploaded",
|
||||
});
|
||||
}
|
||||
|
||||
const uploadedBy = req.session.user?.id || null;
|
||||
const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null;
|
||||
const files = [];
|
||||
|
||||
// Insert each file into database
|
||||
for (const file of req.files) {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO uploads
|
||||
(filename, original_name, file_path, file_size, mime_type, uploaded_by, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
||||
RETURNING id, filename, original_name, file_path, file_size, mime_type, created_at`,
|
||||
[
|
||||
file.filename,
|
||||
file.originalname,
|
||||
`/uploads/${file.filename}`,
|
||||
file.size,
|
||||
file.mimetype,
|
||||
uploadedBy,
|
||||
]
|
||||
);
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO uploads
|
||||
(filename, original_name, file_path, file_size, mime_type, uploaded_by, folder_id, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
||||
RETURNING id, filename, original_name, file_path, file_size, mime_type, folder_id, created_at`,
|
||||
[
|
||||
file.filename,
|
||||
file.originalname,
|
||||
`/uploads/${file.filename}`,
|
||||
file.size,
|
||||
file.mimetype,
|
||||
uploadedBy,
|
||||
folderId,
|
||||
]
|
||||
);
|
||||
|
||||
files.push({
|
||||
id: result.rows[0].id,
|
||||
filename: result.rows[0].filename,
|
||||
originalName: result.rows[0].original_name,
|
||||
size: result.rows[0].file_size,
|
||||
mimetype: result.rows[0].mime_type,
|
||||
path: result.rows[0].file_path,
|
||||
uploadDate: result.rows[0].created_at,
|
||||
files.push({
|
||||
id: result.rows[0].id,
|
||||
filename: result.rows[0].filename,
|
||||
originalName: result.rows[0].original_name,
|
||||
size: result.rows[0].file_size,
|
||||
mimetype: result.rows[0].mime_type,
|
||||
path: result.rows[0].file_path,
|
||||
uploadDate: result.rows[0].created_at,
|
||||
folderId: result.rows[0].folder_id,
|
||||
});
|
||||
|
||||
logger.info("File uploaded successfully", {
|
||||
fileId: result.rows[0].id,
|
||||
filename: file.filename,
|
||||
userId: uploadedBy,
|
||||
});
|
||||
} catch (dbError) {
|
||||
logger.error("Database insert failed for file:", {
|
||||
filename: file.filename,
|
||||
error: dbError.message,
|
||||
});
|
||||
// Clean up this specific file
|
||||
await fs
|
||||
.unlink(file.path)
|
||||
.catch((err) => logger.error("Failed to clean up file:", err));
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Failed to save uploaded files",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,23 +157,19 @@ router.post(
|
||||
files: files,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error);
|
||||
logger.error("Upload error:", error);
|
||||
|
||||
// If database insert fails, clean up uploaded files
|
||||
// Clean up all uploaded files on error
|
||||
if (req.files) {
|
||||
for (const file of req.files) {
|
||||
try {
|
||||
await fs.unlink(file.path);
|
||||
} catch (unlinkError) {
|
||||
console.error("Error cleaning up file:", unlinkError);
|
||||
logger.error("Error cleaning up file:", unlinkError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -111,9 +177,9 @@ router.post(
|
||||
// Get all uploaded files
|
||||
router.get("/uploads", requireAuth, async (req, res) => {
|
||||
try {
|
||||
// Query files from database
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
const folderId = req.query.folder_id;
|
||||
|
||||
let query = `SELECT
|
||||
id,
|
||||
filename,
|
||||
original_name,
|
||||
@@ -121,13 +187,27 @@ router.get("/uploads", requireAuth, async (req, res) => {
|
||||
file_size,
|
||||
mime_type,
|
||||
uploaded_by,
|
||||
folder_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
used_in_type,
|
||||
used_in_id
|
||||
FROM uploads
|
||||
ORDER BY created_at DESC`
|
||||
);
|
||||
FROM uploads`;
|
||||
|
||||
const params = [];
|
||||
|
||||
if (folderId !== undefined) {
|
||||
if (folderId === "null" || folderId === "") {
|
||||
query += ` WHERE folder_id IS NULL`;
|
||||
} else {
|
||||
query += ` WHERE folder_id = $1`;
|
||||
params.push(parseInt(folderId));
|
||||
}
|
||||
}
|
||||
|
||||
query += ` ORDER BY created_at DESC`;
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
const files = result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
@@ -138,6 +218,7 @@ router.get("/uploads", requireAuth, async (req, res) => {
|
||||
path: row.file_path,
|
||||
uploadDate: row.created_at,
|
||||
uploadedBy: row.uploaded_by,
|
||||
folderId: row.folder_id,
|
||||
usedInType: row.used_in_type,
|
||||
usedInId: row.used_in_id,
|
||||
}));
|
||||
@@ -147,7 +228,7 @@ router.get("/uploads", requireAuth, async (req, res) => {
|
||||
files: files,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error listing files:", error);
|
||||
logger.error("Error listing files:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
@@ -187,7 +268,7 @@ router.delete("/uploads/:filename", requireAuth, async (req, res) => {
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (fileError) {
|
||||
console.warn("File already deleted from disk:", filename);
|
||||
logger.warn("File already deleted from disk:", filename);
|
||||
// Continue anyway since database record is deleted
|
||||
}
|
||||
|
||||
@@ -196,7 +277,339 @@ router.delete("/uploads/:filename", requireAuth, async (req, res) => {
|
||||
message: "File deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting file:", error);
|
||||
logger.error("Error deleting file:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete file by ID
|
||||
router.delete("/uploads/id/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const fileId = parseInt(req.params.id);
|
||||
|
||||
// Get file info first
|
||||
const fileResult = await pool.query(
|
||||
"SELECT filename FROM uploads WHERE id = $1",
|
||||
[fileId]
|
||||
);
|
||||
|
||||
if (fileResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "File not found",
|
||||
});
|
||||
}
|
||||
|
||||
const filename = fileResult.rows[0].filename;
|
||||
const uploadDir = path.join(__dirname, "..", "..", "website", "uploads");
|
||||
const filePath = path.join(uploadDir, filename);
|
||||
|
||||
// Delete from database
|
||||
await pool.query("DELETE FROM uploads WHERE id = $1", [fileId]);
|
||||
|
||||
// Delete physical file
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (fileError) {
|
||||
logger.warn("File already deleted from disk:", filename);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "File deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting file:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ===== FOLDER MANAGEMENT ROUTES =====
|
||||
|
||||
// Create a new folder
|
||||
router.post("/folders", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { name, parent_id } = req.body;
|
||||
|
||||
if (!name || name.trim() === "") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Folder name is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize folder name
|
||||
const sanitizedName = name.trim().replace(/[^a-zA-Z0-9\s\-_]/g, "");
|
||||
|
||||
// Build path
|
||||
let path = `/${sanitizedName}`;
|
||||
if (parent_id) {
|
||||
const parentResult = await pool.query(
|
||||
"SELECT path FROM media_folders WHERE id = $1",
|
||||
[parent_id]
|
||||
);
|
||||
|
||||
if (parentResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Parent folder not found",
|
||||
});
|
||||
}
|
||||
|
||||
path = `${parentResult.rows[0].path}/${sanitizedName}`;
|
||||
}
|
||||
|
||||
const createdBy = req.session.user?.id || null;
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO media_folders (name, parent_id, path, created_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, name, parent_id, path, created_at`,
|
||||
[sanitizedName, parent_id || null, path, createdBy]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
folder: {
|
||||
id: result.rows[0].id,
|
||||
name: result.rows[0].name,
|
||||
parentId: result.rows[0].parent_id,
|
||||
path: result.rows[0].path,
|
||||
createdAt: result.rows[0].created_at,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === "23505") {
|
||||
// Unique constraint violation
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "A folder with this name already exists in this location",
|
||||
});
|
||||
}
|
||||
logger.error("Error creating folder:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get all folders
|
||||
router.get("/folders", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
f.id,
|
||||
f.name,
|
||||
f.parent_id,
|
||||
f.path,
|
||||
f.created_at,
|
||||
(SELECT COUNT(*) FROM uploads WHERE folder_id = f.id) as file_count,
|
||||
(SELECT COUNT(*) FROM media_folders WHERE parent_id = f.id) as subfolder_count
|
||||
FROM media_folders f
|
||||
ORDER BY f.path ASC`
|
||||
);
|
||||
|
||||
const folders = result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
parentId: row.parent_id,
|
||||
path: row.path,
|
||||
createdAt: row.created_at,
|
||||
fileCount: parseInt(row.file_count),
|
||||
subfolderCount: parseInt(row.subfolder_count),
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
folders: folders,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error listing folders:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a folder (and optionally its contents)
|
||||
router.delete("/folders/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const folderId = parseInt(req.params.id);
|
||||
const deleteContents = req.query.delete_contents === "true";
|
||||
|
||||
// Check if folder exists
|
||||
const folderResult = await pool.query(
|
||||
"SELECT id, name FROM media_folders WHERE id = $1",
|
||||
[folderId]
|
||||
);
|
||||
|
||||
if (folderResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Folder not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteContents) {
|
||||
// Delete all files in this folder and subfolders
|
||||
const filesResult = await pool.query(
|
||||
`SELECT u.filename FROM uploads u
|
||||
WHERE u.folder_id = $1 OR u.folder_id IN (
|
||||
SELECT id FROM media_folders WHERE path LIKE (
|
||||
SELECT path || '%' FROM media_folders WHERE id = $1
|
||||
)
|
||||
)`,
|
||||
[folderId]
|
||||
);
|
||||
|
||||
// Delete physical files
|
||||
const uploadDir = path.join(__dirname, "..", "..", "website", "uploads");
|
||||
for (const row of filesResult.rows) {
|
||||
try {
|
||||
await fs.unlink(path.join(uploadDir, row.filename));
|
||||
} catch (err) {
|
||||
logger.warn(`Could not delete file: ${row.filename}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete folder (cascade will delete subfolders and DB records)
|
||||
await pool.query("DELETE FROM media_folders WHERE id = $1", [folderId]);
|
||||
} else {
|
||||
// Check if folder has contents
|
||||
const contentsCheck = await pool.query(
|
||||
`SELECT
|
||||
(SELECT COUNT(*) FROM uploads WHERE folder_id = $1) as file_count,
|
||||
(SELECT COUNT(*) FROM media_folders WHERE parent_id = $1) as subfolder_count`,
|
||||
[folderId]
|
||||
);
|
||||
|
||||
const fileCount = parseInt(contentsCheck.rows[0].file_count);
|
||||
const subfolderCount = parseInt(contentsCheck.rows[0].subfolder_count);
|
||||
|
||||
if (fileCount > 0 || subfolderCount > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Folder contains ${fileCount} file(s) and ${subfolderCount} subfolder(s). Delete contents first or use delete_contents=true`,
|
||||
});
|
||||
}
|
||||
|
||||
await pool.query("DELETE FROM media_folders WHERE id = $1", [folderId]);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Folder deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting folder:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Move files to a folder
|
||||
router.patch("/uploads/move", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { file_ids, folder_id } = req.body;
|
||||
|
||||
if (!Array.isArray(file_ids) || file_ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "file_ids array is required",
|
||||
});
|
||||
}
|
||||
|
||||
const targetFolderId = folder_id || null;
|
||||
|
||||
// Verify folder exists if provided
|
||||
if (targetFolderId) {
|
||||
const folderCheck = await pool.query(
|
||||
"SELECT id FROM media_folders WHERE id = $1",
|
||||
[targetFolderId]
|
||||
);
|
||||
|
||||
if (folderCheck.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Target folder not found",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Move files
|
||||
const result = await pool.query(
|
||||
`UPDATE uploads
|
||||
SET folder_id = $1, updated_at = NOW()
|
||||
WHERE id = ANY($2::int[])
|
||||
RETURNING id`,
|
||||
[targetFolderId, file_ids]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${result.rowCount} file(s) moved successfully`,
|
||||
movedCount: result.rowCount,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error moving files:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Bulk delete files
|
||||
router.post("/uploads/bulk-delete", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { file_ids } = req.body;
|
||||
|
||||
if (!Array.isArray(file_ids) || file_ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "file_ids array is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Get filenames first
|
||||
const filesResult = await pool.query(
|
||||
"SELECT filename FROM uploads WHERE id = ANY($1::int[])",
|
||||
[file_ids]
|
||||
);
|
||||
|
||||
// Delete from database
|
||||
const result = await pool.query(
|
||||
"DELETE FROM uploads WHERE id = ANY($1::int[])",
|
||||
[file_ids]
|
||||
);
|
||||
|
||||
// Delete physical files
|
||||
const uploadDir = path.join(__dirname, "..", "..", "website", "uploads");
|
||||
for (const row of filesResult.rows) {
|
||||
try {
|
||||
await fs.unlink(path.join(uploadDir, row.filename));
|
||||
} catch (err) {
|
||||
logger.warn(`Could not delete file: ${row.filename}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${result.rowCount} file(s) deleted successfully`,
|
||||
deletedCount: result.rowCount,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error bulk deleting files:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
|
||||
Reference in New Issue
Block a user