feat: Add Demo Mode for Testing and Evaluation (#37)

* demo things.
This commit is contained in:
V
2025-02-27 12:25:25 -07:00
committed by GitHub
parent c6a969b5cd
commit fc83e527b7
7 changed files with 239 additions and 5587 deletions

View File

@@ -14,3 +14,5 @@ DUMBDROP_TITLE=DumbDrop # Site title displayed in header
APPRISE_URL= # Apprise URL for notifications (e.g., tgram://bottoken/ChatID)
APPRISE_MESSAGE=New file uploaded - {filename} ({size}), Storage used {storage}
APPRISE_SIZE_UNIT=auto # Size unit for notifications (auto, B, KB, MB, GB, TB)
DEMO_MODE=false

View File

@@ -13,6 +13,7 @@ No auth (unless you want it now!), no storage, no nothing. Just a simple file up
- [Security](#security)
- [Development](#development)
- [Technical Details](#technical-details)
- [Demo Mode](demo.md)
- [Contributing](#contributing)
- [License](#license)

28
demo.md Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ const { ensureDirectoryExists } = require('./utils/fileUtils');
const { securityHeaders, requirePin } = require('./middleware/security');
const { safeCompare } = require('./utils/security');
const { initUploadLimiter, pinVerifyLimiter, downloadLimiter } = require('./middleware/rateLimiter');
const { injectDemoBanner, demoMiddleware } = require('./utils/demoMode');
// Create Express app
const app = express();
@@ -34,6 +35,9 @@ const { router: uploadRouter } = require('./routes/upload');
const fileRoutes = require('./routes/files');
const authRoutes = require('./routes/auth');
// Add demo middleware before your routes
app.use(demoMiddleware);
// Use routes with appropriate middleware
app.use('/api/auth', pinVerifyLimiter, authRoutes);
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');
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
html = html.replace('{{AUTO_UPLOAD}}', config.autoUpload.toString());
html = injectDemoBanner(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');
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
html = injectDemoBanner(html);
res.send(html);
});
@@ -77,6 +83,7 @@ app.use((req, res, next) => {
if (req.path === 'index.html') {
html = html.replace('{{AUTO_UPLOAD}}', config.autoUpload.toString());
}
html = injectDemoBanner(html);
res.send(html);
} catch (err) {
next();
@@ -117,6 +124,21 @@ async function initialize() {
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;
} catch (err) {
logger.error(`Initialization failed: ${err.message}`);

View File

@@ -14,6 +14,7 @@ const { getUniqueFilePath, getUniqueFolderPath } = require('../utils/fileUtils')
const { sendNotification } = require('../services/notifications');
const fs = require('fs');
const { cleanupIncompleteUploads } = require('../utils/cleanup');
const { isDemoMode, createMockUploadResponse } = require('../utils/demoMode');
// Store ongoing uploads
const uploads = new Map();

184
src/utils/demoMode.js Normal file
View 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
};