Files
drhider/PLAN-upload-layers.md
T
“Naeel” 11716259a1
Deploy drhider / validate (push) Canceled after 0s
1
2026-09-05 13:27:35 +03:00

17 KiB
Raw Blame History

ПЛАН: вынос 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. Файлы мелкие — каждая функция в отдельном файле.
  6. 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 (по шагам, после каждого — проверка)

Строго последовательно, чтобы не сломать прод:

  1. zip/* — подключить fflate (CDN) и написать listZipFiles(file, allowedExt) на fflate.unzip с filter (не распаковывать не-doc) + рекурсия по вложенным zip. Юнит-тест: кириллица, вложенный 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 (логика приложения у каждого своя).
  6. Дерево файлов (сворачивание по папкам/архивам) — см. 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, не кода слоя).