Files
upload-platform/PLAN.md
T

17 KiB
Raw Blame History

Upload Platform — подробный план реализации

LEGACY — документ устарел и не является планом текущей реализации. Этот файл описывает отменённую архитектуру с VM upload, backend sessions, Flask API, лимитами загрузки и слоем /api/process. Текущий проект работает как browser-only file-picker: выбор файлов, папок и ZIP выполняется в браузере, а backend не принимает и не хранит выбранные файлы.

Актуальное описание: README.md и upload/README.md. История перехода на picker-only и исправлений находится в HISTORY/2026-09-05-file-picker.md.

TODO следующей правки: UX загрузки

  • Показывать таймер и понятное пояснение уже до первого сетевого ответа.
  • Отдельно отображать этапы PUT в буфер ВМ, GET в RAM, DELETE из буфера.
  • Показывать прогресс пачки: текущий файл, общее количество, уже доставленные файлы.
  • Не оставлять пользователя без обратной связи при DNS/TLS/CORS-задержке и повторных попытках.
  • Перед отменой явно сообщать, сколько файлов уже доставлено в RAM и что будет сохранено.
  • Проверить UX на мобильном экране и при медленном соединении.

Весь текст ниже сохранён только как исторический план и не должен использоваться для новых изменений без отдельного пересмотра требований.

Целевой исполнитель: Flash / GPT-5.6 Luna. План самодостаточен: читай и кодируй по нему. Источник-референс (паттерн уже отлажен): drhider v0.0.75 (site/templates/index.html, site/routes/api_bp.py, site/session.py, upload/). Здесь — standalone проект: те же 2 слоя, но вынесены в отдельный репозиторий и демонстрируются на минимальной обёртке.


0. Цель и границы

Что делаем: отдельный переиспользуемый сервис загрузки файлов через ВМ-буфер. Паттерн: браузер → PUT на ВМ-буфер → Flask POST /api/upload_refs (egress pull) → сессия.

Что входит (переиспользуемое):

  • Слой 1 — выбор файлов (фронт): таблица/дерево, дедуп, раскрытие ZIP, рекурсивный обход папки.
  • Слой 2 — закачка через ВМ (фронт + бэк): PUT, pull, сессия, лимиты, валидации.

Что НЕ входит (у каждого приложения своё):

  • Слой 3 — логика приложения (обфускация/обработка/SSE-прогресс). Здесь — только стаб-заглушка, которая показывает, что файлы доехали до сессии (список + размеры), без реальной обработки.

Жёсткие требования:

  1. Vanilla JS, без React/сборщика. Единственная новая зависимость — fflate (CDN) для ZIP.
  2. Каждая функция — отдельный файл (мелкие файлы).
  3. Всё, что отличает один проект от другого — в конфиге, не в коде слоёв.
  4. Безопасность сохранить 1:1: SSRF-валидация URL, _safe_name (path traversal), ретраи pull, лимиты, esc() имён (self-XSS).
  5. Запуск на Nubes: структура site/app.py, python site/app.py, 0.0.0.0:5000, маршрут /health → 200 (иначе liveness-проба платформы убивает под), debug=False.

1. Архитектура (слои)

Слой Где Ответственность
0. Конфиг общий vmUploadUrl, allowedExt, лимиты, ретраи
1. Выбор файлов фронт таблица/дерево, дедуп, раскрытие ZIP, папка, путь, статусы, кнопки
2. Закачка через ВМ фронт + бэк PUT на ВМ (фронт) → upload_refs pull (бэк) → сессия
3. Логика приложения бэк стаб (в реальном проекте своя — сюда приходит SSE/обработка)

Слой 3 здесь — заглушка. Он НЕ переиспользуется. Его задача — доказать, что слой 2 корректно передаёт файлы в сессию и что в новом проекте к нему легко прицепить свою логику.


2. Структура проекта (обязательная)

