Colors, auto mkdir for uploads/, et cetera

This commit is contained in:
coast 2025-10-09 11:47:33 +03:30
parent 243ba4d1c7
commit 36345026a7
7 changed files with 214 additions and 177 deletions

2
.gitignore vendored
View file

@ -1 +1 @@
node_modules
node_modules

View file

@ -1 +1,2 @@
logs = enable
log_to_file = false

164
package-lock.json generated
View file

@ -7,10 +7,15 @@
"": {
"name": "webui",
"version": "1.0.0",
"license": "ISC",
"license": "GPLv2",
"dependencies": {
"express": "^4.17.1",
"multer": "^1.4.5-lts.1"
"chalk": "^5.3.0",
"express": "^4.19.2",
"morgan": "^1.10.0",
"multer": "^2.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/accepts": {
@ -38,6 +43,24 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/basic-auth/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@ -117,18 +140,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 0.8"
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
@ -168,12 +203,6 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -483,12 +512,6 @@
"node": ">= 0.10"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -579,6 +602,34 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/morgan": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"license": "MIT",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.1.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/morgan/node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -586,22 +637,21 @@
"license": "MIT"
},
"node_modules/multer": {
"version": "1.4.5-lts.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
"deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"mkdirp": "^0.5.6",
"object-assign": "^4.1.1",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
"type-is": "^1.6.18",
"xtend": "^4.0.2"
},
"engines": {
"node": ">= 6.0.0"
"node": ">= 10.16.0"
}
},
"node_modules/negotiator": {
@ -646,6 +696,15 @@
"node": ">= 0.8"
}
},
"node_modules/on-headers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -661,12 +720,6 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -720,26 +773,19 @@
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -916,20 +962,14 @@
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
"safe-buffer": "~5.2.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View file

@ -1,20 +1,20 @@
{
"name": "webui",
"version": "1.0.0",
"description": "WebUI is a lightweight and easy-to-use web application for managing files on your server. It provides a simple interface to upload, download, delete, and preview files.",
"main": "script.js",
"description": "WebUI for managing server files",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "https://codeberg.org/Soccera/webui.git"
},
"keywords": [],
"author": "",
"dependencies": {
"express": "^4.17.1",
"multer": "^1.4.5-lts.1"
}
"chalk": "^5.3.0",
"express": "^4.19.2",
"morgan": "^1.10.0",
"multer": "^2.0.0"
},
"engines": {
"node": ">=18.0.0"
},
"author": "Coasteen",
"license": "GPLv2"
}

198
server.js
View file

