### Copy Example Environment File Source: https://github.com/dumbwareio/dumbdrop/blob/main/LOCAL_DEVELOPMENT.md Copy the example environment file to create your own configuration file. ```bash cp .env.example .env ``` -------------------------------- ### Start Development Server Source: https://github.com/dumbwareio/dumbdrop/blob/main/LOCAL_DEVELOPMENT.md Start the Dumbdrop development server using the npm script. ```bash npm run dev ``` -------------------------------- ### Install Project Dependencies Source: https://github.com/dumbwareio/dumbdrop/blob/main/LOCAL_DEVELOPMENT.md Install all necessary project dependencies using npm. ```bash npm install ``` -------------------------------- ### Install Apprise for Notifications Source: https://github.com/dumbwareio/dumbdrop/blob/main/LOCAL_DEVELOPMENT.md Install the Apprise library using pip, which is used for testing notifications. ```bash pip install apprise ``` -------------------------------- ### Start Docker Compose Services Source: https://github.com/dumbwareio/dumbdrop/blob/main/README.md Deploy the services defined in your docker-compose.yml file in detached mode. ```bash docker compose up -d ``` -------------------------------- ### Start File Upload Process Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Initiates the file upload process, including initialization, chunked uploading, and progress updates. Handles potential errors during the upload. ```javascript async start() { try { this.updateProgress(0); // Initial progress update await this.initUpload(); if (this.file.size > 0) { // Only upload chunks if file is not empty await this.uploadChunks(); } else { console.log(`Skipping chunk upload for zero-byte file: ${this.file.name}`); // Server handles zero-byte completion in /init this.updateProgress(100); // Mark as complete on client too } return true; } catch (error) { console.error('Upload failed:', error); if (this.progressElement) { this.progressElement.infoSpan.textContent = `Error: ${error.message}`; this.progressElement.infoSpan.style.color = 'var(--danger-color)'; } return false; } } ``` -------------------------------- ### Run DumbDrop with Docker Source: https://github.com/dumbwareio/dumbdrop/blob/main/README.md Quickly start DumbDrop using a single Docker command. This maps port 3000 and mounts a local uploads directory. ```bash docker run -p 3000:3000 -v ./uploads:/app/uploads dumbwareio/dumbdrop:latest ``` -------------------------------- ### Setup PIN Input Event Listeners Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/login.html Attaches event listeners for input, keydown (backspace), paste, and form submission to handle PIN entry and verification. ```javascript const setupPinHandling = () => { const form = document.getElementById('pin-form'); const inputs = [...form.querySelectorAll('.pin-digit')]; inputs.forEach((input, index) => { input.addEventListener('input', (e) => handlePinInput(e, index)); input.addEventListener('keydown', (e) => { if (e.key === 'Backspace' && !input.value && index > 0) { inputs[index - 1].focus(); inputs[index - 1].value = ''; inputs[index - 1].classList.remove('filled'); } }); // Handle paste event input.addEventListener('paste', (e) => { e.preventDefault(); const pastedData = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, inputs.length); if (pastedData.length > 0) { pastedData.split('').forEach((digit, i) => { if (i < inputs.length) { inputs[i].value = digit; inputs[i].classList.add('filled'); } }); if (pastedData.length === inputs.length) { verifyPin(pastedData); } else if (pastedData.length < inputs.length) { inputs[pastedData.length].focus(); } } }); }); form.addEventListener('submit', (e) => { e.preventDefault(); const pin = inputs.map(input => input.value).join(''); if (pin.length === inputs.length) { verifyPin(pin); } }); // Focus first input inputs[0].focus(); }; ``` -------------------------------- ### Start File Uploads Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Initiates the file upload process. It disables the upload button, clears progress indicators, generates a batch ID, and prepares to process files. ```javascript async function startUploads() { try { uploadButton.disabled = true; document.getElementById('uploadProgress').innerHTML = ''; const batchId = generateBatchId(); const results = []; // Process ``` -------------------------------- ### Download File API Call Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Initiates a file download by making a GET request to the server. Handles path encoding to support subdirectories. Displays an error toast if the download fails. ```javascript async downloadFile(filePath) { try { // Encode each part of the path separately to handle subdirectories const encodedPath = filePath.split('/').map(part => encodeURIComponent(part)).join('/'); const url = `/api/files/download/${encodedPath}`; window.open(url, '_blank'); } catch (error) { console.error('Download failed:', error); Toastify({ text: `Download failed: ${error.message}`, duration: 3000, gravity: "bottom", position: "right", style: { background: "#f44336" } }).showToast(); } } ``` -------------------------------- ### Setup Modal Event Listeners Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Configures event listeners for the rename modal, allowing it to close when clicking outside the modal content or when the Escape key is pressed. ```javascript setupModalEventListeners() { // Ensure modal closes when clicking outside this.renameModal.addEventListener('click', (e) => { if (e.target === this.renameModal) { this.cancelRename(); } }); // Handle keyboard shortcuts document.addEventListener('keydown', (e) => { if (this.renameModal.classList.contains('show')) { if (e.key === 'Escape') { this.cancelRename(); } } }); } ``` -------------------------------- ### Docker Compose Configuration with Bind Mount Source: https://github.com/dumbwareio/dumbdrop/blob/main/docs/BIND_MOUNT_FIX.md Configure Docker Compose to use a bind mount for the Dumbdrop upload directory. This setup allows the application to correctly access and manage files stored on the host system. ```yaml services: dumbdrop: image: dumbwareio/dumbdrop:latest ports: - 3000:3000 volumes: - ./uploads:/app/uploads # Bind mount - now works! environment: UPLOAD_DIR: /app/uploads ``` -------------------------------- ### Initialize Theme Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Initializes the theme by checking local storage for a saved theme, falling back to the system's preferred color scheme, and then applying it. ```javascript // Initialize theme const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); setTheme(savedTheme); ``` -------------------------------- ### Initialize Theme and PIN Length Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/login.html Sets up the initial theme based on local storage or system preferences and defines a default PIN length. ```javascript const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.documentElement.setAttribute('data-theme', savedTheme); let pinLength = 4; // Default length, will be updated from server ``` -------------------------------- ### Enable Demo Mode Source: https://github.com/dumbwareio/dumbdrop/blob/main/demo.md Set the DEMO_MODE environment variable to true to enable demo mode. This can be done in your environment or docker-compose.yml file. ```env DEMO_MODE=true ``` -------------------------------- ### Enable File Listing and Management Source: https://github.com/dumbwareio/dumbdrop/blob/main/README.md Set SHOW_FILE_LIST to true to enable the file listing feature with download and delete functionality. ```env SHOW_FILE_LIST=true ``` -------------------------------- ### Configure Upload Parameters Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Sets up constants for chunk size, retry delay, and maximum retries. It reads MAX_RETRIES from server injection, with a fallback to a default value and logs warnings if the value is invalid or missing. ```javascript const CHUNK_SIZE = 1024 * 1024; // 1MB chunks const RETRY_DELAY = 1000; // 1 second delay between retries // Read MAX_RETRIES from the injected server value, with a fallback const MAX_RETRIES_STR = '{{MAX_RETRIES}}'; let maxRetries = 5; // Default value if (MAX_RETRIES_STR && MAX_RETRIES_STR !== '{{MAX_RETRIES}}') { const parsedRetries = parseInt(MAX_RETRIES_STR, 10); if (!isNaN(parsedRetries) && parsedRetries >= 0) { maxRetries = parsedRetries; } else { console.warn(`Invalid MAX_RETRIES value "${MAX_RETRIES_STR}" received from server, defaulting to ${maxRetries}.`); } } else { console.warn('MAX_RETRIES not injected by server, defaulting to 5.'); } window.MAX_RETRIES = maxRetries; // Assign to window for potential global use/debugging console.log(`Max retries for chunk uploads: ${window.MAX_RETRIES}`); ``` -------------------------------- ### Check PIN Requirement and Initialize Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/login.html Fetches from the server to determine if a PIN is required for login and initializes the PIN input fields and handling if necessary. Handles rate limiting errors. ```javascript // Check PIN length and initialize fetch('/api/auth/pin-required') .then(response => { if (response.status === 429) { throw new Error('Too many attempts. Please wait before trying again.'); } return response.json(); }) .then(data => { if (data.required) { pinLength = data.length; createPinInputs(pinLength); setupPinHandling(); } else { window.location.href = '/'; } }) .catch(err => { console.error('Error checking PIN requirement:', err); const errorMessage = err.message === 'Too many attempts. Please wait before trying again.' ? err.message : 'Error checking PIN requirement'; document.getElementById('pin-error').textContent = errorMessage; // Prevent refresh loop by disabling the PIN input form const pinContainer = document.getElementById('pin-container'); if (pinContainer) { pinContainer.style.opacity = '0.5'; pinContainer.style.pointerEvents = 'none'; } }); ``` -------------------------------- ### Direct Access Configuration Source: https://github.com/dumbwareio/dumbdrop/blob/main/README.md No proxy configuration needed for direct access. TRUST_PROXY defaults to false. ```env # TRUST_PROXY=false (default - no need to set) ``` -------------------------------- ### Initialize Upload Request Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Sends an initial POST request to the server's upload initialization endpoint. It includes the filename (using webkitRelativePath if available) and file size, and sets the X-Batch-ID header if provided. Throws an error if the server response is not OK. ```javascript async initUpload() { // Always use webkitRelativePath if available, otherwise fallback to name const uploadPath = this.file.webkitRelativePath || this.file.name; console.log('Initializing upload:', { path: uploadPath, size: this.file.size, batchId: this.batchId }); const headers = { 'Content-Type': 'application/json' }; if (this.batchId) { headers['X-Batch-ID'] = this.batchId; } // Remove leading slash from API path before concatenating const apiUrl = '/api/upload/init'.startsWith('/') ? '/api/upload/init'.substring(1) : '/api/upload/init'; const response = await fetch(apiUrl, { method: 'POST', headers, body: JSON.stringify({ filename: uploadPath.replace(/\\/g, '/'), // Ensure forward slashes fileSize: this.file.size }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.details || error.error || 'Upload initialization failed'); } const data = await response.json(); this.uploadId = data.uploadId; } ``` -------------------------------- ### Drag and Drop Event Handlers Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Sets up event listeners for drag and drop events on a specified drop zone. It includes functions to prevent default behaviors, highlight the drop zone during drag over, and unhighlight it upon leaving or dropping. The `handleDrop` function processes dropped items, distinguishing between folder drops (using `webkitGetAsEntry`) and regular file drops. ```javascript ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, preventDefaults, false); document.body.addEventListener(eventName, preventDefaults, false); }); ['dragenter', 'dragover'].forEach(eventName => { dropZone.addEventListener(eventName, highlight, false); }); ['dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, unhighlight, false); }); dropZone.addEventListener('drop', handleDrop, false); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } function highlight(e) { dropZone.classList.add('highlight'); } function unhighlight(e) { dropZone.classList.remove('highlight'); } function handleDrop(e) { const items = e.dataTransfer.items; if (items && items[0].webkitGetAsEntry) { const loadingItem = document.createElement('div'); loadingItem.className = 'file-item loading'; loadingItem.textContent = 'Processing dropped items...'; fileList.appendChild(loadingItem); getAllFileEntries(items).then(newFiles => { loadingItem.remove(); if (newFiles.length === 0) { console.warn('No valid files found in drop'); return; } files = newFiles; updateFileList(); if (AUTO_UPLOAD) startUploads(); }).catch(error => { console.error('Error processing dropped items:', error); loadingItem.textContent = `Error: ${error.message}`; loadingItem.style.color = 'var(--danger-color)'; setTimeout(() => loadingItem.remove(), 3000); }); } else { files = [...e.dataTransfer.files]; updateFileList(); if (AUTO_UPLOAD) startUploads(); } } ``` -------------------------------- ### Render File List Items Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Recursively renders file and directory items into the provided container. It creates elements for each item and handles nested directories. ```javascript renderItems(items, container, level = 0) { items.forEach(item => { const itemElement = this.createItemElement(item, level); container.appendChild(itemElement); if (item.type === 'directory' && item.children && item.children.length > 0) { const childrenContainer = document.createElement('div'); childrenContainer.className = 'directory-children'; this.renderItems(item.children, childrenContainer, level + 1); container.appendChild(childrenContainer); } }); } ``` -------------------------------- ### New isPathWithinUploadDir Implementation Source: https://github.com/dumbwareio/dumbdrop/blob/main/docs/BIND_MOUNT_FIX.md The new implementation correctly handles non-existent files using path.resolve() and existing files using fs.realpathSync() when requireExists is true. It normalizes paths and includes checks for path traversal and Windows drive consistency. ```javascript function isPathWithinUploadDir(filePath, uploadDir, requireExists = false) { try { // Always resolve the upload directory (must exist) const realUploadDir = fs.realpathSync(uploadDir); let resolvedFilePath; if (requireExists && fs.existsSync(filePath)) { // For existing files, resolve symlinks for security resolvedFilePath = fs.realpathSync(filePath); } else { // For non-existent files (uploads), use path.resolve resolvedFilePath = path.resolve(filePath); } // Normalize paths for consistent comparison const relativePath = path.relative( path.normalize(realUploadDir), path.normalize(resolvedFilePath) ); // Reject paths outside upload directory if (relativePath === '') return true; // Same directory if (relativePath.startsWith('..')) return false; // Path traversal // Windows: Check same drive if (process.platform === 'win32') { if (resolvedFilePath.split(':')[0] !== realUploadDir.split(':')[0]) { return false; } } return true; } catch (err) { logger.error(`Path validation error: ${err.message}`, err); return false; } } ``` -------------------------------- ### FileUploader Class Initialization Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Initializes the FileUploader class with a file, batch ID, and default settings for chunk size, retries, and delay. It also sets up properties to track upload progress and status. ```javascript class FileUploader { constructor(file, batchId) { this.file = file; this.batchId = batchId; this.uploadId = null; this.position = 0; this.progressElement = null; this.chunkSize = 1024 * 1024; // 1MB chunks this.lastUploadedBytes = 0; this.lastUploadTime = null; this.uploadRate = 0; this.maxRetries = window.MAX_RETRIES; // Use configured retries this.retryDelay = RETRY_DELAY; // Use constant } ``` -------------------------------- ### Initialize File List Manager Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Initializes the FileListManager, setting up event listeners for refreshing files and handling modal interactions. It ensures the file list is displayed and loaded upon initialization if SHOW_FILE_LIST is true. ```javascript init() { this.uploadedFilesList.style.display = 'block'; this.refreshBtn.addEventListener('click', () => this.loadFiles()); this.setupModalEventListeners(); this.loadFiles(); } ``` -------------------------------- ### Create Progress Element Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Creates and initializes the HTML elements used to display upload progress, including a label for the file name. ```javascript createProgressElement() { const container = document.createElement('div'); container.className = 'progress-container'; const label = document.createElement('div'); label.className = 'progress-label'; const fileName = this.file.webkitRelativePath || this.file.name; label.textContent = escapeHtml(fileName); const progress = document.createElement('div'); progress.className = 'progress'; const bar = document.createElement('div'); bar.className = ``` -------------------------------- ### Previous isPathWithinUploadDir Implementation Source: https://github.com/dumbwareio/dumbdrop/blob/main/docs/BIND_MOUNT_FIX.md The previous implementation used fs.realpathSync() on potentially non-existent files, causing failures with Docker bind mounts and leading to disappearing files. It silently failed on errors. ```javascript function isPathWithinUploadDir(filePath, uploadDir) { try { // This would fail for non-existent files! const realFilePath = fs.realpathSync(filePath); const realUploadDir = fs.realpathSync(uploadDir); const relativePath = path.relative(realUploadDir, realFilePath); return !relativePath.startsWith('..'); } catch (err) { return false; // Silently fail - files disappear! } } ``` -------------------------------- ### Verify PIN with Server Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/login.html Sends the entered PIN to the server for verification and handles the response, including redirects on success or error messages on failure. ```javascript const verifyPin = async (pin) => { try { const response = await fetch('/api/auth/verify-pin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pin }), credentials: 'include', // Ensure cookies are sent // redirect: 'follow' // Follow server redirects }); const data = await response.json(); // Simplified success and error handling if (data.success) { window.location.href = '/'; } else { // Show error message document.getElementById('pin-error').textContent = data.error || 'Authentication failed'; // Determine if it's a lockout scenario const isLockedOut = data.error.includes('Too many PIN verification attempts'); const inputs = [...document.querySelectorAll('.pin-digit')]; if (isLockedOut) { // Disable inputs if locked out inputs.forEach(input => { input.disabled = true; input.classList.add('locked'); }); } else { // Reset inputs for other error types inputs.forEach(input => { input.value = ''; input.classList.remove('filled'); }); inputs[0].focus(); } } } catch (err) { console.error('Error verifying PIN:', err); document.getElementById('pin-error').textContent = 'Error verifying PIN'; } }; ``` -------------------------------- ### Initiate Rename Item Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Prepares the UI for renaming an item by populating an input field and showing a modal. It also sets up an event listener for the Enter key to confirm the rename. ```javascript async renameItem(itemPath, itemType, currentName) { this.currentRenameData = { itemPath, itemType, currentName }; this.renameInput.value = currentName; this.renameModal.classList.add('show'); this.renameInput.focus(); this.renameInput.select(); // Add Enter key support for this specific rename session const handleKeydown = (e) => { if (e.key === 'Enter') { this.confirmRename(); } }; this.renameInput.addEventListener('keydown', handleKeydown); // Store the handler so we can remove it later this.currentKeydownHandler = handleKeydown; } ``` -------------------------------- ### Run Path Validation Tests Source: https://github.com/dumbwareio/dumbdrop/blob/main/docs/BIND_MOUNT_FIX.md Execute the path validation tests for the Dumbdrop application. ```bash npm test -- test/path-validation.test.js ``` -------------------------------- ### Configure DumbDrop with Docker Compose Source: https://github.com/dumbwareio/dumbdrop/blob/main/README.md Set up DumbDrop using Docker Compose for more customization. This includes environment variables for configuration like upload directory, title, file size limits, and optional PIN protection. ```yaml services: dumbdrop: image: dumbwareio/dumbdrop:latest ports: - 3000:3000 volumes: # Where your uploaded files will land - ./uploads:/app/uploads environment: # Explicitly set upload directory inside the container UPLOAD_DIR: /app/uploads # The title shown in the web interface DUMBDROP_TITLE: DumbDrop # Maximum file size in MB MAX_FILE_SIZE: 1024 # Optional PIN protection (leave empty to disable) DUMBDROP_PIN: 123456 # Upload without clicking button AUTO_UPLOAD: false # The base URL for the application # You must update this to the url you use to access your site BASE_URL: http://localhost:3000 ``` -------------------------------- ### Clone Dumbdrop Repository Source: https://github.com/dumbwareio/dumbdrop/blob/main/LOCAL_DEVELOPMENT.md Clone the Dumbdrop project repository from GitHub and navigate into the project directory. ```bash git clone https://github.com/yourusername/dumbdrop.git cd dumbdrop ``` -------------------------------- ### Configure Auto Upload and File List Visibility Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Determines whether auto-upload is enabled and if the file list should be shown based on server-injected configuration values. ```javascript const AUTO_UPLOAD_STR = '{{AUTO_UPLOAD}}'; const AUTO_UPLOAD = ['true', '1', 'yes'].includes(AUTO_UPLOAD_STR.toLowerCase()); const SHOW_FILE_LIST_STR = '{{SHOW_FILE_LIST}}'; const SHOW_FILE_LIST = ['true', '1', 'yes'].includes(SHOW_FILE_LIST_STR.toLowerCase()); ``` -------------------------------- ### Create Element for File or Directory Item Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Generates the HTML element for a single file or directory item, including its icon, name, details (size, date/file count), and action buttons. ```javascript createItemElement(item, level) { const itemDiv = document.createElement('div'); itemDiv.className = `uploaded-file-item ${item.type === 'directory' ? 'directory-item' : ''}`; const icon = item.type === 'directory' ? '📁' : '📄'; const name = item.name; const details = item.type === 'directory' ? `${item.formattedSize} • ${this.countFilesInDirectory(item)} files` : `${item.formattedSize} • ${new Date(item.uploadDate).toLocaleDateString()}`; // Create info section const infoDiv = document.createElement('div'); infoDiv.className = 'uploaded-file-info'; const nameDiv = document.createElement('div'); nameDiv.className = 'uploaded-file-name'; nameDiv.textContent = `${icon} ${name}`; const detailsDiv = document.createElement('div'); detailsDiv.className = 'uploaded-file-details'; detailsDiv.textContent = details; infoDiv.appendChild(nameDiv); infoDiv.appendChild(detailsDiv); // Create actions section const actionsDiv = document.createElement('div'); actionsDiv.className = 'uploaded-file-actions'; // Download button (only for files) if (item.type === 'file') { const downloadBtn = document.createElement('button'); downloadBtn.className = 'action-btn download-btn'; downloadBtn.textContent = 'Download'; downloadBtn.dataset.path = item.path; downloadBtn.addEventListener('click', (e) => { e.stopPropagation(); ``` -------------------------------- ### Load Files from API Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Fetches the list of uploaded files from the '/api/files' endpoint. Handles potential HTTP errors and displays loading or error messages accordingly. ```javascript async loadFiles() { try { this.showLoading(); const response = await fetch('/api/files'); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); this.displayFiles(data); } catch (error) { console.error('Failed to load files:', error); this.showError('Failed to load files: ' + error.message); } } ``` -------------------------------- ### Enable Trust Proxy Source: https://github.com/dumbwareio/dumbdrop/blob/main/README.md Enable this setting if DumbDrop is deployed behind a trusted reverse proxy. ```env TRUST_PROXY=true ``` -------------------------------- ### Set Theme and Update Icons Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Sets the theme attribute on the document element and stores it in local storage. Updates the display of moon and sun icons based on the selected theme. ```javascript document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); // Update icon const moonPaths = document.querySelectorAll('.moon'); const sunPaths = document.querySelectorAll('.sun'); if (theme === 'dark') { moonPaths.forEach(path => path.style.display = 'none'); sunPaths.forEach(path => path.style.display = ''); } else { moonPaths.forEach(path => path.style.display = ''); sunPaths.forEach(path => path.style.display = 'none'); } ``` -------------------------------- ### File Extension Filtering Configuration Source: https://github.com/dumbwareio/dumbdrop/blob/main/README.md Set ALLOWED_EXTENSIONS to restrict file types that can be uploaded. If not set, all extensions are allowed. ```env ALLOWED_EXTENSIONS=.jpg,.jpeg,.png,.pdf,.doc,.docx,.txt ``` -------------------------------- ### Create PIN Input Fields Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/login.html Dynamically generates the required number of password input fields for the PIN. ```javascript const createPinInputs = (length) => { const form = document.getElementById('pin-form'); form.innerHTML = ''; // Clear existing inputs for (let i = 0; i < length; i++) { const input = document.createElement('input'); input.type = 'password'; input.className = 'pin-digit'; input.maxLength = 1; input.pattern = '[0-9]'; input.inputMode = 'numeric'; input.autocomplete = 'off'; input.required = true; form.appendChild(input); } }; ``` -------------------------------- ### Configure Trusted Proxy IPs Source: https://github.com/dumbwareio/dumbdrop/blob/main/README.md For enhanced security, specify the IP addresses of your trusted reverse proxies. This requires TRUST_PROXY to be true. ```env TRUST_PROXY=true TRUSTED_PROXY_IPS=172.17.0.1,10.0.0.1 ``` -------------------------------- ### Add CSS Styles to Document Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Dynamically adds CSS styles to the document's head. This is useful for applying custom styles or themes. ```javascript const style = document.createElement('style'); style.textContent = .folder-files { margin-left: 20px; border-left: 1px solid var(--border-color); padding-left: 10px; margin-top: 5px; } .file-item.nested { font-size: 0.9em; margin-top: 3px; } .file-item.loading { text-align: center; padding: 15px; background: var(--container-bg); border-radius: 5px; margin: 10px 0; animation: pulse 1.5s infinite; } @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } /* Uploaded files listing styles */ .uploaded-files-section { margin-top: 30px; padding: 20px; background: var(--container-bg); border-radius: 10px; border: 1px solid var(--border-color); } .uploaded-files-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid var(--border-color); } .uploaded-files-header h2 { margin: 0; color: var(--text-color); font-size: 1.5em; } .uploaded-files-stats { display: flex; align-items: center; gap: 10px; color: var(--text-color-secondary); font-size: 0.9em; } .refresh-btn { padding: 5px 10px; background: var(--primary-color); color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 0.85em; transition: background 0.3s; } .refresh-btn:hover { background: var(--primary-hover); } .uploaded-files-content { max-height: 400px; overflow-y: auto; } .uploaded-file-item { display: flex; align-items: center; justify-content: space-between; padding: 10px; margin: 5px 0; background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 5px; transition: background 0.3s; } .uploaded-file-item:hover { background: var(--hover-bg, rgba(0, 0, 0, 0.05)); } [data-theme="dark"] .uploaded-file-item:hover { background: rgba(255, 255, 255, 0.05); } .uploaded-file-info { display: flex; flex-direction: column; flex: 1; min-width: 0; } .uploaded-file-name { font-weight: 500; color: var(--text-color); margin-bottom: 2px; word-break: break-all; } .uploaded-file-details { font-size: 0.85em; color: var(--text-color-secondary); } .uploaded-file-actions { display: flex; gap: 5px; margin-left: 10px; } .action-btn { padding: 5px 8px; border: none; border-radius: 3px; cursor: pointer; font-size: 0.8em; transition: all 0.3s; min-width: 60px; } .download-btn { background: var(--success-color); color: white; } .download-btn:hover { background: var(--success-hover, #27ae60); } .rename-btn { background: var(--warning-color, #f39c12); color: white; } .rename-btn:hover { background: var(--warning-hover, #e67e22); } .delete-btn { background: var(--danger-color); color: white; } .delete-btn:hover { background: var(--danger-hover, #c0392b); } .directory-item { border-left: 3px solid var(--primary-color); } .directory-children { margin-left: 20px; border-left: 1px solid var(--border-color); padding-left: 10px; margin-top: 5px; } .loading-message, .error-message { text-align: center; padding: 20px; color: var(--text-color-secondary); } .error-message { color: var(--danger-color); } .empty-message { text-align: center; padding: 40px 20px; color: var(--text-color-secondary); font-style: italic; } /* Rename modal styles */ .rename-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; justify-content: center; align-items: center; } .rename-modal.show { display: flex; } .rename-modal-content { background: var(--container-bg); padding: 20px; border-radius: 10px; border: 1px solid var(--border-color); max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; } .rename-modal h3 { margin: 0 0 15px 0; color: var(--text-color); } .rename-input { width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 5px; background: var(--bg-color); color: var(--text-color); font-size: 14px; margin-bottom: 15px; } .rename-input:focus { outline: none; border-color: var(--primary-color); } .rename-actions { display: flex; gap: 10px; justify-content: flex-end; } .modal-btn { padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; transition: background 0.3s; } .modal-btn-cancel { background: var(--secondary-color, #6c757d); color: white; } .modal-btn-cancel:hover { background: var(--secondary-hover, #5a6268); } .modal-btn-confirm { background: var(--primary-color); color: white; } .modal-btn-confirm:hover { background: var(--primary-hover); } `; document.head.appendChild(style); ``` -------------------------------- ### Docker Compose Configuration with Named Volume Source: https://github.com/dumbwareio/dumbdrop/blob/main/docs/BIND_MOUNT_FIX.md Configure Docker Compose to use a named volume for the Dumbdrop upload directory. This is a standard configuration that was already working. ```yaml services: dumbdrop: image: dumbwareio/dumbdrop:latest ports: - 3000:3000 volumes: - dumbdrop_uploads:/app/uploads # Named volume volumes: dumbdrop_uploads: ``` -------------------------------- ### Custom Notification Message Template Source: https://github.com/dumbwareio/dumbdrop/blob/main/README.md Configure APPRISE_MESSAGE with placeholders for filename, size, and storage. Supports Apprise notification services. ```env APPRISE_MESSAGE: New file uploaded {filename} ({size}), Storage used {storage} ``` -------------------------------- ### Toggle Theme Function Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Toggles the current theme between 'dark' and 'light' by reading the current theme attribute and calling the setTheme function. ```javascript function toggleTheme() { const current = document.documentElement.getAttribute('data-theme'); const next = current === 'dark' ? 'light' : 'dark'; setTheme(next); } ``` -------------------------------- ### Nginx Reverse Proxy Configuration Source: https://github.com/dumbwareio/dumbdrop/blob/main/README.md Configure TRUST_PROXY and TRUSTED_PROXY_IPS for Nginx reverse proxy. ```env TRUST_PROXY=true TRUSTED_PROXY_IPS=172.17.0.1 ``` -------------------------------- ### Handle Individual PIN Input Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/login.html Manages user input for each PIN digit, including validation, styling, and focus management. ```javascript const handlePinInput = (e, index) => { const input = e.target; const form = document.getElementById('pin-form'); const inputs = [...form.querySelectorAll('.pin-digit')]; const value = input.value; // Only allow numbers if (!/^\d*$/.test(value)) { input.value = ''; return; } if (value) { input.classList.add('filled'); // Move to next input if available or submit if last if (index < inputs.length - 1) { inputs[index + 1].focus(); } else { // Auto-submit when last digit is entered const pin = inputs.map(input => input.value).join(''); if (pin.length === inputs.length) { verifyPin(pin); } } } else { input.classList.remove('filled'); } }; ``` -------------------------------- ### Upload Chunk with Retries Source: https://github.com/dumbwareio/dumbdrop/blob/main/public/index.html Uploads a file chunk to the server with a configurable number of retries and a timeout. Includes special handling for 404 errors during retries. ```javascript async uploadChunkWithRetry(chunk, chunkStartPosition) { const chunkApiUrlPath = `/api/upload/chunk/${this.uploadId}`; const chunkApiUrl = chunkApiUrlPath.startsWith('/') ? chunkApiUrlPath.substring(1) : chunkApiUrlPath; let lastError = null; for (let attempt = 0; attempt <= this.maxRetries; attempt++) { try { if (attempt > 0) { console.warn(`Retrying chunk (start: ${chunkStartPosition}) upload for ${this.file.webkitRelativePath || this.file.name} (Attempt ${attempt}/${this.maxRetries})... `); this.updateProgressElementInfo('Retrying attempt ' + attempt + '...', 'var(--warning-color)'); } // Use AbortController for potential timeout or cancellation during fetch const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30-second timeout per attempt const response = await fetch(chunkApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', 'X-Batch-ID': this.batchId // Consider adding 'Content-Range': `bytes ${chunkStartPosition}-${chunkStartPosition + chunk.byteLength - 1}/${this.file.size}` // If the server supports handling potential duplicate chunks via Content-Range }, body: chunk, signal: controller.signal // Add abort signal }); clearTimeout(timeoutId); // Clear timeout if fetch completes if (response.ok) { const data = await response.json(); if (attempt > 0) { console.log(`Chunk upload successful on retry attempt ${attempt} for ${this.file.webkitRelativePath || this.file.name}`); } // Update progress based on server response // this.position is updated by readChunk(), so progress reflects total uploaded this.updateProgress(data.progress); // Success! Exit the retry loop. this.updateProgressElementInfo('uploading...'); return; } else { // Server responded with an error status (4xx, 5xx) let errorText = 'Unknown server error'; try { errorText = await response.text(); } catch (textError) { /* ignore if reading text fails */ } // --- Add Special 404 Handling --- if (response.status === 404 && attempt > 0) { console.warn(`Received 404 Not Found on retry attempt ${attempt} for ${this.file.webkitRelativePath || this.file.name}. Assuming upload completed previously.`); this.updateProgress(100); // Mark as complete return; // Exit retry loop successfully } // --- End Special 404 Handling --- lastError = new Error(`Failed to upload chunk: ${response.status} ${response.statusText}. Server response: ${errorText}`); console.error(`Chunk upload attempt ${attempt} failed: ${lastError.message}`); this.updateProgressElementInfo('Attempt ' + attempt + ' failed: ' + response.statusText, 'var(--danger-color)'); } } catch (error) { // Network error, fetch failed completely, or timeout lastError = error; if (error.name === 'AbortError') { console.error(`Chunk upload attempt ${attempt} timed out after 30 seconds.`); this.updateProgressElementInfo('Attempt ' + attempt + ' timed out', 'var(--danger-color)'); } else { console.error(`Chunk upload attempt ${attempt} failed with network error: ${error.message}`); this.updateProgressElementInfo('Attempt ' + attempt + ' network error', 'var(--danger-color)'); } } // If not the last attempt, wait before retrying if (attempt < this.maxRetries) { // Exponential backoff: 1s, 2s, 4s, ... but capped const delay = Math.min(this.retryDelay * Math.pow(2, attempt), 30000); // Max 30s delay await new Promise(resolve => setTimeout(resolve, delay)); } } // If we exit the loop, all retries have failed. // Position reset is tricky. If the server *did* receive a chunk but failed to respond OK, // simply resending might corrupt data unless the server handles it idempotently. // Failing the whole upload is often safer. // this.position = chunkStartPosition; // Re-enable if server can handle duplicate chunks safely console.error(`Chunk upload failed permanently after ${this.maxRetries} retries for ${this.file.webkitRelativePath || this.file.name}, chunk starting at ${chunkStartPosition}.`); this.updateProgressElementInfo('Upload failed after ' + this.maxRetries + ' retries', 'var(--danger-color)'); throw lastError || new Error(`Chunk upload failed after ${this.maxRetries} retries.`); } ```