feat: Enhance folder upload handling and filename sanitation

- Added support for checking webkitRelativePath in folder uploads, alerting users if their browser does not support this feature.
- Introduced sanitizePathPreserveDirs function to sanitize filenames while preserving directory structure.
- Updated upload route to utilize the new sanitation function and ensure consistent folder naming during uploads.

Fixes #45
This commit is contained in:
greirson
2025-05-02 14:38:28 -07:00
parent 8f4b2ea873
commit ccd06f92bb
3 changed files with 33 additions and 14 deletions

View File

@@ -516,6 +516,15 @@
// Reset the input to allow selecting the same folder again // Reset the input to allow selecting the same folder again
const input = e.target; const input = e.target;
files = [...input.files]; files = [...input.files];
// Check for webkitRelativePath support
const missingRelPath = files.some(f => !('webkitRelativePath' in f) || !f.webkitRelativePath);
if (missingRelPath) {
alert('Your browser does not support folder uploads with structure. Please use a modern browser like Chrome or Edge.');
files = [];
updateFileList();
input.value = '';
return;
}
console.log('Folder selection files:', files.map(f => ({ console.log('Folder selection files:', files.map(f => ({
name: f.name, name: f.name,
path: f.webkitRelativePath, path: f.webkitRelativePath,

View File

@@ -10,7 +10,7 @@ const crypto = require('crypto');
const path = require('path'); const path = require('path');
const { config } = require('../config'); const { config } = require('../config');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const { getUniqueFilePath, getUniqueFolderPath, sanitizeFilename } = require('../utils/fileUtils'); const { getUniqueFilePath, getUniqueFolderPath, sanitizeFilename, sanitizePathPreserveDirs } = require('../utils/fileUtils');
const { sendNotification } = require('../services/notifications'); const { sendNotification } = require('../services/notifications');
const fs = require('fs'); const fs = require('fs');
const { cleanupIncompleteUploads } = require('../utils/cleanup'); const { cleanupIncompleteUploads } = require('../utils/cleanup');
@@ -174,8 +174,8 @@ router.post('/init', async (req, res) => {
// Update batch activity // Update batch activity
batchActivity.set(batchId, Date.now()); batchActivity.set(batchId, Date.now());
// Sanitize filename and convert to forward slashes // Sanitize filename and convert to forward slashes, preserving directory structure
const sanitizedFilename = sanitizeFilename(filename); const sanitizedFilename = sanitizePathPreserveDirs(filename);
const safeFilename = path.normalize(sanitizedFilename) const safeFilename = path.normalize(sanitizedFilename)
.replace(/^(\.\.(\/|\\|$))+/, '') .replace(/^(\.\.(\/|\\|$))+/, '')
.replace(/\\/g, '/') .replace(/\\/g, '/')
@@ -205,35 +205,36 @@ router.post('/init', async (req, res) => {
const pathParts = safeFilename.split('/').filter(Boolean); // Remove empty parts const pathParts = safeFilename.split('/').filter(Boolean); // Remove empty parts
if (pathParts.length > 1) { if (pathParts.length > 1) {
// Handle files within folders // The first part is the root folder name from the client
const originalFolderName = pathParts[0]; const originalFolderName = pathParts[0];
const folderPath = path.join(config.uploadDir, originalFolderName); // Always use a consistent mapping for this batch to avoid collisions
// This ensures all files in the batch go into the same (possibly renamed) root folder
let newFolderName = folderMappings.get(`${originalFolderName}-${batchId}`); let newFolderName = folderMappings.get(`${originalFolderName}-${batchId}`);
const folderPath = path.join(config.uploadDir, newFolderName || originalFolderName);
if (!newFolderName) { if (!newFolderName) {
try { try {
// First ensure parent directories exist // Ensure parent directories exist
await fs.promises.mkdir(path.dirname(folderPath), { recursive: true }); await fs.promises.mkdir(path.dirname(folderPath), { recursive: true });
// Then try to create the target folder // Try to create the target folder
await fs.promises.mkdir(folderPath, { recursive: false }); await fs.promises.mkdir(folderPath, { recursive: false });
newFolderName = originalFolderName; newFolderName = originalFolderName;
} catch (err) { } catch (err) {
if (err.code === 'EEXIST') { if (err.code === 'EEXIST') {
// If the folder exists, generate a unique folder name for this batch
const uniqueFolderPath = await getUniqueFolderPath(folderPath); const uniqueFolderPath = await getUniqueFolderPath(folderPath);
newFolderName = path.basename(uniqueFolderPath); newFolderName = path.basename(uniqueFolderPath);
logger.info(`Folder "${originalFolderName}" exists, using "${newFolderName}"`); logger.info(`Folder "${originalFolderName}" exists, using "${newFolderName}" for batch ${batchId}`);
} else { } else {
throw err; throw err;
} }
} }
// Store the mapping for this batch
folderMappings.set(`${originalFolderName}-${batchId}`, newFolderName); folderMappings.set(`${originalFolderName}-${batchId}`, newFolderName);
} }
// Always apply the mapping for this batch
pathParts[0] = newFolderName; pathParts[0] = newFolderName;
filePath = path.join(config.uploadDir, ...pathParts); filePath = path.join(config.uploadDir, ...pathParts);
// Ensure all parent directories exist for the file
// Ensure all parent directories exist
await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
} }

View File

@@ -165,11 +165,20 @@ function sanitizeFilename(fileName) {
return sanitized; return sanitized;
} }
function sanitizePathPreserveDirs(filePath) {
// Split on forward slashes, sanitize each part, and rejoin
return filePath
.split('/')
.map(part => sanitizeFilename(part))
.join('/');
}
module.exports = { module.exports = {
formatFileSize, formatFileSize,
calculateDirectorySize, calculateDirectorySize,
ensureDirectoryExists, ensureDirectoryExists,
getUniqueFilePath, getUniqueFilePath,
getUniqueFolderPath, getUniqueFolderPath,
sanitizeFilename sanitizeFilename,
sanitizePathPreserveDirs
}; };