mirror of
https://github.com/DumbWareio/DumbDrop.git
synced 2025-10-23 07:41:58 +00:00
Compare commits
5 Commits
6cf39e4639
...
80b5426c52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80b5426c52 | ||
|
|
cb7e49b0e1 | ||
|
|
5666569580 | ||
|
|
6f1b93ed39 | ||
|
|
105d2a7412 |
@@ -82,4 +82,8 @@ ALLOWED_IFRAME_ORIGINS=
|
||||
# 002: Files 664 (rw-rw-r--), Dirs 775 (rwxrwxr-x) - Good for group sharing
|
||||
# 007: Files 660 (rw-rw----), Dirs 770 (rwxrwx---) - More restrictive
|
||||
# 077: Files 600 (rw-------), Dirs 700 (rwx------) - Most restrictive
|
||||
# UMASK=022
|
||||
# UMASK=022
|
||||
|
||||
# Custom footer links (comma-separated, format: "Link Text @ URL")
|
||||
# Example: FOOTER_LINKS=My Site @ https://example.com, Another Link @ https://another.org
|
||||
FOOTER_LINKS=
|
||||
|
||||
3
.github/workflows/docker-publish.yml
vendored
3
.github/workflows/docker-publish.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main # Trigger the workflow on pushes to the main branch
|
||||
- dev # Trigger the workflow on pushes to the dev branch
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
@@ -39,6 +40,8 @@ jobs:
|
||||
images: |
|
||||
name=dumbwareio/dumbdrop
|
||||
tags: |
|
||||
# Add :dev tag for pushes to the dev branch
|
||||
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }}
|
||||
# the semantic versioning tags add "latest" when a version tag is present
|
||||
# but since version tags aren't being used (yet?) let's add "latest" anyway
|
||||
type=raw,value=latest
|
||||
|
||||
@@ -95,7 +95,7 @@ For local development setup, troubleshooting, and advanced usage, see the dedica
|
||||
| BASE_URL | Base URL for the application | http://localhost:PORT | No |
|
||||
| MAX_FILE_SIZE | Maximum file size in MB | 1024 | No |
|
||||
| DUMBDROP_PIN | PIN protection (4-10 digits) | None | No |
|
||||
| DUMBDROP_TITLE | Site title displayed in header | DumbDrop | No |
|
||||
| DUMBDROP_TITLE | Title displayed in the browser tab | DumbDrop | No |
|
||||
| APPRISE_URL | Apprise URL for notifications | None | No |
|
||||
| APPRISE_MESSAGE | Notification message template | New file uploaded {filename} ({size}), Storage used {storage} | No |
|
||||
| APPRISE_SIZE_UNIT | Size unit for notifications (B, KB, MB, GB, TB, or Auto) | Auto | No |
|
||||
@@ -104,6 +104,7 @@ For local development setup, troubleshooting, and advanced usage, see the dedica
|
||||
| ALLOWED_IFRAME_ORIGINS | Comma-separated list of origins allowed to embed the app in an iframe | None | No |
|
||||
| UPLOAD_DIR | Directory for uploads (Docker/production; should be `/app/uploads` in container) | None (see LOCAL_UPLOAD_DIR fallback) | No |
|
||||
| LOCAL_UPLOAD_DIR | Directory for uploads (local dev, fallback: './local_uploads') | ./local_uploads | No |
|
||||
| FOOTER_LINKS | Comma-separated custom footer links (Format: "Text @ URL") | None | No |
|
||||
|
||||
- **UPLOAD_DIR** is used in Docker/production. If not set, LOCAL_UPLOAD_DIR is used for local development. If neither is set, the default is `./local_uploads`.
|
||||
- **Docker Note:** The Dockerfile now only creates the `uploads` directory inside the container. The host's `./local_uploads` is mounted to `/app/uploads` and should be managed on the host system.
|
||||
|
||||
@@ -16,6 +16,7 @@ services:
|
||||
BASE_URL: http://localhost:3000 # The base URL for the application
|
||||
|
||||
# Additional available environment variables (commented out with defaults)
|
||||
# FOOTER_LINKS: "My Site @ https://example.com,Docs @ https://docs.example.com" # Custom footer links
|
||||
# PORT: 3000 # Server port (default: 3000)
|
||||
# NODE_ENV: production # Node environment (development/production)
|
||||
# DEBUG: false # Debug mode for verbose logging (default: false in production, true in development)
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
<div id="uploadProgress"></div>
|
||||
<div id="fileList" class="file-list"></div>
|
||||
<button id="uploadButton" class="upload-button" style="display: none;">Upload Files</button>
|
||||
<footer>
|
||||
{{FOOTER_CONTENT}}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script defer>
|
||||
@@ -962,9 +965,8 @@
|
||||
setTheme(next);
|
||||
}
|
||||
|
||||
// Initialize theme
|
||||
const savedTheme = localStorage.getItem('theme') ||
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
// Apply theme on initial load
|
||||
const savedTheme = localStorage.getItem('theme') || 'system';
|
||||
setTheme(savedTheme);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -39,6 +39,7 @@ body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 80px;
|
||||
color: var(--text-color);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
@@ -359,3 +360,47 @@ button:disabled {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer Styles */
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
border-top: 1px solid var(--border-color);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer-separator {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
footer {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.footer-separator {
|
||||
margin: 0 0.3em;
|
||||
}
|
||||
}
|
||||
|
||||
58
src/app.js
58
src/app.js
@@ -46,20 +46,48 @@ app.use('/api/files', requirePin(config.pin), downloadLimiter, fileRoutes);
|
||||
|
||||
// Root route
|
||||
app.get('/', (req, res) => {
|
||||
// Check if the PIN is configured and the cookie exists
|
||||
if (config.pin && (!req.cookies?.DUMBDROP_PIN || !safeCompare(req.cookies.DUMBDROP_PIN, config.pin))) {
|
||||
return res.redirect('/login.html');
|
||||
try {
|
||||
// Check if the PIN is configured and the cookie exists
|
||||
if (config.pin && (!req.cookies?.DUMBDROP_PIN || !safeCompare(req.cookies.DUMBDROP_PIN, config.pin))) {
|
||||
return res.redirect('/login.html');
|
||||
}
|
||||
|
||||
let html = fs.readFileSync(path.join(__dirname, '../public', 'index.html'), 'utf8');
|
||||
|
||||
// Standard replacements
|
||||
html = html.replace(/{{SITE_TITLE}}/g, config.siteTitle);
|
||||
html = html.replace('{{AUTO_UPLOAD}}', config.autoUpload.toString());
|
||||
html = html.replace('{{MAX_RETRIES}}', config.clientMaxRetries.toString());
|
||||
// Ensure baseUrl has a trailing slash for correct asset linking
|
||||
const baseUrlWithSlash = config.baseUrl.endsWith('/') ? config.baseUrl : config.baseUrl + '/';
|
||||
html = html.replace(/{{BASE_URL}}/g, baseUrlWithSlash);
|
||||
|
||||
// Generate Footer Content
|
||||
let footerHtml = ''; // Initialize empty
|
||||
if (config.footerLinks && config.footerLinks.length > 0) {
|
||||
// If custom links exist, use only them
|
||||
footerHtml = config.footerLinks.map(link =>
|
||||
`<a href="${link.url}" target="_blank" rel="noopener noreferrer">${link.text}</a>`
|
||||
).join('<span class="footer-separator"> | </span>');
|
||||
} else {
|
||||
// Otherwise, use only the default static link
|
||||
footerHtml = `<span class="footer-static">Built by <a href="https://www.dumbware.io/" target="_blank" rel="noopener noreferrer">Dumbwareio</a></span>`;
|
||||
}
|
||||
html = html.replace('{{FOOTER_CONTENT}}', footerHtml);
|
||||
|
||||
// Inject demo banner if applicable
|
||||
html = injectDemoBanner(html);
|
||||
|
||||
// Send the final processed HTML
|
||||
res.send(html);
|
||||
|
||||
} catch (err) {
|
||||
logger.error(`Error processing index.html for / route: ${err.message}`);
|
||||
// Check if headers have already been sent before trying to send an error response
|
||||
if (!res.headersSent) {
|
||||
res.status(500).send('Error loading page');
|
||||
}
|
||||
}
|
||||
|
||||
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 = html.replace('{{MAX_RETRIES}}', config.clientMaxRetries.toString());
|
||||
// Ensure baseUrl has a trailing slash for correct asset linking
|
||||
const baseUrlWithSlash = config.baseUrl.endsWith('/') ? config.baseUrl : config.baseUrl + '/';
|
||||
html = html.replace(/{{BASE_URL}}/g, baseUrlWithSlash);
|
||||
html = injectDemoBanner(html);
|
||||
res.send(html);
|
||||
});
|
||||
|
||||
// Login route
|
||||
@@ -108,6 +136,10 @@ app.use(express.static('public'));
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
|
||||
logger.error(`Unhandled error: ${err.message}`);
|
||||
// Check if headers have already been sent before trying to send an error response
|
||||
if (res.headersSent) {
|
||||
return next(err); // Pass error to default handler if headers sent
|
||||
}
|
||||
res.status(500).json({
|
||||
message: 'Internal server error',
|
||||
error: process.env.NODE_ENV === 'development' ? err.message : undefined
|
||||
|
||||
@@ -12,7 +12,7 @@ console.log('Loaded ENV:', {
|
||||
NODE_ENV: process.env.NODE_ENV
|
||||
});
|
||||
const { validatePin } = require('../utils/security');
|
||||
const logger = require('../utils/logger');
|
||||
const logger = require('../utils/logger'); // Use the default logger instance
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { version } = require('../../package.json'); // Get version from package.json
|
||||
@@ -66,15 +66,15 @@ function determineUploadDirectory() {
|
||||
let uploadDir;
|
||||
if (process.env.UPLOAD_DIR) {
|
||||
uploadDir = process.env.UPLOAD_DIR;
|
||||
logConfig(`Upload directory set from UPLOAD_DIR: ${uploadDir}`);
|
||||
logger.info(`Upload directory set from UPLOAD_DIR: ${uploadDir}`);
|
||||
} else if (process.env.LOCAL_UPLOAD_DIR) {
|
||||
uploadDir = process.env.LOCAL_UPLOAD_DIR;
|
||||
logConfig(`Upload directory using LOCAL_UPLOAD_DIR fallback: ${uploadDir}`, 'warning');
|
||||
logger.warn(`Upload directory using LOCAL_UPLOAD_DIR fallback: ${uploadDir}`);
|
||||
} else {
|
||||
uploadDir = './local_uploads';
|
||||
logConfig(`Upload directory using default fallback: ${uploadDir}`, 'warning');
|
||||
logger.warn(`Upload directory using default fallback: ${uploadDir}`);
|
||||
}
|
||||
logConfig(`Final upload directory path: ${require('path').resolve(uploadDir)}`);
|
||||
logger.info(`Final upload directory path: ${require('path').resolve(uploadDir)}`);
|
||||
return uploadDir;
|
||||
}
|
||||
|
||||
@@ -95,12 +95,12 @@ function ensureLocalUploadDirExists(uploadDir) {
|
||||
try {
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
logConfig(`Created local upload directory: ${uploadDir}`);
|
||||
logger.info(`Created local upload directory: ${uploadDir}`);
|
||||
} else {
|
||||
logConfig(`Local upload directory exists: ${uploadDir}`);
|
||||
logger.info(`Local upload directory exists: ${uploadDir}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logConfig(`Failed to create local upload directory: ${uploadDir}. Error: ${err.message}`, 'warning');
|
||||
logger.warn(`Failed to create local upload directory: ${uploadDir}. Error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,34 @@ function ensureLocalUploadDirExists(uploadDir) {
|
||||
const resolvedUploadDir = determineUploadDirectory();
|
||||
ensureLocalUploadDirExists(resolvedUploadDir);
|
||||
|
||||
/**
|
||||
* Function to parse the FOOTER_LINKS environment variable
|
||||
* @param {string} linksString - The input string containing links
|
||||
* @returns {Array} - An array of objects containing text and URL
|
||||
*/
|
||||
const parseFooterLinks = (linksString) => {
|
||||
if (!linksString) {
|
||||
return [];
|
||||
}
|
||||
return linksString.split(',')
|
||||
.map(linkPair => {
|
||||
const parts = linkPair.split('@').map(part => part.trim());
|
||||
if (parts.length === 2 && parts[0] && parts[1]) {
|
||||
// Basic URL validation (starts with http/https)
|
||||
if (parts[1].startsWith('http://') || parts[1].startsWith('https://')) {
|
||||
return { text: parts[0], url: parts[1] };
|
||||
} else {
|
||||
logger.warn(`Invalid URL format in FOOTER_LINKS for "${parts[0]}": ${parts[1]}. Skipping.`);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Invalid format in FOOTER_LINKS: "${linkPair}". Expected "Text @ URL". Skipping.`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(link => link !== null); // Remove null entries from invalid formats
|
||||
};
|
||||
|
||||
/**
|
||||
* Application configuration
|
||||
* Loads and validates environment variables
|
||||
@@ -178,7 +206,18 @@ const config = {
|
||||
* Set via DUMBDROP_TITLE in .env
|
||||
*/
|
||||
siteTitle: process.env.DUMBDROP_TITLE || DEFAULT_SITE_TITLE,
|
||||
/**
|
||||
* Parsed custom footer links
|
||||
* Set via FOOTER_LINKS in .env (e.g., "Link 1 @ URL1, Link 2 @ URL2")
|
||||
*/
|
||||
_footerLinksRaw: (() => {
|
||||
const rawValue = process.env.FOOTER_LINKS;
|
||||
console.log(`[CONFIG] Raw FOOTER_LINKS from process.env: '${rawValue}'`);
|
||||
return rawValue; // Keep the original flow, just log
|
||||
})(),
|
||||
footerLinks: parseFooterLinks(process.env.FOOTER_LINKS),
|
||||
|
||||
// =====================
|
||||
// =====================
|
||||
// =====================
|
||||
// Notification settings
|
||||
@@ -227,9 +266,8 @@ const config = {
|
||||
}
|
||||
const retries = parseInt(envValue, 10);
|
||||
if (isNaN(retries) || retries < 0) {
|
||||
logConfig(
|
||||
logger.warn(
|
||||
`Invalid CLIENT_MAX_RETRIES value: "${envValue}". Using default: ${defaultValue}`,
|
||||
'warning',
|
||||
);
|
||||
return logAndReturn('CLIENT_MAX_RETRIES', defaultValue, true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user