Compare commits

...
7 Commits
Author SHA1 Message Date
“Naeel” ff4895b63b v0.0.73: фиксы по код-ревью Соннета — SSRF, path traversal, слабый SID, proc_error, zip-бомба, self-XSS
Deploy drhider / validate (push) Canceled after 0s
2026-08-24 19:58:27 +03:00
“Naeel” 4b73657e4c docs: код-ревью Соннета — 15 находок (SSRF, path traversal, слабый SID, zip-бомба, idx-мисматч и др.) 2026-08-24 19:50:50 +03:00
“Naeel” 82acc84311 docs: промпт для код-ревью Соннетом — строго ограниченный список файлов + 8 вопросов 2026-08-24 19:37:36 +03:00
“Naeel” 933ee79e3c docs: TODO — баг кнопки «Обфусцировать» активна при 0 файлов после «Новой сессии» (исправить при следующей правке, v0.0.73) 2026-08-24 19:35:42 +03:00
“Naeel” b0c055f926 docs: массовое тестирование v0.0.72 — 4 блока (папка/цикл/прерывание/UI) все OK; найден мелкий баг кнопки при 0 файлов 2026-08-24 19:34:49 +03:00
“Naeel” 44e12db886 docs: тест v0.0.72 на проде — статусы «не извлечён»/«пропущен (лимит)», счётчик дедупа, полный цикл OK 2026-08-24 19:28:05 +03:00
“Naeel” 08e4788a35 docs: временный сбой сертификата gitea — push починился сам, диагностика 2026-08-24 19:23:37 +03:00
13 changed files with 301 additions and 11 deletions
+2
View File
@@ -106,7 +106,9 @@
- **2026-08-24-time-tickers-estimates.md** — v0.0.64–0.0.65: тикер текущего файла на всех этапах, мгновенные оценки времени по файлам и суммарно.
- **2026-08-24-folder-select-implemented.md** — v0.0.68: кнопка «Выбрать папку» (webkitdirectory, рекурсивно, относительный путь).
- **2026-08-24-folder-zip-not-lost.md** — v0.0.69: архивы из папки без документов не теряются (добавляются как есть); раскрытие zip с документами; склонение счётчика.
- **2026-08-24-v073-security-fixes.md** — v0.0.73: 6 фиксов по ревью Соннета (SSRF, path traversal, слабый SID, proc_error, zip-бомба, self-XSS); 1 отклонено (cleanup), 5 отложено.
- **2026-08-24-pull-retry-limit-counter.md** — v0.0.72: ретраи pull из ВМ, корректная ошибка лимита сессии, счётчик дедупа.
- **2026-08-24-code-review-sonnet.md** — код-ревью Соннета: 15 багов (SSRF, path traversal, слабый SID, zip-бомба и др.), промпт в docs/code-review-sonnet.md.
- **2026-08-24-upload-logic-schema.md** — подробная схема логики загрузки + найденные баги/несоответствия.
- **2026-08-24-help-button-limits-text.md** — v0.0.70: заметная кнопка HELP (вместо «?») + пояснение статуса «пропущен» в ограничениях.
- **2026-08-24-ui-locks-session-freeze.md** — v0.0.66: блокировки UI (выбор/удаление) на время работы, заморозка сессии + кнопка «Новая сессия».
@@ -0,0 +1,39 @@
# v0.0.73 — фиксы по код-ревью Соннета (безопасность + явные баги) (2026-08-24)
_code-fixes. По ревью `History/sonnet/2026-08-24-code-review-sonnet.md` (промпт `docs/code-review-sonnet.md`)._
_Критично перепроверены в коде; спорные пункты — отклонены/отложены (см. ниже)._
## Исправлено (6 фиксов)
1. **SSRF** (`api_bp.py::upload_refs`) — добавлена валидация `url.startswith(VM_UPLOAD_PREFIX)`;
недоверенный URL пропускается. Константа `VM_UPLOAD_PREFIX = "https://contracts.kube5s.ru/drhider-upload/"`.
2. **Path traversal / zip slip** (`api_bp.py`) — функция `_safe_name()`: нормализует слэши,
отбрасывает `..` и абсолютные пути, сохраняя подпапки (`Подпапка/Акт.txt` → как есть,
`../../evil.pdf` → ""). Применена в `upload` и `upload_refs`.
Unit-тест 8 кейсов — все OK.
3. **Слабый SID** (`session.py`) — `uuid.uuid4().hex[:12]` → `uuid.uuid4().hex` (128 бит).
4. **Серверная ошибка не отображалась** — серверное событие `event: error` (конфликт с встроенным
EventSource) → `event: proc_error`; добавлен клиентский `addEventListener('proc_error', …)`
с показом сообщения.
5. **ZIP-бомба: обход через поддельный `file_size`** (`extractor.py`) — добавлена проверка
`total_uncompressed > MAX_UNCOMPRESSED` ПОСЛЕ `zf.read()` с `break` (ранний выход).
6. **Self-XSS через имя файла** (`index.html`) — добавлена `esc()` и применена к `f.name`
в `rr()` и `procRow()`.
## Отклонено (критично к Соннету)
- **«cleanup(sid) после /download»** — НЕ сделано: ZIP и CSV скачиваются РАЗДЕЛЬНЫМИ запросами;
удаление сессии после отдачи ZIP сломало бы скачивание CSV. Сессия и так чистится по TTL (30 мин).
## Отложено (требуют решения/риск)
- idx-мисматч при `expand_zips` (фильтр расширений на бэке) — связано с фичей v0.0.69
(zip без документов «как есть»), требует решения по поведению.
- Отмена не проверяется в extract-фазе (.doc liberta 120с) — отдельный фикс.
- LLM-таймаут тихо обнуляет чанк — логирование/статус.
- Debug-эндпоинт `session_files` — ограничить/закрыть.
- LLM prompt injection — задокументировать (класс риска, не фикс кода).
## Проверка
- `py_compile` (app.py, api_bp.py, session.py, extractor.py) — OK; `node --check` — OK.
- `_safe_name` unit-тест — 8/8 OK.
- `create_app()` стартует (VERSION 0.0.73).
- Версия 0.0.72 → 0.0.73.
@@ -0,0 +1,29 @@
# Временный сбой сертификата gitea и push (2026-08-24)
_infra. Прецедент, чтобы не терять время в следующий раз._
## Симптом
`git push origin master` к `gitea.services.ngcloud.ru` падал:
```
SSL: certificate subject name (*.ngcloud.ru) does not match target host name 'gitea.services.ngcloud.ru'
```
При этом в браузере `https://gitea.services.ngcloud.ru/Nail/drhider.git` открывался.
## Диагностика
- `gitea.services.ngcloud.ru` → `194.31.9.41` (стабильно, и с машины, и с ВМ).
- На `194.31.9.41:443` отдавался **wildcard `*.ngcloud.ru`** (SAN: `*.ngcloud.ru, ngcloud.ru`),
который НЕ покрывает `gitea.services.ngcloud.ru` (два уровня: `services.ngcloud.ru`).
- Внутренний ClusterIP `10.96.52.11` (gitea.mgmt.nubes.ru, hosts ВМ) недоступен ни с машины, ни с ВМ.
- На ВМ git-настроек нет (нет sslVerify=false), `credential.helper=store`.
## Итог
Проблема была **на стороне gitea/ngcloud.ru** — временно отдавался неверный (wildcard) сертификат.
Через ~10-20 минут сертификат вернулся корректным (`CN = gitea.services.ngcloud.ru`,
SAN `DNS:gitea.services.ngcloud.ru`) → `git push` прошёл (`0479afb..e31476e`).
## Урок
- Если push к gitea падает по SSL «*.ngcloud.ru doesn't match» — это временный сбой сертификата
на стороне gitea (не код). Проверить: `echo | openssl s_client -connect gitea.services.ngcloud.ru:443 -servername gitea.services.ngcloud.ru | openssl x509 -noout -subject -ext subjectAltName`.
- Не менять git config (`sslVerify=false`) и не синкать на ВМ — подождать и повторить push.
- Аналогичный сбой DNS был у `contracts.kube5s.ru` в 18:42 (см. `2026-08-24-pull-dns-transient.md`) —
вероятно, общее инфраструктурное происшествие ngcloud.ru в этот день.
@@ -0,0 +1,49 @@
# Код-ревью DrHider — ответ Соннета (2026-08-24)
_sonnet. Промпт: `docs/code-review-sonnet.md` (12 файлов кода, 8 вопросов)._
_Ревью по v0.0.72. Ничего не исправлено — только задокументировано._
## 🔴 Критичные
| Баг | Файл | ~строка | Суть | Фикс |
|---|---|---|---|---|
| **SSRF** | `api_bp.py` `upload_refs` | ~93 | `url` из клиентского JSON без валидации → `httpx.stream("GET", url)` + `follow_redirects=True`. Доступ к metadata/kube-apiserver | валидировать URL-prefix == `VM_UPLOAD_URL` до запроса |
| **Path traversal / zip slip** | `api_bp.py` + `builder.py` | ~88, ~55 | `name = ref.get("name")` без санитизации → `../../evil.md` в выходном ZIP | `name = os.path.basename(ref.get("name",""))` |
## 🟠 Средние
| Баг | Файл | ~строка | Суть |
|---|---|---|---|
| Серверная ошибка не отображается | `api_bp.py` + `index.html` | ~388, ~840 | сервер шлёт `event: error` — это встроенное имя EventSource; клиент не слушает кастомное → «SSE connection failed» вместо реального msg. Переименовать в `proc_error` |
| idx-мисматч при `expand_zips` на бэке | `obfuscator.py` + `index.html` | ~90, sendIdx | фронт фильтрует zip по `.pdf/.doc/.docx/.txt/.md`, бэк расширяет ВСЕ файлы → число/порядок не совпадает → `sendIdx[idx]=undefined` |
| Слабый SID (48 бит) | `session.py` | 85 | `uuid4().hex[:12]`; убрать `[:12]` → 128 бит |
| ZIP-бомба: обход через поддельный `file_size` | `extractor.py` | ~240 | лимит проверяется по `info.file_size` (central dir, подделывается) ДО чтения; проверять ПОСЛЕ `zf.read()` |
| Неатомарность pull при ошибке | `api_bp.py` `upload_refs` | ~115 | при исчерпании ретраев 502 БЕЗ `session_id`; частичная сессия-сирота до TTL |
## 🟡 Мелкие
| Баг | Файл | ~строка | Суть |
|---|---|---|---|
| Отмена не работает в extract-фазе | `obfuscator.py` | ~130 | в extract-цикле нет `cancel_event.is_set()`; .doc (liberta 120с) → отмена ждёт все извлечения |
| TOCTOU: сессия между check и pause_ttl | `api_bp.py` | ~165-171 | `get_files` (lock снят) → `pause_ttl`; маловероятно |
| `fileTimers` не заполняется | `index.html` | ~792 | мёртвый код, fallback всегда 0.0с |
| Self-XSS: `f.name` в innerHTML | `index.html` | ~280, ~380 | имя файла без escaping → `textContent`/`escapeHtml()` |
| ZIP с non-doc расширениями → мусор | `extractor.py` | ~257 | фильтровать расширения в `expand_zips` |
| Сессия не чистится после `/download` | `api_bp.py` | ~430 | 500МБ висит 30 мин → OOM; `cleanup(sid)` |
| LLM-таймаут тихо обнуляет чанк | `llm_client.py` + `scanner.py` | ~70, ~220 | `except → return []`, PII не найден без предупреждения |
| Debug-эндпоинт `session_files` | `api_bp.py` | ~139 | список файлов любой сессии по SID без токена |
| LLM prompt injection | `scanner.py` | ~200 | текст документа дословно в prompt → подавление NER |
## Подтверждено корректным (Соннет)
- Различимость «сессия не найдена» vs «лимит» ✓
- Гонок на `cancel_event`/`queue` нет (thread-safe) ✓
- Утечек httpx/zipfile/EventSource/таймеров нет ✓
- Пустой txt, битый pdf, 0 файлов/все сверх лимита — обрабатываются ✓
## Что уже исправлено ранее (до этого ревью)
- Ретраи pull (v0.0.72) — Соннет отметил остаточную неатомарность.
- Разделение статусов «пропущен (лимит)» / «не извлечён» (v0.0.71).
- Счётчик дедупа (v0.0.72).
## TODO-связь
- Открытый баг кнопки «Обфусцировать» при 0 файлов — `docs/WhatTODO.md` (не покрыт ревью Соннета).
@@ -0,0 +1,32 @@
# Массовое тестирование v0.0.72 на проде — 4 блока (2026-08-24)
_tests. Прод https://drhider.pythonk8s.dev.nubes.ru/, редеплой 19:25 (v0.0.72)._
## Блок 1 — выбор папки (всё ✓)
- Рекурсия + относительный путь: `Подпапка/Акт1.txt`.
- Вложенный ZIP с кириллицей: `архивы/док.zip` → `архивы/Отчёт/Справка_Сидорова.txt`, `архивы/Счёт_на_оплату.txt` (путь zip + путь внутри).
- Дедуп: дубль `Договор1.txt` отсеян → «Добавлено из папки: 4 файла» (было бы 5).
- Не-документ `.bin` (60 МБ) — пропущен.
## Блок 2 — полный цикл (всё ✓)
- 4 файла: «Обработано 4 файлов: 9.9с, ИИ 9.7с, токены 2508».
- CSV замен (152 Б): `+7 111. → +7_000_000_0001`, `Ивановым И.И. → Семёнов_0001`,
`Петрова Анна → Николаев_0001`, `Сидорова → Семёнов_0002`.
- ZIP (775 Б) с путями: `Договор1.md`, `Подпапка/Акт1.md`, `архивы/Отчёт/Справка_Сидорова.md`, `архивы/Счёт_на_оплату.md`.
- Содержимое обезличено: «Договор с Семёнов_0001, тел +7_000_000_0001» и т.д.
## Блок 3 — прерывание (всё ✓)
- 9 txt, прервано на 9-й секунде: «⏹ Остановлено. Сохранено 4 из 9 файлов + таблица замен (6.5с, токены 3183)».
- Частичный ZIP — только 4 готовых: `файл_3.md, файл_4.md, файл_5.md, файл_7.md`.
- CSV замен сохранён (1149 Б). Кнопки скачивания видны.
## Блок 4 — заморозка/UI (всё ✓)
- После прерывания: `fi.disabled`, `ub.disabled`, «Новая сессия» видна (заморозка).
- HELP открывает модалку (display:flex).
- «Новая сессия»: список очищен, статус пуст, dl-кнопки скрыты.
## Найденный мелкий баг
- После «Новой сессии» кнопка «Обфусцировать» активна при 0 файлов
(`resetAll()`: `rr()` ставит `ub.disabled=cntMain===0` (true), затем `ub.disabled=false` перекрывает).
Нажатие безопасно (`uploadFiles` → `if(sf.length===0) return`), но кнопка выглядит активной.
Фикс — 1 строка (не перекрывать после `rr()`). Открыто, ждёт решения.
@@ -0,0 +1,22 @@
# Тест v0.0.72 на проде — статусы, счётчик, полный цикл (2026-08-24)
_tests. Прод https://drhider.pythonk8s.dev.nubes.ru/, редеплой 19:25 (v0.0.72)._
## Сценарий
Папка: битый PDF (scan1.pdf) + 3 txt + файл 51 МБ (Big.txt).
## Результаты (все ✓)
1. **Счётчик дедупа (v0.0.72)** — папка с дублем [Договор.txt, Договор.txt, Акт.txt]
→ «✅ Добавлено из папки: 3 файла» (дубль отсеян, раньше было бы 4).
2. **Статус «не извлечён» (v0.0.71)** — во время обработки битый `scan1.pdf` в группе
«✓ Обработанные (1)» со статусом «не извлечён» (раньше было «пропущен»).
3. **Статус «пропущен (лимит)» (v0.0.71)** — `Big.txt 51.0 MB` в отдельной группе
«⛔ Пропущены (сверх лимита)» (раньше попадал в «Ожидают обработки»).
4. **Полный цикл** — «✅ Обработано 3 файлов: 5.6с, ИИ 5.4с» (3 txt), битый/большой исключены.
5. **HELP + текст ограничений** (v0.0.70) — на месте.
6. **Ретраи pull (v0.0.72)** — загрузка прошла штатно (код на бэке, косвенная проверка).
## Замечания
- Статусы «не извлечён»/«пропущен (лимит)» видны ТОЛЬКО во время обработки
(3-секционная таблица); после завершения `rr()` рисует обычную таблицу.
- Страница приведена в чистое состояние («Новая сессия» → «Нет выбранных файлов»).
+13 -1
View File
@@ -2,4 +2,16 @@
не надо ДОГАДОК !
не надо самостоятельно что либо "УЛуЧШАТЬ" - никаких изменений рабочего кода без прямого разрешения
есть сомнения - лучше остановись и спроси
ВСЁ записывай в хистори ! что планировал что сделал в чём ошибся и тд
ВСЁ записывай в хистори ! что планировал что сделал в чём ошибся и тд
---
## TODO (следующая правка кода)
1. **Кнопка «Обфусцировать» активна при 0 файлов после «Новой сессии»** (найдено 2026-08-24,
массовое тестирование v0.0.72, `History/tests/2026-08-24-v072-many-tests.md`).
Причина: `resetAll()` сначала вызывает `rr()` (ставит `ub.disabled = cntMain === 0` → true),
затем `ub.disabled = false` перекрывает это. Нажатие безопасно (`uploadFiles` проверяет
`sf.length === 0`), но кнопка выглядит активной при пустом списке.
Фикс: в `resetAll()` не перекрывать `ub.disabled` после `rr()` (либо перенести `ub.disabled`
установку до `rr()`). Бамп до v0.0.73 + редеплой.
+70
View File
@@ -0,0 +1,70 @@
# Код-ревью DrHider — промпт для Соннета
Ты — строгий ревьюер. Проведи код-ревью сервиса DrHider (Flask + vanilla JS) —
обфускация персональных данных в документах.
## СТРОГО просматривай ТОЛЬКО эти файлы (больше ничего не читай)
- `site/app.py`
- `site/routes/api_bp.py`
- `site/session.py`
- `site/templates/index.html`
- `drhider/__init__.py`
- `drhider/builder.py`
- `drhider/config.py`
- `drhider/extractor.py`
- `drhider/llm_client.py`
- `drhider/obfuscator.py`
- `drhider/replacer.py`
- `drhider/scanner.py`
НЕ смотри: `History/`, `docs/`, `tests/`, `file_list.csv`, `files/`, `TMP/`, `TSTFILES/`.
## Контекст (кратко, не лезь в файлы)
- Загрузка файлов: браузер → PUT на ВМ-буфер (`contracts.kube5s.ru/drhider-upload/`),
затем `POST /api/upload_refs` → Flask тянет файлы egress-GET (pull) в сессию.
- Обработка: `GET /api/process_stream/<sid>` (SSE) → воркер-поток → `obfuscate_files()`
→ извлечение текста → regex+LLM-сканирование → замена → ZIP + mapping.csv.
- Лимиты: 50 МБ/файл, 500 МБ/сессия.
- Есть мягкая отмена (кнопка «Прервать», `cancel_event`).
## Вопросы — ответь по каждому, с указанием файла и строки
1. **Корректность загрузки** (`api_bp.py::upload_refs`, `index.html::uploadFiles`):
- атомарность pull при ошибке на середине списка файлов;
- ретраи при DNS/сетевых сбоях — покрыты ли все transient-ошибки;
- различимы ли «сессия не найдена» vs «превышен лимит сессии».
2. **SSE/потоки** (`api_bp.py::process_stream`, `obfuscator.py`):
- корректна ли обработка disconnect клиента и остановка воркера;
- нет ли гонок при `cancel_event`/`queue`/`pause_ttl`/`resume_ttl`;
- TTL сессии: не умирает ли сессия во время долгой обработки.
3. **Соответствие индексов** фронт↔бэк (события `start/done/file_*` с `idx`):
- `obfuscator.py::expand_zips` и `_dedupe_file_names` могут изменить число/порядок файлов
после того, как фронт уже построил список — не ломается ли `sendIdx[d.idx]`?
4. **Статусы файлов** (`index.html`): консистентность «пропущен (лимит)» / «не извлечён» /
«done» / «analyzed» / «pending»; нет ли путей, где статус зависает или перетирается.
5. **Безопасность** (`extractor.py`, `session.py`, `api_bp.py`):
- path traversal через имена файлов (в т.ч. из ZIP);
- zip-бомба (расширение, сжатие, суммарный размер);
- инъекции/экранирование имён в HTML (`index.html` — вывод имён файлов).
6. **Утечки/ресурсы**: закрытие `httpx`-сессий, `zipfile`, `EventSource`/`XHR`, таймеры
(`setInterval`) — всё ли очищается в `resetAll`/`finishProcUI`/`finally`.
7. **Краевые случаи**: пустой файл, файл без текста, битый PDF, дубли имён, отмена
на каждом этапе, 0 файлов, все файлы сверх лимита.
8. **Что я мог пропустить** — любые реальные баги/риски, не перечисленные выше.
## Правила ответа
- БЕЗ воды. Только конкретика: файл, функция, строка, суть бага, серьёзность (🔴/🟠/🟡), фикс.
- Не предлагай «улучшения» вне рамок ревью (рефакторинг, стиль) — только баги и риски.
- Если тебе нужен контекст, которого нет выше (назначение функции, формат данных,
ожидаемое поведение), — **задай уточняющий вопрос, не домысливай**.
- В конце — итоговая таблица: баг | файл | строка | серьёзность | что делать.
+3
View File
@@ -339,6 +339,9 @@ def expand_zips(files: List[Tuple[str, bytes, str]]) -> List[Tuple[str, bytes, s
inner_data = zf.read(info)
total_uncompressed += len(inner_data)
if total_uncompressed > MAX_UNCOMPRESSED:
log.warning("ZIP uncompressed limit exceeded (mid-read): %s", fname)
break
# Вложенные ZIP добавляем в очередь на повторную распаковку
queue.append((name, inner_data, ""))
+1 -1
View File
@@ -21,7 +21,7 @@ if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root)
# Версия приложения (меняется при изменениях)
VERSION = "0.0.72"
VERSION = "0.0.73"
def setup_logging():
+29 -6
View File
@@ -34,6 +34,24 @@ log = logging.getLogger("routes.api_bp")
PULL_RETRIES = 3
PULL_RETRY_DELAY = 2 # секунды между попытками
# Доверенный префикс ВМ-буфера — валидация URL при pull (защита от SSRF)
VM_UPLOAD_PREFIX = "https://contracts.kube5s.ru/drhider-upload/"
def _safe_name(name: str) -> str:
"""Санитизировать имя файла: защита от path traversal, сохраняя подпапки.
Запрещает '..' и абсолютные пути; нормализует слэши. Возвращает "" если
имя пустое или небезопасное.
"""
if not name:
return ""
name = name.replace("\\", "/")
parts = [p for p in name.split("/") if p and p != "."]
if not parts or any(p == ".." for p in parts):
return ""
return "/".join(parts)
def _disconnect_exceptions():
"""Исключения, означающие отключение клиента SSE."""
@@ -54,16 +72,17 @@ def upload():
added = 0
had_unnamed = False
for f in uploaded:
if not f.filename:
name = _safe_name(f.filename)
if not name:
had_unnamed = True
continue
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, name, 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))
MAX_FILE_BYTES // (1024 * 1024), sid, name, len(data))
continue
if not add_file(sid, f.filename, data):
if not add_file(sid, name, data):
log.warning("upload: session not found/limit, sid=%s file=%r", sid, f.filename)
return jsonify({"ok": False, "error": "Session not found"}), 404
added += 1
@@ -96,10 +115,14 @@ def upload_refs():
try:
with httpx.Client(timeout=120, follow_redirects=True) as client:
for ref in refs:
name = ref.get("name")
name = _safe_name(ref.get("name") or "")
url = ref.get("url")
if not name or not url:
continue
# SSRF-защита: тянуть можно ТОЛЬКО с доверенного ВМ-буфера
if not url.startswith(VM_UPLOAD_PREFIX):
log.warning("upload_refs: unsafe URL, skip sid=%s url=%r", sid, url)
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",
@@ -381,7 +404,7 @@ def process_stream(sid):
_, msg = evt
log.error("process_stream: error event sid=%s msg=%r", sid, msg)
try:
yield f"event: error\ndata: {json.dumps({'error': msg})}\n\n"
yield f"event: proc_error\ndata: {json.dumps({'error': msg})}\n\n"
except _disconnect_exceptions() as e:
log.warning("process_stream: disconnect on error sid=%s err=%r", sid, e)
return
+1 -1
View File
@@ -104,7 +104,7 @@ def create_session() -> str:
Returns:
Уникальный идентификатор сессии (UUID).
"""
sid = uuid.uuid4().hex[:12]
sid = uuid.uuid4().hex
with _lock:
_sessions[sid] = {
"files": [],
+11 -2
View File
@@ -280,6 +280,7 @@ function fmtSec(s) {
// Эмпирическая оценка времени обработки файла: сек/МБ (ориентировочно, до старта)
const EST_MB_SEC = 12;
function estForFile(f) { return f ? Math.max(1, Math.round(f.size / 1048576 * EST_MB_SEC)) : 0; }
function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function rr() {
if (procPhase === 'processing') { renderProcTable(); return; }
@@ -290,7 +291,7 @@ function rr() {
const rowCls = over ? ' class="row-over"' : '';
const stTxt = over ? '<span style="color:#c0392b;">🔥 не учитывается</span>'
: '<span style="color:#7d3c98;">~' + fmtSec(estForFile(f)) + '</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>';
return '<tr id="row-' + i + '"' + rowCls + '><td class="name-cell">' + esc(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;
@@ -307,7 +308,7 @@ function procRow(i, stTxt) {
const over = overNames.has(f.name);
const cls = (procState[i] && procState[i].st === 'current') ? ' class="row-current"'
: (over ? ' class="row-over"' : '');
return '<tr' + cls + '><td class="name-cell">' + f.name + '</td><td class="num-cell">' + fs(f.size) + '</td><td class="num-cell" style="font-size:12px;">' + stTxt + '</td><td></td></tr>';
return '<tr' + cls + '><td class="name-cell">' + esc(f.name) + '</td><td class="num-cell">' + fs(f.size) + '</td><td class="num-cell" style="font-size:12px;">' + stTxt + '</td><td></td></tr>';
}
function renderProcTable() {
@@ -936,6 +937,14 @@ async function uploadFiles() {
document.getElementById('newSessionBtn').style.display = 'inline-block';
resolve();
});
activeES.addEventListener('proc_error', function(e) {
const d = JSON.parse(e.data);
finishProcUI();
setBusy(false);
st.className = 'status error';
st.textContent = 'Ошибка обработки: ' + (d.error || 'неизвестная ошибка');
resolve();
});
activeES.onerror = function() {
finishProcUI();
reject(new Error('SSE connection failed'));