Files
loadtest/site/static/app.js
T
2026-07-11 10:01:06 +04:00

288 lines
9.2 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;
// WebSocket с 8KB чанками и server ACK
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
var wsUrl = proto + '//' + location.host + '/ws-upload';
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 uploadFileWS(wsUrl, 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;
}
/** uploadFileWS(url, f, idx) — WebSocket с 8KB чанками и server ACK */
function uploadFileWS(url, f, idx) {
return new Promise(function (resolve, reject) {
var ws = new WebSocket(url);
var CHUNK = 8 * 1024;
var offset = 0;
var startTime = Date.now();
var lastProgress = 0;
var timer;
ws.binaryType = 'arraybuffer';
ws.onopen = function () {
readNext();
};
function readNext() {
var end = Math.min(offset + CHUNK, f.size);
var reader = new FileReader();
reader.onload = function (e) {
ws.send(e.target.result);
};
reader.readAsArrayBuffer(f.file.slice(offset, end));
}
ws.onmessage = function (e) {
try {
var r = JSON.parse(e.data);
if (r.ack) {
offset = r.ack;
var pct = Math.round(offset / f.size * 100);
if (pct - lastProgress >= 10) {
lastProgress = pct;
f.progress = pct;
renderTable();
}
if (offset < f.size) {
readNext();
} else {
ws.send('DONE');
}
} else if (r.ok) {
clearTimeout(timer);
f.progress = 100;
renderTable();
ws.close();
resolve({ bytes: r.bytes, elapsed_ms: Date.now() - startTime });
}
} catch (err) {
reject(new Error('Bad response'));
}
};
ws.onerror = function () { reject(new Error('WebSocket error')); };
ws.onclose = function () {
if (offset < f.size) reject(new Error('WS closed early'));
};
timer = setTimeout(function () {
ws.close();
reject(new Error('Timeout'));
}, 300000);
});
}
// HTTP upload больше не используется
function uploadFileHTTP() {}
// 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();