Initial commit: simple file server
Add a new simple-file-server project. Includes Express server, auth and file routes, static client assets (public), and a db/connection module that uses MariaDB to log file actions and initialize a users table. Add Dockerfile and docker-compose.yml (exposes 3000 → 8080 and mounts ./uploads), .env.example, .gitignore, package.json and package-lock.json, and an uploads scaffold. This provides a ready-to-run app with container support and basic DB integration.
This commit is contained in:
216
routes/files.js
Normal file
216
routes/files.js
Normal file
@@ -0,0 +1,216 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const multer = require('multer');
|
||||
const AdmZip = require('adm-zip');
|
||||
const archiver = require('archiver');
|
||||
const { logFileAction } = require('../db/connection');
|
||||
|
||||
const UPLOADS_DIR = path.join(__dirname, '../uploads');
|
||||
|
||||
// Helper to get safe path
|
||||
function getSafePath(reqPath) {
|
||||
const safeInfo = path.parse(reqPath);
|
||||
// Prevent directory traversal
|
||||
if (reqPath.includes('..') || path.isAbsolute(reqPath)) {
|
||||
// Simple sanitization, better to use path.normalize and check prefix
|
||||
const normalized = path.normalize(path.join(UPLOADS_DIR, reqPath));
|
||||
if (!normalized.startsWith(UPLOADS_DIR)) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
return path.join(UPLOADS_DIR, reqPath);
|
||||
}
|
||||
|
||||
// Ensure uploads dir exists
|
||||
if (!fs.existsSync(UPLOADS_DIR)) fs.mkdirSync(UPLOADS_DIR);
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
// Support uploading to subfolders via query param or body
|
||||
// Multer processes before body, so we might need to send path in header or move file after
|
||||
// For simplicity: upload to temp then move, or just root for now and move API.
|
||||
// Let's rely on the query param 'path' passed to the URL
|
||||
let uploadPath = UPLOADS_DIR;
|
||||
if (req.query.path) {
|
||||
const safe = getSafePath(req.query.path);
|
||||
if (safe) uploadPath = safe;
|
||||
}
|
||||
if (!fs.existsSync(uploadPath)) fs.mkdirSync(uploadPath, { recursive: true });
|
||||
cb(null, uploadPath)
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
cb(null, file.originalname)
|
||||
}
|
||||
});
|
||||
const upload = multer({ storage: storage });
|
||||
|
||||
// List Files & Folders
|
||||
router.get('/files', (req, res) => {
|
||||
const relPath = req.query.path || '';
|
||||
const fullPath = getSafePath(relPath);
|
||||
|
||||
if (!fullPath || !fs.existsSync(fullPath)) {
|
||||
return res.status(400).json({ error: 'Invalid path' });
|
||||
}
|
||||
|
||||
fs.readdir(fullPath, { withFileTypes: true }, (err, entries) => {
|
||||
if (err) return res.status(500).json({ error: 'Read error' });
|
||||
|
||||
const content = entries.map(entry => {
|
||||
const stats = fs.statSync(path.join(fullPath, entry.name));
|
||||
return {
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
size: stats.size,
|
||||
mtime: stats.mtime,
|
||||
path: path.join(relPath, entry.name).replace(/\\/g, '/')
|
||||
};
|
||||
});
|
||||
|
||||
// Sort folders first
|
||||
content.sort((a, b) => (a.isDirectory === b.isDirectory ? 0 : a.isDirectory ? -1 : 1));
|
||||
res.json(content);
|
||||
});
|
||||
});
|
||||
|
||||
// Upload
|
||||
router.post('/upload', upload.single('file'), async (req, res) => {
|
||||
if (!req.file) return res.status(400).send('No file');
|
||||
await logFileAction(req.file.originalname, 'UPLOAD', req.session?.user?.username);
|
||||
res.send('Uploaded');
|
||||
});
|
||||
|
||||
// Create Folder
|
||||
router.post('/folders', (req, res) => {
|
||||
const relPath = req.body.path || '';
|
||||
const name = req.body.name;
|
||||
if (!name) return res.status(400).json({ error: 'Name required' });
|
||||
|
||||
const fullPath = getSafePath(path.join(relPath, name));
|
||||
if (!fullPath) return res.status(400).json({ error: 'Invalid path' });
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
fs.mkdirSync(fullPath, { recursive: true });
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(400).json({ error: 'Exists' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete Item (File or Folder)
|
||||
router.delete('/files', async (req, res) => {
|
||||
const relPath = req.query.path;
|
||||
const fullPath = getSafePath(relPath);
|
||||
|
||||
if (!fullPath || !fs.existsSync(fullPath)) return res.status(404).json({ error: 'Not found' });
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(fullPath);
|
||||
if (stats.isDirectory()) {
|
||||
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||
} else {
|
||||
fs.unlinkSync(fullPath);
|
||||
}
|
||||
await logFileAction(relPath, 'DELETE', req.session?.user?.username);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Download File
|
||||
router.get('/download', async (req, res) => {
|
||||
const relPath = req.query.path;
|
||||
const fullPath = getSafePath(relPath);
|
||||
if (fullPath && fs.existsSync(fullPath)) {
|
||||
await logFileAction(relPath, 'DOWNLOAD', req.session?.user?.username);
|
||||
res.download(fullPath);
|
||||
} else {
|
||||
res.status(404).send('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
// ZIP Preview
|
||||
router.get('/zip/preview', (req, res) => {
|
||||
const relPath = req.query.path;
|
||||
const fullPath = getSafePath(relPath);
|
||||
|
||||
if (!fullPath || !fs.existsSync(fullPath)) return res.status(404).json({ error: 'Not found' });
|
||||
|
||||
try {
|
||||
const zip = new AdmZip(fullPath);
|
||||
const zipEntries = zip.getEntries();
|
||||
const entries = zipEntries.map(entry => ({
|
||||
name: entry.entryName,
|
||||
isDirectory: entry.isDirectory,
|
||||
size: entry.header.size
|
||||
}));
|
||||
res.json(entries);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Failed to read ZIP' });
|
||||
}
|
||||
});
|
||||
|
||||
// Bulk Download
|
||||
router.post('/bulk-download', (req, res) => {
|
||||
const paths = req.body.paths; // Array of relative paths
|
||||
if (!paths || !Array.isArray(paths) || paths.length === 0) return res.status(400).send('No files');
|
||||
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
|
||||
res.attachment('download.zip');
|
||||
archive.pipe(res);
|
||||
|
||||
paths.forEach(relPath => {
|
||||
const fullPath = getSafePath(relPath);
|
||||
if (fullPath && fs.existsSync(fullPath)) {
|
||||
const stats = fs.statSync(fullPath);
|
||||
if (stats.isDirectory()) {
|
||||
archive.directory(fullPath, path.basename(relPath));
|
||||
} else {
|
||||
archive.file(fullPath, { name: path.basename(relPath) });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
// Move/Copy
|
||||
router.post('/ops/move', async (req, res) => {
|
||||
const { source, destination } = req.body;
|
||||
const srcPath = getSafePath(source);
|
||||
const destPath = getSafePath(destination); // Destination should include the new filename/dirname
|
||||
|
||||
if (!srcPath || !destPath || !fs.existsSync(srcPath)) return res.status(400).json({ error: 'Invalid' });
|
||||
|
||||
try {
|
||||
fs.renameSync(srcPath, destPath);
|
||||
await logFileAction(source, 'MOVE', req.session?.user?.username);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/ops/copy', async (req, res) => {
|
||||
const { source, destination } = req.body;
|
||||
const srcPath = getSafePath(source);
|
||||
const destPath = getSafePath(destination);
|
||||
|
||||
if (!srcPath || !destPath || !fs.existsSync(srcPath)) return res.status(400).json({ error: 'Invalid' });
|
||||
|
||||
try {
|
||||
fs.cpSync(srcPath, destPath, { recursive: true });
|
||||
await logFileAction(source, 'COPY', req.session?.user?.username);
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user