Files
nidus/web/ts/files.ts
T

187 lines
5.6 KiB
TypeScript

// 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<void> {
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<HTMLButtonElement>(".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<File[]> {
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()));
});
})();