diff --git a/.env b/.env index cc0be73..3eb07f8 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ +PORT=3001 ATLAS_URI="mongodb://localhost:27017/file-upload" \ No newline at end of file diff --git a/config.js b/config.js index acc2611..ee43118 100644 --- a/config.js +++ b/config.js @@ -1,12 +1,24 @@ const mongoose = require("mongoose"); +// Configure mongoose for serverless +mongoose.set('strictQuery', false); + const connectDB = async () => { + // If already connected, return + if (mongoose.connection.readyState >= 1) { + console.log("MongoDB already connected"); + return; + } + try { - await mongoose.connect(process.env.ATLAS_URI); + await mongoose.connect(process.env.ATLAS_URI, { + serverSelectionTimeoutMS: 5000, // Timeout after 5s instead of 30s + socketTimeoutMS: 45000, + }); console.log("MongoDB connected"); } catch (err) { - console.error(err.message); - process.exit(1); + console.error("MongoDB connection error:", err.message); + throw err; // Throw error to be caught by middleware } }; diff --git a/controllers/fileController.js b/controllers/fileController.js new file mode 100644 index 0000000..5a0e3aa --- /dev/null +++ b/controllers/fileController.js @@ -0,0 +1,181 @@ +const mongoose = require("mongoose"); +const archiver = require("archiver"); +const { Transform } = require("stream"); +const FileModel = require("../models/File"); + +class FileController { + // Upload single file + async uploadFile(req, res) { + try { + res.status(201).json({ + text: "File uploaded successfully !", + file: req.file + }); + } catch (error) { + console.log(error); + res.status(400).json({ + error: { text: "Unable to upload the file", error }, + }); + } + } + + // Upload multiple files + async uploadFiles(req, res) { + try { + res.status(201).json({ + text: "Files uploaded successfully !", + files: req.files + }); + } catch (error) { + console.log(error); + res.status(400).json({ + error: { text: `Unable to upload files`, error }, + }); + } + } + + // Get all files + async getAllFiles(req, res) { + try { + const files = await FileModel.getAllFiles(); + res.status(200).json(files); + } catch (error) { + console.log(error); + res.status(400).json({ + error: { text: `Unable to retrieve files`, error }, + }); + } + } + + // Download a single file by id + async downloadFile(req, res) { + try { + const { fileId } = req.params; + + // Check if file exists + const file = await FileModel.getFileById(fileId); + if (file.length === 0) { + return res.status(404).json({ error: { text: "File not found" } }); + } + + // Set the headers + res.set("Content-Type", file[0].contentType); + res.set("Content-Disposition", `attachment; filename=${file[0].filename}`); + + // Create a stream to read from the bucket + const downloadStream = FileModel.openDownloadStream(fileId); + + // Pipe the stream to the response + downloadStream.pipe(res); + } catch (error) { + console.log(error); + res.status(400).json({ error: { text: `Unable to download file`, error } }); + } + } + + // Download multiple files in a zip file + async downloadFilesZip(req, res) { + try { + const files = await FileModel.getAllFiles(); + if (files.length === 0) { + return res.status(404).json({ error: { text: "No files found" } }); + } + + res.set("Content-Type", "application/zip"); + res.set("Content-Disposition", `attachment; filename=files.zip`); + res.set("Access-Control-Allow-Origin", "*"); + + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + archive.pipe(res); + + files.forEach((file) => { + const downloadStream = FileModel.openDownloadStream(file._id); + archive.append(downloadStream, { name: file.filename }); + }); + + archive.finalize(); + } catch (error) { + console.log(error); + res.status(400).json({ + error: { text: `Unable to download files`, error }, + }); + } + } + + // Download multiple files in base64 format + async downloadFilesBase64(req, res) { + try { + const files = await FileModel.getAllFiles(); + + const filesData = await Promise.all( + files.map((file) => { + return new Promise((resolve, _reject) => { + FileModel.openDownloadStream(file._id).pipe( + (() => { + const chunks = []; + return new Transform({ + transform(chunk, encoding, done) { + chunks.push(chunk); + done(); + }, + flush(done) { + const fbuf = Buffer.concat(chunks); + const fileBase64String = fbuf.toString("base64"); + resolve({ + filename: file.filename, + contentType: file.contentType, + data: fileBase64String, + size: file.length, + uploadDate: file.uploadDate + }); + done(); + }, + }); + })() + ); + }); + }) + ); + res.status(200).json(filesData); + } catch (error) { + console.log(error); + res.status(400).json({ + error: { text: `Unable to retrieve files`, error }, + }); + } + } + + // Rename a file + async renameFile(req, res) { + try { + const { fileId } = req.params; + const { filename } = req.body; + await FileModel.renameFile(fileId, filename); + res.status(200).json({ text: "File renamed successfully !" }); + } catch (error) { + console.log(error); + res.status(400).json({ + error: { text: `Unable to rename file`, error }, + }); + } + } + + // Delete a file + async deleteFile(req, res) { + try { + const { fileId } = req.params; + await FileModel.deleteFile(fileId); + res.status(200).json({ text: "File deleted successfully !" }); + } catch (error) { + console.log(error); + res.status(400).json({ + error: { text: `Unable to delete file`, error }, + }); + } + } +} + +module.exports = new FileController(); diff --git a/index.js b/index.js index 7583514..11a9000 100644 --- a/index.js +++ b/index.js @@ -2,190 +2,60 @@ const express = require("express"); const bodyParser = require("body-parser"); const logger = require("morgan"); const dotenv = require("dotenv"); +const path = require("path"); const connectDB = require("./config"); -const mongoose = require("mongoose"); -const { upload } = require("./utils/upload"); -const archiver = require("archiver"); -const { Transform } = require("stream"); +const fileRoutes = require("./routes/fileRoutes"); +const fs = require("fs"); dotenv.config(); const app = express(); -// Connect to database -connectDB(); - -// Connect to MongoDB GridFS bucket using mongoose -let bucket; -(() => { - mongoose.connection.on("connected", () => { - bucket = new mongoose.mongo.GridFSBucket(mongoose.connection.db, { - bucketName: "uploads", - }); - }); -})(); +// Lazy database connection - only connect when needed +let isConnected = false; +const ensureDbConnection = async () => { + if (!isConnected) { + await connectDB(); + isConnected = true; + } +}; // Middleware for parsing request body and logging requests app.use(bodyParser.json()); app.use(logger("dev")); -/* Routes for API endpoints */ -// Upload a single file -app.post("/upload/file", upload().single("file"), async (req, res) => { - try { - res.status(201).json({ text: "File uploaded successfully !" }); - } catch (error) { - console.log(error); - res.status(400).json({ - error: { text: "Unable to upload the file", error }, - }); - } -}); - -// Upload multiple files -app.post("/upload/files", upload().array("files"), async (req, res) => { - try { - res.status(201).json({ text: "Files uploaded successfully !" }); - } catch (error) { - console.log(error); - res.status(400).json({ - error: { text: `Unable to upload files`, error }, - }); - } -}); - -// Download a file by id -app.get("/download/files/:fileId", async (req, res) => { - try { - const { fileId } = req.params; - - // Check if file exists - const file = await bucket - .find({ _id: new mongoose.Types.ObjectId(fileId) }) - .toArray(); - if (file.length === 0) { - return res.status(404).json({ error: { text: "File not found" } }); - } - - // set the headers - res.set("Content-Type", file[0].contentType); - res.set("Content-Disposition", `attachment; filename=${file[0].filename}`); - - // create a stream to read from the bucket - const downloadStream = bucket.openDownloadStream( - new mongoose.Types.ObjectId(fileId) - ); - - // pipe the stream to the response - downloadStream.pipe(res); - } catch (error) { - console.log(error); - res.status(400).json({ error: { text: `Unable to download file`, error } }); - } -}); - -// Download multiple files in a zip file -app.get("/download/files-zip", async (req, res) => { +// Middleware to ensure DB connection for API routes +app.use(async (req, res, next) => { try { - const files = await bucket.find().toArray(); - if (files.length === 0) { - return res.status(404).json({ error: { text: "No files found" } }); - } - res.set("Content-Type", "application/zip"); - res.set("Content-Disposition", `attachment; filename=files.zip`); - res.set("Access-Control-Allow-Origin", "*"); - const archive = archiver("zip", { - zlib: { level: 9 }, - }); - - archive.pipe(res); - - files.forEach((file) => { - const downloadStream = bucket.openDownloadStream( - new mongoose.Types.ObjectId(file._id) - ); - archive.append(downloadStream, { name: file.filename }); - }); - - archive.finalize(); + await ensureDbConnection(); + next(); } catch (error) { - console.log(error); - res.status(400).json({ - error: { text: `Unable to download files`, error }, - }); + console.error("Database connection error:", error); + res.status(500).json({ error: "Database connection failed" }); } }); -// Download multiple files in base64 format -app.get("/download/files-base64", async (_req, res) => { - try { - const cursor = bucket.find(); - const files = await cursor.toArray(); - - const filesData = await Promise.all( - files.map((file) => { - return new Promise((resolve, _reject) => { - bucket.openDownloadStream(file._id).pipe( - (() => { - const chunks = []; - return new Transform({ - // transform method will - transform(chunk, encoding, done) { - chunks.push(chunk); - done(); - }, - flush(done) { - const fbuf = Buffer.concat(chunks); - const fileBase64String = fbuf.toString("base64"); - resolve(fileBase64String); - done(); - }, - }); - })() - ); - }); - }) - ); - res.status(200).json(filesData); - } catch (error) { - console.log(error); - res.status(400).json({ - error: { text: `Unable to retrieve files`, error }, - }); +// Serve the main HTML file for the root route +app.get("/", (req, res) => { + const indexPath = path.join(__dirname, "public", "index.html"); + if (fs.existsSync(indexPath)) { + res.sendFile(indexPath); + } else { + res.json({ message: "File Upload API with MongoDB - Server is running" }); } }); -// Rename a file -app.put("/rename/file/:fileId", async (req, res) => { - try { - const { fileId } = req.params; - const { filename } = req.body; - await bucket.rename(new mongoose.Types.ObjectId(fileId), filename); - res.status(200).json({ text: "File renamed successfully !" }); - } catch (error) { - console.log(error); - res.status(400).json({ - error: { text: `Unable to rename file`, error }, - }); - } -}); +// API Routes +app.use("/", fileRoutes); -// Delete a file -app.delete("/delete/file/:fileId", async (req, res) => { - try { - const { fileId } = req.params; - await bucket.delete(new mongoose.Types.ObjectId(fileId)); - res.status(200).json({ text: "File deleted successfully !" }); - } catch (error) { - console.log(error); - res.status(400).json({ - error: { text: `Unable to delete file`, error }, - }); - } -}); +// Export the Express app for Vercel +module.exports = app; -// Server listening on port 3000 for incoming requests -const port = process.env.PORT || 3000; -app.listen(port, () => { - console.log(`Server listening on port ${port}`); -}); +// Server listening on port 3001 for incoming requests (for local development) +if (require.main === module) { + const port = process.env.PORT || 3001; + app.listen(port, () => { + console.log(`Server listening on port ${port}`); + console.log(`Visit http://localhost:${port} to access the UI`); + }); +} diff --git a/models/File.js b/models/File.js new file mode 100644 index 0000000..acb2472 --- /dev/null +++ b/models/File.js @@ -0,0 +1,49 @@ +const mongoose = require("mongoose"); + +class FileModel { + constructor() { + this.bucket = null; + this.initBucket(); + } + + initBucket() { + mongoose.connection.on("connected", () => { + this.bucket = new mongoose.mongo.GridFSBucket(mongoose.connection.db, { + bucketName: "uploads", + }); + }); + } + + getBucket() { + return this.bucket; + } + + async getAllFiles() { + return await this.bucket.find().toArray(); + } + + async getFileById(fileId) { + return await this.bucket + .find({ _id: new mongoose.Types.ObjectId(fileId) }) + .toArray(); + } + + async renameFile(fileId, newFilename) { + await this.bucket.rename( + new mongoose.Types.ObjectId(fileId), + newFilename + ); + } + + async deleteFile(fileId) { + await this.bucket.delete(new mongoose.Types.ObjectId(fileId)); + } + + openDownloadStream(fileId) { + return this.bucket.openDownloadStream( + new mongoose.Types.ObjectId(fileId) + ); + } +} + +module.exports = new FileModel(); diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..0bc6d57 --- /dev/null +++ b/public/index.html @@ -0,0 +1,100 @@ + + + + + + File Upload with MongoDB + + + +
+
+

