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);
});