v0.0.50: лимиты загрузки (>100МБ/файл, >1ГБ сумма) — красная подсветка в таблице, исключение из обфускации
Deploy drhider / validate (push) Canceled after 0s
Deploy drhider / validate (push) Canceled after 0s
This commit is contained in:
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user