230 lines
7.7 KiB
JavaScript
230 lines
7.7 KiB
JavaScript
// 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;
|
|
});
|