# ПЛАН: вынос 2 слоёв загрузки-через-ВМ в переиспользуемый модуль > Для Flash. Источник-референс: drhider v0.0.75 (`site/templates/index.html`, > `site/routes/api_bp.py`, `site/session.py`). Цель — **1:1** по поведению, но в виде > **переиспользуемого программного слоя ВНУТРИ приложения** (не отдельного сервиса): > его встраивает и drhider, и любые другие проекты. --- ## Требования (согласовано с владельцем) 1. **2 слоя структурно выделены** — их можно взять и перенести в другой проект **без изменения кода** (только свой конфиг). 2. **Без общей репы** — модуль = подпапка внутри проекта, перенос копированием. 3. **Не мудрить** — главное: агент в новом проекте за 2 минуты понял, как интегрировать (для этого короткий `README.md` внутри модуля). 4. **НЕ поломать текущий рабочий код drhider** — это рефакторинг 1:1 (extract, не rewrite). 5. **Файлы мелкие** — каждая функция в отдельном файле. --- ## 0. Контекст (что уже проверено в drhider) Паттерн загрузки (шлюз managed-кластера рвёт тела >64КБ, egress не ограничен): браузер → `PUT` на ВМ-буфер → Flask `POST /api/upload_refs` → egress `GET` (pull) → сессия. Что НЕЛЬЗЯ потерять при переносе (накоплено и отлажено): - дедуп «имя+размер», иначе суффикс `_2`; - раскрытие ZIP с путями, фильтр расширений (`.pdf .doc .docx .txt .md`); - рекурсивный обход папки (`webkitdirectory`), путь в таблице; - **SSRF-валидация** URL (`url.startswith(VM_UPLOAD_PREFIX)`); - **`_safe_name`** (path traversal, сохраняет подпапки); - **ретраи pull** (3 попытки, пауза 2 с); - лимиты 50 МБ/файл, 500 МБ/сессия; - различение «сессия не найдена» vs «лимит»; - статусы «пропущен (лимит)» / «не извлечён»; - `esc()` для имён (self-XSS). --- ## 1. Граница слоёв | Слой | Где | Ответственность | drhider-источник | |---|---|---|---| | **0. Конфиг** | общий | `VM_UPLOAD_URL`, `allowedExt`, лимиты | константы в `index.html`/`api_bp.py` | | **1. Выбор файлов** | фронт | таблица, дедуп, раскрытие ZIP, путь, статусы, кнопки | `index.html` | | **2. Закачка через ВМ** | фронт + бэк | PUT на ВМ (фронт) → `upload_refs` pull (бэк) → сессия | `index.html` + `api_bp.py` + `session.py` | | **3. Логика приложения** | бэк | обфускация/обработка, SSE-прогресс | `drhider/*`, `process_stream` — **НЕ выносим** | **Слой 3 НЕ выносим** — он у каждого приложения свой. В drhider остаётся: `drhider/*` (extractor/scanner/replacer/builder/obfuscator), `process_stream`, `process`, `download`, `csv_download`, `cancel`. **Всё, что у drhider своё (URL, расширения, лимиты) — в конфиг (слой 0), а не в код слоя.** --- ## 2. Структура модуля (каждая функция — отдельный файл) ``` upload/ README.md # ← главное: как интегрировать (для агента) config.example.json # слой 0: образец конфига frontend/ zip/ # распаковка ZIP (чистые функции, без DOM) dos_to_ms.js # dosToMs(date, time) decode_zip_name.js # decodeZipName(bytes, isUtf8) inflate_raw.js # inflateRaw(bytes) parse_zip.js # parseZip(buf) list_zip_files.js # listZipFiles(file) — рекурсия по вложенным zip table/ # слой 1: выбор файлов fs.js # fs(b) — формат размера fmt_sec.js # fmtSec(s) — формат секунд esc.js # esc(s) — экранирование HTML est_for_file.js # estForFile(f) + EST_MB_SEC (из конфига) add_file_with_dedup.js # addFileWithDedup(file) render.js # rr() — обычная таблица proc_row.js # procRow(i, stTxt) render_proc_table.js # renderProcTable() — 3-секционная таблица rm_file.js # rm(i) — удаление строки set_status.js # ss(idx, h) — статус в ячейке on_files_change.js # обработчик change on_folder_change.js # обработчик webkitdirectory change init_upload_table.js # initUploadTable(cfg) — сборка слоя 1 (состояние внутри) upload/ # слой 2 (фронт): закачка через ВМ put_to_vm.js # PUT одного файла (XHR, progress → setStatus) upload_via_vm.js # uploadViaVM(files, vmUploadUrl) — цикл PUT + POST upload_refs backend/ upload_refs/ # слой 2 (бэк): pull + валидация __init__.py config.py # PULL_RETRIES, PULL_RETRY_DELAY, VM_UPLOAD_PREFIX (из конфига) safe_name.py # _safe_name(name) pull_file.py # pull с ретраями (3×2с, stream, лимит 50МБ) blueprint.py # create_upload_refs_blueprint(cfg) → Blueprint session/ __init__.py state.py # _sessions, _lock, _start_timer (общее состояние) create_session.py # create_session() add_file.py # add_file(sid, name, content) get_files.py # get_files(sid), file_count(sid) store_result.py # store_result(sid, zip), get_result(sid) store_csv.py # store_csv(sid, csv), get_csv(sid) ttl.py # touch, pause_ttl, resume_ttl cancel.py # request_cancel, get_cancel_event cleanup.py # cleanup(sid) ``` > Примечание по `backend/session/`: там **общее глобальное состояние** (`_sessions`, `_lock`). > Поэтому `state.py` выделен отдельно, а каждая операция — свой файл, импортирующий `state`. > Дробить `state.py` на файл-на-переменную не нужно — это данные, а не функции. --- ## 3. Маппинг: что откуда взять (v0.0.75, имена функций точные) Переносится **без изменения логики**. Меняется только способ обращения: глобальные переменные → параметры/состояние модуля. ### Фронт (`site/templates/index.html`) | Функция в drhider | → файл модуля | Что меняется при переносе | |---|---|---| | `dosToMs` | `zip/dos_to_ms.js` | ничего (чистая) | | `decodeZipName` | `zip/decode_zip_name.js` | ничего | | `inflateRaw` | `zip/inflate_raw.js` | ничего | | `parseZip` | `zip/parse_zip.js` | ничего | | `listZipFiles` | `zip/list_zip_files.js` | `allowedExt` → параметр (сейчас захардкожен внутри) | | `fs` | `table/fs.js` | ничего | | `fmtSec` | `table/fmt_sec.js` | ничего | | `esc` | `table/esc.js` | ничего | | `estForFile` | `table/est_for_file.js` | `EST_MB_SEC` → конфиг | | `addFileWithDedup` | `table/add_file_with_dedup.js` | `MAX_FILE_BYTES`/`MAX_SESSION_BYTES` → конфиг; `sf`/`fileMeta`/`overNames` → состояние модуля | | `rr` | `table/render.js` | доступ к `sf`/`overNames` через состояние | | `procRow` | `table/proc_row.js` | ничего (чистая отрисовка) | | `renderProcTable` | `table/render_proc_table.js` | `procState`/`procExtractRate` → параметры/состояние | | `rm` | `table/rm_file.js` | `busy`, `fi.files` → состояние/колбэк | | `ss` | `table/set_status.js` | ничего | | `fi` change handler | `table/on_files_change.js` | вызывается из `initUploadTable` | | `folderInput` change handler | `table/on_folder_change.js` | вызывается из `initUploadTable` | | PUT-цикл из `uploadFiles()` | `upload/put_to_vm.js` | `VM_UPLOAD_URL` → параметр; `ss` → колбэк прогресса | | `uploadFiles()` фазы 1+1b | `upload/upload_via_vm.js` | **только** PUT + `POST upload_refs`; SSE-часть остаётся в drhider | > **Граница слоя 2/3:** `uploadFiles()` сейчас смешанный — в нём и закачка (слой 2), и SSE (слой 3). > При выносе разделить: `uploadViaVM(files, vmUploadUrl)` делает PUT + POST `/api/upload_refs` > и возвращает `{ok, session, count}`; drhider после этого запускает **свою** SSE-обработку. ### Бэк (`site/routes/api_bp.py`, `site/session.py`) | Сейчас | → модуль | Что меняется | |---|---|---| | `PULL_RETRIES`, `PULL_RETRY_DELAY`, `VM_UPLOAD_PREFIX` | `upload_refs/config.py` | читаются из конфига | | `_safe_name` | `upload_refs/safe_name.py` | ничего | | тело `upload_refs` (цикл pull) | `upload_refs/pull_file.py` | вынести ретраи/стрим/лимит в `pull_file(client, url)` | | endpoint `upload_refs` | `upload_refs/blueprint.py` | `create_upload_refs_blueprint(cfg)` — Blueprint с префиксом из конфига | | `session.py` целиком | `backend/session/*` | дробление на файл-на-функцию (1:1) | `session.py` переносится **1:1** — это самодостаточный модуль, просто дробится на функции. --- ## 4. Интерфейсы (точные сигнатуры) ### Слой 1 — `initUploadTable(cfg) -> api` ``` cfg = { allowedExt: ['.pdf','.doc','.docx','.txt','.md'], maxFileBytes: 50*1024*1024, maxSessionBytes: 500*1024*1024, fileInputEl, folderInputEl, tableBodyEl, countEl, uploadBtnEl, onFilesChanged(list) // колбэк } api = { pickFiles(), pickFolder(), // привязка к addFiles(File[]), // дедуп+раскрытие zip+фильтр+лимиты getFiles() -> [{name,size,file}], // ТОЛЬКО учитываемые (без over) getOverNames() -> Set, // сверх лимита setStatus(name, html), // статус в ячейке render(), clear() } ``` ### Слой 2 — фронт `uploadViaVM(files, vmUploadUrl)` ``` uploadViaVM(files, vmUploadUrl) -> Promise<{ok, session, count, error?}> 1. token = crypto.randomUUID() 2. для каждого файла: PUT vmUploadUrl + token + '_' + k (XHR, progress -> setStatus) refs.push({name, size, url}) 3. POST /api/upload_refs {session, files: refs} -> {ok, session, count} | {ok:false, error} ``` ### Слой 2 — бэк `create_upload_refs_blueprint(cfg)` ``` POST /api/upload_refs {session?, files:[{name,size,url}]} - name = _safe_name(name) (path traversal) - url.startswith(VM_UPLOAD_PREFIX) (SSRF) - size > 50МБ -> delete+skip - pull: 3 ретрая, пауза 2с - len(content) > 50МБ -> delete+skip - add_file: нет сессии -> 404; лимит 500МБ -> skip - delete url (best-effort) -> {ok, session, count} ``` --- ## 5. Конфиг (слой 0) ```json { "vmUploadUrl": "https://contracts.kube5s.ru/drhider-upload/", "allowedExt": [".pdf", ".doc", ".docx", ".txt", ".md"], "maxFileBytes": 52428800, "maxSessionBytes": 524288000, "apiPrefix": "/api", "pullRetries": 3, "pullRetryDelay": 2 } ``` Всё, что отличает drhider от другого проекта, задаётся **здесь**. Код слоёв не содержит ни одного drhider-специфичного значения. --- ## 6. Порядок работы для Flash (по шагам, после каждого — проверка) Строго последовательно, чтобы не сломать прод: 1. **`zip/*`** — вынести 5 чистых функций, написать юнит-тест (кириллица, вложенный zip, не-doc расширения). Самый изолированный кусок — безопасен. 2. **`table/*`** (слой 1) — вынести функции таблицы/выбора, состояние `sf`/`fileMeta`/`overNames` увести внутрь `init_upload_table.js`. 3. **`upload/*`** (слой 2 фронт) — PUT + `upload_refs`, параметризовать `vmUploadUrl`. 4. **`backend/upload_refs/*`** + **`backend/session/*`** — вынести `_safe_name`, pull, Blueprint, сессию. 5. **Подключить модуль обратно в drhider** — заменить встроенный код вызовами модуля, **по одному куску**, прогоняя тесты после каждого. Слой 3 не трогать. 6. **`README.md`** модуля — короткая инструкция для агента нового проекта (раздел 8). --- ## 7. Обязательный чек-лист приёмки (без этого «1:1» не достичь) - [ ] дедуп имя+размер, суффикс `_2` при другом размере; - [ ] рекурсивный обход папки, путь (в т.ч. из архива) в таблице; - [ ] раскрытие ZIP только документов, вложенные ZIP; - [ ] SSRF-валидация URL, `_safe_name`, ретраи pull — на месте; - [ ] лимиты 50/500 МБ, статусы «пропущен (лимит)» и «не извлечён»; - [ ] `esc()` имён (нет XSS); - [ ] кнопка загрузки disabled при 0 файлов; - [ ] заморозка UI во время загрузки/обработки; - [ ] прогресс загрузки (PUT) в таблице; - [ ] поведение drhider на проде не изменилось. --- ## 8. Что писать в `README.md` модуля (главное для агента нового проекта) 1. **Что это** — 2 слоя и их ответственность (таблицей из раздела 1). 2. **Как подключить фронт** — подключить `zip/*`, `table/*`, `upload/*` как ES-модули (`