17 KiB
ПЛАН: вынос 2 слоёв загрузки-через-ВМ в переиспользуемый модуль
Для Flash. Источник-референс: drhider v0.0.75 (
site/templates/index.html,site/routes/api_bp.py,site/session.py). Цель — 1:1 по поведению, но в виде переиспользуемого программного слоя ВНУТРИ приложения (не отдельного сервиса): его встраивает и drhider, и любые другие проекты.
Требования (согласовано с владельцем)
- 2 слоя структурно выделены — их можно взять и перенести в другой проект без изменения кода (только свой конфиг).
- Без общей репы — модуль = подпапка внутри проекта, перенос копированием.
- Не мудрить — главное: агент в новом проекте за 2 минуты понял, как интегрировать
(для этого короткий
README.mdвнутри модуля). - НЕ поломать текущий рабочий код drhider — это рефакторинг 1:1 (extract, не rewrite).
- Файлы мелкие — каждая функция в отдельном файле.
- Vanilla JS, без React/сборщика. ZIP раскрытие — на
fflate(вместо ручного парсера): проще и надёжнее (unzip+filterпо расширению до распаковки). Дерево файлов (сворачивание по папкам/архивам) — отдельный планPLAN-file-picker.md.
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) — на базе fflate
list_zip_files.js # listZipFiles(file, allowedExt) — fflate.unzip + filter + рекурсия
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 | → файл модуля | Что меняется при переносе |
|---|---|---|
listZipFiles (+ вложенные parseZip/inflateRaw/decodeZipName/dosToMs) |
zip/list_zip_files.js |
заменяется на fflate (unzip + filter по allowedExt + рекурсия по вложенным zip); ручной парсер НЕ переносим |
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)
{
"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 (по шагам, после каждого — проверка)
Строго последовательно, чтобы не сломать прод:
zip/*— подключитьfflate(CDN) и написатьlistZipFiles(file, allowedExt)наfflate.unzipсfilter(не распаковывать не-doc) + рекурсия по вложенным zip. Юнит-тест: кириллица, вложенный zip, не-doc расширения.table/*(слой 1) — вынести функции таблицы/выбора, состояниеsf/fileMeta/overNamesувести внутрьinit_upload_table.js.upload/*(слой 2 фронт) — PUT +upload_refs, параметризоватьvmUploadUrl.backend/upload_refs/*+backend/session/*— вынести_safe_name, pull, Blueprint, сессию.- Подключить модуль обратно в drhider — заменить встроенный код вызовами модуля, по одному куску, прогоняя тесты после каждого. Слой 3 не трогать.
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 модуля (главное для агента нового проекта)
- Что это — 2 слоя и их ответственность (таблицей из раздела 1).
- Как подключить фронт — подключить
zip/*,table/*,upload/*как ES-модули (<script type="module">), вызватьinitUploadTable(cfg)иuploadViaVM(...). - Как подключить бэк — зарегистрировать
create_upload_refs_blueprint(cfg)в приложении, импортироватьsession(create/add/get). - Какой конфиг задать — один блок
config.example.jsonс пояснением каждого поля. - Что НЕ трогать — слой 3 (логика приложения у каждого своя).
- Дерево файлов (сворачивание по папкам/архивам) — см.
PLAN-file-picker.md(слой 1, vanilla + fflate).
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, не кода слоя).