v0.0.50: лимиты загрузки (>100МБ/файл, >1ГБ сумма) — красная подсветка в таблице, исключение из обфускации
Deploy drhider / validate (push) Canceled after 0s

This commit is contained in:
“Naeel”
2026-08-20 07:25:02 +03:00
parent 0a94d3f449
commit 551ed8cf42
3 changed files with 99 additions and 19 deletions
+36
View File
@@ -0,0 +1,36 @@
# 2026-08-20 — Лимиты загрузки + предупреждение в таблице (v0.0.50)
## Проблема
Юзер грузил всё подряд (сотни МБ, включая >200МБ) → HTTP 413 / SSE-обрыв.
Требование: понятная политика лимитов, но файлы сверх лимита НЕ молча скрывать —
показывать в таблице, чтобы юзер не повторял загрузку в недоумении.
## Решение (фронт, index.html)
Лимиты:
- 1 файл > 100 МБ → «не учитывается»
- сумма учитываемых > 1 ГБ → следующий файл тоже «не учитывается»
Логика:
- `MAX_FILE_BYTES = 100МБ`, `MAX_SESSION_BYTES = 1ГБ` (константы).
- `addFileWithDedup`: файл сверх лимита добавляется в `sf` (виден в таблице),
но имя кладётся в `overNames` (Set). Учитываемость считаем по не-overNames.
- `rr()`: строки с `overNames` получают класс `row-over` (красный фон/текст),
статус «🔥 не учитывается»; счётчик: «N учитываются + M свыше лимита · СУММА».
- `uploadFiles()`: строит `toSend` ТОЛЬКО из учитываемых, `sendIdx` маппит
idx-отправленного → idx в `sf` (прогресс/таймеры/start/done пишут в верную строку).
Если все переборные → «Нет файлов для обфускации (все превышают лимит)».
- Шапка: добавлена строка «Ограничения: файл не более 100 МБ, суммарно не более
1 ГБ. Файлы сверх лимита помечаются красным и не участвуют в обфускации.»
- CSS: `.row-over td { background: rgba(220,38,38,.10) !important; color:#c0392b; }`.
- `rm`/`resetAll` — чистят `overNames`.
Бэк НЕ менялся (MAX_CONTENT_LENGTH=200МБ, MAX_SESSION_BYTES=500МБ остались как защита
на сервере). Внимание: бэк-лимит 200МБ/запрос и 500МБ/сессия всё ещё ниже фронтовых
1ГБ — для файлов 100МБ-200МБ фронт пустит, но одиночный запрос >200МБ упадёт 413.
Это можно согласовать позже, если нужно.
## Проверка
- node --check секции <script> index.html — OK.
- py_compile app.py — OK.
- Лог-тест лимитов через node: 120МБ→перебор, 13 учитываемых ~995МБ, >1ГБ→перебор — верно.
- VERSION поднята до 0.0.50.
+1 -1
View File
@@ -20,7 +20,7 @@ if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root)
# Версия приложения (меняется при изменениях)
VERSION = "0.0.49"
VERSION = "0.0.50"
def create_app():
+62 -18
View File
@@ -59,6 +59,9 @@
.name-cell { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.num-cell { text-align: right; white-space: nowrap; }
.empty-row td { color: var(--muted); text-align: center; padding: 20px; }
.row-over td { background: rgba(220,38,38,.10) !important; color:#c0392b; }
.row-over .num-cell { color:#c0392b; }
.row-over .name-cell { color:#c0392b; }
.remove-btn {
cursor: pointer; color: #f87171; background: none; border: none;
padding: 2px 4px; font-size: 14px; line-height: 1;
@@ -127,7 +130,8 @@
<div class="card-body">
<p class="sub">
Загрузите документы (.docx, .doc, .pdf, .txt, .md, .zip) — получите ZIP с обезличенными копиями.<br>
CSV с таблицей замен скачивается отдельно.
CSV с таблицей замен скачивается отдельно.<br>
<span style="color:#c0392b;">Ограничения: файл не более 100 МБ, суммарно не более 1 ГБ. Файлы сверх лимита помечаются красным и не участвуют в обфускации.</span>
</p>
<div class="file-input-wrap">
<input type="file" id="fileInput" multiple accept=".doc,.docx,.pdf,.txt,.zip">
@@ -191,6 +195,9 @@ const st = document.getElementById('status');
const db = document.getElementById('dlBtns');
let sf = [];
let fileMeta = new Map(); // имя -> {size, mtime} для дедупа/суффиксов
let overNames = new Set(); // имена файлов сверх лимита (не участвуют в обфускации)
const MAX_FILE_BYTES = 100 * 1024 * 1024; // 100 МБ на файл
const MAX_SESSION_BYTES = 1024 * 1024 * 1024; // 1 ГБ суммарно
let currentSid = '';
let activeES = null; // активный EventSource
let activeXHR = null; // активный XHR
@@ -201,6 +208,7 @@ function resetAll() {
currentSid = '';
sf = [];
fileMeta = new Map();
overNames = new Set();
fi.value = '';
rr();
st.className = '';
@@ -218,12 +226,22 @@ function fs(b) { return b < 1024 ? b + ' B' : b < 1048576 ? (b / 1024).toFixed(1
function rr() {
if (sf.length === 0) { fl.innerHTML = '<tr class="empty-row"><td colspan="4">Нет выбранных файлов</td></tr>'; }
else { fl.innerHTML = sf.map((f, i) => '<tr id="row-' + i + '"><td class="name-cell">' + f.name + '</td><td class="num-cell">' + fs(f.size) + '</td><td class="num-cell" id="st-' + i + '" style="font-size:12px;">—</td><td><button class="remove-btn" onclick="rm(' + i + ')">✕</button></td></tr>').join(''); }
fc.textContent = sf.length + ' файлов';
ub.disabled = sf.length === 0;
else {
fl.innerHTML = sf.map((f, i) => {
const over = overNames.has(f.name);
const rowCls = over ? ' class="row-over"' : '';
const stTxt = over ? '<span style="color:#c0392b;">🔥 не учитывается</span>' : '<span style="color:#999;">—</span>';
return '<tr id="row-' + i + '"' + rowCls + '><td class="name-cell">' + f.name + '</td><td class="num-cell">' + fs(f.size) + '</td><td class="num-cell" id="st-' + i + '" style="font-size:12px;">' + stTxt + '</td><td><button class="remove-btn" onclick="rm(' + i + ')">✕</button></td></tr>';
}).join('');
}
const overCount = sf.filter(f => overNames.has(f.name)).length;
const totalSize = sf.reduce((s, f) => s + (overNames.has(f.name) ? 0 : f.size), 0);
const cntMain = sf.length - overCount;
fc.textContent = (overCount ? cntMain + ' учитываются + ' + overCount + ' свыше лимита' : sf.length) + ' файлов · ' + fs(totalSize);
ub.disabled = cntMain === 0;
}
function rm(i) { fileMeta.delete(sf[i].name); sf.splice(i, 1); const d = new DataTransfer(); sf.forEach(f => d.items.add(f)); fi.files = d.files; rr(); }
function rm(i) { const nm = sf[i].name; overNames.delete(nm); fileMeta.delete(nm); sf.splice(i, 1); const d = new DataTransfer(); sf.forEach(f => d.items.add(f)); fi.files = d.files; rr(); }
window.addEventListener('load', () => { sf = []; fileMeta = new Map(); fi.value = ''; rr(); /* прогрев upstream-соединения */ fetch('/health').catch(() => {}); });
// ═══════════ Нативная распаковка ZIP (без внешних библиотек) ═══════════
@@ -338,6 +356,20 @@ function addFileWithDedup(file) {
name = base + '_' + n + ext;
}
fileMeta.set(name, { size, mtime });
// Определяем, превышает ли файл лимиты (не участвует в обфускации)
let over = size > MAX_FILE_BYTES; // >100 МБ на файл
if (!over) {
// проверка суммарного лимита 1 ГБ по учитываемым файлам (без этого и прочих переборных)
const sum = sf.reduce((s, f) => s + (overNames.has(f.name) ? 0 : f.size), 0);
if (sum + size > MAX_SESSION_BYTES) over = true;
}
if (over) {
overNames.add(name);
sf.push(new File([file], name, { lastModified: mtime }));
return;
}
sf.push(new File([file], name, { lastModified: mtime }));
}
@@ -382,6 +414,13 @@ async function uploadFiles() {
if (sf.length === 0) return;
// Сохранить список до очистки
const files = sf.slice();
// Только учитываемые файлы идут на загрузку/обфускацию (переборные пропускаем)
const toSend = [];
const sendIdx = []; // sendIdx[k] = индекс в sf для отправленного файла
for (let i = 0; i < files.length; i++) {
if (!overNames.has(files[i].name)) { toSend.push(files[i]); sendIdx.push(i); }
else { ss(i, '<span style="color:#c0392b;">пропущен</span>'); }
}
// Обрубить всё что могло остаться от предыдущего раза
if (activeES) { activeES.close(); activeES = null; }
if (activeXHR) { activeXHR.abort(); activeXHR = null; }
@@ -392,11 +431,14 @@ async function uploadFiles() {
document.getElementById('statsBlock').classList.remove('show');
document.getElementById('liveBlock').classList.remove('show');
ub.disabled = true;
const total = files.length;
const total = toSend.length;
if (total === 0) { st.className = 'status error'; st.textContent = 'Нет файлов для обфускации (все превышают лимит).'; ub.disabled = false; return; }
// Фаза 1: загрузка (XMLHttpRequest — прогресс в строке)
for (let i = 0; i < total; i++) {
const f = files[i], n = i + 1;
for (let k = 0; k < total; k++) {
const f = toSend[k];
const i = sendIdx[k]; // индекс в sf для вывода прогресса
const n = k + 1;
st.className = 'status progress';
st.textContent = 'Загрузка (этап 1/2) ' + n + '/' + total + ': ' + f.name;
try {
@@ -446,8 +488,8 @@ async function uploadFiles() {
}
// Фаза 2: обработка (SSE — прогресс по каждому файлу)
// Сброс загрузочных статусов — теперь этап обработки
for (let i = 0; i < total; i++) ss(i, '<span style="color:#2563eb">⏳</span>');
// Сброс загрузочных статусов — теперь этап обработки (только отправленные)
for (let k = 0; k < total; k++) ss(sendIdx[k], '<span style="color:#2563eb">⏳</span>');
const fileTimers = {}; // idx -> performance.now()
const fileIntervals = {}; // idx -> setInterval id
st.className = 'status progress';
@@ -479,18 +521,19 @@ async function uploadFiles() {
activeES = new EventSource('/api/process_stream/' + currentSid);
activeES.addEventListener('start', function(e) {
const d = JSON.parse(e.data);
const idx = sendIdx[d.idx]; // индекс в sf для вывода прогресса
// Таймер тикает ТОЛЬКО у текущего файла; таймеры остальных останавливаем
for (const k in fileIntervals) {
clearInterval(fileIntervals[k]);
delete fileIntervals[k];
}
fileTimers[d.idx] = performance.now();
fileIntervals[d.idx] = setInterval(function() {
const elapsed = ((performance.now() - fileTimers[d.idx]) / 1000).toFixed(1);
ss(d.idx, '<span style="color:#2563eb">⏳ ' + elapsed + 'с</span>');
fileTimers[idx] = performance.now();
fileIntervals[idx] = setInterval(function() {
const elapsed = ((performance.now() - fileTimers[idx]) / 1000).toFixed(1);
ss(idx, '<span style="color:#2563eb">⏳ ' + elapsed + 'с</span>');
}, 200);
// Показываем текущий файл и его размер в live-блоке
const f = sf[d.idx];
const f = sf[idx];
const sz = f ? fs(f.size) : '';
currentLiveFile = 'Файл ' + (d.idx + 1) + '/' + total + ': ' + d.name + (sz ? ' (' + sz + ')' : '');
liveFileEl.textContent = currentLiveFile;
@@ -509,12 +552,13 @@ async function uploadFiles() {
});
activeES.addEventListener('done', function(e) {
const d = JSON.parse(e.data);
clearInterval(fileIntervals[d.idx]);
const idx = sendIdx[d.idx];
clearInterval(fileIntervals[idx]);
// Время на КОНКРЕТНЫЙ файл приходит с бэка (extract_text + замена, без общего LLM)
const sec = (typeof d.elapsed === 'number' && d.elapsed > 0)
? d.elapsed.toFixed(1)
: ((performance.now() - (fileTimers[d.idx] || performance.now())) / 1000).toFixed(1);
ss(d.idx, '<span style="color:#22c55e">✓ ' + sec + 'с</span>');
: ((performance.now() - (fileTimers[idx] || performance.now())) / 1000).toFixed(1);
ss(idx, '<span style="color:#22c55e">✓ ' + sec + 'с</span>');
});
activeES.addEventListener('complete', function(e) {
activeES.close();