Files
loadtest/site/static/app.js
T
2026-07-11 09:53:21 +04:00

263 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() — загрузка всех файлов через один WebSocket */
async function uploadAll() {
if (uploading || files.length === 0) return;
uploading = true;
uploadBtn.disabled = true;
clearBtn.disabled = true;
var totalBytes = 0;
var totalElapsed = 0;
var okCount = 0;
var errCount = 0;
// HTTP POST на /upload (base64, с retry)
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 uploadFileHTTP(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();
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;
}
/** uploadFileHTTP(f, idx) — загрузить файл как raw body (без base64) */
function uploadFileHTTP(f, idx) {
return new Promise(function (resolve, reject) {
var startTime = Date.now();
var RETRY = 3;
function doUpload(attempt) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.timeout = 300000;
xhr.setRequestHeader('X-File-Name', encodeURIComponent(f.name));
xhr.setRequestHeader('X-File-Size', f.size);
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);
resolve({ bytes: r.bytes, elapsed_ms: Date.now() - startTime });
} catch (e) {
if (attempt < RETRY) { doUpload(attempt + 1); return; }
reject(new Error('Bad response'));
}
};
xhr.onerror = function () {
if (attempt < RETRY) { doUpload(attempt + 1); return; }
reject(new Error('Network error'));
};
xhr.ontimeout = function () {
if (attempt < RETRY) { doUpload(attempt + 1); return; }
reject(new Error('Timeout'));
};
xhr.send(f.file);
}
doUpload(1);
});
}
// WebSocket upload больше не используется; оставлен для справки
function uploadFileWS() {}
/** 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();