"use strict"; // File browser page behavior: uploading via the "Upload files"/"Upload // folder" buttons and via drag & drop onto the listing, both re-using the // same upload() call. Folder uploads (button or drop) recreate their // directory structure server-side by encoding the relative path in each // uploaded file's name (see FormData.append's third argument below). (function initFileBrowser() { const dropZone = document.getElementById("file-drop-zone"); const filesInput = document.getElementById("upload-files-input"); const folderInput = document.getElementById("upload-folder-input"); const newFolderButton = document.getElementById("new-folder-button"); const status = document.getElementById("upload-status"); if (!dropZone) { return; } const uploadUrl = dropZone.dataset.uploadUrl || window.location.pathname; function setStatus(msg) { if (status) { status.textContent = msg; } } async function upload(files) { if (files.length === 0) { return; } const formData = new FormData(); for (const file of files) { // A folder-selected/dropped file's relative path (e.g. // "photos/2024/img.jpg") is sent as a parallel "paths" field // (same order as "files") since the server strips any // directory component from the file's own filename per the // multipart spec — see internal/web/files.go. const relPath = file.webkitRelativePath || file.name; formData.append("files", file, file.name); formData.append("paths", relPath); } setStatus(`Uploading ${files.length} file(s)…`); try { const resp = await fetch(uploadUrl, { method: "POST", body: formData }); if (!resp.ok) { const text = await resp.text(); setStatus(`Upload failed: ${text}`); return; } window.location.reload(); } catch (err) { setStatus(`Upload failed: ${String(err)}`); } } filesInput?.addEventListener("change", () => { void upload(Array.from(filesInput.files || [])); filesInput.value = ""; }); folderInput?.addEventListener("change", () => { void upload(Array.from(folderInput.files || [])); folderInput.value = ""; }); const folderNameRe = /^[a-zA-Z0-9_-]{1,64}$/; newFolderButton?.addEventListener("click", async () => { const name = window.prompt("New folder name:"); if (!name) { return; } if (!folderNameRe.test(name)) { setStatus("Folder name must be 1-64 letters, digits, '-' or '_'."); return; } setStatus(`Creating folder "${name}"…`); try { const resp = await fetch(`${uploadUrl}?mkdir=1`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ name }), }); if (!resp.ok) { const text = await resp.text(); setStatus(`Could not create folder: ${text}`); return; } window.location.reload(); } catch (err) { setStatus(`Could not create folder: ${String(err)}`); } }); // Recursively walk a dropped DataTransferItem (file or directory) into // a flat list of File objects, using the browser's non-standard but // widely-supported webkitGetAsEntry/FileSystemEntry APIs to support // dragging & dropping whole folders. function readEntry(entry) { return new Promise((resolve) => { if (entry.isFile) { entry.file((file) => { Object.defineProperty(file, "webkitRelativePath", { value: entry.fullPath.replace(/^\//, ""), }); resolve([file]); }, () => resolve([])); return; } if (entry.isDirectory) { const reader = entry.createReader(); const allEntries = []; const readBatch = () => { reader.readEntries(async (batch) => { if (batch.length === 0) { const nested = await Promise.all(allEntries.map(readEntry)); resolve(nested.flat()); return; } allEntries.push(...batch); readBatch(); }, () => resolve([])); }; readBatch(); return; } resolve([]); }); } dropZone.addEventListener("dragover", (e) => { e.preventDefault(); dropZone.classList.add("border-indigo-400", "bg-indigo-50"); }); dropZone.addEventListener("dragleave", () => { dropZone.classList.remove("border-indigo-400", "bg-indigo-50"); }); dropZone.addEventListener("drop", (e) => { e.preventDefault(); dropZone.classList.remove("border-indigo-400", "bg-indigo-50"); const items = e.dataTransfer?.items; if (!items) { return; } const entries = []; for (const item of Array.from(items)) { const entry = item.webkitGetAsEntry?.(); if (entry) { entries.push(entry); } } if (entries.length === 0) { // Fallback for browsers without webkitGetAsEntry support: flat // files only, no folder traversal. void upload(Array.from(e.dataTransfer?.files || [])); return; } void Promise.all(entries.map(readEntry)).then((groups) => upload(groups.flat())); }); })();