From 252757eb2183cb62e5c6ecf3dabf2d7e8405f98b Mon Sep 17 00:00:00 2001 From: Thayol Date: Sun, 13 Sep 2026 09:59:37 +0200 Subject: [PATCH] Remove JS magic for async upload reply --- internal/server/server_test.go | 9 +++ web/static/app.js | 117 ++++----------------------------- web/static/style.css | 3 + 3 files changed, 26 insertions(+), 103 deletions(-) diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 24af11b..f4916e3 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -538,6 +538,15 @@ func TestFormPostRendersHTML(t *testing.T) { if !strings.Contains(string(page), "Delete token") { t.Error("the success page does not show the delete token") } + // The script asks for this same page and writes it into the document it is + // running in, so a fragment would leave the browser with a half a page. + if !strings.HasPrefix(string(page), "") || + !strings.Contains(string(page), "") { + t.Error("the success page is not a whole document") + } + if !strings.Contains(string(page), "static/app.js") { + t.Error("the success page does not load the script, so its copy buttons stay dead") + } } func TestBasePathMounting(t *testing.T) { diff --git a/web/static/app.js b/web/static/app.js index 2591673..8c925a9 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -3,13 +3,11 @@ (function () { 'use strict'; - var base = document.body.dataset.base || '/'; - // Copy buttons appear on the server-rendered result page too, which has no // upload form, so they are wired before anything form-specific bails out. // They ship hidden so that without this script there is no dead button. - function wireCopy(root) { - var buttons = root.querySelectorAll('button.copy[data-copy]'); + function wireCopy() { + var buttons = document.querySelectorAll('button.copy[data-copy]'); Array.prototype.forEach.call(buttons, function (button) { var target = document.getElementById(button.dataset.copy); if (!target) return; @@ -26,7 +24,7 @@ }); }); } - wireCopy(document); + wireCopy(); // Destructive buttons ask first. The attribute carries the question, so the // markup decides what is worth confirming and this stays generic. @@ -53,7 +51,6 @@ var maxSize = parseInt(form.dataset.maxSize, 10) || 0; function formatSize(n) { - if (n === null || n === undefined) return 'unlimited'; var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'], i = 0; while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } return (i === 0 ? n : n.toFixed(2).replace(/\.00$/, '')) + ' ' + units[i]; @@ -116,7 +113,10 @@ var xhr = new XMLHttpRequest(); xhr.open('POST', form.action); - xhr.setRequestHeader('Accept', 'application/json'); + // Ask for the page a plain form post would have been given. What a + // finished upload looks like is the server's business, and it already + // renders it; this script only adds the progress bar on the way there. + xhr.setRequestHeader('Accept', 'text/html'); xhr.upload.onprogress = function (ev) { if (!ev.lengthComputable) return; @@ -132,15 +132,13 @@ }; xhr.onload = function () { - submit.disabled = false; - var body; - try { body = JSON.parse(xhr.responseText); } catch (err) { body = null; } - if (xhr.status >= 200 && xhr.status < 300 && body) { - showResult(body); - } else { - progress.hidden = true; - showError(body && body.error ? body.error : 'Upload failed (' + xhr.status + ').'); - } + // Whatever came back, the result page or an error page, is the page the + // browser should be showing now. Writing it into this document rather + // than navigating leaves the address on the form's own URL, so a reload + // offers a fresh form instead of posting the file a second time. + document.open(); + document.write(xhr.responseText); + document.close(); }; xhr.onerror = function () { submit.disabled = false; @@ -158,91 +156,4 @@ if (m < 60) return m + 'm ' + (s % 60) + 's'; return Math.floor(m / 60) + 'h ' + (m % 60) + 'm'; } - - // Mirrors web/templates/result.html, so the JavaScript and no-JavaScript - // paths show the same thing. - function showResult(r) { - var card = el('section', 'card result'); - card.appendChild(el('h1', null, 'Uploaded')); - card.appendChild(el('p', 'filename', r.filename)); - - var links = el('div', 'links'); - links.appendChild(linkField( - 'Share link', 'a page showing size, expiry and a download button', - 'link-info', r.info_url)); - links.appendChild(linkField( - 'Direct download', 'the file itself, for curl, wget or a browser', - 'link-file', r.url)); - card.appendChild(links); - - var dl = document.createElement('dl'); - addRow(dl, 'Size', formatSize(r.size)); - addRow(dl, 'Expires', r.expires || 'never'); - addRow(dl, 'SHA-256', r.sha256, 'mono wrap'); - card.appendChild(dl); - - var warn = el('div', 'warn'); - warn.appendChild(el('h2', null, 'Delete token')); - warn.appendChild(el('p', null, - 'Shown once. Keep it alongside the share link: pasting it under ' + - '"Remove this file" on that page deletes the file before it expires.')); - warn.appendChild(el('p', 'mono wrap', r.delete_token)); - card.appendChild(warn); - - var actions = el('p', 'actions'); - var again = document.createElement('a'); - again.className = 'button'; - again.href = base; - again.textContent = 'Upload another file'; - actions.appendChild(again); - card.appendChild(actions); - - form.parentNode.insertBefore(card, form); - form.hidden = true; - wireCopy(card); - card.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - - function el(tag, cls, text) { - var node = document.createElement(tag); - if (cls) node.className = cls; - if (text !== undefined && text !== null) node.textContent = text; - return node; - } - - // One labelled, read-only URL with a copy button beside it. - function linkField(label, hint, id, value) { - var field = el('div', 'field'); - var span = document.createElement('span'); - span.appendChild(document.createTextNode(label + ' ')); - span.appendChild(el('em', null, hint)); - field.appendChild(span); - - var row = el('div', 'copyrow'); - var input = document.createElement('input'); - input.type = 'text'; - input.id = id; - input.readOnly = true; - input.value = value; - row.appendChild(input); - - var button = el('button', 'copy', 'Copy'); - button.type = 'button'; - button.hidden = true; - button.dataset.copy = id; - row.appendChild(button); - - field.appendChild(row); - return field; - } - - function addRow(dl, label, value, cls) { - var dt = document.createElement('dt'); - dt.textContent = label; - var dd = document.createElement('dd'); - dd.textContent = value; - if (cls) dd.className = cls; - dl.appendChild(dt); - dl.appendChild(dd); - } })(); diff --git a/web/static/style.css b/web/static/style.css index 28890d0..fae7f5b 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -84,6 +84,9 @@ input[type=text], input[type=password] { #upload { display: flex; flex-direction: column; } #upload > .drop { order: -1; } #upload > button { align-self: flex-start; } +/* An author `display` beats the browser's own [hidden] rule, so anything given + a display here has to say what hidden means for it. */ +#upload[hidden] { display: none; } .drop { border: 2px dashed var(--line);