diff --git a/site/app.py b/site/app.py index ae580de..598ccbf 100644 --- a/site/app.py +++ b/site/app.py @@ -4,7 +4,7 @@ from flask import Flask, render_template, request app = Flask(__name__) -VERSION = '1.0.7' +VERSION = '1.0.8' @app.route('/') @@ -34,15 +34,18 @@ def upload(): if missing: data += '=' * (4 - missing) raw = base64.b64decode(data) - elapsed = round((time.time() - t0) * 1000) - return {'ok': True, 'bytes': len(raw), 'elapsed_ms': elapsed} - # старый режим: файл напрямую (может не работать через DDOS-Guard) - for f in request.files.values(): - while True: - chunk = f.read(8192) - if not chunk: - break - total += len(chunk) + total = len(raw) + # файловый режим + elif request.files: + for f in request.files.values(): + while True: + chunk = f.read(8192) + if not chunk: + break + total += len(chunk) + # raw-режим: тело как есть + else: + total = len(request.get_data()) elapsed = round((time.time() - t0) * 1000) return {'ok': True, 'bytes': total, 'elapsed_ms': elapsed} diff --git a/site/static/app.js b/site/static/app.js index d5e97c9..08235e4 100644 --- a/site/static/app.js +++ b/site/static/app.js @@ -143,15 +143,14 @@ function warmup() { }); } -/** uploadAll() — последовательная загрузка всех файлов */ +/** uploadAll() — последовательная загрузка всех файлов чанками по 100KB */ async function uploadAll() { if (uploading || files.length === 0) return; uploading = true; uploadBtn.disabled = true; clearBtn.disabled = true; - // Прогрев соединения - await warmup(); + var CHUNK = 100 * 1024; // 100KB raw chunks var totalBytes = 0; var totalElapsed = 0; @@ -160,24 +159,44 @@ async function uploadAll() { for (var i = 0; i < files.length; i++) { var f = files[i]; - if (f.status === 'ok') continue; // уже загружен — пропускаем + if (f.status === 'ok') continue; f.status = 'uploading'; f.progress = 0; renderTable(); - try { - var result = await uploadOneWithRetry(f, i); + var fileBytes = 0; + var fileElapsed = 0; + var offset = 0; + var totalChunks = Math.ceil(f.size / CHUNK); + var failed = false; + + while (offset < f.size && !failed) { + var end = Math.min(offset + CHUNK, f.size); + var blob = f.file.slice(offset, end); + + try { + var result = await uploadChunk(blob); + fileBytes += result.bytes; + fileElapsed += result.elapsed_ms; + f.progress = Math.round((offset + (end - offset)) / f.size * 100); + renderTable(); + } catch (e) { + f.status = 'err'; + f.error = 'Чанк ' + (Math.floor(offset/CHUNK)+1) + '/' + totalChunks + ': ' + e.message; + failed = true; + errCount++; + } + offset = end; + } + + if (!failed) { 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; + f.elapsed = fileElapsed; + f.speed = fileElapsed > 0 ? Math.round(fileBytes / (fileElapsed / 1000)) : null; + totalBytes += fileBytes; + totalElapsed += fileElapsed; okCount++; - } catch (e) { - f.status = 'err'; - f.error = e.message; - errCount++; } renderTable(); } @@ -185,7 +204,6 @@ async function uploadAll() { uploading = false; renderTable(); - // Сводка в fileSummary var summary = files.length + ' файл.'; if (okCount > 0) { var avgSpeed = totalElapsed > 0 ? Math.round(totalBytes / (totalElapsed / 1000)) : 0; @@ -196,90 +214,28 @@ async function uploadAll() { fileSummary.textContent = summary; } -/** uploadOneWithRetry(f, idx) — ретрай до 3 раз */ -function uploadOneWithRetry(f, idx) { - var MAX_RETRIES = 3; - var RETRY_DELAY = 500; // ms - +/** uploadChunk(blob) — отправить чанк как raw тело */ +function uploadChunk(blob) { 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); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/upload'); + xhr.timeout = 10000; + xhr.setRequestHeader('Content-Type', 'application/octet-stream'); + xhr.onload = function () { + try { + var r = JSON.parse(xhr.responseText); + if (r.ok) { + resolve({ bytes: r.bytes, elapsed_ms: r.elapsed_ms }); } else { - reject(e); + reject(new Error(r.error || 'Ошибка сервера')); } - }); - } - - 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); + } catch (e) { + reject(new Error('Bad JSON')); + } }; - - reader.onerror = function () { - reject(new Error('Ошибка чтения файла')); - }; - - reader.readAsDataURL(f.file); + xhr.onerror = function () { reject(new Error('Сеть')); }; + xhr.ontimeout = function () { reject(new Error('Таймаут')); }; + xhr.send(blob); }); }