mirror of
https://github.com/DumbWareio/DumbDrop.git
synced 2025-10-23 07:41:58 +00:00
@@ -14,3 +14,5 @@ DUMBDROP_TITLE=DumbDrop # Site title displayed in header
|
|||||||
APPRISE_URL= # Apprise URL for notifications (e.g., tgram://bottoken/ChatID)
|
APPRISE_URL= # Apprise URL for notifications (e.g., tgram://bottoken/ChatID)
|
||||||
APPRISE_MESSAGE=New file uploaded - {filename} ({size}), Storage used {storage}
|
APPRISE_MESSAGE=New file uploaded - {filename} ({size}), Storage used {storage}
|
||||||
APPRISE_SIZE_UNIT=auto # Size unit for notifications (auto, B, KB, MB, GB, TB)
|
APPRISE_SIZE_UNIT=auto # Size unit for notifications (auto, B, KB, MB, GB, TB)
|
||||||
|
|
||||||
|
DEMO_MODE=false
|
||||||
|
@@ -13,6 +13,7 @@ No auth (unless you want it now!), no storage, no nothing. Just a simple file up
|
|||||||
- [Security](#security)
|
- [Security](#security)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Technical Details](#technical-details)
|
- [Technical Details](#technical-details)
|
||||||
|
- [Demo Mode](demo.md)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
|
28
demo.md
Normal file
28
demo.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
## Demo Mode
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
DumbDrop includes a demo mode that allows testing the application without actually storing files. Perfect for trying out the interface or development testing.
|
||||||
|
|
||||||
|
### Enabling Demo Mode
|
||||||
|
Set in your environment or docker-compose.yml:
|
||||||
|
```env
|
||||||
|
DEMO_MODE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Demo Features
|
||||||
|
- 🚫 No actual file storage - files are processed in memory
|
||||||
|
- 🎯 Full UI experience with upload/download simulation
|
||||||
|
- 🔄 Maintains all functionality including:
|
||||||
|
- Drag and drop
|
||||||
|
- Progress tracking
|
||||||
|
- Multiple file uploads
|
||||||
|
- Directory structure
|
||||||
|
- File listings
|
||||||
|
- 🚨 Clear visual indicator (red banner) showing demo status
|
||||||
|
- 🧹 Auto-cleans upload directory on startup
|
||||||
|
- Files are processed but not written to disk
|
||||||
|
- Upload progress is simulated
|
||||||
|
- File metadata stored in memory
|
||||||
|
- Maintains same API responses as production
|
||||||
|
- Cleared on server restart
|
5588
package-lock.json
generated
5588
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
src/app.js
22
src/app.js
@@ -16,6 +16,7 @@ const { ensureDirectoryExists } = require('./utils/fileUtils');
|
|||||||
const { securityHeaders, requirePin } = require('./middleware/security');
|
const { securityHeaders, requirePin } = require('./middleware/security');
|
||||||
const { safeCompare } = require('./utils/security');
|
const { safeCompare } = require('./utils/security');
|
||||||
const { initUploadLimiter, pinVerifyLimiter, downloadLimiter } = require('./middleware/rateLimiter');
|
const { initUploadLimiter, pinVerifyLimiter, downloadLimiter } = require('./middleware/rateLimiter');
|
||||||
|
const { injectDemoBanner, demoMiddleware } = require('./utils/demoMode');
|
||||||
|
|
||||||
// Create Express app
|
// Create Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -34,6 +35,9 @@ const { router: uploadRouter } = require('./routes/upload');
|
|||||||
const fileRoutes = require('./routes/files');
|
const fileRoutes = require('./routes/files');
|
||||||
const authRoutes = require('./routes/auth');
|
const authRoutes = require('./routes/auth');
|
||||||
|
|
||||||
|
// Add demo middleware before your routes
|
||||||
|
app.use(demoMiddleware);
|
||||||
|
|
||||||
// Use routes with appropriate middleware
|
// Use routes with appropriate middleware
|
||||||
app.use('/api/auth', pinVerifyLimiter, authRoutes);
|
app.use('/api/auth', pinVerifyLimiter, authRoutes);
|
||||||
app.use('/api/upload', requirePin(config.pin), initUploadLimiter, uploadRouter);
|
app.use('/api/upload', requirePin(config.pin), initUploadLimiter, uploadRouter);
|
||||||
@@ -49,6 +53,7 @@ app.get('/', (req, res) => {
|
|||||||
let html = fs.readFileSync(path.join(__dirname, '../public', 'index.html'), 'utf8');
|
let html = fs.readFileSync(path.join(__dirname, '../public', 'index.html'), 'utf8');
|
||||||
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
|
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
|
||||||
html = html.replace('{{AUTO_UPLOAD}}', config.autoUpload.toString());
|
html = html.replace('{{AUTO_UPLOAD}}', config.autoUpload.toString());
|
||||||
|
html = injectDemoBanner(html);
|
||||||
res.send(html);
|
res.send(html);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,6 +66,7 @@ app.get('/login.html', (req, res) => {
|
|||||||
|
|
||||||
let html = fs.readFileSync(path.join(__dirname, '../public', 'login.html'), 'utf8');
|
let html = fs.readFileSync(path.join(__dirname, '../public', 'login.html'), 'utf8');
|
||||||
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
|
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
|
||||||
|
html = injectDemoBanner(html);
|
||||||
res.send(html);
|
res.send(html);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,6 +83,7 @@ app.use((req, res, next) => {
|
|||||||
if (req.path === 'index.html') {
|
if (req.path === 'index.html') {
|
||||||
html = html.replace('{{AUTO_UPLOAD}}', config.autoUpload.toString());
|
html = html.replace('{{AUTO_UPLOAD}}', config.autoUpload.toString());
|
||||||
}
|
}
|
||||||
|
html = injectDemoBanner(html);
|
||||||
res.send(html);
|
res.send(html);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next();
|
next();
|
||||||
@@ -117,6 +124,21 @@ async function initialize() {
|
|||||||
logger.info('Apprise notifications enabled');
|
logger.info('Apprise notifications enabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After initializing demo middleware
|
||||||
|
if (process.env.DEMO_MODE === 'true') {
|
||||||
|
logger.info('[DEMO] Running in demo mode - uploads will not be saved');
|
||||||
|
// Clear any existing files in upload directory
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(config.uploadDir);
|
||||||
|
for (const file of files) {
|
||||||
|
fs.unlinkSync(path.join(config.uploadDir, file));
|
||||||
|
}
|
||||||
|
logger.info('[DEMO] Cleared upload directory');
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`[DEMO] Failed to clear upload directory: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Initialization failed: ${err.message}`);
|
logger.error(`Initialization failed: ${err.message}`);
|
||||||
|
@@ -14,6 +14,7 @@ const { getUniqueFilePath, getUniqueFolderPath } = require('../utils/fileUtils')
|
|||||||
const { sendNotification } = require('../services/notifications');
|
const { sendNotification } = require('../services/notifications');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { cleanupIncompleteUploads } = require('../utils/cleanup');
|
const { cleanupIncompleteUploads } = require('../utils/cleanup');
|
||||||
|
const { isDemoMode, createMockUploadResponse } = require('../utils/demoMode');
|
||||||
|
|
||||||
// Store ongoing uploads
|
// Store ongoing uploads
|
||||||
const uploads = new Map();
|
const uploads = new Map();
|
||||||
|
184
src/utils/demoMode.js
Normal file
184
src/utils/demoMode.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* Demo mode utilities
|
||||||
|
* Provides demo banner and demo-related functionality
|
||||||
|
* Used to clearly indicate when application is running in demo mode
|
||||||
|
*/
|
||||||
|
|
||||||
|
const multer = require('multer');
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const logger = require('./logger');
|
||||||
|
const { config } = require('../config');
|
||||||
|
|
||||||
|
const isDemoMode = () => process.env.DEMO_MODE === 'true';
|
||||||
|
|
||||||
|
const getDemoBannerHTML = () => `
|
||||||
|
<div id="demo-banner" style="
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
">
|
||||||
|
🚀 DEMO MODE - This is a demonstration only. Files will not be saved. 🚀
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const injectDemoBanner = (html) => {
|
||||||
|
if (!isDemoMode()) return html;
|
||||||
|
return html.replace(
|
||||||
|
'<body>',
|
||||||
|
'<body>' + getDemoBannerHTML()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock storage for demo files and uploads
|
||||||
|
const demoFiles = new Map();
|
||||||
|
const demoUploads = new Map();
|
||||||
|
|
||||||
|
// Configure demo upload handling
|
||||||
|
const storage = multer.memoryStorage();
|
||||||
|
const upload = multer({ storage });
|
||||||
|
|
||||||
|
// Create demo routes with exact path matching
|
||||||
|
const demoRouter = express.Router();
|
||||||
|
|
||||||
|
// Mock upload init - match exact path
|
||||||
|
demoRouter.post('/api/upload/init', (req, res) => {
|
||||||
|
const { filename, fileSize } = req.body;
|
||||||
|
const uploadId = 'demo-' + Math.random().toString(36).substr(2, 9);
|
||||||
|
|
||||||
|
demoUploads.set(uploadId, {
|
||||||
|
filename,
|
||||||
|
fileSize,
|
||||||
|
bytesReceived: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[DEMO] Initialized upload for ${filename} (${fileSize} bytes)`);
|
||||||
|
|
||||||
|
return res.json({ uploadId });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock chunk upload - match exact path and handle large files
|
||||||
|
demoRouter.post('/api/upload/chunk/:uploadId',
|
||||||
|
express.raw({
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
limit: config.maxFileSize
|
||||||
|
}),
|
||||||
|
(req, res) => {
|
||||||
|
const { uploadId } = req.params;
|
||||||
|
const upload = demoUploads.get(uploadId);
|
||||||
|
|
||||||
|
if (!upload) {
|
||||||
|
return res.status(404).json({ error: 'Upload not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunkSize = req.body.length;
|
||||||
|
upload.bytesReceived += chunkSize;
|
||||||
|
|
||||||
|
// Calculate progress
|
||||||
|
const progress = Math.min(
|
||||||
|
Math.round((upload.bytesReceived / upload.fileSize) * 100),
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(`[DEMO] Chunk received for ${upload.filename}, progress: ${progress}%`);
|
||||||
|
|
||||||
|
// If upload is complete
|
||||||
|
if (upload.bytesReceived >= upload.fileSize) {
|
||||||
|
const fileId = 'demo-' + Math.random().toString(36).substr(2, 9);
|
||||||
|
const mockFile = {
|
||||||
|
id: fileId,
|
||||||
|
name: upload.filename,
|
||||||
|
size: upload.fileSize,
|
||||||
|
url: `/api/files/${fileId}`,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
demoFiles.set(fileId, mockFile);
|
||||||
|
demoUploads.delete(uploadId);
|
||||||
|
|
||||||
|
logger.success(`[DEMO] Upload completed: ${upload.filename} (${upload.fileSize} bytes)`);
|
||||||
|
|
||||||
|
// Return completion response
|
||||||
|
return res.json({
|
||||||
|
bytesReceived: upload.bytesReceived,
|
||||||
|
progress,
|
||||||
|
complete: true,
|
||||||
|
file: mockFile
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
bytesReceived: upload.bytesReceived,
|
||||||
|
progress
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock upload cancel - match exact path
|
||||||
|
demoRouter.post('/api/upload/cancel/:uploadId', (req, res) => {
|
||||||
|
const { uploadId } = req.params;
|
||||||
|
demoUploads.delete(uploadId);
|
||||||
|
logger.info(`[DEMO] Upload cancelled: ${uploadId}`);
|
||||||
|
return res.json({ message: 'Upload cancelled' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock file download - match exact path
|
||||||
|
demoRouter.get('/api/files/:id', (req, res) => {
|
||||||
|
const file = demoFiles.get(req.params.id);
|
||||||
|
if (!file) {
|
||||||
|
return res.status(404).json({
|
||||||
|
message: 'Demo Mode: File not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.json({
|
||||||
|
message: 'Demo Mode: This would download the file in production',
|
||||||
|
file
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock file list - match exact path
|
||||||
|
demoRouter.get('/api/files', (req, res) => {
|
||||||
|
return res.json({
|
||||||
|
files: Array.from(demoFiles.values()),
|
||||||
|
message: 'Demo Mode: Showing mock file list'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update middleware to handle errors
|
||||||
|
const demoMiddleware = (req, res, next) => {
|
||||||
|
if (!isDemoMode()) return next();
|
||||||
|
|
||||||
|
logger.debug(`[DEMO] Incoming request: ${req.method} ${req.path}`);
|
||||||
|
|
||||||
|
// Handle payload too large errors
|
||||||
|
demoRouter(req, res, (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(`[DEMO] Error handling request: ${err.message}`);
|
||||||
|
if (err.type === 'entity.too.large') {
|
||||||
|
return res.status(413).json({
|
||||||
|
error: 'Payload too large',
|
||||||
|
message: `File size exceeds limit of ${config.maxFileSize} bytes`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.status(500).json({
|
||||||
|
error: 'Internal server error',
|
||||||
|
message: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isDemoMode,
|
||||||
|
injectDemoBanner,
|
||||||
|
demoMiddleware
|
||||||
|
};
|
Reference in New Issue
Block a user