| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- /*
- * Gallery archive builder — the admin's "Rebuild now" button.
- *
- * The archive normally rebuilds itself in the background, but the photographer
- * often wants it finished before sending the link out. Building a multi-gigabyte
- * ZIP cannot happen in one request on a 60 s execution limit, so this loops:
- * POST step, get back {done, total, finished}, repeat until finished.
- *
- * Every piece of state lives on the server, so this loop is disposable — closing
- * the tab pauses the build, "Resume" picks it up at the photo it stopped on, and
- * the background worker would eventually finish it regardless.
- */
- (function () {
- 'use strict';
- var card = document.getElementById('archive-card');
- if (!card) return;
- var api = card.dataset.api;
- var slug = card.dataset.slug;
- var csrf = card.dataset.csrf;
- var buildBtn = document.getElementById('archive-build');
- var cancelBtn = document.getElementById('archive-cancel');
- var bar = document.getElementById('archive-bar');
- var fill = bar.querySelector('span');
- var status = document.getElementById('archive-status');
- var running = false;
- var stopped = false;
- var startedAt = 0;
- var startedFrom = 0;
- function post(action) {
- var body = new FormData();
- body.append('slug', slug);
- body.append('action', action);
- return fetch(api, {
- method: 'POST',
- headers: { 'X-CSRF-Token': csrf },
- body: body
- }).then(function (response) {
- return response.json().catch(function () {
- return { error: 'Server returned a non-JSON response (HTTP ' + response.status + ')' };
- });
- });
- }
- /* Rough finish time from the rate this run has actually achieved. */
- function eta(done, total) {
- var elapsed = (Date.now() - startedAt) / 1000;
- var processed = done - startedFrom;
- if (processed < 1 || elapsed < 1) return '';
- var remaining = Math.round((total - done) * (elapsed / processed));
- if (remaining < 60) return ' · about a minute left';
- return ' · about ' + Math.round(remaining / 60) + ' min left';
- }
- function show(done, total, suffix) {
- bar.hidden = false;
- fill.style.width = (total ? Math.round((done / total) * 100) : 0) + '%';
- status.textContent = done + ' of ' + total + ' photos' + (suffix || '');
- }
- function fail(message, done, total) {
- running = false;
- bar.hidden = total > 0 ? false : true;
- status.innerHTML = '';
- status.appendChild(document.createTextNode(message + ' '));
- var resume = document.createElement('button');
- resume.className = 'btn-ghost';
- resume.style.margin = '0';
- resume.textContent = 'Resume';
- resume.addEventListener('click', function () { run('step'); });
- status.appendChild(resume);
- buildBtn.disabled = false;
- cancelBtn.hidden = true;
- }
- function run(action) {
- running = true;
- stopped = false;
- buildBtn.disabled = true;
- cancelBtn.hidden = false;
- startedAt = Date.now();
- startedFrom = -1;
- var retried = false;
- function step(which) {
- post(which).then(function (result) {
- if (stopped) return;
- if (result.error && !result.total) {
- // A transient S3 hiccup is worth one silent retry; anything
- // else needs the photographer to see it.
- if (!retried) {
- retried = true;
- step('step');
- return;
- }
- fail(result.error, 0, 0);
- return;
- }
- retried = false;
- if (startedFrom < 0) startedFrom = result.done;
- if (result.finished) {
- running = false;
- show(result.total, result.total, ' · done');
- // Reload so the status line, size and share-side button all
- // reflect the archive that now exists.
- window.location.reload();
- return;
- }
- if (result.error) {
- show(result.done, result.total, '');
- fail(result.error, result.done, result.total);
- return;
- }
- show(result.done, result.total, eta(result.done, result.total));
- step('step');
- }).catch(function (err) {
- if (!stopped) fail('Network error: ' + err.message, 0, 0);
- });
- }
- step(action);
- }
- buildBtn.addEventListener('click', function () {
- if (running) return;
- // "start" discards any half-finished attempt and opens a fresh upload.
- run(buildBtn.dataset.resume ? 'step' : 'start');
- });
- cancelBtn.addEventListener('click', function () {
- stopped = true;
- running = false;
- post('cancel').then(function () { window.location.reload(); });
- });
- })();
|