@ -1,122 +1,116 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const express = require('express')
const multer = require('multer')
const path = require('path')
const fs = require('fs')
const morgan = require('morgan')
const chalk = require('chalk').default
const app = express();
const app = express()
const uploadDir = path.join(__dirname, 'uploads')
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true })
function loadConfig() {
try {
const configPath = path.join(__dirname, 'config.conf');
const content = fs.readFileSync(configPath, 'utf8');
const match = content.match(/^logs\s*=\s*(.*)$/m);
if (match && match[1].trim().toLowerCase() === 'disable') {
return { loggingEnabled: false };
}
} catch (e) {
}
return { loggingEnabled: true };
try {
const content = fs.readFileSync(path.join(__dirname, 'config.conf'), 'utf8')
const match = content.match(/^logs\s*=\s*(.*)$/m)
if (match && match[1].trim().toLowerCase() === 'disable') return { loggingEnabled: false }
} catch {}
return { loggingEnabled: true }
}
const config = loadConfig();
const config = loadConfig()
function logAction(message) {
if (config.loggingEnabled) {
console.log(`[ACTION] ${message}`);
}
const colors = {
info: chalk.cyanBright,
warn: chalk.yellowBright,
error: chalk.redBright,
action: chalk.greenBright,
sys: chalk.magentaBright
}
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + '-' + file.originalname);
}
});
function log(level, msg) {
if (!config.loggingEnabled) return
const time = new Date().toISOString().replace('T', ' ').split('.')[0]
const colorFn = colors[level.toLowerCase()] || chalk.white
console.log(`[${time}] [${level.toUpperCase()}] ${colorFn(msg)}`)
}
const upload = multer({ storage: storage });
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadDir),
filename: (req, file, cb) => cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}-${file.originalname}`)
})
const upload = multer({ storage }).array('files')
app.use(express.static(__dirname));
app.use(express.static(__dirname))
if (config.loggingEnabled) app.use(morgan('dev'))
app.post('/upload', upload.array('files'), (req, res) => {
if (!req.files || req.files.length === 0) {
logAction(`FAILED upload attempt: No files received.`);
return res.status(400).send('No files were uploaded.');
}
const uploadedNames = req.files.map(f => f.filename).join(', ');
logAction(`UPLOADED ${req.files.length} file(s): ${uploadedNames}`);
res.send(`Successfully uploaded ${req.files.length} file(s)!`);
});
app.post('/upload', (req, res) => {
upload(req, res, err => {
if (err) {
log('error', `UPLOAD ERROR: ${err.message}`)
return res.status(500).send('Upload error')
}
if (!req.files?.length) {
log('warn', `UPLOAD FAILED: No files received`)
return res.status(400).send('No files uploaded')
}
const names = req.files.map(f => f.filename).join(', ')
log('action', `UPLOADED ${req.files.length} file(s): ${names}`)
res.send(`Uploaded ${req.files.length} file(s)!`)
})
})
app.get('/files', (req, res) => {
logAction(`LIST files requested.`);
const directoryPath = path.join(__dirname, 'uploads');
fs.readdir(directoryPath, (err, files) => {
if (err) {
return res.status(500).send('Unable to scan directory: ' + err);
}
const fileListPromises = files.map(file => {
return new Promise((resolve) => {
const filePath = path.join(directoryPath, file);
fs.stat(filePath, (err, stats) => {
if (err) {
return resolve(null);
}
resolve({
name: file,
path: `/uploads/${file}`,
size: stats.size,
date: stats.mtime
});
});
});
});
Promise.all(fileListPromises).then(fileList => {
res.json(fileList.filter(file => file !== null));
});
});
});
log('info', 'Listing all files')
fs.readdir(uploadDir, (err, files) => {
if (err) {
log('error', `Unable to read uploads directory: ${err.message}`)
return res.status(500).send('Unable to scan directory')
}
const fileList = files.map(file => {
const stat = fs.statSync(path.join(uploadDir, file))
return { name: file, path: `/uploads/${file}`, size: stat.size, date: stat.mtime }
})
res.json(fileList)
})
})
app.get('/uploads/:filename', (req, res) => {
const filename = req.params.filename;
logAction(`PREVIEW/ACCESS file: ${filename}`);
const filePath = path.join(__dirname, 'uploads', filename);
res.sendFile(filePath, err => {
if (err) {
res.status(404).send(`File not found: ${filename}`);
}
});
});
const filename = req.params.filename
const filePath = path.join(uploadDir, filename)
log('info', `Serving file: ${filename}`)
res.sendFile(filePath, err => {
if (err) {
log('error', `File not found: ${filename}`)
res.status(404).send('File not found')
}
})
})
app.get('/download/:filename', (req, res) => {
const filename = req.params.filename;
logAction(`DOWNLOAD file: ${filename}`);
const filePath = path.join(__dirname, 'uploads', filename);
res.download(filePath, err => {
if (err) {
res.status(404).send(`File not found for download: ${filename}`);
}
});
});
const filename = req.params.filename
const filePath = path.join(uploadDir, filename)
log('action', `Downloading file: ${filename}`)
res.download(filePath, err => {
if (err) {
log('error', `Download failed for: ${filename}`)
res.status(404).send('File not found')
}
})
})
app.delete('/delete/:filename', (req, res) => {
const filename = req.params.filename;
const filePath = path.join(__dirname, 'uploads', filename);
fs.unlink(filePath, err => {
if (err) {
logAction(`FAILED deletion of file: ${filename}`);
return res.status(404).send('File not found');
}
logAction(`DELETED file: ${filename}`);
res.send('File deleted successfully');
});
});
const filename = req.params.filename
const filePath = path.join(uploadDir, filename)
fs.unlink(filePath, err => {
if (err) {
log('error', `Failed to delete: ${filename}`)
return res.status(404).send('File not found')
}
log('action', `Deleted file: ${filename}`)
res.send('File deleted successfully')
})
})
app.listen(3000, () => {
if (config.loggingEnabled) {
console.log('[*] Server is running on http://localhost:3000');
}
});
const PORT = 3000
app.listen(PORT, () => log('sys', `Server running on http://localhost:${PORT}`))

2
start.sh Executable file
View file

@ -0,0 +1,2 @@
#!/usr/bin/env sh
node server.js

Binary file not shown.