docs: детализировать PLAN-upload-layers (2 слоя, файл-на-функцию, README для агента)
This commit is contained in:
@@ -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 # обработчик <input type=file> 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(), // привязка к <input>
|
||||
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-модули
|
||||
(`<script type="module">`), вызвать `initUploadTable(cfg)` и `uploadViaVM(...)`.
|
||||
3. **Как подключить бэк** — зарегистрировать `create_upload_refs_blueprint(cfg)` в приложении,
|
||||
импортировать `session` (create/add/get).
|
||||
4. **Какой конфиг задать** — один блок `config.example.json` с пояснением каждого поля.
|
||||
5. **Что НЕ трогать** — слой 3 (логика приложения у каждого своя).
|
||||
|
||||
---
|
||||
|
||||
## 9. Что НЕ менять в drhider (риск регресса)
|
||||
|
||||
- `drhider/*` (extractor/scanner/replacer/builder/obfuscator) — слой 3, не трогать.
|
||||
- `process_stream` (SSE), `process`, `download`, `csv_download`, `cancel` — слой 3.
|
||||
- SSE-часть `uploadFiles()`, `downloadZip()`, `downloadCsv()`, `resetAll()`, `setBusy()`,
|
||||
`finishProcUI()` — приложение drhider.
|
||||
- Рефакторить drhider под модуль — только после того, как модуль заработает на новом сервисе.
|
||||
|
||||
---
|
||||
|
||||
## 10. Открытое решение (спросить у владельца)
|
||||
|
||||
- **Куда класть модуль в новом проекте**: папка `upload/` внутри проекта (согласовано —
|
||||
без общей репы). CORS на ВМ уже настроен под существующий origin — менять только если
|
||||
реально другой домен (это правка nginx, не кода слоя).
|
||||
Reference in New Issue
Block a user