From ad12ee717f7ac9f44fb3254144eee845c64c5817 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 14 Mar 2025 18:11:32 -0400 Subject: [PATCH] add folders uploading --- fileManager.js | 20 +++- folderManager.js | 11 +-- getFileList.php | 1 - styles.css | 10 ++ upload.js | 252 +++++++++++++++++++++++++++++++++-------------- upload.php | 39 +++++++- 6 files changed, 246 insertions(+), 87 deletions(-) diff --git a/fileManager.js b/fileManager.js index 0f96102..fdde063 100644 --- a/fileManager.js +++ b/fileManager.js @@ -93,18 +93,28 @@ window.updateRowHighlight = function (checkbox) { export function loadFileList(folderParam) { const folder = folderParam || "root"; - return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&t=" + new Date().getTime()) + // Request a recursive listing from the server. + return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) .then(response => response.json()) .then(data => { const fileListContainer = document.getElementById("fileList"); fileListContainer.innerHTML = ""; if (data.files && data.files.length > 0) { + // Map each file so that we have a full name that includes subfolder information. + // We assume that getFileList.php returns a property 'path' that contains the full relative path (e.g. "subfolder/filename.txt") + data.files = data.files.map(file => { + // If file.path exists, use that; otherwise fallback to file.name. + file.fullName = (file.path || file.name).trim().toLowerCase(); + return file; + }); + // Save fileData and render file table using your full list. fileData = data.files; renderFileTable(folder); } else { fileListContainer.textContent = "No files found."; updateFileActionButtons(); } + // Return the full file objects. return data.files || []; }) .catch(error => { @@ -696,15 +706,15 @@ export function editFile(fileName, folder) { theme: theme, viewportMargin: Infinity }); - + // Ensure height adjustment window.currentEditor = editor; - + // Adjust height AFTER modal appears setTimeout(() => { adjustEditorSize(); // Set initial height }, 50); - + // Attach modal resize observer observeModalResize(modal); @@ -716,7 +726,7 @@ export function editFile(fileName, folder) { document.getElementById("closeEditorX").addEventListener("click", function () { modal.remove(); }); - + document.getElementById("decreaseFont").addEventListener("click", function () { currentFontSize = Math.max(8, currentFontSize - 2); editor.getWrapperElement().style.fontSize = currentFontSize + "px"; diff --git a/folderManager.js b/folderManager.js index 72ef376..aaf29ef 100644 --- a/folderManager.js +++ b/folderManager.js @@ -116,12 +116,12 @@ export async function loadFolderTree(selectedFolder) { } let html = ""; - // Build the root row without inline styles. + // Build the root row. html += `
[-] (Root)
`; - // If no folders exist (empty array), render a default tree with just (Root). + if (folders.length === 0) { html += ``; } else { - const tree = buildFolderTree(folders); - html += renderFolderTree(tree, "", "block"); + const tree = buildFolderTree(folders); // your existing function + html += renderFolderTree(tree, "", "block"); // your existing function } container.innerHTML = html; @@ -164,7 +164,7 @@ export async function loadFolderTree(selectedFolder) { }); }); - // Attach toggle event for the root toggle. + // Attach toggle events (same as your original logic). const rootToggle = container.querySelector("#rootRow .folder-toggle"); if (rootToggle) { rootToggle.addEventListener("click", function (e) { @@ -184,7 +184,6 @@ export async function loadFolderTree(selectedFolder) { }); } - // Attach toggle events for all folder toggles. container.querySelectorAll(".folder-toggle").forEach(toggle => { toggle.addEventListener("click", function (e) { e.stopPropagation(); diff --git a/getFileList.php b/getFileList.php index d7fff8d..793c0e3 100644 --- a/getFileList.php +++ b/getFileList.php @@ -13,7 +13,6 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { } $folder = isset($_GET['folder']) ? trim($_GET['folder']) : 'root'; - // Allow only safe characters in the folder parameter (letters, numbers, underscores, dashes, spaces, and forward slashes). if ($folder !== 'root' && !preg_match('/^[A-Za-z0-9_\- \/]+$/', $folder)) { echo json_encode(["error" => "Invalid folder name."]); diff --git a/styles.css b/styles.css index 1a4047e..06b7ef9 100644 --- a/styles.css +++ b/styles.css @@ -277,6 +277,16 @@ body.dark-mode header { color: white; } +/* Default (light mode) */ +.material-icons.folder-icon { + color: black; +} + +/* Dark mode */ +body.dark-mode .material-icons.folder-icon { + color: white; +} + /* =========================================================== FORMS & LOGIN =========================================================== */ diff --git a/upload.js b/upload.js index c3fe401..917c2be 100644 --- a/upload.js +++ b/upload.js @@ -1,10 +1,17 @@ -// upload.js - import { loadFileList, displayFilePreview, initFileActions } from './fileManager.js'; import { showToast, escapeHTML } from './domUtils.js'; +import { loadFolderTree } from './folderManager.js'; export function initUpload() { const fileInput = document.getElementById("file"); + + // Enhancement: Allow folder upload with subfolders by setting directory attributes. + if (fileInput) { + fileInput.setAttribute("webkitdirectory", ""); + fileInput.setAttribute("mozdirectory", ""); + fileInput.setAttribute("directory", ""); + } + const progressContainer = document.getElementById("uploadProgressContainer"); const uploadForm = document.getElementById("uploadFileForm"); const dropArea = document.getElementById("uploadDropArea"); @@ -12,7 +19,7 @@ export function initUpload() { // Helper function: set the drop area's default layout using CSS classes. function setDropAreaDefault() { if (dropArea) { - dropArea.innerHTML = ` + dropArea.innerHTML = `
Drop files here or click 'Choose files'
@@ -31,13 +38,11 @@ export function initUpload() { `; } -} + } // Initialize drop area. if (dropArea) { - // Instead of inline styles here, ensure dropArea is styled in CSS. - // But if necessary, you can add minimal inline styles that you later override: - dropArea.classList.add("upload-drop-area"); // Define in CSS if needed. + dropArea.classList.add("upload-drop-area"); setDropAreaDefault(); dropArea.addEventListener("dragover", function (e) { @@ -62,7 +67,7 @@ export function initUpload() { }); } - // When files are selected, update file info container. + // When files are selected, update file info container and build progress list. if (fileInput) { fileInput.addEventListener("change", function () { const files = fileInput.files; @@ -89,53 +94,128 @@ export function initUpload() { fileInfoContainer.innerHTML = `No files selected`; } } - - // Build progress list using CSS classes. + + // Convert FileList to an array and assign a unique uploadIndex to each file. + const allFiles = Array.from(files); + allFiles.forEach((file, index) => { + file.uploadIndex = index; + }); + progressContainer.innerHTML = ""; - if (files.length > 0) { - const allFiles = Array.from(files); + if (allFiles.length > 0) { const maxDisplay = 10; const list = document.createElement("ul"); list.classList.add("upload-progress-list"); - allFiles.forEach((file, index) => { - const li = document.createElement("li"); - li.classList.add("upload-progress-item"); - // For dynamic display, we still set display property via JS. - li.style.display = (index < maxDisplay) ? "flex" : "none"; - - const preview = document.createElement("div"); - preview.className = "file-preview"; // Already styled in CSS. - displayFilePreview(file, preview); - - const nameDiv = document.createElement("div"); - nameDiv.classList.add("upload-file-name"); - nameDiv.textContent = file.name; - - const progDiv = document.createElement("div"); - progDiv.classList.add("progress", "upload-progress-div"); - // If needed, dynamic style for flex sizing remains: - progDiv.style.flex = "0 0 250px"; - progDiv.style.marginLeft = "5px"; - - const progBar = document.createElement("div"); - progBar.classList.add("progress-bar"); - progBar.style.width = "0%"; - progBar.innerText = "0%"; - - progDiv.appendChild(progBar); - li.appendChild(preview); - li.appendChild(nameDiv); - li.appendChild(progDiv); - li.progressBar = progBar; - li.startTime = Date.now(); - list.appendChild(li); - }); - if (allFiles.length > maxDisplay) { - const extra = document.createElement("li"); - extra.classList.add("upload-progress-extra"); - extra.textContent = `Uploading additional ${allFiles.length - maxDisplay} file(s)...`; - extra.style.display = "flex"; // If dynamic, otherwise define in CSS. - list.appendChild(extra); + + // Check if any file has a relative path (i.e. folder upload). + const hasRelativePaths = allFiles.some(file => file.webkitRelativePath && file.webkitRelativePath.trim() !== ""); + + if (hasRelativePaths) { + // Group files by folder. + const fileGroups = {}; + allFiles.forEach(file => { + let folderName = "Root"; + if (file.webkitRelativePath && file.webkitRelativePath.trim() !== "") { + const parts = file.webkitRelativePath.split("/"); + if (parts.length > 1) { + folderName = parts.slice(0, parts.length - 1).join("/"); + } + } + if (!fileGroups[folderName]) { + fileGroups[folderName] = []; + } + fileGroups[folderName].push(file); + }); + + // Create a list element for each folder group. + Object.keys(fileGroups).forEach(folderName => { + // Folder header with Material Icon. + const folderLi = document.createElement("li"); + folderLi.classList.add("upload-folder-group"); + folderLi.innerHTML = `folder ${folderName}:`; + list.appendChild(folderLi); + + // Nested list for files in this folder. + const nestedUl = document.createElement("ul"); + nestedUl.classList.add("upload-folder-group-list"); + fileGroups[folderName] + .sort((a, b) => a.uploadIndex - b.uploadIndex) + .forEach(file => { + const li = document.createElement("li"); + li.classList.add("upload-progress-item"); + li.style.display = "flex"; + li.dataset.uploadIndex = file.uploadIndex; + + const preview = document.createElement("div"); + preview.className = "file-preview"; + displayFilePreview(file, preview); + + const nameDiv = document.createElement("div"); + nameDiv.classList.add("upload-file-name"); + // Only show the file's basename. + nameDiv.textContent = file.name; + + const progDiv = document.createElement("div"); + progDiv.classList.add("progress", "upload-progress-div"); + progDiv.style.flex = "0 0 250px"; + progDiv.style.marginLeft = "5px"; + + const progBar = document.createElement("div"); + progBar.classList.add("progress-bar"); + progBar.style.width = "0%"; + progBar.innerText = "0%"; + + progDiv.appendChild(progBar); + li.appendChild(preview); + li.appendChild(nameDiv); + li.appendChild(progDiv); + li.progressBar = progBar; + li.startTime = Date.now(); + nestedUl.appendChild(li); + }); + list.appendChild(nestedUl); + }); + } else { + // Normal flat list (no grouping) + allFiles.forEach((file, index) => { + const li = document.createElement("li"); + li.classList.add("upload-progress-item"); + li.style.display = (index < maxDisplay) ? "flex" : "none"; + li.dataset.uploadIndex = index; + + const preview = document.createElement("div"); + preview.className = "file-preview"; + displayFilePreview(file, preview); + + const nameDiv = document.createElement("div"); + nameDiv.classList.add("upload-file-name"); + nameDiv.textContent = file.name; + + const progDiv = document.createElement("div"); + progDiv.classList.add("progress", "upload-progress-div"); + progDiv.style.flex = "0 0 250px"; + progDiv.style.marginLeft = "5px"; + + const progBar = document.createElement("div"); + progBar.classList.add("progress-bar"); + progBar.style.width = "0%"; + progBar.innerText = "0%"; + + progDiv.appendChild(progBar); + li.appendChild(preview); + li.appendChild(nameDiv); + li.appendChild(progDiv); + li.progressBar = progBar; + li.startTime = Date.now(); + list.appendChild(li); + }); + if (allFiles.length > maxDisplay) { + const extra = document.createElement("li"); + extra.classList.add("upload-progress-extra"); + extra.textContent = `Uploading additional ${allFiles.length - maxDisplay} file(s)...`; + extra.style.display = "flex"; + list.appendChild(extra); + } } progressContainer.appendChild(list); } @@ -152,9 +232,19 @@ export function initUpload() { return; } const allFiles = Array.from(files); + // Make sure each file has an uploadIndex (if not already assigned). + allFiles.forEach((file, index) => { + if (typeof file.uploadIndex === "undefined") file.uploadIndex = index; + }); const maxDisplay = 10; const folderToUse = window.currentFolder || "root"; - const listItems = progressContainer.querySelectorAll("li"); + // Build a mapping of uploadIndex => progress element. + const progressElements = {}; + // Query all file list items (they have the class "upload-progress-item") + const listItems = progressContainer.querySelectorAll("li.upload-progress-item"); + listItems.forEach(item => { + progressElements[item.dataset.uploadIndex] = item; + }); let finishedCount = 0; let allSucceeded = true; const uploadResults = new Array(allFiles.length).fill(false); @@ -163,6 +253,10 @@ export function initUpload() { const formData = new FormData(); formData.append("file[]", file); formData.append("folder", folderToUse); + // If a relative path is available, send it. + if (file.webkitRelativePath && file.webkitRelativePath !== "") { + formData.append("relativePath", file.webkitRelativePath); + } const xhr = new XMLHttpRequest(); let currentPercent = 0; @@ -170,8 +264,9 @@ export function initUpload() { xhr.upload.addEventListener("progress", function (e) { if (e.lengthComputable) { currentPercent = Math.round((e.loaded / e.total) * 100); - if (index < maxDisplay && listItems[index]) { - const elapsed = (Date.now() - listItems[index].startTime) / 1000; + const li = progressElements[file.uploadIndex]; + if (li) { + const elapsed = (Date.now() - li.startTime) / 1000; let speed = ""; if (elapsed > 0) { const spd = e.loaded / elapsed; @@ -179,8 +274,8 @@ export function initUpload() { else if (spd < 1048576) speed = (spd / 1024).toFixed(1) + " KB/s"; else speed = (spd / 1048576).toFixed(1) + " MB/s"; } - listItems[index].progressBar.style.width = currentPercent + "%"; - listItems[index].progressBar.innerText = currentPercent + "% (" + speed + ")"; + li.progressBar.style.width = currentPercent + "%"; + li.progressBar.innerText = currentPercent + "% (" + speed + ")"; } } }); @@ -192,15 +287,16 @@ export function initUpload() { } catch (e) { jsonResponse = null; } + const li = progressElements[file.uploadIndex]; if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) { - if (index < maxDisplay && listItems[index]) { - listItems[index].progressBar.style.width = "100%"; - listItems[index].progressBar.innerText = "Done"; + if (li) { + li.progressBar.style.width = "100%"; + li.progressBar.innerText = "Done"; } - uploadResults[index] = true; + uploadResults[file.uploadIndex] = true; } else { - if (index < maxDisplay && listItems[index]) { - listItems[index].progressBar.innerText = "Error"; + if (li) { + li.progressBar.innerText = "Error"; } allSucceeded = false; } @@ -212,10 +308,11 @@ export function initUpload() { }); xhr.addEventListener("error", function () { - if (index < maxDisplay && listItems[index]) { - listItems[index].progressBar.innerText = "Error"; + const li = progressElements[file.uploadIndex]; + if (li) { + li.progressBar.innerText = "Error"; } - uploadResults[index] = false; + uploadResults[file.uploadIndex] = false; allSucceeded = false; finishedCount++; console.error("Error uploading file:", file.name); @@ -225,10 +322,11 @@ export function initUpload() { }); xhr.addEventListener("abort", function () { - if (index < maxDisplay && listItems[index]) { - listItems[index].progressBar.innerText = "Aborted"; + const li = progressElements[file.uploadIndex]; + if (li) { + li.progressBar.innerText = "Aborted"; } - uploadResults[index] = false; + uploadResults[file.uploadIndex] = false; allSucceeded = false; finishedCount++; console.error("Upload aborted for file:", file.name); @@ -247,12 +345,17 @@ export function initUpload() { initFileActions(); serverFiles = (serverFiles || []).map(item => item.name.trim().toLowerCase()); allFiles.forEach((file, index) => { - const fileName = file.name.trim().toLowerCase(); - if (index < maxDisplay && listItems[index]) { - if (!uploadResults[index] || !serverFiles.includes(fileName)) { - listItems[index].progressBar.innerText = "Error"; - allSucceeded = false; + // Skip verification for folder-uploaded files. + if (file.webkitRelativePath && file.webkitRelativePath.trim() !== "") { + return; + } + const clientFileName = file.name.trim().toLowerCase(); + if (!uploadResults[file.uploadIndex] || !serverFiles.includes(clientFileName)) { + const li = progressElements[file.uploadIndex]; + if (li) { + li.progressBar.innerText = "Error"; } + allSucceeded = false; } }); setTimeout(() => { @@ -267,6 +370,9 @@ export function initUpload() { .catch(error => { console.error("Error fetching file list:", error); showToast("Some files may have failed to upload. Please check the list."); + }) + .finally(() => { + loadFolderTree(window.currentFolder); }); } }); diff --git a/upload.php b/upload.php index a5654b2..df06e15 100644 --- a/upload.php +++ b/upload.php @@ -49,10 +49,45 @@ foreach ($_FILES["file"]["name"] as $index => $fileName) { exit; } + // --- Minimal Folder/Subfolder Logic --- + // Check if a relativePath was provided (from a folder upload) + $relativePath = ''; + if (isset($_POST['relativePath'])) { + // In case of multiple files, relativePath may be an array. + if (is_array($_POST['relativePath'])) { + $relativePath = $_POST['relativePath'][$index] ?? ''; + } else { + $relativePath = $_POST['relativePath']; + } + } + if (!empty($relativePath)) { + // Extract the directory part from the relative path. + $subDir = dirname($relativePath); + if ($subDir !== '.' && $subDir !== '') { + // If uploading to root, don't add the "root" folder in the path. + if ($folder === 'root') { + $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $subDir . DIRECTORY_SEPARATOR; + } else { + $uploadDir = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . $subDir . DIRECTORY_SEPARATOR; + } + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0775, true); + } + // Use the basename from the relative path. + $safeFileName = basename($relativePath); + } + } + // --- End Minimal Folder/Subfolder Logic --- + $targetPath = $uploadDir . $safeFileName; + if (move_uploaded_file($_FILES["file"]["tmp_name"][$index], $targetPath)) { - // Build the metadata key, including the folder if not in root. - $metaKey = ($folder !== 'root') ? $folder . "/" . $safeFileName : $safeFileName; + // Build the metadata key. + if (!empty($relativePath)) { + $metaKey = ($folder !== 'root') ? $folder . "/" . $relativePath : $relativePath; + } else { + $metaKey = ($folder !== 'root') ? $folder . "/" . $safeFileName : $safeFileName; + } if (!isset($metadata[$metaKey])) { $uploadedDate = date(DATE_TIME_FORMAT); $uploader = $_SESSION['username'] ?? "Unknown";