const zone = document.getElementById('drop-zone'); const input = document.getElementById('fileInput'); const uploadBtn = document.getElementById('uploadBtn'); const cancelBtn = document.getElementById('cancelBtn'); const progressText = document.getElementById("progress-text"); const speedText = document.getElementById("speed-text"); const etaText = document.getElementById("eta-text"); const progressBar = document.getElementById("progress-bar"); const progressContainer = document.getElementById("progress-container"); let currentXhr = null; const CHUNK_THRESHOLD = 1024 * 1024 * 1024; const CHUNK_SIZE = 10 * 1024 * 1024; const MAX_PARALLEL_UPLOADS = 4; const MAX_RETRIES = 3; function formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; } function formatTime(seconds) { if (!isFinite(seconds) || seconds < 0) return "--:--"; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); return [ h > 0 ? h : null, (h > 0 ? m.toString().padStart(2, '0') : m), s.toString().padStart(2, '0') ].filter(Boolean).join(':'); } function startUI() { uploadBtn.disabled = true; uploadBtn.innerText = "UPLOADING..."; cancelBtn.classList.remove('hidden'); progressContainer.classList.remove("hidden"); progressText.classList.remove("hidden"); document.getElementById("stats-text").classList.remove("hidden"); } function updateProgress(loaded, total, startTime) { const percent = Math.round((loaded / total) * 100); progressBar.style.width = percent + "%"; progressText.innerText = percent + "%"; const elapsed = (Date.now() - startTime) / 1000; if (elapsed <= 0) return; const speed = loaded / elapsed; const remaining = total - loaded; speedText.innerText = formatBytes(speed) + "/S"; etaText.innerText = formatTime(remaining / speed); } function redirect(data) { const key = data.view_key; if (!key) { alert("Invalid server response"); return; } window.location.href = "/f/" + key; } zone.onclick = () => input.click(); zone.ondragover = e => { e.preventDefault(); zone.classList.add('active'); }; zone.ondragleave = () => zone.classList.remove('active'); zone.ondrop = e => { e.preventDefault(); zone.classList.remove('active'); if (e.dataTransfer.files.length) { input.files = e.dataTransfer.files; input.dispatchEvent(new Event('change')); } }; input.onchange = () => { const files = Array.from(input.files || []); if (!files.length) { uploadBtn.disabled = true; return; } const total = files.reduce((a, f) => a + f.size, 0); document.getElementById('dz-text').innerText = files.length === 1 ? `${files[0].name} [${formatBytes(files[0].size)}]` : `${files.length} FILES [${formatBytes(total)}]`; uploadBtn.disabled = false; }; uploadBtn.onclick = () => { const files = input.files; if (!files.length) return; if (files.length === 1 && files[0].size > CHUNK_THRESHOLD) { uploadChunked(files[0]); } else if (files.length === 1) { uploadSingle(files[0]); } else { uploadMulti(files); } }; cancelBtn.onclick = e => { e.stopPropagation(); if (currentXhr) currentXhr.abort(); localStorage.clear(); location.reload(); }; function commonFormData() { const fd = new FormData(); fd.append("once", document.getElementById("once").checked ? "true" : "false"); fd.append("duration", parseInt(document.getElementById("duration").value, 10)); return fd; } function uploadSingle(file) { startUI(); const fd = commonFormData(); fd.append("file", file); const xhr = new XMLHttpRequest(); currentXhr = xhr; const startTime = Date.now(); xhr.upload.onprogress = e => { if (e.lengthComputable) updateProgress(e.loaded, file.size, startTime); }; xhr.onload = () => { try { if (xhr.status < 200 || xhr.status >= 300) throw new Error(); redirect(JSON.parse(xhr.responseText)); } catch { alert("Server error"); } }; xhr.onerror = () => { if (xhr.statusText !== "abort") { alert("Upload failed"); location.reload(); } }; xhr.open("POST", "/api/files/upload"); xhr.send(fd); } function uploadMulti(files) { startUI(); const fd = commonFormData(); const list = Array.from(files); list.forEach(f => fd.append("files", f)); const total = list.reduce((a, f) => a + f.size, 0); const xhr = new XMLHttpRequest(); currentXhr = xhr; const startTime = Date.now(); xhr.upload.onprogress = e => { if (e.lengthComputable) updateProgress(e.loaded, total, startTime); }; xhr.onload = () => { try { if (xhr.status < 200 || xhr.status >= 300) throw new Error(); redirect(JSON.parse(xhr.responseText)); } catch { alert("Server error"); } }; xhr.onerror = () => { if (xhr.statusText !== "abort") { alert("Upload failed"); location.reload(); } }; xhr.open("POST", "/api/files/upload-multi"); xhr.send(fd); } async function uploadChunked(file) { startUI(); const totalChunks = Math.ceil(file.size / CHUNK_SIZE); const initRes = await fetch("/api/files/upload/init", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ filename: file.name, totalChunks, size: file.size }) }); const { fileId } = await initRes.json(); let uploadedBytes = 0; const startTime = Date.now(); const chunks = Array.from({ length: totalChunks }, (_, i) => ({ index: i, start: i * CHUNK_SIZE, end: Math.min((i + 1) * CHUNK_SIZE, file.size), retries: 0, uploading: false, done: false })); let active = 0; let completed = 0; function uploadChunk(chunk) { return new Promise((res, rej) => { const blob = file.slice(chunk.start, chunk.end); const fd = new FormData(); fd.append("chunk", blob); const xhr = new XMLHttpRequest(); currentXhr = xhr; let last = 0; xhr.upload.onprogress = e => { if (!e.lengthComputable) return; const delta = e.loaded - last; last = e.loaded; uploadedBytes += delta; updateProgress(uploadedBytes, file.size, startTime); }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { chunk.done = true; completed++; res(); } else { rej(); } }; xhr.onerror = rej; xhr.open("POST", "/api/files/upload/chunk"); xhr.setRequestHeader("fileId", fileId); xhr.setRequestHeader("chunkIndex", chunk.index); xhr.send(fd); }); } return new Promise((resolve, reject) => { function next() { if (completed === totalChunks) return finish(); while (active < MAX_PARALLEL_UPLOADS) { const chunk = chunks.find(c => !c.done && !c.uploading); if (!chunk) break; chunk.uploading = true; active++; uploadChunk(chunk) .then(() => { active--; next(); }) .catch(() => { active--; if (chunk.retries++ < MAX_RETRIES) { chunk.uploading = false; } else { reject(); } next(); }); } } async function finish() { try { const res = await fetch("/api/files/upload/complete", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ fileId, filename: file.name, totalChunks }) }); redirect(await res.json()); resolve(); } catch { reject(); } } next(); }); } function copy(id) { const el = document.getElementById(id); el.select(); document.execCommand('copy'); }