Files
loadtest/site/static/app.js
T
2026-07-10 23:16:13 +04:00

259 lines
8.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() — последовательная загрузка всех файлов чанками по 100KB */
async function uploadAll() {
if (uploading || files.length === 0) return;
uploading = true;
uploadBtn.disabled = true;
clearBtn.disabled = true;
var CHUNK = 100 * 1024; // 100KB raw chunks
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();
var fileBytes = 0;
var fileElapsed = 0;
var offset = 0;
var totalChunks = Math.ceil(f.size / CHUNK);
var failed = false;
while (offset < f.size && !failed) {
var end = Math.min(offset + CHUNK, f.size);
var blob = f.file.slice(offset, end);
try {
var result = await uploadChunk(blob);
fileBytes += result.bytes;
fileElapsed += result.elapsed_ms;
f.progress = Math.round((offset + (end - offset)) / f.size * 100);
renderTable();
} catch (e) {
f.status = 'err';
f.error = 'Чанк ' + (Math.floor(offset/CHUNK)+1) + '/' + totalChunks + ': ' + e.message;
failed = true;
errCount++;
}
offset = end;
}
if (!failed) {
f.status = 'ok';
f.elapsed = fileElapsed;
f.speed = fileElapsed > 0 ? Math.round(fileBytes / (fileElapsed / 1000)) : null;
totalBytes += fileBytes;
totalElapsed += fileElapsed;
okCount++;
}
renderTable();
}
uploading = false;
renderTable();
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;
}
/** uploadChunk(blob) — отправить чанк как raw тело */
function uploadChunk(blob) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.timeout = 10000;
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
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('Bad JSON'));
}
};
xhr.onerror = function () { reject(new Error('Сеть')); };
xhr.ontimeout = function () { reject(new Error('Таймаут')); };
xhr.send(blob);
});
}
/** 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();