upload-platform/
├── requirements.txt
├── README.md                      # что это + как интегрировать слои в другой проект
├── PLAN.md                        # этот план
├── config.json                    # слой 0 (рабочий конфиг ЭТОГО демо)
├── upload/                        # ← переиспользуемый модуль (копируется в любой проект)
│   ├── README.md                  # короткая инструкция интеграции (для агента)
│   ├── config.example.json        # образец конфига
│   ├── frontend/
│   │   ├── zip/
│   │   │   └── list_zip_files.js  # listZipFiles(file, allowedExt) на fflate
│   │   ├── table/                 # слой 1
│   │   │   ├── fs.js
│   │   │   ├── esc.js
│   │   │   ├── add_file_with_dedup.js
│   │   │   ├── render.js
│   │   │   ├── on_files_change.js
│   │   │   ├── on_folder_change.js
│   │   └── upload/                # слой 2 (фронт)
│   │       ├── put_to_vm.js
│   │       └── upload_via_vm.js
│           ├── state.py
│           ├── create_session.py
│           ├── add_file.py
│           ├── get_files.py
│           └── cleanup.py
└── site/                          # демо-обёртка (НЕ переиспользуется)
    ├── app.py                     # точка входа, / и /health
    ├── routes/
    │   └── api_bp.py              # регистрирует blueprint из upload/ + стаб слоя 3
    ├── templates/
    │   └── index.html             # слой 1 + 2 (подключает ES-модули из upload/frontend)
    └── static/
        ├── style.css
        └── vendor/fflate.min.js   # fflate (локально, не CDN — надёжнее)

site/__init__.py НЕ создавать — конфликтует со stdlib site.py (Nubes HowTo). Импорты бэка — всегда без префикса site. (запуск python site/app.py добавляет site/ в path). Без factory pattern — платформа ждёт app на уровне модуля.


3. Слой 0 — конфиг (config.json / config.example.json)

{
  "vmUploadUrl": "https://contracts.kube5s.ru/drhider-upload/",
  "allowedExt": [".pdf", ".doc", ".docx", ".txt", ".md"],
  "maxFileBytes": 52428800,
  "maxSessionBytes": 524288000,
  "apiPrefix": "/api",
  "pullRetries": 3,
  "pullRetryDelay": 2
}

Каждое поле:

  • vmUploadUrl — префикс ВМ-буфера (для PUT и SSRF-валидации pull).
  • allowedExt — допустимые расширения (фильтр и в ZIP, и в папке).
  • maxFileBytes / maxSessionBytes — лимиты.
  • apiPrefix — префикс маршрутов бэка.
  • pullRetries / pullRetryDelay — ретраи pull (защита от разовых DNS/сетевых сбоев).

В бэке конфиг читается из os.getenv (на Nubes — через jsonEnv), значения по умолчанию — из config.example.json. Никаких drhider-специфичных значений в коде слоёв.


4. Слой 1 — выбор файлов (фронт)

Поведение (1:1 с drhider)

  • Выбор файлов (<input type="file" multiple>).
  • Выбор папки (webkitdirectory) → рекурсивный обход, в таблице виден относительный путь.
  • .zip → раскрытие через fflate.unzip с filter по allowedExt (не распаковывать не-doc), рекурсивно по вложенным .zip; путь в таблице — Папка/архив.zip/Внутри/файл.txt.
  • Дедуп по «путь+размер»; имя совпало, размер другой → суффикс _2.
  • Лимиты: файл > maxFileBytes → «не учитывается»; сумма учитываемых > maxSessionBytes → тоже.
  • Имена экранируются через esc() (self-XSS).
  • Кнопка загрузки disabled при 0 учитываемых файлов; UI замораживается во время загрузки.

Интерфейс initUploadTable(cfg) -> api

cfg = {
  allowedExt, maxFileBytes, maxSessionBytes,
  fileInputEl, folderInputEl, tableBodyEl, countEl, uploadBtnEl,
  onFilesChanged(list)
}
api = {
  pickFiles(), pickFolder(),
  addFiles(File[]),              // дедуп + раскрытие zip + фильтр + лимиты
  getFiles() -> [{path,name,size,file}],   // ТОЛЬКО учитываемые
  getOverNames() -> Set,
  setStatus(path, html),
  render(), clear()
}

Состояние (sf, fileMeta, overNames) — внутри init_upload_table.js, наружу не торчит.

Файлы и их функции

Файл Функция Примечание
zip/list_zip_files.js listZipFiles(file, allowedExt) fflate.unzip + filter + рекурсия
table/fs.js fs(b) формат размера
table/esc.js esc(s) экранирование HTML
table/add_file_with_dedup.js addFileWithDedup(file) дедуп + лимиты
table/render.js render() обычная таблица
table/set_status.js setStatus(path, html) статус в ячейке
table/on_files_change.js обработчик file input
table/on_folder_change.js обработчик webkitdirectory
table/init_upload_table.js initUploadTable(cfg) сборка слоя 1

Дерево (сворачивание по папкам/архивам) — фаза 2, см. PLAN-file-picker.md (в drhider). В первой итерации — плоская таблица с колонкой «путь», как сейчас в drhider.


