mirror of
https://github.com/DumbWareio/DumbDrop.git
synced 2025-11-03 05:23:39 +00:00
feat: improve file upload handling with atomic file and folder creation
- Refactor getUniqueFilePath and getUniqueFolderPath to use async/await and atomic file operations - Enhance upload initialization to handle file and folder naming conflicts more robustly - Implement file handle management to prevent resource leaks - Add error handling for file and folder creation scenarios - Ensure parent directories are created recursively when needed
This commit is contained in:
93
server.js
93
server.js
@@ -224,31 +224,55 @@ const folderMappings = new Map();
|
|||||||
const batchUploads = new Map();
|
const batchUploads = new Map();
|
||||||
|
|
||||||
// Add these helper functions before the routes
|
// Add these helper functions before the routes
|
||||||
function getUniqueFilePath(filePath) {
|
async function getUniqueFilePath(filePath) {
|
||||||
const dir = path.dirname(filePath);
|
const dir = path.dirname(filePath);
|
||||||
const ext = path.extname(filePath);
|
const ext = path.extname(filePath);
|
||||||
const baseName = path.basename(filePath, ext);
|
const baseName = path.basename(filePath, ext);
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
let newPath = filePath;
|
let finalPath = filePath;
|
||||||
|
|
||||||
while (fs.existsSync(newPath)) {
|
while (true) {
|
||||||
newPath = path.join(dir, `${baseName} (${counter})${ext}`);
|
try {
|
||||||
counter++;
|
// Try to create the file exclusively - will fail if file exists
|
||||||
|
const fileHandle = await fs.promises.open(finalPath, 'wx');
|
||||||
|
// Return both the path and handle instead of closing it
|
||||||
|
return { path: finalPath, handle: fileHandle };
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'EEXIST') {
|
||||||
|
// File exists, try next number
|
||||||
|
finalPath = path.join(dir, `${baseName} (${counter})${ext}`);
|
||||||
|
counter++;
|
||||||
|
} else {
|
||||||
|
throw err; // Other errors should be handled by caller
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUniqueFolderPath(folderPath) {
|
async function getUniqueFolderPath(folderPath) {
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
let newPath = folderPath;
|
let finalPath = folderPath;
|
||||||
|
|
||||||
while (fs.existsSync(newPath)) {
|
while (true) {
|
||||||
newPath = `${folderPath} (${counter})`;
|
try {
|
||||||
counter++;
|
// Try to create the directory - mkdir with recursive:false is atomic
|
||||||
|
await fs.promises.mkdir(finalPath, { recursive: false });
|
||||||
|
return finalPath;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'EEXIST') {
|
||||||
|
// Folder exists, try next number
|
||||||
|
finalPath = `${folderPath} (${counter})`;
|
||||||
|
counter++;
|
||||||
|
} else if (err.code === 'ENOENT') {
|
||||||
|
// Parent directory doesn't exist, create it first
|
||||||
|
await fs.promises.mkdir(path.dirname(finalPath), { recursive: true });
|
||||||
|
// Then try again with the same path
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
throw err; // Other errors should be handled by caller
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate batch ID format
|
// Validate batch ID format
|
||||||
@@ -282,6 +306,7 @@ app.post('/upload/init', async (req, res) => {
|
|||||||
|
|
||||||
const uploadId = crypto.randomBytes(16).toString('hex');
|
const uploadId = crypto.randomBytes(16).toString('hex');
|
||||||
let filePath = path.join(uploadDir, safeFilename);
|
let filePath = path.join(uploadDir, safeFilename);
|
||||||
|
let fileHandle;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Handle file/folder duplication
|
// Handle file/folder duplication
|
||||||
@@ -296,14 +321,21 @@ app.post('/upload/init', async (req, res) => {
|
|||||||
let newFolderName = folderMappings.get(`${originalFolderName}-${batchId}`);
|
let newFolderName = folderMappings.get(`${originalFolderName}-${batchId}`);
|
||||||
|
|
||||||
if (!newFolderName) {
|
if (!newFolderName) {
|
||||||
// Always check if the folder exists, even for new uploads
|
try {
|
||||||
if (fs.existsSync(folderPath)) {
|
// Try to create the folder atomically first
|
||||||
const uniqueFolderPath = getUniqueFolderPath(folderPath);
|
await fs.promises.mkdir(folderPath, { recursive: false });
|
||||||
newFolderName = path.basename(uniqueFolderPath);
|
|
||||||
log.info(`Folder "${originalFolderName}" exists, using "${newFolderName}" instead`);
|
|
||||||
} else {
|
|
||||||
newFolderName = originalFolderName;
|
newFolderName = originalFolderName;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'EEXIST') {
|
||||||
|
// Folder exists, get a unique name
|
||||||
|
const uniqueFolderPath = await getUniqueFolderPath(folderPath);
|
||||||
|
newFolderName = path.basename(uniqueFolderPath);
|
||||||
|
log.info(`Folder "${originalFolderName}" exists, using "${newFolderName}" instead`);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
folderMappings.set(`${originalFolderName}-${batchId}`, newFolderName);
|
folderMappings.set(`${originalFolderName}-${batchId}`, newFolderName);
|
||||||
|
|
||||||
// Clean up mapping after 5 minutes
|
// Clean up mapping after 5 minutes
|
||||||
@@ -315,25 +347,34 @@ app.post('/upload/init', async (req, res) => {
|
|||||||
// Replace the original folder path with the mapped one and keep original file name
|
// Replace the original folder path with the mapped one and keep original file name
|
||||||
pathParts[0] = newFolderName;
|
pathParts[0] = newFolderName;
|
||||||
filePath = path.join(uploadDir, ...pathParts);
|
filePath = path.join(uploadDir, ...pathParts);
|
||||||
} else {
|
|
||||||
// This is a single file
|
// Ensure parent directories exist
|
||||||
filePath = getUniqueFilePath(filePath);
|
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the directory exists before creating the write stream
|
// For both single files and files in folders, get a unique path and file handle
|
||||||
await ensureDirectoryExists(filePath);
|
const result = await getUniqueFilePath(filePath);
|
||||||
|
filePath = result.path;
|
||||||
|
fileHandle = result.handle;
|
||||||
|
|
||||||
|
// Create upload entry (using the file handle we already have)
|
||||||
uploads.set(uploadId, {
|
uploads.set(uploadId, {
|
||||||
safeFilename: path.relative(uploadDir, filePath),
|
safeFilename: path.relative(uploadDir, filePath),
|
||||||
filePath,
|
filePath,
|
||||||
fileSize,
|
fileSize,
|
||||||
bytesReceived: 0,
|
bytesReceived: 0,
|
||||||
writeStream: fs.createWriteStream(filePath, { flags: 'wx' })
|
writeStream: fileHandle.createWriteStream()
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info(`Initialized upload for ${path.relative(uploadDir, filePath)} (${fileSize} bytes)`);
|
log.info(`Initialized upload for ${path.relative(uploadDir, filePath)} (${fileSize} bytes)`);
|
||||||
res.json({ uploadId });
|
res.json({ uploadId });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Clean up file handle if something went wrong
|
||||||
|
if (fileHandle) {
|
||||||
|
await fileHandle.close().catch(() => {});
|
||||||
|
// Try to remove the file if it was created
|
||||||
|
fs.unlink(filePath).catch(() => {});
|
||||||
|
}
|
||||||
log.error(`Failed to initialize upload: ${err.message}`);
|
log.error(`Failed to initialize upload: ${err.message}`);
|
||||||
res.status(500).json({ error: 'Failed to initialize upload' });
|
res.status(500).json({ error: 'Failed to initialize upload' });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user