Compare commits

..

No commits in common. "243ba4d1c70130eb0da7a4fafe7439cced3f27ff" and "e363f780d77aa86dc557f71bfd05e10f519a8c73" have entirely different histories.

6 changed files with 168 additions and 410 deletions

View file

@ -1,11 +1,10 @@
# WebUI - A Simple File Management Tool # WebUI - A Simple File Management Tool
WebUI is a (not very) lightweight and easy-to-use web application for managing files on your server. WebUI is a lightweight and easy-to-use web application for managing files on your server.
## Screenshot ## Screenshot
![Preview of WebUI](preview.png) ![Preview of WebUI](preview.png)
![Preview of WebUI 2](preview2.png)
## Getting Started ## Getting Started

View file

@ -1 +0,0 @@
logs = enable

View file

@ -7,7 +7,6 @@
<style> <style>
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
overflow-y: scroll;
} }
#file-tree { #file-tree {
margin-top: 20px; margin-top: 20px;
@ -15,18 +14,6 @@
ul { ul {
list-style-type: none; list-style-type: none;
} }
li {
padding: 5px 0;
display: flex;
align-items: center;
}
li button {
margin-left: 10px;
}
.file-info {
flex-grow: 1;
padding-left: 5px;
}
.preview-modal { .preview-modal {
position: fixed; position: fixed;
top: 0; top: 0;
@ -35,83 +22,32 @@
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.8); background: rgba(0, 0, 0, 0.8);
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 1000; z-index: 1000;
overflow-y: auto;
padding: 20px;
} }
.preview-content { .preview-content {
background: white; background: white;
padding: 20px; padding: 20px;
border-radius: 5px; border-radius: 5px;
max-width: 90%; max-width: 90%;
max-height: 90vh; max-height: 90%;
overflow: auto; overflow: auto;
position: relative;
} }
img { img {
max-width: 100%; max-width: 100%;
max-height: 400px; max-height: 400px;
display: block;
margin: 0 auto;
}
.upload-item {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.progress-bar {
width: 200px;
height: 20px;
border: 1px solid #ccc;
margin-left: 10px;
overflow: hidden;
border-radius: 3px;
}
.progress-bar-fill {
height: 100%;
width: 0;
background-color: #4CAF50;
transition: width 0.1s;
}
#batch-actions button {
margin-right: 10px;
padding: 8px 15px;
}
.preview-controls {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 90%;
margin-top: 10px;
margin-bottom: 10px;
}
.preview-controls button {
padding: 10px 20px;
font-size: 16px;
} }
</style> </style>
</head> </head>
<body> <body>
<h1>Upload Files</h1> <h1>Upload Files</h1>
<form id="upload-form" action="/upload" method="post" enctype="multipart/form-data"> <form id="upload-form" action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="files" multiple required id="file-input"> <input type="file" name="files" multiple required>
<button type="submit" id="upload-button">Upload</button> <button type="submit">Upload</button>
</form> </form>
<h2>Files to Upload</h2>
<ul id="upload-list">
</ul>
<h2>Directory Structure</h2> <h2>Directory Structure</h2>
<div id="batch-actions">
<button id="select-all">Select All</button>
<button id="batch-download">Download Selected</button>
<button id="batch-delete">Delete Selected</button>
<button id="batch-preview">Preview Selected (Text/Image)</button>
</div>
<div id="file-tree"> <div id="file-tree">
<ul id="file-list"></ul> <ul id="file-list"></ul>
</div> </div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 961 KiB

346
script.js
View file

