From a18877415d5f91e455b73b757f4e9d32da41ae5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Tue, 25 Aug 2026 07:57:55 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20PLAN-upload-?= =?UTF-8?q?layers=20(2=20=D1=81=D0=BB=D0=BE=D1=8F,=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB-=D0=BD=D0=B0-=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8E,?= =?UTF-8?q?=20README=20=D0=B4=D0=BB=D1=8F=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PLAN-upload-layers.md | 290 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 PLAN-upload-layers.md diff --git a/PLAN-upload-layers.md b/PLAN-upload-layers.md new file mode 100644 index 0000000..09dd10f --- /dev/null +++ b/PLAN-upload-layers.md @@ -0,0 +1,290 @@ +# ПЛАН: вынос 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-модули + (`