async function createBox() { const response = await fetch("/box", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ retention_key: el.expiry?.value || defaultRetention, password: el.password?.value || "", allow_zip: isOneTimeDownloadSelected() || !el.allowZip || el.allowZip.checked, files: files.map((item) => ({ name: item.displayName, size: item.file.size })), }), }); const result = await readJSON(response); if (!response.ok) throw new Error(result.error || "Could not create upload box"); return result; } async function readJSON(response) { try { return await response.json(); } catch (_) { return {}; } } async function markFileStatus(item, status) { if (!item.boxID || !item.boxFile) return; try { await fetch(`/box/${item.boxID}/files/${item.boxFile.id}/status`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status }), }); } catch (_) { // Best effort only. The upload endpoint also marks hard failures. } } function setFileFailed(item, message) { item.failed = true; item.uploaded = false; item.error = message || "Failed to upload"; item.loaded = item.file.size; item.row?.classList.remove("is-working", "is-uploaded"); item.row?.classList.add("is-failed"); if (item.row) item.row.title = item.error; setRowProgress(item, 100); updateOverallProgress(); } function markCompletedImpact(item) { const key = item.boxFile?.id || item.displayName; if (completedImpactKeys.has(key)) return; completedImpactKeys.add(key); flashProgressBar(item.row?.querySelector(".upload-progress-bar")); } function uploadFile(item, onComplete) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append("file", item.file, item.displayName); xhr.open("POST", item.boxFile.upload_path); xhr.upload.addEventListener("loadstart", () => { item.loaded = 0; item.failed = false; item.uploaded = false; item.row?.classList.remove("is-failed", "is-uploaded"); item.row?.classList.add("is-working"); setRowProgress(item, 2); updateOverallProgress(); }); xhr.upload.addEventListener("progress", (event) => { if (!event.lengthComputable) return; item.loaded = Math.min(event.loaded, item.file.size); const percent = (event.loaded / event.total) * 100; setRowProgress(item, percent >= 100 ? 99 : percent); updateOverallProgress(); }); xhr.addEventListener("load", async () => { if (xhr.status < 200 || xhr.status >= 300) { let message = "Upload failed"; try { message = JSON.parse(xhr.responseText).error || message; } catch (_) {} setFileFailed(item, message); await markFileStatus(item, "failed"); reject(new Error(message)); return; } item.uploaded = true; item.failed = false; item.loaded = item.file.size; item.row?.classList.remove("is-working", "is-failed"); item.row?.classList.add("is-uploaded"); if (item.row) item.row.title = "Uploaded"; setRowProgress(item, 100); markCompletedImpact(item); try { const result = JSON.parse(xhr.responseText); if (result.file) { item.boxFile = result.file; const icon = item.row?.querySelector(".upload-file-icon"); if (icon && result.file.thumbnail_path) { item.row.classList.add("has-thumbnail"); icon.src = result.file.thumbnail_path; } else if (icon && result.file.icon_path && !item.previewURL) { icon.src = result.file.icon_path; } } } catch (_) {} updateOverallProgress(); onComplete(); resolve(); }); xhr.addEventListener("error", async () => { setFileFailed(item, "Network error while uploading"); await markFileStatus(item, "failed"); reject(new Error("Network error while uploading")); }); xhr.addEventListener("abort", async () => { setFileFailed(item, "Upload cancelled"); await markFileStatus(item, "failed"); reject(new Error("Upload cancelled")); }); markFileStatus(item, "uploading"); xhr.send(formData); }); }