@ -1,265 +1,115 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
fetchFiles(); fetchFiles();
document.getElementById('upload-form').addEventListener('submit', handleFileUpload); document.getElementById('upload-form').addEventListener('submit', handleFileUpload);
document.getElementById('file-input').addEventListener('change', updateUploadList);
document.getElementById('select-all').addEventListener('click', handleSelectAll);
document.getElementById('batch-delete').addEventListener('click', handleBatchDelete);
document.getElementById('batch-download').addEventListener('click', handleBatchDownload);
document.getElementById('batch-preview').addEventListener('click', handleBatchPreview);
}); });
function fetchFiles() { function fetchFiles() {
fetch('/files') fetch('/files')
.then(response => response.json()) .then(response => response.json())
.then(files => { .then(files => {
const fileList = document.getElementById('file-list'); const fileList = document.getElementById('file-list');
fileList.innerHTML = ''; fileList.innerHTML = '';
files.forEach(file => { files.forEach(file => {
const li = document.createElement('li'); const li = document.createElement('li');
li.textContent = `${file.name} (Size: ${file.size} bytes, Date: ${new Date(file.date).toLocaleString()})`;
const checkbox = document.createElement('input'); const downloadButton = document.createElement('button');
checkbox.type = 'checkbox'; downloadButton.textContent = 'Download';
checkbox.value = file.name; downloadButton.onclick = () => {
checkbox.dataset.path = `/uploads/${file.name}`; window.location.href = `/download/${file.name}`;
const extension = file.name.split('.').pop().toLowerCase(); };
checkbox.dataset.fileType = extension;
const fileInfo = document.createElement('span'); const deleteButton = document.createElement('button');
fileInfo.className = 'file-info'; deleteButton.textContent = 'Delete';
fileInfo.textContent = `${file.name} (Size: ${file.size} bytes, Date: ${new Date(file.date).toLocaleString()})`; deleteButton.onclick = () => {
fetch(`/delete/${file.name}`, { method: 'DELETE' })
.then(response => {
if (response.ok) {
li.remove();
alert('File deleted successfully!');
} else {
alert('Error deleting file');
}
});
};
li.appendChild(checkbox); const previewButton = document.createElement('button');
li.appendChild(fileInfo); previewButton.textContent = 'Preview';
fileList.appendChild(li); previewButton.onclick = () => {
}); showPreview(file);
}) };
.catch(error => {
console.error('Error fetching file list:', error);
});
}
function updateUploadList(event) { li.appendChild(downloadButton);
const fileInput = event.target; li.appendChild(deleteButton);
const uploadList = document.getElementById('upload-list'); li.appendChild(previewButton);
uploadList.innerHTML = ''; fileList.appendChild(li);
});
if (fileInput.files.length === 0) return; })
.catch(error => {
for (const file of fileInput.files) { console.error('Error fetching file list:', error);
const li = document.createElement('li'); });
li.className = 'upload-item';
li.innerHTML = `
<span>${file.name} (Size: ${Math.round(file.size / 1024)} KB)</span>
<div id="progress-${file.name.replace(/[^a-zA-Z0-9]/g, '-')}" class="progress-bar">
<div class="progress-bar-fill" style="width: 0;"></div>
</div>
`;
uploadList.appendChild(li);
}
} }
function handleFileUpload(event) { function handleFileUpload(event) {
event.preventDefault(); event.preventDefault();
const form = event.target; const form = event.target;
const formData = new FormData(form); const formData = new FormData(form);
const fileInput = document.getElementById('file-input');
if (fileInput.files.length === 0) { fetch('/upload', {
alert('Please select files to upload.'); method: 'POST',
return; body: formData
} })
.then(response => response.text())
const xhr = new XMLHttpRequest(); .then(message => {
alert(message);
xhr.upload.addEventListener('progress', (e) => { form.reset();
if (e.lengthComputable) { fetchFiles();
const percentComplete = (e.loaded / e.total) * 100; })
const progressBars = document.querySelectorAll('.progress-bar-fill'); .catch(error => {
console.error('Error uploading file:', error);
progressBars.forEach(bar => { alert('File upload failed!');
bar.style.width = percentComplete.toFixed(2) + '%'; });
});
}
}, false);
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
alert(xhr.responseText);
form.reset();
document.getElementById('upload-list').innerHTML = '';
fetchFiles();
} else {
alert('File upload failed! Server responded with status ' + xhr.status);
}
});
xhr.addEventListener('error', () => {
console.error('Error uploading file:', xhr.statusText);
alert('File upload failed!');
});
xhr.open('POST', '/upload');
xhr.send(formData);
} }
function getSelectedFiles() { function showPreview(file) {
const selectedCheckboxes = document.querySelectorAll('#file-list input[type="checkbox"]:checked'); const previewContainer = document.createElement('div');
return Array.from(selectedCheckboxes).map(checkbox => ({ previewContainer.className = 'preview-modal';
name: checkbox.value,
path: checkbox.dataset.path, const closeButton = document.createElement('button');
type: checkbox.dataset.fileType closeButton.textContent = 'Close';
})); closeButton.onclick = () => {
} document.body.removeChild(previewContainer);
};
function handleSelectAll() {
const checkboxes = document.querySelectorAll('#file-list input[type="checkbox"]'); const content = document.createElement('div');
const allChecked = Array.from(checkboxes).every(cb => cb.checked); content.className = 'preview-content';
checkboxes.forEach(cb => { if (file.name.endsWith('.txt')) {
cb.checked = !allChecked; const textPreview = document.createElement('iframe');
}); textPreview.src = file.path;
} textPreview.style.width = '100%';
textPreview.style.height = '200px';
function handleBatchDelete() { content.appendChild(textPreview);
const filesToDelete = getSelectedFiles(); } else if (file.name.match(/\.(jpg|jpeg|png|gif)$/)) {
if (filesToDelete.length === 0) { const imgPreview = document.createElement('img');
alert('No files selected for deletion.'); imgPreview.src = file.path;
return; imgPreview.onload = () => {
} document.body.appendChild(previewContainer);
};
if (!confirm(`Are you sure you want to delete ${filesToDelete.length} file(s)?`)) { imgPreview.onerror = () => {
return; alert('Error loading image preview');
} document.body.removeChild(previewContainer);
};
const deletePromises = filesToDelete.map(file => content.appendChild(imgPreview);
fetch(`/delete/${file.name}`, { method: 'DELETE' }) } else {
.then(response => { content.textContent = 'Preview not available for this file type.';
if (response.ok) { }
return { name: file.name, status: 'Success' };
} else { previewContainer.appendChild(closeButton);
return { name: file.name, status: 'Failed' }; previewContainer.appendChild(content);
} if (!file.name.match(/\.(jpg|jpeg|png|gif)$/)) {
}) document.body.appendChild(previewContainer);
); }
Promise.all(deletePromises)
.then(results => {
const successfulDeletes = results.filter(r => r.status === 'Success');
const failedDeletes = results.filter(r => r.status === 'Failed');
if (successfulDeletes.length > 0) {
alert(`Successfully deleted ${successfulDeletes.length} file(s).`);
}
if (failedDeletes.length > 0) {
alert(`Failed to delete ${failedDeletes.length} file(s). Check console for errors.`);
}
fetchFiles();
})
.catch(error => {
console.error('Error during batch deletion:', error);
alert('An error occurred during batch deletion.');
});
}
function handleBatchDownload() {
const filesToDownload = getSelectedFiles();
if (filesToDownload.length === 0) {
alert('No files selected for download.');
return;
}
filesToDownload.forEach(file => {
window.location.href = `/download/${file.name}`;
});
alert(`Initiating download for ${filesToDownload.length} file(s). Check your browser's download manager.`);
}
function handleBatchPreview() {
const selectedFiles = getSelectedFiles();
const previewableFiles = selectedFiles.filter(file =>
file.type === 'txt' || file.type.match(/^(jpg|jpeg|png|gif)$/)
);
if (previewableFiles.length === 0) {
alert('No previewable files (.txt, .jpg, .png, .gif) selected.');
return;
}
showMultiPreview(previewableFiles);
}
function showMultiPreview(files) {
let currentIndex = 0;
const previewContainer = document.createElement('div');
previewContainer.className = 'preview-modal';
const content = document.createElement('div');
content.className = 'preview-content';
const controls = document.createElement('div');
controls.className = 'preview-controls';
const prevButton = document.createElement('button');
prevButton.textContent = 'Previous';
prevButton.onclick = () => {
if (currentIndex > 0) {
currentIndex--;
updatePreviewContent();
}
};
const nextButton = document.createElement('button');
nextButton.textContent = 'Next';
nextButton.onclick = () => {
if (currentIndex < files.length - 1) {
currentIndex++;
updatePreviewContent();
}
};
const closeButton = document.createElement('button');
closeButton.textContent = 'Close Preview';
closeButton.onclick = () => {
document.body.removeChild(previewContainer);
};
controls.appendChild(prevButton);
controls.appendChild(closeButton);
controls.appendChild(nextButton);
const updatePreviewContent = () => {
content.innerHTML = '';
const file = files[currentIndex];
prevButton.disabled = currentIndex === 0;
nextButton.disabled = currentIndex === files.length - 1;
const counter = document.createElement('p');
counter.textContent = `File ${currentIndex + 1} of ${files.length}: ${file.name}`;
content.appendChild(counter);
if (file.type === 'txt') {
const textPreview = document.createElement('iframe');
textPreview.src = file.path;
textPreview.style.width = '100%';
textPreview.style.height = '400px';
content.appendChild(textPreview);
} else if (file.type.match(/^(jpg|jpeg|png|gif)$/)) {
const imgPreview = document.createElement('img');
imgPreview.src = file.path;
content.appendChild(imgPreview);
} else {
content.textContent = `Preview not available for this file type: ${file.name}.`;
}
};
previewContainer.appendChild(controls);
previewContainer.appendChild(content);
document.body.appendChild(previewContainer);
updatePreviewContent();
} }

