fix(backup): ссылка на архив переживает F5 и повторное скачивание

HEAD-проба в downloadBackup() сжигала одноразовый тикет: Express 4
прогоняет HEAD через GET-хендлер /api/backup/:token, который удалял
тикет и файл до отдачи архива, поэтому настоящий GET всегда получал
404 «Ссылка на бэкап устарела».

- server.js: GET /api/backup/:token больше не удаляет тикет и файл —
  ссылка живёт BACKUP_TTL_MS (30 мин), Range-докачка работает
- вынес dropBackupTicket(), fs.rmSync обёрнут в try/catch
- BACKUP_TICKETS_MAX = 3: лишние тикеты вычищаются по возрасту
- sweepBackupStorage() режет с запасом 5 минут сверх TTL
- settings.js: убрана HEAD-проба, в #backupStatus рендерится реальная
  кликабельная ссылка вместо невидимого синтетического <a>
- AGENTS.md: раздел 8 — новое поведение + предупреждение про HEAD
This commit is contained in:
dev
2026-10-04 09:07:16 +03:00
parent 76d36e5c35
commit 19b2365b4c
3 changed files with 34 additions and 25 deletions
+13 -7
View File
@@ -863,11 +863,19 @@ async function downloadBackup() {
showError('Ошибка формирования бэкапа: ' + (data.error || res.status));
return;
}
const head = await fetch(data.url, { method: 'HEAD' }).catch(() => null);
if (head && !head.ok) {
if (st) { st.classList.add('err'); st.textContent = 'Ссылка на архив устарела, попробуйте ещё раз'; }
showError('Ошибка скачивания бэкапа');
return;
const size = formatBackupSize(data.size);
if (st) {
st.classList.remove('err');
st.innerHTML = '';
const link = document.createElement('a');
link.href = data.url;
link.download = data.filename || 'whatido-backup.tar.gz';
link.textContent = data.filename || 'whatido-backup.tar.gz';
link.style.color = 'var(--accent)';
st.appendChild(link);
const note = document.createElement('span');
note.textContent = `${size ? ` (${size})` : ''} — ссылка активна 30 минут, скачивание можно повторить`;
st.appendChild(note);
}
const a = document.createElement('a');
a.href = data.url;
@@ -875,8 +883,6 @@ async function downloadBackup() {
document.body.appendChild(a);
a.click();
a.remove();
const size = formatBackupSize(data.size);
if (st) st.textContent = `Архив ${data.filename}${size ? ` (${size})` : ''} — скачивается браузером`;
showToast('Бэкап сформирован' + (size ? ': ' + size : ''));
} catch (err) {
if (st) { st.classList.add('err'); st.textContent = 'Не удалось сформировать бэкап: ' + err.message; }