7 Commits

Author SHA1 Message Date
Greirson Lee-Thorp
80b5426c52 Merge branch 'dev' into feat--support-PUID/PGID/UMASK 2025-05-05 20:08:27 -07:00
greirson
cb7e49b0e1 refactor(app): Improve error handling and HTML response processing in root route
- Enhanced error logging to include specific context for index.html processing errors.
- Added checks to ensure headers are sent only when appropriate, preventing potential issues with multiple responses.
- Improved comments for clarity on the purpose of code sections, particularly around HTML processing and error handling.

feat(config): Log raw FOOTER_LINKS from environment variables

- Introduced logging for the raw FOOTER_LINKS value to aid in debugging and configuration verification.
- Maintained existing functionality while enhancing visibility into configuration values.
2025-05-05 17:19:40 -07:00
greirson
5666569580 Merge branch 'main' of https://github.com/DumbWareio/DumbDrop into dev, add dev branch push to dockerhub 2025-05-05 16:53:58 -07:00
Greirson Lee-Thorp
6f1b93ed39 feat: footer (#53)
* fix: Correct BASE_URL handling in login.html for API requests

* feat(upload): Implement persistent state via metadata for resumability (#50) (#51)

* feat: Enhance chunk upload functionality with configurable retry logic

- Introduced MAX_RETRIES configuration to allow dynamic adjustment of retry attempts for chunk uploads.
- Updated index.html to read MAX_RETRIES from server-side configuration, providing a default value if not set.
- Implemented retry logic in uploadChunkWithRetry method, including exponential backoff and error handling for network issues.
- Added console warnings for invalid or missing MAX_RETRIES values to improve debugging.

This commit improves the robustness of file uploads by allowing configurable retry behavior, enhancing user experience during upload failures.

* feat: Enhance upload functionality with metadata management and improved error handling

- Introduced persistent metadata management for uploads, allowing resumability and better tracking of upload states.
- Added special handling for 404 responses during chunk uploads, logging warnings and marking uploads as complete if previously finished.
- Implemented metadata directory creation and validation in app.js to ensure proper upload management.
- Updated upload.js to include metadata read/write functions, improving the robustness of the upload process.
- Enhanced cleanup routines to handle stale metadata and incomplete uploads, ensuring a cleaner state.

This commit significantly improves the upload process by adding metadata support, enhancing error handling, and ensuring better resource management during uploads.

Fixes #24

* feat(footer): Add custom footer links and styles, enhance README and configuration

- Introduced FOOTER_LINKS environment variable for customizable footer links in the application.
- Updated index.html to render footer content dynamically based on FOOTER_LINKS.
- Enhanced styles for the footer in styles.css to improve visual presentation.
- Updated .env.example and README.md to document the new FOOTER_LINKS configuration and its usage.

This commit enhances the user interface by allowing dynamic footer content and improves documentation for better developer understanding.

* fix(footer): Correct footer content rendering logic in app.js

- Initialized footerHtml variable to handle dynamic and static footer content based on the presence of custom footer links.
- Updated logic to ensure that if custom links exist, they are used; otherwise, a default static link is displayed.

This commit improves the footer rendering by ensuring the correct content is displayed based on configuration.
2025-05-05 16:25:10 -07:00
gitmotion
6cf39e4639 - Update dockerfile to use existing PUID, PGID if exists and create one as fallback.
- include environment variables in docker compose as well
2025-05-04 18:58:52 -07:00
greirson
65d10a6576 feat(docker): Enhance Docker configuration and entrypoint management
- Updated .env.example to include user/group ID and umask settings for better file permission management in Docker.
- Modified docker-compose.yml to specify version and added restart policy for the service.
- Enhanced Dockerfile to create user/group based on ARG values and set umask for file creation.
- Introduced entrypoint.sh script to manage user permissions and execute commands with appropriate privileges.

These changes improve the Docker setup for better handling of file permissions and user management, ensuring a smoother development and deployment experience.
2025-05-04 17:59:23 -07:00
Greirson Lee-Thorp
105d2a7412 feat(upload): Implement persistent state via metadata for resumability (#50)
* feat: Enhance chunk upload functionality with configurable retry logic

- Introduced MAX_RETRIES configuration to allow dynamic adjustment of retry attempts for chunk uploads.
- Updated index.html to read MAX_RETRIES from server-side configuration, providing a default value if not set.
- Implemented retry logic in uploadChunkWithRetry method, including exponential backoff and error handling for network issues.
- Added console warnings for invalid or missing MAX_RETRIES values to improve debugging.

This commit improves the robustness of file uploads by allowing configurable retry behavior, enhancing user experience during upload failures.

* feat: Enhance upload functionality with metadata management and improved error handling

- Introduced persistent metadata management for uploads, allowing resumability and better tracking of upload states.
- Added special handling for 404 responses during chunk uploads, logging warnings and marking uploads as complete if previously finished.
- Implemented metadata directory creation and validation in app.js to ensure proper upload management.
- Updated upload.js to include metadata read/write functions, improving the robustness of the upload process.
- Enhanced cleanup routines to handle stale metadata and incomplete uploads, ensuring a cleaner state.

This commit significantly improves the upload process by adding metadata support, enhancing error handling, and ensuring better resource management during uploads.
2025-05-04 11:33:01 -07:00
10 changed files with 358 additions and 46 deletions

View File

@@ -65,4 +65,25 @@ AUTO_UPLOAD=false
# Comma-separated list of origins allowed to embed the app in an iframe (optional) # Comma-separated list of origins allowed to embed the app in an iframe (optional)
# ALLOWED_IFRAME_ORIGINS=https://example.com,https://another.com # ALLOWED_IFRAME_ORIGINS=https://example.com,https://another.com
ALLOWED_IFRAME_ORIGINS= ALLOWED_IFRAME_ORIGINS=
# --- Docker Specific Settings ---
# User and Group IDs for file permissions
# Sets the user/group the application runs as inside the container.
# Files created in the mapped volume (e.g., ./local_uploads) will have this ownership.
# Set these to match your host user's ID/GID to avoid permission issues.
# Find your IDs with `id -u` and `id -g` on Linux/macOS.
# PUID=1000
# PGID=1000
# File Mode Creation Mask (Umask)
# Controls the default permissions for newly created files.
# 022 (default): Files 644 (rw-r--r--), Dirs 755 (rwxr-xr-x)
# 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
# Custom footer links (comma-separated, format: "Link Text @ URL")
# Example: FOOTER_LINKS=My Site @ https://example.com, Another Link @ https://another.org
FOOTER_LINKS=

View File

@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- main # Trigger the workflow on pushes to the main branch - main # Trigger the workflow on pushes to the main branch
- dev # Trigger the workflow on pushes to the dev branch
jobs: jobs:
build-and-push: build-and-push:
@@ -39,6 +40,8 @@ jobs:
images: | images: |
name=dumbwareio/dumbdrop name=dumbwareio/dumbdrop
tags: | 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 # 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 # but since version tags aren't being used (yet?) let's add "latest" anyway
type=raw,value=latest type=raw,value=latest

View File

@@ -1,8 +1,17 @@
# Base stage for shared configurations # Base stage for shared configurations
FROM node:20-alpine as base FROM node:20-alpine as base
# Install python and create virtual environment with minimal dependencies # Add user and group IDs as arguments with defaults
RUN apk add --no-cache python3 py3-pip && \ ARG PUID=1000
ARG PGID=1000
# Default umask (complement of 022 is 755 for dirs, 644 for files)
ARG UMASK=022
# Install necessary packages:
# - shadow: for user/group management (usermod, groupmod)
# - su-exec: lightweight sudo alternative
# - python3, pip: for apprise dependency
RUN apk add --no-cache shadow su-exec python3 py3-pip && \
python3 -m venv /opt/venv && \ python3 -m venv /opt/venv && \
rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
@@ -14,51 +23,90 @@ RUN . /opt/venv/bin/activate && \
# Add virtual environment to PATH # Add virtual environment to PATH
ENV PATH="/opt/venv/bin:$PATH" ENV PATH="/opt/venv/bin:$PATH"
# Create group and user with fallback to prevent build failures
# We use the ARG values here, but with a fallback mechanism to avoid build failures
RUN addgroup -g ${PGID} nodeuser 2>/dev/null || \
(echo "Group with GID ${PGID} already exists, creating with alternate GID" && addgroup nodeuser) && \
adduser -u ${PUID} -G nodeuser -s /bin/sh -D nodeuser 2>/dev/null || \
(echo "User with UID ${PUID} already exists, creating with alternate UID" && adduser -G nodeuser -s /bin/sh -D nodeuser)
WORKDIR /usr/src/app WORKDIR /usr/src/app
# Set UMASK - this applies to processes run by the user created in this stage
# The entrypoint will also set it based on the ENV var at runtime.
RUN umask ${UMASK}
# Dependencies stage # Dependencies stage
FROM base as deps FROM base as deps
COPY package*.json ./ # Change ownership early so npm cache is owned correctly
RUN chown nodeuser:nodeuser /usr/src/app
USER nodeuser
COPY --chown=nodeuser:nodeuser package*.json ./
RUN npm ci --only=production && \ RUN npm ci --only=production && \
# Remove npm cache # Remove npm cache
npm cache clean --force npm cache clean --force
# Switch back to root temporarily for steps requiring root privileges if any
# USER root
# Development stage # Development stage
# Note: Running dev stage as non-root might require adjustments
# depending on tooling (e.g., nodemon needing specific permissions)
# For now, let's keep it simpler and potentially run dev as root or figure out permissions later if needed.
FROM deps as development FROM deps as development
USER root # Switch back to root for installing dev deps and copying files owned by host
ENV NODE_ENV=development ENV NODE_ENV=development
# Install dev dependencies # Install dev dependencies
COPY --chown=nodeuser:nodeuser package*.json ./
RUN npm install && \ RUN npm install && \
npm cache clean --force npm cache clean --force
# Create upload directory # Create and own upload/data directories
RUN mkdir -p uploads # Using local_uploads based on project structure, also create standard uploads
RUN mkdir -p /usr/src/app/local_uploads /usr/src/app/uploads && \
chown -R nodeuser:nodeuser /usr/src/app/local_uploads /usr/src/app/uploads
# Copy source with specific paths to avoid unnecessary files # Copy source code - ensure ownership is correct if needed later
COPY src/ ./src/ COPY --chown=nodeuser:nodeuser src/ ./src/
COPY public/ ./public/ COPY --chown=nodeuser:nodeuser public/ ./public/
COPY __tests__/ ./__tests__/ COPY --chown=nodeuser:nodeuser __tests__/ ./__tests__/
COPY dev/ ./dev/ COPY --chown=nodeuser:nodeuser dev/ ./dev/
COPY .eslintrc.json .eslintignore ./ COPY --chown=nodeuser:nodeuser .eslintrc.json .eslintignore .prettierrc nodemon.json ./
# Expose port # Expose port
EXPOSE 3000 EXPOSE 3000
CMD ["npm", "run", "dev"] # We won't switch user yet for dev, might cause issues with host mounts/debugging
# USER nodeuser
# CMD ["npm", "run", "dev"] # Default CMD, likely overridden by compose
# Production stage # Production stage
FROM deps as production FROM deps as production
USER root # Switch back to root for creating dirs and copying files
ENV NODE_ENV=production ENV NODE_ENV=production
# Create upload directory # Create and own upload/data directories
RUN mkdir -p uploads RUN mkdir -p /usr/src/app/local_uploads /usr/src/app/uploads && \
chown -R nodeuser:nodeuser /usr/src/app /usr/src/app/local_uploads /usr/src/app/uploads
# Copy only necessary source files # Copy only necessary source files and ensure ownership
COPY src/ ./src/ COPY --chown=nodeuser:nodeuser src/ ./src/
COPY public/ ./public/ COPY --chown=nodeuser:nodeuser public/ ./public/
# Copy the entrypoint script and make it executable
COPY --chown=nodeuser:nodeuser src/scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Expose port # Expose port
EXPOSE 3000 EXPOSE 3000
# Set the entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# Default command to run (passed to entrypoint)
CMD ["npm", "start"] CMD ["npm", "start"]
# USER nodeuser # User switch happens inside entrypoint script using su-exec

View File

@@ -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 | | BASE_URL | Base URL for the application | http://localhost:PORT | No |
| MAX_FILE_SIZE | Maximum file size in MB | 1024 | No | | MAX_FILE_SIZE | Maximum file size in MB | 1024 | No |
| DUMBDROP_PIN | PIN protection (4-10 digits) | None | 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_URL | Apprise URL for notifications | None | No |
| APPRISE_MESSAGE | Notification message template | New file uploaded {filename} ({size}), Storage used {storage} | 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 | | 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 | | 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 | | 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 | | 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`. - **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. - **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.

View File

@@ -16,10 +16,22 @@ services:
BASE_URL: http://localhost:3000 # The base URL for the application BASE_URL: http://localhost:3000 # The base URL for the application
# Additional available environment variables (commented out with defaults) # 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) # PORT: 3000 # Server port (default: 3000)
# NODE_ENV: production # Node environment (development/production) # NODE_ENV: production # Node environment (development/production)
# DEBUG: false # Debug mode for verbose logging (default: false in production, true in development) # DEBUG: false # Debug mode for verbose logging (default: false in production, true in development)
# APPRISE_URL: "" # Apprise notification URL for upload notifications (default: none) # APPRISE_URL: "" # Apprise notification URL for upload notifications (default: none)
# APPRISE_MESSAGE: "New file uploaded - {filename} ({size}), Storage used {storage}" # Notification message template with placeholders: {filename}, {size}, {storage} # APPRISE_MESSAGE: "New file uploaded - {filename} ({size}), Storage used {storage}" # Notification message template with placeholders: {filename}, {size}, {storage}
# APPRISE_SIZE_UNIT: "Auto" # Size unit for notifications (B, KB, MB, GB, TB, or Auto) # APPRISE_SIZE_UNIT: "Auto" # Size unit for notifications (B, KB, MB, GB, TB, or Auto)
# ALLOWED_EXTENSIONS: ".jpg,.jpeg,.png,.pdf,.doc,.docx,.txt" # Comma-separated list of allowed file extensions (default: all allowed) # ALLOWED_EXTENSIONS: ".jpg,.jpeg,.png,.pdf,.doc,.docx,.txt" # Comma-separated list of allowed file extensions (default: all allowed)
# PUID: 1000 # User ID for file ownership (default: 1000)
# PGID: 1000 # Group ID for file ownership (default: 1000)
# UMASK: "000" # File permissions mask (default: 000)
restart: unless-stopped
# user: "${PUID}:${PGID}" # Don't set user here, entrypoint handles it
# Consider adding healthcheck
# healthcheck:
# test: ["CMD", "curl", "--fail", "http://localhost:3000/health"] # Assuming a /health endpoint exists
# interval: 30s
# timeout: 10s
# retries: 3

View File

@@ -49,6 +49,9 @@
<div id="uploadProgress"></div> <div id="uploadProgress"></div>
<div id="fileList" class="file-list"></div> <div id="fileList" class="file-list"></div>
<button id="uploadButton" class="upload-button" style="display: none;">Upload Files</button> <button id="uploadButton" class="upload-button" style="display: none;">Upload Files</button>
<footer>
{{FOOTER_CONTENT}}
</footer>
</div> </div>
<script defer> <script defer>
@@ -962,9 +965,8 @@
setTheme(next); setTheme(next);
} }
// Initialize theme // Apply theme on initial load
const savedTheme = localStorage.getItem('theme') || const savedTheme = localStorage.getItem('theme') || 'system';
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
setTheme(savedTheme); setTheme(savedTheme);
</script> </script>
</body> </body>

View File

@@ -39,6 +39,7 @@ body {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding-top: 2rem; padding-top: 2rem;
padding-bottom: 80px;
color: var(--text-color); color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
} }
@@ -359,3 +360,47 @@ button:disabled {
font-size: 1.125rem; 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;
}
}

View File

@@ -46,20 +46,48 @@ app.use('/api/files', requirePin(config.pin), downloadLimiter, fileRoutes);
// Root route // Root route
app.get('/', (req, res) => { app.get('/', (req, res) => {
// Check if the PIN is configured and the cookie exists try {
if (config.pin && (!req.cookies?.DUMBDROP_PIN || !safeCompare(req.cookies.DUMBDROP_PIN, config.pin))) { // Check if the PIN is configured and the cookie exists
return res.redirect('/login.html'); 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 // Login route
@@ -108,6 +136,10 @@ app.use(express.static('public'));
// Error handling middleware // Error handling middleware
app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
logger.error(`Unhandled error: ${err.message}`); 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({ res.status(500).json({
message: 'Internal server error', message: 'Internal server error',
error: process.env.NODE_ENV === 'development' ? err.message : undefined error: process.env.NODE_ENV === 'development' ? err.message : undefined

View File

@@ -12,7 +12,7 @@ console.log('Loaded ENV:', {
NODE_ENV: process.env.NODE_ENV NODE_ENV: process.env.NODE_ENV
}); });
const { validatePin } = require('../utils/security'); 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 fs = require('fs');
const path = require('path'); const path = require('path');
const { version } = require('../../package.json'); // Get version from package.json const { version } = require('../../package.json'); // Get version from package.json
@@ -66,15 +66,15 @@ function determineUploadDirectory() {
let uploadDir; let uploadDir;
if (process.env.UPLOAD_DIR) { if (process.env.UPLOAD_DIR) {
uploadDir = 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) { } else if (process.env.LOCAL_UPLOAD_DIR) {
uploadDir = 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 { } else {
uploadDir = './local_uploads'; 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; return uploadDir;
} }
@@ -95,12 +95,12 @@ function ensureLocalUploadDirExists(uploadDir) {
try { try {
if (!fs.existsSync(uploadDir)) { if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true }); fs.mkdirSync(uploadDir, { recursive: true });
logConfig(`Created local upload directory: ${uploadDir}`); logger.info(`Created local upload directory: ${uploadDir}`);
} else { } else {
logConfig(`Local upload directory exists: ${uploadDir}`); logger.info(`Local upload directory exists: ${uploadDir}`);
} }
} catch (err) { } 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(); const resolvedUploadDir = determineUploadDirectory();
ensureLocalUploadDirExists(resolvedUploadDir); 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 * Application configuration
* Loads and validates environment variables * Loads and validates environment variables
@@ -178,7 +206,18 @@ const config = {
* Set via DUMBDROP_TITLE in .env * Set via DUMBDROP_TITLE in .env
*/ */
siteTitle: process.env.DUMBDROP_TITLE || DEFAULT_SITE_TITLE, 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 // Notification settings
@@ -227,9 +266,8 @@ const config = {
} }
const retries = parseInt(envValue, 10); const retries = parseInt(envValue, 10);
if (isNaN(retries) || retries < 0) { if (isNaN(retries) || retries < 0) {
logConfig( logger.warn(
`Invalid CLIENT_MAX_RETRIES value: "${envValue}". Using default: ${defaultValue}`, `Invalid CLIENT_MAX_RETRIES value: "${envValue}". Using default: ${defaultValue}`,
'warning',
); );
return logAndReturn('CLIENT_MAX_RETRIES', defaultValue, true); return logAndReturn('CLIENT_MAX_RETRIES', defaultValue, true);
} }

110
src/scripts/entrypoint.sh Normal file
View File

@@ -0,0 +1,110 @@
#!/bin/sh
# Simple entrypoint script to manage user permissions and execute CMD
# Exit immediately if a command exits with a non-zero status.
set -e
# Function to log messages
log_info() {
echo "[INFO] Entrypoint: $1"
}
log_warning() {
echo "[WARN] Entrypoint: $1"
}
log_error() {
echo "[ERROR] Entrypoint: $1" >&2
}
log_info "Starting entrypoint script..."
# Default user/group/umask values
DEFAULT_UID=1000
DEFAULT_GID=1000
DEFAULT_UMASK=022
# Default upload directory if not set by user (should align with Dockerfile/compose)
DEFAULT_UPLOAD_DIR="/usr/src/app/local_uploads"
# Check if PUID or PGID environment variables are set by the user
if [ -z "${PUID}" ] && [ -z "${PGID}" ]; then
# --- Run as Root ---
log_info "PUID/PGID not set, running as root."
# Set umask (use UMASK env var if provided, otherwise default)
CURRENT_UMASK=${UMASK:-$DEFAULT_UMASK}
log_info "Setting umask to ${CURRENT_UMASK}"
umask "${CURRENT_UMASK}"
# Execute the command passed to the entrypoint as root
log_info "Executing command as root: $@"
exec "$@"
else
# --- Run as Custom User (nodeuser with adjusted UID/GID) ---
log_info "PUID/PGID set, configuring user 'nodeuser'..."
# Use provided UID/GID or default if only one is set
CURRENT_UID=${PUID:-$DEFAULT_UID}
CURRENT_GID=${PGID:-$DEFAULT_GID}
CURRENT_UMASK=${UMASK:-$DEFAULT_UMASK}
# Read the upload directory from ENV var or use default
TARGET_UPLOAD_DIR=${UPLOAD_DIR:-$DEFAULT_UPLOAD_DIR}
log_info "Target UID: ${CURRENT_UID}, GID: ${CURRENT_GID}, UMASK: ${CURRENT_UMASK}"
log_info "Target Upload Dir: ${TARGET_UPLOAD_DIR}"
# Check if user/group exists (should exist from Dockerfile)
if ! getent group nodeuser > /dev/null 2>&1; then
log_warning "Group 'nodeuser' not found, creating with GID ${CURRENT_GID}..."
addgroup -g "${CURRENT_GID}" nodeuser
else
EXISTING_GID=$(getent group nodeuser | cut -d: -f3)
if [ "${EXISTING_GID}" != "${CURRENT_GID}" ]; then
log_info "Updating 'nodeuser' group GID from ${EXISTING_GID} to ${CURRENT_GID}..."
groupmod -o -g "${CURRENT_GID}" nodeuser
fi
fi
if ! getent passwd nodeuser > /dev/null 2>&1; then
log_warning "User 'nodeuser' not found, creating with UID ${CURRENT_UID}..."
adduser -u "${CURRENT_UID}" -G nodeuser -s /bin/sh -D nodeuser
else
EXISTING_UID=$(getent passwd nodeuser | cut -d: -f3)
if [ "${EXISTING_UID}" != "${CURRENT_UID}" ]; then
log_info "Updating 'nodeuser' user UID from ${EXISTING_UID} to ${CURRENT_UID}..."
usermod -o -u "${CURRENT_UID}" nodeuser
fi
fi
# Ensure the base application directory ownership is correct
log_info "Ensuring ownership of /usr/src/app..."
chown -R nodeuser:nodeuser /usr/src/app || log_warning "Could not chown /usr/src/app"
# Ensure the target upload directory exists and has correct ownership
if [ -n "${TARGET_UPLOAD_DIR}" ]; then
if [ ! -d "${TARGET_UPLOAD_DIR}" ]; then
log_info "Creating directory: ${TARGET_UPLOAD_DIR}"
# Use -p to create parent directories as needed
mkdir -p "${TARGET_UPLOAD_DIR}"
# Chown after creation
chown nodeuser:nodeuser "${TARGET_UPLOAD_DIR}" || log_warning "Could not chown ${TARGET_UPLOAD_DIR}"
else
# Directory exists, ensure ownership
log_info "Ensuring ownership of ${TARGET_UPLOAD_DIR}..."
chown -R nodeuser:nodeuser "${TARGET_UPLOAD_DIR}" || log_warning "Could not chown ${TARGET_UPLOAD_DIR}"
fi
else
log_warning "UPLOAD_DIR variable is not set or is empty, skipping ownership check for upload directory."
fi
# Set the umask
log_info "Setting umask to ${CURRENT_UMASK}"
umask "${CURRENT_UMASK}"
# Execute the command passed to the entrypoint using su-exec to drop privileges
log_info "Executing command as nodeuser (${CURRENT_UID}:${CURRENT_GID}): $@"
exec su-exec nodeuser "$@"
fi
log_info "Entrypoint script finished (should not reach here if exec worked)."