/**
* app.js — UI выбора и загрузки файлов.
*
* Выбираем файлы через ,
* показываем в таблице: имя, размер, прогресс, статус.
* Кнопка «Загрузить» — 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 = '
| Нет файлов — выберите файлы |
';
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 = '' +
'
' +
'
' + f.progress + '%' +
'
';
} else if (f.status === 'ok') {
progressHTML = '✓ ' + formatSpeed(f.speed) + '';
} else if (f.status === 'err') {
progressHTML = '✗ ' + escHtml(f.error || '') + '';
} else {
progressHTML = '—';
}
// Статус
var statusHTML = '';
if (f.status === 'uploading') {
statusHTML = '↑ загрузка...';
} else if (f.status === 'ok') {
statusHTML = '✓ ' + (f.elapsed ? (f.elapsed / 1000).toFixed(1) + 'с' : '') + '';
} else if (f.status === 'err') {
statusHTML = '✗ ' + escHtml(f.error || '') + '';
} else {
statusHTML = 'ожидает';
}
rows.push(
'' +
'| ' + escHtml(f.name) + ' | ' +
'' + formatSize(f.size) + ' | ' +
'' + progressHTML + ' | ' +
'' + statusHTML + ' | ' +
' | ' +
'
'
);
}
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();