📁 File Management System

+

Upload, manage, and download your files

+
+ + +
+
+

Upload Files

+
+
+

Single File Upload

+
+ + +
+
+
+

Multiple Files Upload

+
+ + +
+
+
+
+
+ + +
+
+

Bulk Download Options

+
+ + +
+
+
+ + +
+
+
+

Uploaded Files

+ +
+
+ + + + + + + + + + + + + + + +
FilenameSizeTypeUpload DateActions
No files uploaded yet
+
+
+
+ + +
+
+ + + + + + + diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..7c83773 --- /dev/null +++ b/public/script.js @@ -0,0 +1,319 @@ +// API Base URL +const API_BASE = ''; + +// Global variables +let currentFileIdForRename = null; + +// DOM Elements +const singleUploadForm = document.getElementById('singleUploadForm'); +const multipleUploadForm = document.getElementById('multipleUploadForm'); +const filesTableBody = document.getElementById('filesTableBody'); +const refreshBtn = document.getElementById('refreshBtn'); +const downloadZipBtn = document.getElementById('downloadZipBtn'); +const downloadBase64Btn = document.getElementById('downloadBase64Btn'); +const toast = document.getElementById('toast'); +const renameModal = document.getElementById('renameModal'); +const newFilenameInput = document.getElementById('newFilename'); +const confirmRenameBtn = document.getElementById('confirmRename'); +const cancelRenameBtn = document.getElementById('cancelRename'); + +// Toast Notification +function showToast(message, type = 'info') { + toast.textContent = message; + toast.className = `toast ${type} show`; + setTimeout(() => { + toast.classList.remove('show'); + }, 3000); +} + +// Format file size +function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; +} + +// Format date +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleString(); +} + +// Load all files +async function loadFiles() { + try { + const response = await fetch(`${API_BASE}/files`); + const files = await response.json(); + + if (files.length === 0) { + filesTableBody.innerHTML = 'No files uploaded yet'; + return; + } + + filesTableBody.innerHTML = files.map(file => ` + + ${file.filename} + ${formatFileSize(file.length)} + ${file.contentType || 'N/A'} + ${formatDate(file.uploadDate)} + +
+ + + +
+ + + `).join(''); + } catch (error) { + console.error('Error loading files:', error); + showToast('Failed to load files', 'error'); + } +} + +// Upload single file +singleUploadForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const formData = new FormData(); + const fileInput = document.getElementById('singleFile'); + const submitBtn = singleUploadForm.querySelector('button[type="submit"]'); + + formData.append('file', fileInput.files[0]); + + // Disable button during upload + submitBtn.disabled = true; + submitBtn.innerHTML = ' Uploading...'; + + try { + const response = await fetch(`${API_BASE}/upload/file`, { + method: 'POST', + body: formData + }); + + const result = await response.json(); + if (response.ok) { + showToast(result.text, 'success'); + fileInput.value = ''; + loadFiles(); + } else { + showToast(result.error.text, 'error'); + } + } catch (error) { + console.error('Error uploading file:', error); + showToast('Failed to upload file', 'error'); + } finally { + // Re-enable button + submitBtn.disabled = false; + submitBtn.textContent = 'Upload File'; + } +}); + +// Upload multiple files +multipleUploadForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const formData = new FormData(); + const filesInput = document.getElementById('multipleFiles'); + const submitBtn = multipleUploadForm.querySelector('button[type="submit"]'); + + for (let file of filesInput.files) { + formData.append('files', file); + } + + // Disable button during upload + submitBtn.disabled = true; + submitBtn.innerHTML = ' Uploading...'; + + try { + const response = await fetch(`${API_BASE}/upload/files`, { + method: 'POST', + body: formData + }); + + const result = await response.json(); + if (response.ok) { + showToast(result.text, 'success'); + filesInput.value = ''; + loadFiles(); + } else { + showToast(result.error.text, 'error'); + } + } catch (error) { + console.error('Error uploading files:', error); + showToast('Failed to upload files', 'error'); + } finally { + // Re-enable button + submitBtn.disabled = false; + submitBtn.textContent = 'Upload Files'; + } +}); + +// Download single file +async function downloadFile(fileId, filename) { + try { + const response = await fetch(`${API_BASE}/download/files/${fileId}`); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + showToast('File downloaded successfully', 'success'); + } catch (error) { + console.error('Error downloading file:', error); + showToast('Failed to download file', 'error'); + } +} + +// Download all files as ZIP +downloadZipBtn.addEventListener('click', async () => { + try { + const response = await fetch(`${API_BASE}/download/files-zip`); + if (!response.ok) { + const error = await response.json(); + showToast(error.error.text, 'error'); + return; + } + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'files.zip'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + showToast('All files downloaded as ZIP', 'success'); + } catch (error) { + console.error('Error downloading ZIP:', error); + showToast('Failed to download ZIP', 'error'); + } +}); + +// Download all files as Base64 +downloadBase64Btn.addEventListener('click', async () => { + try { + const response = await fetch(`${API_BASE}/download/files-base64`); + if (!response.ok) { + const error = await response.json(); + showToast(error.error.text, 'error'); + return; + } + const filesData = await response.json(); + + // Create a JSON file with all base64 data + const dataStr = JSON.stringify(filesData, null, 2); + const blob = new Blob([dataStr], { type: 'application/json' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'files-base64.json'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + showToast('Base64 files downloaded successfully', 'success'); + } catch (error) { + console.error('Error downloading base64:', error); + showToast('Failed to download base64 files', 'error'); + } +}); + +// Open rename modal +function openRenameModal(fileId, currentFilename) { + currentFileIdForRename = fileId; + newFilenameInput.value = currentFilename; + renameModal.classList.add('show'); + newFilenameInput.focus(); +} + +// Close rename modal +function closeRenameModal() { + renameModal.classList.remove('show'); + currentFileIdForRename = null; + newFilenameInput.value = ''; +} + +// Confirm rename +confirmRenameBtn.addEventListener('click', async () => { + const newFilename = newFilenameInput.value.trim(); + if (!newFilename) { + showToast('Please enter a filename', 'error'); + return; + } + + try { + const response = await fetch(`${API_BASE}/rename/file/${currentFileIdForRename}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ filename: newFilename }) + }); + + const result = await response.json(); + if (response.ok) { + showToast(result.text, 'success'); + closeRenameModal(); + loadFiles(); + } else { + showToast(result.error.text, 'error'); + } + } catch (error) { + console.error('Error renaming file:', error); + showToast('Failed to rename file', 'error'); + } +}); + +// Cancel rename +cancelRenameBtn.addEventListener('click', closeRenameModal); + +// Close modal on outside click +renameModal.addEventListener('click', (e) => { + if (e.target === renameModal) { + closeRenameModal(); + } +}); + +// Delete file +async function deleteFile(fileId) { + if (!confirm('Are you sure you want to delete this file?')) { + return; + } + + try { + const response = await fetch(`${API_BASE}/delete/file/${fileId}`, { + method: 'DELETE' + }); + + const result = await response.json(); + if (response.ok) { + showToast(result.text, 'success'); + loadFiles(); + } else { + showToast(result.error.text, 'error'); + } + } catch (error) { + console.error('Error deleting file:', error); + showToast('Failed to delete file', 'error'); + } +} + +// Refresh files +refreshBtn.addEventListener('click', () => { + loadFiles(); + showToast('Files refreshed', 'info'); +}); + +// Load files on page load +document.addEventListener('DOMContentLoaded', loadFiles); diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..2f6a0e7 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,330 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} + +header { + text-align: center; + color: white; + margin-bottom: 30px; + padding: 20px; +} + +header h1 { + font-size: 2.5rem; + margin-bottom: 10px; +} + +header p { + font-size: 1.1rem; + opacity: 0.9; +} + +.card { + background: white; + border-radius: 12px; + padding: 25px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + margin-bottom: 25px; +} + +/* Upload Section */ +.upload-section .upload-options { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-top: 20px; +} + +.upload-option { + padding: 20px; + border: 2px dashed #667eea; + border-radius: 8px; + background: #f8f9ff; +} + +.upload-option h3 { + color: #667eea; + margin-bottom: 15px; + font-size: 1.1rem; +} + +.upload-option input[type="file"] { + display: block; + width: 100%; + margin-bottom: 15px; + padding: 10px; + border: 1px solid #ddd; + border-radius: 6px; + background: white; +} + +/* Buttons */ +.btn { + padding: 12px 24px; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); +} + +.btn-secondary { + background: #6c757d; + color: white; +} + +.btn-secondary:hover { + background: #5a6268; + transform: translateY(-2px); +} + +.btn-success { + background: #28a745; + color: white; + padding: 8px 16px; + font-size: 0.9rem; +} + +.btn-danger { + background: #dc3545; + color: white; + padding: 8px 16px; + font-size: 0.9rem; +} + +.btn-warning { + background: #ffc107; + color: #000; + padding: 8px 16px; + font-size: 0.9rem; +} + +.btn-info { + background: #17a2b8; + color: white; + padding: 8px 16px; + font-size: 0.9rem; +} + +.btn-small { + padding: 8px 16px; + font-size: 0.9rem; +} + +/* Download Section */ +.download-section .download-options { + display: flex; + gap: 15px; + justify-content: center; + flex-wrap: wrap; +} + +/* Files Section */ +.files-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.files-header h2 { + color: #333; +} + +.table-container { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +thead { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +th { + padding: 15px; + text-align: left; + font-weight: 600; + text-transform: uppercase; + font-size: 0.9rem; + letter-spacing: 0.5px; +} + +td { + padding: 15px; + border-bottom: 1px solid #eee; +} + +tbody tr:hover { + background: #f8f9ff; +} + +.no-files { + text-align: center; + color: #999; + font-style: italic; + padding: 40px !important; +} + +.action-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +/* Toast Notification */ +.toast { + position: fixed; + bottom: 30px; + right: 30px; + padding: 15px 25px; + background: #333; + color: white; + border-radius: 8px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3); + transform: translateX(400px); + transition: transform 0.3s ease; + z-index: 1000; + max-width: 350px; +} + +.toast.show { + transform: translateX(0); +} + +.toast.success { + background: #28a745; +} + +.toast.error { + background: #dc3545; +} + +.toast.info { + background: #17a2b8; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; +} + +.modal.show { + display: flex; +} + +.modal-content { + background: white; + padding: 30px; + border-radius: 12px; + max-width: 400px; + width: 90%; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); +} + +.modal-content h3 { + margin-bottom: 20px; + color: #333; +} + +.modal-content input { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 1rem; + margin-bottom: 20px; +} + +.modal-actions { + display: flex; + gap: 10px; + justify-content: flex-end; +} + +/* Responsive Design */ +@media (max-width: 768px) { + header h1 { + font-size: 1.8rem; + } + + .upload-options { + grid-template-columns: 1fr !important; + } + + .download-options { + flex-direction: column; + } + + .action-buttons { + flex-direction: column; + } + + table { + font-size: 0.85rem; + } + + th, td { + padding: 10px; + } +} + +/* Loading Spinner */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} diff --git a/routes/fileRoutes.js b/routes/fileRoutes.js new file mode 100644 index 0000000..31250fa --- /dev/null +++ b/routes/fileRoutes.js @@ -0,0 +1,35 @@ +const express = require("express"); +const { upload } = require("../utils/upload"); +const fileController = require("../controllers/fileController"); + +const router = express.Router(); + +// Upload a single file +router.post("/upload/file", (req, res, next) => { + upload().single("file")(req, res, next); +}, fileController.uploadFile); + +// Upload multiple files +router.post("/upload/files", (req, res, next) => { + upload().array("files")(req, res, next); +}, fileController.uploadFiles); + +// Get all files +router.get("/files", fileController.getAllFiles); + +// Download a file by id +router.get("/download/files/:fileId", fileController.downloadFile); + +// Download multiple files in a zip file +router.get("/download/files-zip", fileController.downloadFilesZip); + +// Download multiple files in base64 format +router.get("/download/files-base64", fileController.downloadFilesBase64); + +// Rename a file +router.put("/rename/file/:fileId", fileController.renameFile); + +// Delete a file +router.delete("/delete/file/:fileId", fileController.deleteFile); + +module.exports = router; diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..9b6a863 --- /dev/null +++ b/vercel.json @@ -0,0 +1,9 @@ +{ + "version": 2, + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.js" + } + ] +}