Скачивание образов было доработано по итогам ревью: зависшие операции, гонка запусков, отмена при проверке и распаковке
- Проверка «уже скачивается» идёт по всем операциям каталога (glob), а не по последним 50; скачивания без живого потока закрываются при старте (reap_stale_downloads), их .part удаляются, а отмена такой операции завершает её сразу, не дожидаясь потока. - Два запуска одного файла больше не пишут в один .part: проверки и создание операции под замком, файл создаётся монопольно (O_EXCL), вторая копия завершается ошибкой, не трогая чужой файл; осиротевший .part без владельца убирается перед новым запуском. - Отмена во время проверки контрольной суммы и распаковки .img.xz останавливает работу: операция завершается как отменённая, недокачанный файл, архив и недораспакованный образ удаляются. - Битый .img.xz после неудачной распаковки удаляется с понятным сообщением, повторное скачивание разрешено; проверка «файл уже есть» учитывает распакованный .img и qcow2 после конвертации. - Ссылки с кириллицей (домен IDNA, путь и параметры в процентах) открываются, некорректный порт и непонятные символы отклоняются сразу; ошибки сети описаны словами, исходный текст — в журнале. - Прогресс раз в секунду перечитывает JSON операции, чтобы не затирать cancel_requested и замечать отмену из другого процесса; операции читаются один раз на страницу; отмена в карточке — не ошибка. - Формулировки карточек и сообщения о недоступной архитектуре упрощены; добавлены тесты на каждое поведение. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
Этот коммит содержится в:
@@ -1,6 +1,7 @@
|
||||
{#- Идущие скачивания (операции типа download) с живым прогрессом. Ожидает переменную kind ('iso' | 'disk'). -#}
|
||||
{#- Идущие скачивания (операции типа download) с живым прогрессом. Ожидает переменную kind ('iso' | 'disk');
|
||||
страница может заранее положить в dl результат downloads_panel(kind), чтобы не читать операции дважды. -#}
|
||||
{% from "_ui.html" import icon %}
|
||||
{% set dl = downloads_panel(kind) %}
|
||||
{% set dl = dl if dl is defined else downloads_panel(kind) %}
|
||||
{% if dl.active %}
|
||||
<article class="card downloads" data-downloads>
|
||||
<div class="card-header">
|
||||
@@ -20,7 +21,8 @@
|
||||
<div class="meta mt-1" data-dl="message">{{ op.message }}</div>
|
||||
</div>
|
||||
<a class="btn btn-sm btn-ghost hide-sm" href="/operations/{{ op.id }}">Подробнее</a>
|
||||
<form method="post" action="/downloads/{{ op.id }}/cancel" class="inline-form" data-dl="cancel" data-confirm="Скачивание {{ op.filename }} остановится, а недокачанный файл будет удалён." data-confirm-title="Отменить скачивание?" data-confirm-button="Остановить" data-toast="Скачивание отменено" data-toast-tone="warning">
|
||||
{#- Без data-toast: сервер лишь просит поток остановиться, а итог («Скачивание отменено») покажет опрос ниже. -#}
|
||||
<form method="post" action="/downloads/{{ op.id }}/cancel" class="inline-form" data-dl="cancel" data-confirm="Скачивание {{ op.filename }} остановится, а недокачанный файл будет удалён." data-confirm-title="Отменить скачивание?" data-confirm-button="Остановить">
|
||||
<button class="btn btn-sm btn-danger-soft" {% if op.cancel_requested %}disabled{% endif %}>{{ icon('x', 'icon-sm') }}Отменить</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -54,13 +56,19 @@
|
||||
if (op.status !== 'success' && op.status !== 'error') return;
|
||||
clearInterval(timer);
|
||||
progress.classList.remove('indeterminate');
|
||||
progress.classList.add(op.status === 'success' ? 'success' : 'danger');
|
||||
cancel.hidden = true;
|
||||
item.dataset.status = op.status;
|
||||
if (op.status === 'success') {
|
||||
progress.classList.add('success');
|
||||
item.dataset.status = op.status;
|
||||
try { sessionStorage.setItem('virtualityToast', JSON.stringify({ message: op.message || 'Файл скачан', tone: 'success' })); } catch (_) {}
|
||||
setTimeout(function () { location.replace(location.pathname); }, 900);
|
||||
} else if (op.cancelled) {
|
||||
// Отмена по просьбе пользователя — не ошибка: предупреждение, без красной строки и красного процента.
|
||||
item.dataset.status = 'cancelled';
|
||||
if (window.vToast) window.vToast(op.message || 'Скачивание отменено', 'warning');
|
||||
} else {
|
||||
progress.classList.add('danger');
|
||||
item.dataset.status = op.status;
|
||||
message.classList.add('download-error');
|
||||
if (window.vToast) window.vToast(op.message || 'Скачивание не удалось', 'danger');
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{#- Карточка «Каталог готовых систем» для страницы /disk-images. -#}
|
||||
{#- Карточка «Каталог готовых систем» для страницы /disk-images; страница может заранее положить в dl результат downloads_panel('disk'). -#}
|
||||
{% from "_ui.html" import icon, badge %}
|
||||
{% set dl = downloads_panel('disk') %}
|
||||
{% set dl = dl if dl is defined else downloads_panel('disk') %}
|
||||
<article class="card">
|
||||
<div class="card-header">
|
||||
<div><h2>Каталог готовых систем</h2><p>Официальные образы для вашего процессора. Скачаем на сервер и проверим контрольную сумму — останется только создать машину.</p></div>
|
||||
<div><h2>Каталог готовых систем</h2><p>Официальные образы, подходящие этому серверу. Скачаем на сервер и проверим контрольную сумму — останется только создать машину.</p></div>
|
||||
</div>
|
||||
{% if dl.catalog %}
|
||||
<div class="list">
|
||||
@@ -31,6 +31,6 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card-body"><p class="muted">Для процессора этого сервера готовых образов в каталоге нет. Можно скачать образ по ссылке или загрузить с компьютера.</p></div>
|
||||
<div class="card-body"><p class="muted">Подходящих этому серверу готовых образов в каталоге нет. Можно скачать образ по ссылке или загрузить с компьютера.</p></div>
|
||||
{% endif %}
|
||||
</article>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{#- Карточка «Скачать по ссылке». Ожидает переменную kind ('iso' | 'disk'). -#}
|
||||
{% from "_ui.html" import icon %}
|
||||
<article class="card">
|
||||
<div class="card-header"><div><h2>Скачать по ссылке</h2><p>Сервер скачает файл сам — не нужно гонять его через компьютер</p></div></div>
|
||||
<div class="card-header"><div><h2>Скачать по ссылке</h2><p>Сервер скачает файл сам — не нужно сначала загружать его на компьютер</p></div></div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/downloads/url" class="stack-sm" data-busy="Проверяем ссылку…">
|
||||
<input type="hidden" name="kind" value="{{ kind }}">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
{% block content %}
|
||||
{% set kind = 'disk' %}
|
||||
{% set dl = downloads_panel(kind) %}
|
||||
{% include "_downloads_active.html" %}
|
||||
<section class="grid grid-main">
|
||||
<div class="stack">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
{% block content %}
|
||||
{% set kind = 'iso' %}
|
||||
{% set dl = downloads_panel(kind) %}
|
||||
{% include "_downloads_active.html" %}
|
||||
<section class="grid grid-main">
|
||||
<article class="card">
|
||||
|
||||
Ссылка в новой задаче
Block a user