diff --git a/Cargo.lock b/Cargo.lock index 52b61d5b..11adb533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1750,7 +1750,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oxicloud" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "argon2", diff --git a/src/interfaces/api/handlers/file_handler.rs b/src/interfaces/api/handlers/file_handler.rs index 02e98bb3..50ca4884 100644 --- a/src/interfaces/api/handlers/file_handler.rs +++ b/src/interfaces/api/handlers/file_handler.rs @@ -64,7 +64,19 @@ impl FileHandler { } if name == "file" { - let filename = field.file_name().unwrap_or("unnamed").to_string(); + let raw_filename = field.file_name().unwrap_or("unnamed").to_string(); + // Browsers send the full relative path (e.g. "Screenshots/file.png") + // as the filename for folder uploads via webkitRelativePath. + // Strip path components to get the basename only. + // This also prevents path-traversal attacks. + let filename = raw_filename + .rsplit('/') + .next() + .unwrap_or(&raw_filename) + .rsplit('\\') + .next() + .unwrap_or(&raw_filename) + .to_string(); let content_type = field .content_type() .unwrap_or("application/octet-stream") diff --git a/static/js/fileOperations.js b/static/js/fileOperations.js index 189c82b7..b2aa6aac 100644 --- a/static/js/fileOperations.js +++ b/static/js/fileOperations.js @@ -279,7 +279,9 @@ const fileOps = { const formData = new FormData(); formData.append('folder_id', targetFolderId); - formData.append('file', file); + // Use file.name as the explicit filename to prevent the browser + // from sending the full webkitRelativePath as the filename + formData.append('file', file, file.name); const displayName = file.webkitRelativePath || file.name; diff --git a/static/js/ui.js b/static/js/ui.js index 487a53c7..0f1195d8 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -291,7 +291,15 @@ const ui = { e._oxiHandled = true; // Mark as handled for document-level fallback dropzone.classList.remove('active'); if (e.dataTransfer.files.length > 0) { - fileOps.uploadFiles(e.dataTransfer.files); + // Detect folder drops: files from folder drops have webkitRelativePath set + const hasRelativePaths = Array.from(e.dataTransfer.files).some( + f => f.webkitRelativePath && f.webkitRelativePath.includes('/') + ); + if (hasRelativePaths) { + fileOps.uploadFolderFiles(e.dataTransfer.files); + } else { + fileOps.uploadFiles(e.dataTransfer.files); + } } setTimeout(() => { dropzone.style.display = 'none'; @@ -327,7 +335,15 @@ const ui = { if (e._oxiHandled) return; if (e.dataTransfer.files.length > 0) { - fileOps.uploadFiles(e.dataTransfer.files); + // Detect folder drops: files from folder drops have webkitRelativePath set + const hasRelativePaths = Array.from(e.dataTransfer.files).some( + f => f.webkitRelativePath && f.webkitRelativePath.includes('/') + ); + if (hasRelativePaths) { + fileOps.uploadFolderFiles(e.dataTransfer.files); + } else { + fileOps.uploadFiles(e.dataTransfer.files); + } } setTimeout(() => {