Compare commits

...
2 Commits
6 changed files with 115 additions and 10 deletions
+45
View File
@@ -0,0 +1,45 @@
# drhider — тесты реальной обфускации и фикс .doc (2026-08-23, v0.0.60–0.0.61)
_Кластер `naeel-test-3`, v0.0.60 → v0.0.61. Корпус: `/mnt/y/T` (flat+many, 309 файлов pdf/doc/docx)._
## Проблема: .doc не конвертируется (v0.0.60)
Симптом: `.doc`-файлы в ZIP с заглушкой
`[DOC — conversion failed: libreoffice: failed to launch javaldx - java may not function correctly]`.
**Диагностика:**
- `.doc` конвертирует сервис **liberta** (`CONVERT_SERVICE_URL` дефолт
`https://liberta.containerk8s.dev.nubes.ru/convert`; в деплое не переопределён).
- liberta **жива** (200 на `/` и `/health`), последовательный `/convert` — 200 (~3с).
- **Под нагрузкой падает**: 5 параллельных `/convert` → 2-3 возвращают 500 `javaldx`
(JVM/libreoffice не тянет конкурентность).
- drhider конвертировал `.doc` **параллельно** (ThreadPoolExecutor max_workers=4 в `obfuscator.py`)
→ бил liberta потоком → часть конверсий падала.
## Фикс: v0.0.61 — параллельность убрана
- `obfuscator.py`: удалён ThreadPoolExecutor — `.doc` конвертируются **последовательно**.
- `scanner.py`: `_LLM_CONCURRENCY = 1` — LLM-чанки **последовательно**.
- Версия 0.0.60 → 0.0.61.
## Результаты после фикса (v0.0.61)
### Контрольный .doc (7 файлов)
Все конвертируются по 2.4–3.9с (реальное время), **javaldx-ошибок нет**.
Единственный FAIL — `mini_78b` (78 байт, битый файл) → «conversion produced no .docx» (не связано с параллельностью).
### Полный набор (30 файлов)
upload=30, zip=25 (5 мини-PDF пустые), **convfail=1** (только mini_78b), mapping=86,
tokens=61983, llm=79.9с, proc=118.9с. Сравнение с v0.0.60 (было 7 convfail) — регрессий нет.
### Долгий прогон (2×100 файлов)
| Сессия | upload | zip | convfail | mapping | tokens | llm | всего |
|---|---|---|---|---|---|---|---|
| SET1 | 100 | 99 | 0 | 430 | 700474 | 985с | 1142.5с |
| SET2 | 100 | 98 | 0 | 493 | 671170 | 841с | 972.1с |
| Итого | 200 | 197 | **0** | 923 | ~1.37 млн | ~1826с | ~35 мин |
## Выводы
1. `.doc`/liberta починено: **0 ошибок конверсии на 200 файлах**.
2. Обфускация на объёме стабильна (923 замены, без регрессий).
3. Цена «без параллельности» (по требованию): LLM-этап медленнее
(100 файлов ≈ 16–19 мин; с параллельностью было бы ~4× быстрее).
4. Файлы вне ZIP (2–3 шт) — битые/пустые PDF (нет текста), не ошибка.
+36
View File
@@ -0,0 +1,36 @@
# 2026-08-24 — Лимиты: 50 МБ на файл, 500 МБ на сессию (v0.0.62)
## Контекст
- После переезда на naeel-test-3 ресурсы инстанса уменьшены до 1 CPU / 2048 MiB
(Штурвал, подтверждено kubectl: requests=limits 1CPU/2Gi).
- При 2Gi памяти лимит 1ГБ/сессия (фронт) + отсутствие лимита на файл стали опасны
(OOM-риск, сессии in-memory и теряются при рестарте).
- Пользователь: вернуть лимит — один файл 50 МБ, всего 500 МБ, и показывать лимиты во фронте.
## Изменения
Фронт (site/templates/index.html):
- `MAX_FILE_BYTES = 50МБ`, `MAX_SESSION_BYTES = 500МБ` (было 1ГБ, лимит на файл был убран).
- `addFileWithDedup`: файл сверх 50 МБ → `overNames` («не учитывается», красный, не обфусцируется);
суммарный лимит 500 МБ — как раньше.
- Шапка: «Ограничения: один файл не более 50 МБ, суммарно не более 500 МБ...».
Бэк:
- `site/session.py`: добавлен `MAX_FILE_BYTES = 50МБ` (рядом с `MAX_SESSION_BYTES = 500МБ`).
- `site/routes/api_bp.py` (защита):
- `POST /api/upload`: файл >50 МБ пропускается (log, continue).
- `POST /api/upload_refs`: ref с size>50МБ пропускается + DELETE с ВМ;
после pull проверяется фактический len(content)>50МБ → пропуск + DELETE.
- `site/app.py`: `MAX_CONTENT_LENGTH` 200МБ → 50МБ (защита памяти, согласовано с лимитом файла).
Версия: 0.0.61 → 0.0.62.
## Проверка
- py_compile app.py / session.py / api_bp.py — OK.
- node --check (извлечённый <script> index.html) — OK.
- get_errors — нет.
## Прочее (диагностика 5с)
- Медленная загрузка страницы ~5с — НЕ кластер/приложение: `time_namelookup=5.15с`
(медленный локальный резолвер: 8.8.8.8 первым в /etc/resolv.conf недоступен, фолбэк на 10.96.x.x).
Тот же паттерн на ВМ (5.4с). drhider отвечает за ~0.03с после соединения.
- Kubeconfig naeel-test-3 обновлён на ВМ (~/.kube/config-naeel-test-3), kubectl работает.
+2 -2
View File
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root) sys.path.insert(0, _sys_path_root)
# Версия приложения (меняется при изменениях) # Версия приложения (меняется при изменениях)
VERSION = "0.0.61" VERSION = "0.0.62"
def setup_logging(): def setup_logging():
@@ -71,7 +71,7 @@ def create_app():
# Конфигурация # Конфигурация
app.config["VERSION"] = VERSION app.config["VERSION"] = VERSION
app.config["MAX_CONTENT_LENGTH"] = 200 * 1024 * 1024 # 200 MB app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024 # 50 MB (лимит на один файл)
# Регистрируем blueprint'ы # Регистрируем blueprint'ы
from routes import register_routes from routes import register_routes
+23 -1
View File
@@ -22,7 +22,8 @@ from flask import Blueprint, request, send_file, jsonify, Response, stream_with_
from drhider import obfuscate_files, LLMClient from drhider import obfuscate_files, LLMClient
from session import (create_session, add_file, get_files, store_result, from session import (create_session, add_file, get_files, store_result,
get_result, store_csv, get_csv, cleanup, file_count) get_result, store_csv, get_csv, cleanup, file_count,
MAX_FILE_BYTES)
api_bp = Blueprint("api", __name__, url_prefix="/api") api_bp = Blueprint("api", __name__, url_prefix="/api")
log = logging.getLogger("routes.api_bp") log = logging.getLogger("routes.api_bp")
@@ -52,6 +53,10 @@ def upload():
continue continue
data = f.read() data = f.read()
log.info("upload: sid=%s file=%r size=%d", sid, f.filename, len(data)) log.info("upload: sid=%s file=%r size=%d", sid, f.filename, len(data))
if len(data) > MAX_FILE_BYTES:
log.warning("upload: file exceeds %dMB, skipped sid=%s file=%r size=%d",
MAX_FILE_BYTES // (1024 * 1024), sid, f.filename, len(data))
continue
if not add_file(sid, f.filename, data): if not add_file(sid, f.filename, data):
log.warning("upload: session not found/limit, sid=%s file=%r", sid, f.filename) log.warning("upload: session not found/limit, sid=%s file=%r", sid, f.filename)
return jsonify({"ok": False, "error": "Session not found"}), 404 return jsonify({"ok": False, "error": "Session not found"}), 404
@@ -89,10 +94,27 @@ def upload_refs():
url = ref.get("url") url = ref.get("url")
if not name or not url: if not name or not url:
continue continue
# Лимит на один файл (50 МБ): сверх лимита — пропускаем (не участвует)
if (ref.get("size") or 0) > MAX_FILE_BYTES:
log.warning("upload_refs: file exceeds %dMB, skip sid=%s file=%r size=%s",
MAX_FILE_BYTES // (1024 * 1024), sid, name, ref.get("size"))
try:
client.delete(url)
except Exception:
pass
continue
with client.stream("GET", url) as resp: with client.stream("GET", url) as resp:
resp.raise_for_status() resp.raise_for_status()
content = b"".join(resp.iter_bytes()) content = b"".join(resp.iter_bytes())
log.info("upload_refs: pulled sid=%s file=%r size=%d", sid, name, len(content)) log.info("upload_refs: pulled sid=%s file=%r size=%d", sid, name, len(content))
if len(content) > MAX_FILE_BYTES:
log.warning("upload_refs: pulled file exceeds %dMB, skip sid=%s file=%r size=%d",
MAX_FILE_BYTES // (1024 * 1024), sid, name, len(content))
try:
client.delete(url)
except Exception:
pass
continue
if not add_file(sid, name, content): if not add_file(sid, name, content):
log.warning("upload_refs: session not found/limit, sid=%s file=%r", sid, name) log.warning("upload_refs: session not found/limit, sid=%s file=%r", sid, name)
return jsonify({"ok": False, "error": "Session not found"}), 404 return jsonify({"ok": False, "error": "Session not found"}), 404
+3 -2
View File
@@ -21,8 +21,9 @@ from typing import Dict, List, Tuple, Optional
TTL_SECONDS = 30 * 60 # 30 минут TTL_SECONDS = 30 * 60 # 30 минут
# Максимальный суммарный объём файлов в одной сессии (защита памяти) # Максимальный объём одного файла и суммарный объём файлов в сессии (защита памяти)
MAX_SESSION_BYTES = 500 * 1024 * 1024 # 500 MB MAX_FILE_BYTES = 50 * 1024 * 1024 # 50 MB на один файл
MAX_SESSION_BYTES = 500 * 1024 * 1024 # 500 MB суммарно на сессию
_sessions: Dict[str, dict] = {} _sessions: Dict[str, dict] = {}
_lock = threading.Lock() _lock = threading.Lock()
+6 -5
View File
@@ -131,7 +131,7 @@
<p class="sub"> <p class="sub">
Загрузите документы (.docx, .doc, .pdf, .txt, .md, .zip) — получите ZIP с обезличенными копиями.<br> Загрузите документы (.docx, .doc, .pdf, .txt, .md, .zip) — получите ZIP с обезличенными копиями.<br>
CSV с таблицей замен скачивается отдельно.<br> CSV с таблицей замен скачивается отдельно.<br>
<span style="color:#c0392b;">Ограничения: суммарно не более 1 ГБ. Файлы сверх суммарного лимита помечаются красным и не участвуют в обфускации.</span> <span style="color:#c0392b;">Ограничения: один файл не более 50 МБ, суммарно не более 500 МБ. Файлы сверх лимитов помечаются красным и не участвуют в обфускации.</span>
</p> </p>
<div class="file-input-wrap"> <div class="file-input-wrap">
<input type="file" id="fileInput" multiple accept=".doc,.docx,.pdf,.txt,.zip"> <input type="file" id="fileInput" multiple accept=".doc,.docx,.pdf,.txt,.zip">
@@ -196,7 +196,8 @@ const db = document.getElementById('dlBtns');
let sf = []; let sf = [];
let fileMeta = new Map(); // имя -> {size, mtime} для дедупа/суффиксов let fileMeta = new Map(); // имя -> {size, mtime} для дедупа/суффиксов
let overNames = new Set(); // имена файлов сверх лимита (не участвуют в обфускации) let overNames = new Set(); // имена файлов сверх лимита (не участвуют в обфускации)
const MAX_SESSION_BYTES = 1024 * 1024 * 1024; // 1 ГБ суммарно (лимит 100МБ/файл убран) const MAX_FILE_BYTES = 50 * 1024 * 1024; // 50 МБ на один файл
const MAX_SESSION_BYTES = 500 * 1024 * 1024; // 500 МБ суммарно на сессию
const VM_UPLOAD_URL = 'https://contracts.kube5s.ru/drhider-upload/'; // ВМ-буфер: PUT больших файлов (шлюз кластера их рвёт) const VM_UPLOAD_URL = 'https://contracts.kube5s.ru/drhider-upload/'; // ВМ-буфер: PUT больших файлов (шлюз кластера их рвёт)
// Тест-режим: открыть страницу с ?upload-only=1 → только загрузка, без обработки // Тест-режим: открыть страницу с ?upload-only=1 → только загрузка, без обработки
const TEST_UPLOAD_ONLY = new URLSearchParams(location.search).get('upload-only') === '1'; const TEST_UPLOAD_ONLY = new URLSearchParams(location.search).get('upload-only') === '1';
@@ -369,11 +370,11 @@ function addFileWithDedup(file) {
} }
fileMeta.set(name, { size, mtime }); fileMeta.set(name, { size, mtime });
// Определяем, превышает ли файл суммарный лимит (не участвует в обфускации) // Определяем, превышает ли файл лимит (по размеру файла или суммарный) — не участвует в обфускации
// Лимит 100МБ/файл убран — учитываются все файлы по размеру.
let over = false; let over = false;
if (size > MAX_FILE_BYTES) over = true; // лимит на один файл — 50 МБ
const sum = sf.reduce((s, f) => s + (overNames.has(f.name) ? 0 : f.size), 0); const sum = sf.reduce((s, f) => s + (overNames.has(f.name) ? 0 : f.size), 0);
if (sum + size > MAX_SESSION_BYTES) over = true; if (sum + size > MAX_SESSION_BYTES) over = true; // суммарный лимит сессии — 500 МБ
if (over) { if (over) {
overNames.add(name); overNames.add(name);