Files
loadtest/site/static/app.js
T
2026-07-10 21:19:31 +04:00

303 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* app.js — UI выбора и загрузки файлов.
*
* Выбираем файлы через <input type="file" multiple>,
* показываем в таблице: имя, размер, прогресс, статус.
* Кнопка «Загрузить» — XHR на /upload, контент не сохраняется.
*/
var files = []; // [{ name, size, lastModified, file, status, progress, speed, elapsed }]
var uploading = false; // флаг: идёт загрузка
var fileInput = document.getElementById('fileInput');
var fileTable = document.getElementById('fileTable');
var fileSummary = document.getElementById('fileSummary');
var clearBtn = document.getElementById('clearBtn');
var uploadBtn = document.getElementById('uploadBtn');
fileInput.addEventListener('change', function () {
addFiles(fileInput.files);
fileInput.value = '';
});
/** formatSize(bytes) → "1.2 KB", "3.4 MB", "900 B" */
function formatSize(bytes) {
if (!bytes || bytes === 0) return '—';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
return (bytes / 1073741824).toFixed(2) + ' GB';
}
/** formatSpeed(bytesPerSec) → "1.2 MB/s" */
function formatSpeed(bps) {
if (!bps || bps === 0) return '—';
if (bps < 1024) return bps + ' B/s';
if (bps < 1048576) return (bps / 1024).toFixed(1) + ' KB/s';
return (bps / 1048576).toFixed(1) + ' MB/s';
}
/** addFiles(fileList) — добавить файлы в массив и перерисовать */
function addFiles(fileList) {
for (var i = 0; i < fileList.length; i++) {
var f = fileList[i];
files.push({
name: f.name,
size: f.size,
lastModified: f.lastModified,
file: f, // сохраняем File-объект для XHR
status: '', // '' | 'uploading' | 'ok' | 'err'
progress: 0, // 0-100
speed: null, // bytes/sec
elapsed: null // ms
});
}
renderTable();
}
/** removeFile(i) — удалить файл по индексу */
function removeFile(i) {
if (uploading) return;
files.splice(i, 1);
renderTable();
}
/** renderTable() — перерисовать таблицу и сводку */
function renderTable() {
if (files.length === 0) {
fileTable.innerHTML = '<tr class="empty-row"><td colspan="5">Нет файлов — выберите файлы</td></tr>';
fileSummary.textContent = 'Нет файлов';
clearBtn.disabled = true;
uploadBtn.disabled = true;
return;
}
var totalSize = 0;
var rows = [];
for (var i = 0; i < files.length; i++) {
var f = files[i];
totalSize += f.size;
// Прогресс-бар
var progressHTML = '';
if (f.status === 'uploading') {
progressHTML = '<div style="display:flex;align-items:center;gap:4px;">' +
'<div style="flex:1;height:6px;background:#e0e0e0;border-radius:3px;overflow:hidden;">' +
'<div style="height:100%;width:' + f.progress + '%;background:var(--brand);border-radius:3px;transition:width .1s;"></div>' +
'</div>' +
'<span style="font-size:11px;min-width:32px;">' + f.progress + '%</span>' +
'</div>';
} else if (f.status === 'ok') {
progressHTML = '<span style="color:var(--green);font-size:12px;">✓ ' + formatSpeed(f.speed) + '</span>';
} else if (f.status === 'err') {
progressHTML = '<span style="color:var(--red);font-size:12px;">✗ ' + escHtml(f.error || '') + '</span>';
} else {
progressHTML = '<span style="color:var(--muted);">—</span>';
}
// Статус
var statusHTML = '';
if (f.status === 'uploading') {
statusHTML = '<span style="color:var(--brand);">↑ загрузка...</span>';
} else if (f.status === 'ok') {
statusHTML = '<span style="color:var(--green);">✓ ' + (f.elapsed ? (f.elapsed / 1000).toFixed(1) + 'с' : '') + '</span>';
} else if (f.status === 'err') {
statusHTML = '<span style="color:var(--red);">✗ ' + escHtml(f.error || '') + '</span>';
} else {
statusHTML = '<span style="color:var(--muted);">ожидает</span>';
}
rows.push(
'<tr>' +
'<td class="name-cell" title="' + escHtml(f.name) + '">' + escHtml(f.name) + '</td>' +
'<td class="num-cell">' + formatSize(f.size) + '</td>' +
'<td>' + progressHTML + '</td>' +
'<td>' + statusHTML + '</td>' +
'<td><button class="remove-btn" onclick="removeFile(' + i + ')" title="Удалить"' + (uploading ? ' disabled style="opacity:.3;cursor:not-allowed;"' : '') + '>✕</button></td>' +
'</tr>'
);
}
fileTable.innerHTML = rows.join('');
fileSummary.textContent = files.length + ' файл., ' + formatSize(totalSize);
clearBtn.disabled = uploading;
uploadBtn.disabled = uploading || files.length === 0;
if (uploading) {
uploadBtn.textContent = '⏳ Загрузка...';
} else {
uploadBtn.textContent = 'Загрузить';
}
}
/** warmup() — прогрев соединения перед загрузкой */
function warmup() {
return new Promise(function (resolve) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/health');
xhr.timeout = 3000;
xhr.onload = function () { resolve(true); };
xhr.onerror = function () { resolve(false); };
xhr.ontimeout = function () { resolve(false); };
xhr.send();
});
}
/** uploadAll() — последовательная загрузка всех файлов */
async function uploadAll() {
if (uploading || files.length === 0) return;
uploading = true;
uploadBtn.disabled = true;
clearBtn.disabled = true;
// Прогрев соединения
await warmup();
var totalBytes = 0;
var totalElapsed = 0;
var okCount = 0;
var errCount = 0;
for (var i = 0; i < files.length; i++) {
var f = files[i];
if (f.status === 'ok') continue; // уже загружен — пропускаем
f.status = 'uploading';
f.progress = 0;
renderTable();
try {
var result = await uploadOneWithRetry(f, i);
f.status = 'ok';
f.elapsed = result.elapsed_ms;
f.speed = result.elapsed_ms > 0 ? Math.round(result.bytes / (result.elapsed_ms / 1000)) : null;
totalBytes += result.bytes;
totalElapsed += result.elapsed_ms;
okCount++;
} catch (e) {
f.status = 'err';
f.error = e.message;
errCount++;
}
renderTable();
}
uploading = false;
renderTable();
// Сводка в fileSummary
var summary = files.length + ' файл.';
if (okCount > 0) {
var avgSpeed = totalElapsed > 0 ? Math.round(totalBytes / (totalElapsed / 1000)) : 0;
summary += ' | ✓ ' + okCount + ' / ✗ ' + errCount;
summary += ' | ' + formatSize(totalBytes) + ' за ' + (totalElapsed / 1000).toFixed(1) + 'с';
summary += ' | ~' + formatSpeed(avgSpeed);
}
fileSummary.textContent = summary;
}
/** uploadOneWithRetry(f, idx) — ретрай до 3 раз */
function uploadOneWithRetry(f, idx) {
var MAX_RETRIES = 3;
var RETRY_DELAY = 500; // ms
return new Promise(function (resolve, reject) {
var attempt = 0;
function tryUpload() {
attempt++;
uploadOne(f, idx).then(resolve).catch(function (e) {
if (attempt < MAX_RETRIES) {
f.progress = 0;
f.status = 'uploading';
renderTable();
setTimeout(tryUpload, RETRY_DELAY);
} else {
reject(e);
}
});
}
tryUpload();
});
}
/** uploadOne(fileEntry, idx) — читает файл в base64, отправляет как поле формы */
function uploadOne(f, idx) {
return new Promise(function (resolve, reject) {
// Шаг 1: прочитать файл в base64
f.status = 'uploading';
f.progress = 0;
renderTable();
var reader = new FileReader();
reader.onload = function () {
// base64-строка готова, теперь отправляем
var t0 = Date.now();
var xhr = new XMLHttpRequest();
var fd = new FormData();
fd.append('data', reader.result);
fd.append('name', f.name);
fd.append('orig_size', f.size);
xhr.open('POST', '/upload');
xhr.upload.onprogress = function (e) {
if (e.lengthComputable) {
f.progress = Math.round(e.loaded / e.total * 100);
renderTable();
}
};
xhr.onload = function () {
try {
var r = JSON.parse(xhr.responseText);
if (r.ok) {
resolve({ bytes: r.bytes, elapsed_ms: r.elapsed_ms });
} else {
reject(new Error(r.error || 'Ошибка сервера'));
}
} catch (e) {
reject(new Error('Некорректный ответ'));
}
};
xhr.onerror = function () {
var elapsed = Date.now() - t0;
reject(new Error('Сеть (' + (elapsed / 1000).toFixed(0) + 'с)'));
};
xhr.ontimeout = function () {
reject(new Error('Таймаут (' + (xhr.timeout / 1000) + 'с)'));
};
xhr.timeout = 600000; // 10 минут (base64 +33%)
xhr.send(fd);
};
reader.onerror = function () {
reject(new Error('Ошибка чтения файла'));
};
reader.readAsDataURL(f.file);
});
}
/** clearAll() — очистить всё */
function clearAll() {
if (uploading) return;
files = [];
renderTable();
}
/** escHtml(s) — экранирование HTML */
function escHtml(s) {
if (s == null) return '';
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// Первичный рендер
renderTable();