From f50d8d5ce02ad7b89e5909fc6b10fd0fceefab80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Fri, 10 Jul 2026 08:07:36 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20/upload=20=D1=8D=D0=BD=D0=B4=D0=BF?= =?UTF-8?q?=D0=BE=D0=B8=D0=BD=D1=82,=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8=D1=82=D1=8C?= =?UTF-8?q?,=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B5=D1=81=D1=81-=D0=B1?= =?UTF-8?q?=D0=B0=D1=80=D1=8B,=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C,=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20v1.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/app.py | 26 +++++- site/static/app.js | 169 ++++++++++++++++++++++++++++++++++++-- site/templates/index.html | 10 ++- 3 files changed, 192 insertions(+), 13 deletions(-) diff --git a/site/app.py b/site/app.py index 632bf07..8155cd4 100644 --- a/site/app.py +++ b/site/app.py @@ -1,11 +1,33 @@ -from flask import Flask, render_template +import time +from flask import Flask, render_template, request app = Flask(__name__) +VERSION = '1.0.0' + @app.route('/') def index(): - return render_template('index.html') + return render_template('index.html', version=VERSION) + + +@app.route('/health') +def health(): + return {'ok': True, 'version': VERSION} + + +@app.route('/upload', methods=['POST']) +def upload(): + t0 = time.time() + total = 0 + for f in request.files.values(): + while True: + chunk = f.read(8192) + if not chunk: + break + total += len(chunk) + elapsed = round((time.time() - t0) * 1000) + return {'ok': True, 'bytes': total, 'elapsed_ms': elapsed} if __name__ == '__main__': diff --git a/site/static/app.js b/site/static/app.js index fd1cb58..e391e23 100644 --- a/site/static/app.js +++ b/site/static/app.js @@ -1,18 +1,19 @@ /** - * app.js — UI выбора файлов из локали. + * app.js — UI выбора и загрузки файлов. * * Выбираем файлы через , - * показываем в таблице: имя, размер, кнопка удаления. - * - * Никакой загрузки на сервер — чисто клиент. + * показываем в таблице: имя, размер, прогресс, статус. + * Кнопка «Загрузить» — XHR на /upload, контент не сохраняется. */ -var files = []; // [{ name, size, lastModified }] +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); @@ -28,6 +29,14 @@ function formatSize(bytes) { 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++) { @@ -35,7 +44,12 @@ function addFiles(fileList) { files.push({ name: f.name, size: f.size, - lastModified: f.lastModified + lastModified: f.lastModified, + file: f, // сохраняем File-объект для XHR + status: '', // '' | 'uploading' | 'ok' | 'err' + progress: 0, // 0-100 + speed: null, // bytes/sec + elapsed: null // ms }); } renderTable(); @@ -43,6 +57,7 @@ function addFiles(fileList) { /** removeFile(i) — удалить файл по индексу */ function removeFile(i) { + if (uploading) return; files.splice(i, 1); renderTable(); } @@ -50,9 +65,10 @@ function removeFile(i) { /** renderTable() — перерисовать таблицу и сводку */ function renderTable() { if (files.length === 0) { - fileTable.innerHTML = 'Нет файлов — выберите файлы'; + fileTable.innerHTML = 'Нет файлов — выберите файлы'; fileSummary.textContent = 'Нет файлов'; clearBtn.disabled = true; + uploadBtn.disabled = true; return; } @@ -61,22 +77,157 @@ function renderTable() { 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 = ''; + } 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 = '✗ ошибка'; + } else { + statusHTML = 'ожидает'; + } + rows.push( '' + '' + escHtml(f.name) + '' + '' + formatSize(f.size) + '' + - '' + + '' + progressHTML + '' + + '' + statusHTML + '' + + '' + '' ); } fileTable.innerHTML = rows.join(''); fileSummary.textContent = files.length + ' файл., ' + formatSize(totalSize); - clearBtn.disabled = false; + clearBtn.disabled = uploading; + uploadBtn.disabled = uploading || files.length === 0; + + if (uploading) { + uploadBtn.textContent = '⏳ Загрузка...'; + } else { + uploadBtn.textContent = 'Загрузить'; + } +} + +/** uploadAll() — последовательная загрузка всех файлов */ +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; + + 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 uploadOne(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; +} + +/** uploadOne(fileEntry, idx) — XHR одного файла, возвращает {bytes, elapsed_ms} */ +function uploadOne(f, idx) { + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + var fd = new FormData(); + fd.append('file', f.file, f.name); + + var t0 = Date.now(); + + 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 = 300000; // 5 минут на файл + xhr.send(fd); + }); } /** clearAll() — очистить всё */ function clearAll() { + if (uploading) return; files = []; renderTable(); } diff --git a/site/templates/index.html b/site/templates/index.html index 9046b74..05c279a 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -81,6 +81,7 @@
📦 Loadtest — загрузка файлов + v{{ version }}
@@ -95,7 +96,9 @@ Имя - Размер + Размер + Прогресс + Статус ✕ @@ -104,7 +107,10 @@