feat: raw binary chunked upload (100KB), drop base64 form, bump 1.0.7→1.0.8
Deploy loadtest / validate (push) Waiting to run
Deploy loadtest / validate (push) Waiting to run
This commit is contained in:
+13
-10
@@ -4,7 +4,7 @@ from flask import Flask, render_template, request
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
VERSION = '1.0.7'
|
VERSION = '1.0.8'
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@@ -34,15 +34,18 @@ def upload():
|
|||||||
if missing:
|
if missing:
|
||||||
data += '=' * (4 - missing)
|
data += '=' * (4 - missing)
|
||||||
raw = base64.b64decode(data)
|
raw = base64.b64decode(data)
|
||||||
elapsed = round((time.time() - t0) * 1000)
|
total = len(raw)
|
||||||
return {'ok': True, 'bytes': len(raw), 'elapsed_ms': elapsed}
|
# файловый режим
|
||||||
# старый режим: файл напрямую (может не работать через DDOS-Guard)
|
elif request.files:
|
||||||
for f in request.files.values():
|
for f in request.files.values():
|
||||||
while True:
|
while True:
|
||||||
chunk = f.read(8192)
|
chunk = f.read(8192)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
total += len(chunk)
|
total += len(chunk)
|
||||||
|
# raw-режим: тело как есть
|
||||||
|
else:
|
||||||
|
total = len(request.get_data())
|
||||||
elapsed = round((time.time() - t0) * 1000)
|
elapsed = round((time.time() - t0) * 1000)
|
||||||
return {'ok': True, 'bytes': total, 'elapsed_ms': elapsed}
|
return {'ok': True, 'bytes': total, 'elapsed_ms': elapsed}
|
||||||
|
|
||||||
|
|||||||
+51
-95
@@ -143,15 +143,14 @@ function warmup() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** uploadAll() — последовательная загрузка всех файлов */
|
/** uploadAll() — последовательная загрузка всех файлов чанками по 100KB */
|
||||||
async function uploadAll() {
|
async function uploadAll() {
|
||||||
if (uploading || files.length === 0) return;
|
if (uploading || files.length === 0) return;
|
||||||
uploading = true;
|
uploading = true;
|
||||||
uploadBtn.disabled = true;
|
uploadBtn.disabled = true;
|
||||||
clearBtn.disabled = true;
|
clearBtn.disabled = true;
|
||||||
|
|
||||||
// Прогрев соединения
|
var CHUNK = 100 * 1024; // 100KB raw chunks
|
||||||
await warmup();
|
|
||||||
|
|
||||||
var totalBytes = 0;
|
var totalBytes = 0;
|
||||||
var totalElapsed = 0;
|
var totalElapsed = 0;
|
||||||
@@ -160,24 +159,44 @@ async function uploadAll() {
|
|||||||
|
|
||||||
for (var i = 0; i < files.length; i++) {
|
for (var i = 0; i < files.length; i++) {
|
||||||
var f = files[i];
|
var f = files[i];
|
||||||
if (f.status === 'ok') continue; // уже загружен — пропускаем
|
if (f.status === 'ok') continue;
|
||||||
|
|
||||||
f.status = 'uploading';
|
f.status = 'uploading';
|
||||||
f.progress = 0;
|
f.progress = 0;
|
||||||
renderTable();
|
renderTable();
|
||||||
|
|
||||||
try {
|
var fileBytes = 0;
|
||||||
var result = await uploadOneWithRetry(f, i);
|
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.status = 'ok';
|
||||||
f.elapsed = result.elapsed_ms;
|
f.elapsed = fileElapsed;
|
||||||
f.speed = result.elapsed_ms > 0 ? Math.round(result.bytes / (result.elapsed_ms / 1000)) : null;
|
f.speed = fileElapsed > 0 ? Math.round(fileBytes / (fileElapsed / 1000)) : null;
|
||||||
totalBytes += result.bytes;
|
totalBytes += fileBytes;
|
||||||
totalElapsed += result.elapsed_ms;
|
totalElapsed += fileElapsed;
|
||||||
okCount++;
|
okCount++;
|
||||||
} catch (e) {
|
|
||||||
f.status = 'err';
|
|
||||||
f.error = e.message;
|
|
||||||
errCount++;
|
|
||||||
}
|
}
|
||||||
renderTable();
|
renderTable();
|
||||||
}
|
}
|
||||||
@@ -185,7 +204,6 @@ async function uploadAll() {
|
|||||||
uploading = false;
|
uploading = false;
|
||||||
renderTable();
|
renderTable();
|
||||||
|
|
||||||
// Сводка в fileSummary
|
|
||||||
var summary = files.length + ' файл.';
|
var summary = files.length + ' файл.';
|
||||||
if (okCount > 0) {
|
if (okCount > 0) {
|
||||||
var avgSpeed = totalElapsed > 0 ? Math.round(totalBytes / (totalElapsed / 1000)) : 0;
|
var avgSpeed = totalElapsed > 0 ? Math.round(totalBytes / (totalElapsed / 1000)) : 0;
|
||||||
@@ -196,90 +214,28 @@ async function uploadAll() {
|
|||||||
fileSummary.textContent = summary;
|
fileSummary.textContent = summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** uploadOneWithRetry(f, idx) — ретрай до 3 раз */
|
/** uploadChunk(blob) — отправить чанк как raw тело */
|
||||||
function uploadOneWithRetry(f, idx) {
|
function uploadChunk(blob) {
|
||||||
var MAX_RETRIES = 3;
|
|
||||||
var RETRY_DELAY = 500; // ms
|
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
var attempt = 0;
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/upload');
|
||||||
function tryUpload() {
|
xhr.timeout = 10000;
|
||||||
attempt++;
|
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||||
uploadOne(f, idx).then(resolve).catch(function (e) {
|
xhr.onload = function () {
|
||||||
if (attempt < MAX_RETRIES) {
|
try {
|
||||||
f.progress = 0;
|
var r = JSON.parse(xhr.responseText);
|
||||||
f.status = 'uploading';
|
if (r.ok) {
|
||||||
renderTable();
|
resolve({ bytes: r.bytes, elapsed_ms: r.elapsed_ms });
|
||||||
setTimeout(tryUpload, RETRY_DELAY);
|
|
||||||
} else {
|
} else {
|
||||||
reject(e);
|
reject(new Error(r.error || 'Ошибка сервера'));
|
||||||
}
|
}
|
||||||
});
|
} catch (e) {
|
||||||
}
|
reject(new Error('Bad JSON'));
|
||||||
|
}
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
xhr.onerror = function () { reject(new Error('Сеть')); };
|
||||||
reader.onerror = function () {
|
xhr.ontimeout = function () { reject(new Error('Таймаут')); };
|
||||||
reject(new Error('Ошибка чтения файла'));
|
xhr.send(blob);
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsDataURL(f.file);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user