diff --git a/web/templates/disk_images.html b/web/templates/disk_images.html index 35c4ab6..8f78f5b 100644 --- a/web/templates/disk_images.html +++ b/web/templates/disk_images.html @@ -51,7 +51,7 @@ -

Файл попадёт в /var/lib/virtuality/disk-images. При создании VM образ будет скопирован/сконвертирован в отдельный qcow2-диск VM.

+

После загрузки .img/.raw прогресс начнётся заново и покажет конвертацию в qcow2. Уже готовый .qcow2 сохраняется без конвертации.

@@ -62,15 +62,14 @@
Raspberry / Orange Pi images - Загрузи .img, затем в “Создать VM” выбери режим “Готовый диск”. + Загрузи .img, дождись конвертации, затем в “Создать VM” выбери режим “Готовый диск”.
Оригинал не трогаем - Virtuality делает отдельный диск VM, чтобы загруженный образ оставался шаблоном. + Virtuality сохраняет исходник и создаёт рядом готовый qcow2-шаблон.
-
qemu-img convert -O qcow2 source.img VM.qcow2
-virt-install --import --disk path=VM.qcow2
+
qemu-img convert -p -f raw -O qcow2 source.img source.qcow2
@@ -126,6 +125,44 @@ virt-install --import --disk path=VM.qcow2 return value.toFixed(value >= 10 || i === 0 ? 0 : 1) + ' ' + units[i]; } + function resetProgress(title, detail) { + bar.style.width = '0%'; + percent.textContent = '0%'; + statusLine.textContent = title; + loadedLine.textContent = detail || '0%'; + speedLine.textContent = 'ожидание…'; + } + + async function pollConvert(operationId) { + resetProgress('Конвертация образа в qcow2…', '0%'); + while (true) { + const response = await fetch('/api/operations/' + operationId, {cache: 'no-store'}); + const payload = await response.json(); + if (!payload.ok) throw new Error(payload.error || 'operation error'); + const op = payload.operation; + const progress = Math.max(0, Math.min(100, Number(op.progress || 0))); + bar.style.width = progress + '%'; + percent.textContent = progress + '%'; + loadedLine.textContent = 'Конвертация: ' + progress + '%'; + speedLine.textContent = op.status; + statusLine.textContent = op.message || 'Конвертация образа…'; + if (op.status === 'success') { + bar.style.width = '100%'; + percent.textContent = '100%'; + statusLine.textContent = 'Конвертация завершена. Обновляем список образов…'; + setTimeout(() => { window.location.href = '/disk-images'; }, 650); + return; + } + if (op.status === 'error') { + statusLine.textContent = 'Ошибка конвертации: ' + (op.message || 'см. журнал операций'); + button.disabled = false; + fileInput.disabled = false; + return; + } + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + fileInput.addEventListener('change', function () { const file = fileInput.files[0]; hint.textContent = file ? file.name + ' · ' + bytesText(file.size) : 'Файл ещё не выбран.'; @@ -143,11 +180,7 @@ virt-install --import --disk path=VM.qcow2 box.hidden = false; button.disabled = true; fileInput.disabled = true; - statusLine.textContent = 'Передача образа на сервер…'; - bar.style.width = '0%'; - percent.textContent = '0%'; - loadedLine.textContent = '0 MB из ' + bytesText(file.size); - speedLine.textContent = 'скорость считается…'; + resetProgress('Передача образа на сервер…', '0 MB из ' + bytesText(file.size)); xhr.upload.onprogress = function (event) { if (!event.lengthComputable) { @@ -164,8 +197,14 @@ virt-install --import --disk path=VM.qcow2 if (pct >= 100) statusLine.textContent = 'Файл передан. Сервер сохраняет образ…'; }; - xhr.onload = function () { + xhr.onload = async function () { if (xhr.status >= 200 && xhr.status < 400) { + let payload = null; + try { payload = JSON.parse(xhr.responseText); } catch (error) {} + if (payload && payload.mode === 'converting' && payload.operation_id) { + await pollConvert(payload.operation_id); + return; + } bar.style.width = '100%'; percent.textContent = '100%'; statusLine.textContent = 'Готово. Обновляем список образов…'; @@ -184,6 +223,8 @@ virt-install --import --disk path=VM.qcow2 }; xhr.open('POST', form.action); + xhr.setRequestHeader('Accept', 'application/json'); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.send(data); });