154
server.js
View file

@ -5,118 +5,92 @@ const fs = require('fs');
const app = express(); const app = express();
function loadConfig() { // configure storage for uploaded files
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 };
}
const config = loadConfig();
function logAction(message) {
if (config.loggingEnabled) {
console.log(`[ACTION] ${message}`);
}
}
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination: (req, file, cb) => { destination: (req, file, cb) => {
cb(null, 'uploads/'); cb(null, 'uploads/');
}, },
filename: (req, file, cb) => { filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + '-' + file.originalname); cb(null, uniqueSuffix + '-' + file.originalname);
} }
}); });
const upload = multer({ storage: storage }); const upload = multer({ storage: storage });
// serve static files
app.use(express.static(__dirname)); app.use(express.static(__dirname));
// upload files
app.post('/upload', upload.array('files'), (req, res) => { app.post('/upload', upload.array('files'), (req, res) => {
if (!req.files || req.files.length === 0) { if (!req.files || req.files.length === 0) {
logAction(`FAILED upload attempt: No files received.`); return res.status(400).send('No files were uploaded.');
return res.status(400).send('No files were uploaded.'); }
} res.send(`Successfully uploaded ${req.files.length} file(s)!`);
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)!`);
}); });
// get list of uploaded files
app.get('/files', (req, res) => { app.get('/files', (req, res) => {
logAction(`LIST files requested.`); const directoryPath = path.join(__dirname, 'uploads');
const directoryPath = path.join(__dirname, 'uploads'); fs.readdir(directoryPath, (err, files) => {
fs.readdir(directoryPath, (err, files) => { if (err) {
if (err) { return res.status(500).send('Unable to scan directory: ' + err);
return res.status(500).send('Unable to scan directory: ' + err); }
} const fileListPromises = files.map(file => {
const fileListPromises = files.map(file => { return new Promise((resolve) => {
return new Promise((resolve) => { const filePath = path.join(directoryPath, file);
const filePath = path.join(directoryPath, file); fs.stat(filePath, (err, stats) => {
fs.stat(filePath, (err, stats) => { if (err) {
if (err) { return resolve(null);
return resolve(null); }
} resolve({
resolve({ name: file,
name: file, path: `/uploads/${file}`,
path: `/uploads/${file}`, size: stats.size,
size: stats.size, date: stats.mtime
date: stats.mtime });
}); });
}); });
}); });
});
Promise.all(fileListPromises).then(fileList => { Promise.all(fileListPromises).then(fileList => {
res.json(fileList.filter(file => file !== null)); res.json(fileList.filter(file => file !== null));
}); });
}); });
}); });
// serve a specific file
app.get('/uploads/:filename', (req, res) => { app.get('/uploads/:filename', (req, res) => {
const filename = req.params.filename; const filePath = path.join(__dirname, 'uploads', req.params.filename);
logAction(`PREVIEW/ACCESS file: ${filename}`); res.sendFile(filePath, err => {
const filePath = path.join(__dirname, 'uploads', filename); if (err) {
res.sendFile(filePath, err => { res.status(404).send('File not found');
if (err) { }
res.status(404).send(`File not found: ${filename}`); });
}
});
}); });
// download a specific file
app.get('/download/:filename', (req, res) => { app.get('/download/:filename', (req, res) => {
const filename = req.params.filename; const filePath = path.join(__dirname, 'uploads', req.params.filename);
logAction(`DOWNLOAD file: ${filename}`); res.download(filePath, err => {
const filePath = path.join(__dirname, 'uploads', filename); if (err) {
res.download(filePath, err => { res.status(404).send('File not found');
if (err) { }
res.status(404).send(`File not found for download: ${filename}`); });
}
});
}); });
// delete a specific file
app.delete('/delete/:filename', (req, res) => { app.delete('/delete/:filename', (req, res) => {
const filename = req.params.filename; const filePath = path.join(__dirname, 'uploads', req.params.filename);
const filePath = path.join(__dirname, 'uploads', filename); fs.unlink(filePath, err => {
fs.unlink(filePath, err => { if (err) {
if (err) { return res.status(404).send('File not found');
logAction(`FAILED deletion of file: ${filename}`); }
return res.status(404).send('File not found'); res.send('File deleted successfully');
} });
logAction(`DELETED file: ${filename}`);
res.send('File deleted successfully');
});
}); });
// start the server
app.listen(3000, () => { app.listen(3000, () => {
if (config.loggingEnabled) { console.log('Server is running on http://localhost:3000');
console.log('[*] Server is running on http://localhost:3000');
}
}); });