// 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(): void { const dropZone = document.getElementById("file-drop-zone") as HTMLDivElement | null; const filesInput = document.getElementById("upload-files-input") as HTMLInputElement | null; const folderInput = document.getElementById("upload-folder-input") as HTMLInputElement | null; const newFolderButton = document.getElementById("new-folder-button") as HTMLButtonElement | null; const sortSelect = document.getElementById("sort-select") as HTMLSelectElement | null; const status = document.getElementById("upload-status") as HTMLParagraphElement | null; if (!dropZone) { return; } const uploadUrl = dropZone.dataset.uploadUrl || window.location.pathname; function setStatus(msg: string): void { if (status) { status.textContent = msg; } } async function upload(files: File[]): Promise { if (files.length === 0) { return; } const formData = new FormData(); for (const file of files) { const relPath = (file as File & { webkitRelativePath?: string }).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)}`); } }); document.querySelectorAll(".delete-entry-button").forEach((button) => { button.addEventListener("click", async () => { const relPath = button.dataset.path || ""; const name = button.dataset.name || relPath; if (!window.confirm(`Delete "${name}"? This cannot be undone.`)) { return; } setStatus(`Deleting "${name}"…`); try { const resp = await fetch(`/web/files/${relPath}`, { method: "DELETE" }); if (!resp.ok) { const text = await resp.text(); setStatus(`Could not delete "${name}": ${text}`); return; } window.location.reload(); } catch (err) { setStatus(`Could not delete "${name}": ${String(err)}`); } }); }); if (sortSelect) { sortSelect.addEventListener("change", () => { const value = sortSelect.value; let sortBy = "name"; let sortDir = "asc"; if (value === "name_asc") { sortBy = "name"; sortDir = "asc"; } else if (value === "name_desc") { sortBy = "name"; sortDir = "desc"; } window.location.href = `${uploadUrl}?sort_by=${sortBy}&sort_dir=${sortDir}`; }); } function readEntry(entry: FileSystemEntry): Promise { return new Promise((resolve) => { if (entry.isFile) { (entry as FileSystemFileEntry).file((file) => { Object.defineProperty(file, "webkitRelativePath", { value: entry.fullPath.replace(/^\//, ""), }); resolve([file]); }, () => resolve([])); return; } if (entry.isDirectory) { const reader = (entry as FileSystemDirectoryEntry).createReader(); const allEntries: FileSystemEntry[] = []; const readBatch = (): void => { 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: FileSystemEntry[] = []; for (const item of Array.from(items)) { const entry = item.webkitGetAsEntry?.(); if (entry) { entries.push(entry); } } if (entries.length === 0) { void upload(Array.from(e.dataTransfer?.files || [])); return; } void Promise.all(entries.map(readEntry)).then((groups) => upload(groups.flat())); }); })();