Files
loadtest/site/static/app.js
T
naeel 69d0a0debb
Deploy loadtest / validate (push) Waiting to run
revert: back to working simple send, bump to 1.0.16
2026-07-11 00:29:30 +04:00

282 lines
9.0 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 на все файлы
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 чанками */
function uploadFileWS(url, f, idx) {
return new Promise(function (resolve, reject) {
var ws = new WebSocket(url);
var CHUNK = 64 * 1024;
var offset = 0;
var startTime = Date.now();
var lastProgress = 0;
var timer;
ws.binaryType = 'arraybuffer';
ws.onopen = function () {
var reader = new FileReader();
reader.onload = function (e) {
ws.send(e.target.result);
offset += CHUNK;
if (offset < f.size) {
var pct = Math.round(offset / f.size * 100);
if (pct - lastProgress >= 10) {
lastProgress = pct;
f.progress = pct;
renderTable();
}
readNext();
} else {
ws.send('DONE');
}
};
function readNext() {
var end = Math.min(offset + CHUNK, f.size);
reader.readAsArrayBuffer(f.file.slice(offset, end));
}
readNext();
};
ws.onmessage = function (e) {
try {
var r = JSON.parse(e.data);
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);
});
}
/** 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();