// This file is part of photo21 // Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay // SPDX-License-Identifier: GPL-3.0-or-later // When user drags files, register them in the file field const dropZone = document.getElementById('drop_zone'); const uploadInput = document.getElementById('id_file_field'); const form = document.getElementById('upload_form'); dropZone.ondrop = function (e) { e.preventDefault(); this.className = 'upload-drop-zone'; uploadInput.files = e.dataTransfer.files; }; dropZone.ondragover = function () { this.className = 'upload-drop-zone drop'; return false; }; dropZone.ondragleave = function () { this.className = 'upload-drop-zone'; return false; }; // When user selects an existing gallery, disable new gallery fields const gallerySelect = document.getElementById('id_gallery'); gallerySelectUpdate = () => { const useGallery = (gallerySelect.value !== ""); document.getElementById('id_new_gallery_title').disabled = useGallery; document.getElementById('id_new_gallery_date_start').disabled = useGallery; document.getElementById('id_new_gallery_date_end').disabled = useGallery; document.getElementById('id_new_gallery_tags').disabled = useGallery; document.getElementById('id_new_gallery_description').disabled = useGallery; }; gallerySelect.addEventListener('change', gallerySelectUpdate); gallerySelectUpdate(); // Hide basic submit button const submitbtn = document.getElementById('submit-id-submit'); submitbtn.style.display = 'none'; // Create upload + pause buttons in a flex row const btnWrap = document.createElement('div'); btnWrap.className = 'd-flex align-items-center gap-2 mt-2'; const ctnbtn = document.createElement('button'); ctnbtn.className = submitbtn.className.replace(/\bmt-\d\b/g, '').trim(); ctnbtn.type = 'button'; ctnbtn.textContent = gettext('Upload'); const pausebtn = document.createElement('button'); pausebtn.className = 'btn btn-warning'; pausebtn.type = 'button'; pausebtn.textContent = gettext('Pause'); pausebtn.style.display = 'none'; btnWrap.appendChild(ctnbtn); btnWrap.appendChild(pausebtn); form.appendChild(btnWrap); // Create progress bar const progressWrap = document.createElement('div'); progressWrap.className = 'progress mt-3'; progressWrap.style.display = 'none'; const progressBar = document.createElement('div'); progressBar.className = 'progress-bar progress-bar-striped progress-bar-animated'; progressBar.setAttribute('role', 'progressbar'); progressBar.style.width = '0%'; progressWrap.appendChild(progressBar); // Concurrency label const concurrencyLabel = document.createElement('small'); concurrencyLabel.className = 'text-muted mt-1'; concurrencyLabel.style.display = 'none'; form.appendChild(progressWrap); form.appendChild(concurrencyLabel); let pause = false; let uploading = false; function beforeUnloadHandler(e) { e.preventDefault(); } window.addEventListener('beforeunload', (e) => { if (uploading) beforeUnloadHandler(e); }); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } pausebtn.addEventListener('click', () => { pause = !pause; pausebtn.textContent = pause ? gettext('Resume') : gettext('Pause'); pausebtn.className = pause ? 'btn btn-success' : 'btn btn-warning'; }); async function uploadFile(file, galleryID, csrfvalue) { const sendform = new FormData(); sendform.append('csrfmiddlewaretoken', csrfvalue); sendform.append('file_field', file); sendform.append('gallery', galleryID); const start = performance.now(); const res = await fetch('/upload/', { method: 'POST', body: sendform, headers: { 'Accept': 'application/json' } }); if (!res.ok) { let detail = ''; try { const j = await res.json(); detail = j.error || ''; } catch (_) {} console.error('Error with ' + file.name + ' (HTTP ' + res.status + ')' + (detail ? ': ' + JSON.stringify(detail) : '')); } return performance.now() - start; } ctnbtn.addEventListener('click', async () => { const files = Array.from(uploadInput.files); if (!files.length) { window.alert(gettext('Please select files first.')); return; } uploading = true; ctnbtn.disabled = true; const csrfvalue = document.getElementsByName('csrfmiddlewaretoken')[0].value; const total = files.length; let completed = 0; // Create gallery / validate form const fdata = new FormData(uploadInput.form); fdata.delete('file_field'); const response = await fetch('/upload/', { method: 'POST', body: fdata, headers: { 'Accept': 'application/json' } }); const returned = await response.json(); if (returned.code != 200) { const msgs = Object.entries(returned.error).map(([f, e]) => f + ': ' + e.join(', ')).join('\n'); window.alert(msgs); ctnbtn.disabled = false; return; } const galleryID = returned.galleryID; uploadInput.disabled = true; gallerySelect.disabled = true; dropZone.style.pointerEvents = 'none'; pausebtn.style.display = 'inline-block'; progressWrap.style.display = 'flex'; concurrencyLabel.style.display = 'block'; // Auto-tuning state const MIN_CONCURRENCY = 1; const MAX_CONCURRENCY = 6; let concurrency = 1; let lastRate = null; let lastMeasureTime = performance.now(); let lastCompleted = 0; function updateProgress() { const pct = Math.round(100 * completed / total); progressBar.style.width = pct + '%'; progressBar.textContent = pct + '% (' + completed + '/' + total + ')'; concurrencyLabel.textContent = gettext('Parallel uploads:') + ' ' + concurrency; } function tuneConcurrency() { const now = performance.now(); const elapsed = now - lastMeasureTime; if (elapsed < 1000) return; const rate = (completed - lastCompleted) / elapsed; lastMeasureTime = now; lastCompleted = completed; if (lastRate === null || rate > lastRate * 1.05) concurrency = Math.min(concurrency + 1, MAX_CONCURRENCY); else if (rate < lastRate * 0.90) concurrency = Math.max(concurrency - 1, MIN_CONCURRENCY); lastRate = rate; } // Parallel worker pool const queue = [...files]; let active = 0; await new Promise((resolve) => { function next() { if (completed === total) { resolve(); return; } while (!pause && active < concurrency && queue.length > 0) { const file = queue.shift(); active++; uploadFile(file, galleryID, csrfvalue) .catch(e => console.error(e)) .finally(() => { completed++; tuneConcurrency(); active--; updateProgress(); next(); }); } if (pause && active === 0 && queue.length > 0) setTimeout(next, 200); } next(); }); // Finalize const fend = new FormData(uploadInput.form); fend.delete('file_field'); fend.delete('new_gallery_title'); fend.delete('new_gallery_date_start'); fend.delete('new_gallery_date_end'); fend.append('gallery', galleryID); fend.append('end', 'end'); await fetch('/upload/', { method: 'POST', body: fend, headers: { 'Accept': 'application/json' } }); uploading = false; uploadInput.disabled = false; gallerySelect.disabled = false; dropZone.style.pointerEvents = ''; pausebtn.style.display = 'none'; progressBar.classList.remove('progress-bar-animated'); progressBar.textContent = gettext('Upload complete — please reload the page'); concurrencyLabel.textContent = gettext('Best concurrency found:') + ' ' + concurrency; });