5. Слой 2 — закачка через ВМ

Фронт: 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}
  • put_to_vm.jsputToVm(file, url, onProgress) (XHR, сырое тело, прогресс в ячейку).
  • upload_via_vm.jsuploadViaVM(files, vmUploadUrl).
  • SSE-часть (слой 3) сюда НЕ входит — возвращаем {ok, session, count}, дальше приложение само.

Бэк: create_upload_refs_blueprint(cfg) -> Blueprint

POST /api/upload_refs  {session?, files:[{name,size,url}]}
  - name = _safe_name(name)            (path traversal)
  - url.startswith(vmUploadUrl)        (SSRF)
  - size > maxFileBytes -> delete+skip
  - pull: pullRetries × (пауза pullRetryDelay), stream, лимит maxFileBytes
  - len(content) > maxFileBytes -> delete+skip
  - add_file: нет сессии -> 404; лимит сессии -> skip
  - delete url (best-effort)
  -> {ok, session, count}
  • upload_refs/config.py — ретраи/префикс из конфига.
  • upload_refs/safe_name.pysafe_name(name) (path traversal, сохраняет подпапки).
  • upload_refs/pull_file.pypull_file(client, url) (ретраи, stream, лимит).
  • upload_refs/blueprint.pycreate_upload_refs_blueprint(cfg).

Сессия (backend/session/)

Самодостаточный модуль, дробится на файл-на-функцию. Общее состояние (_sessions, _lock) — в state.py; операции импортируют state.

  • create_session.pycreate_session()
  • add_file.pyadd_file(sid, name, content) → bool (лимит)
  • get_files.pyget_files(sid), file_count(sid)
  • cleanup.pycleanup(sid)
  • (при необходимости позже: store_result, store_csv, ttl, cancel)

6. Слой 3 — стаб (демо-обёртка site/)

  • site/app.pyapp = Flask(...), @app.route("/")index.html, @app.route("/health")"ok", 200, if __name__ == "__main__": app.run(debug=False, host="0.0.0.0", port=5000).
  • site/routes/api_bp.py — регистрирует create_upload_refs_blueprint(cfg), добавляет демо-маршрут POST /api/process → возвращает список файлов сессии (имя + размер) — доказательство, что слой 2 довёз файлы; в реальном проекте здесь будет своя обработка.
  • site/templates/index.html<script type="module"> подключает initUploadTable + uploadViaVM, по завершении закачки вызывает /api/process и показывает результат.

7. Порядок реализации (по шагам, после каждого — проверка)

  1. requirements.txt + site/app.py с / и /health, debug=False. Проверить локально.
  2. backend/session/* — перенести 1:1 из drhider session.py, разбить на файлы.
  3. backend/upload_refs/*safe_name, pull_file, config, blueprint.
  4. frontend/zip/list_zip_files.jsfflate (локально static/vendor/fflate.min.js), filter по allowedExt, рекурсия по вложенным zip. Юнит: кириллица, вложенный zip, не-doc.
  5. frontend/table/* — слой 1, состояние внутрь init_upload_table.js.
  6. frontend/upload/*putToVm + uploadViaVM.
  7. site/templates/index.html — собрать слои 1+2, подключить стаб слоя 3.
  8. upload/README.md + README.md — инструкция интеграции для агента нового проекта.
  9. Деплой на Nubes — убедиться, что /, /health, закачка и /api/process работают.

8. Чек-лист приёмки (без этого не готово)

  • /health → 200; / → 200 с версией; debug=False.
  • дедуп путь+размер, суффикс _2 при другом размере;
  • рекурсивный обход папки, путь (в т.ч. из архива) в таблице;
  • раскрытие ZIP только документов, вложенные ZIP;
  • SSRF-валидация URL, safe_name, ретраи pull — на месте;
  • лимиты файл/сессия, статусы «пропущен (лимит)» и «не извлечён»;
  • esc() имён (нет XSS);
  • кнопка загрузки disabled при 0 файлов; заморозка UI во время загрузки; прогресс PUT;
  • после закачки /api/process возвращает список файлов сессии.

9. Что НЕ делать

  • React / сборщик / jQuery-деревья (jsTree, Fancytree).
  • site/__init__.py, from site.xxx import ..., factory pattern.
  • CDN для fflate (класть локально в static/vendor/).
  • Реальную обработку файлов (слой 3) — только стаб.
  • Менять drhider под этот проект — модуль копируется, не связывается.