docs: add LAYER2-RESUME.md and layer 2 architecture history record
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# 2026-09-06: Архитектурные решения Слоя 2 и границы независимых API слоёв
|
||||
|
||||
## 1. Контекст и цели
|
||||
Зафиксированы ключевые проектные и архитектурные решения по разработке Слоя 2 в репозитории `upload-platform` перед началом реализации:
|
||||
1. Полная автономность и независимость каждого слоя как отдельного API.
|
||||
2. Легкость интеграции обоих слоёв (Слой 1 + Слой 2) в целевые сервисы (`drhider`, `contractor`) без переделки исходного кода ядра.
|
||||
3. Составлено подробное ТЗ и резюме в файле `LAYER2-RESUME.md`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Архитектурные границы слоёв и API-контракты
|
||||
|
||||
### Слой 1: File Picker (v0.1.13 — готов и протестирован)
|
||||
- **Зона ответственности**: выбор отдельных файлов, выбор папок (`webkitdirectory`), клиентская рекурсивная распаковка ZIP-архивов с помощью `fflate`, дедупликация, фильтрация по расширениям, проверка лимитов, отрисовка древовидного UI.
|
||||
- **API-контракт**: метод `picker.getFiles()` возвращает плоский массив стандартных объектов `File`. Слой полностью изолирован от транспорта и бэкенда.
|
||||
|
||||
### Слой 2: Транзитная доставка через ВМ-буфер в RAM бэкенда (текущая задача)
|
||||
- **Причина существования**: Ingress-контроллер k8s-кластера (`pythonk8s`) обрывает входящие запросы с телом более 64 КБ. Прямой `POST` больших файлов в кластер невозможен; исходящий трафик (egress) из кластера не ограничен.
|
||||
- **Принципиальные решения**:
|
||||
1. **Пофайловый транзит (Per-File Transit)** вместо накопления всей пачки в буфере:
|
||||
- Файл $k$ отправляется браузером через `PUT` в буфер на ВМ.
|
||||
- Браузер сразу отправляет `POST /api/upload_refs` бэкенду во Flask для одного файла $k$.
|
||||
- Бэкенд забирает файл исходящим потоковым `GET` прямо в RAM сессии (`upload/backend/session/state.py`).
|
||||
- Бэкенд немедленно отправляет `DELETE` на ВМ.
|
||||
- Файл на ВМ удалён, буфер чист. Браузер переходит к файлу $k+1$.
|
||||
- В любой момент времени в буфере на ВМ находится максимум один файл.
|
||||
2. **RAM-only (хранение строго в памяти)**:
|
||||
- Никакой записи на диск ни на бэкенде, ни на ВМ в постоянном режиме.
|
||||
3. **Zero Hardcode**:
|
||||
- Полная параметризация через конфигурацию (`vmUploadUrl` на фронтенде, `vmUploadPrefix` на бэкенде для обязательной SSRF-валидации).
|
||||
4. **Автономный mock-буфер**:
|
||||
- In-memory mock WebDAV (`PUT`, `GET`, `DELETE`) внутри тестового стенда `site/app.py` для автономного прогона pytest и Playwright без внешней сети.
|
||||
- **API-контракт фронтенда**: функция `uploadFilesViaVm(files, options)` с колбэками прогресса по файлу/пачке, завершения файла и поддержкой отмены (`AbortController`).
|
||||
- **API-контракт бэкенда**: Blueprint `upload_refs` и менеджер сессий `session` (создание сессии, потоковое вытягивание, хранение в RAM, TTL, мягкая отмена).
|
||||
|
||||
### Слой 3: Бизнес-логика потребителей (не входит в текущую задачу)
|
||||
- Сервисы `drhider` (обфускация, LLM) и `contractor` (парсинг, diff договоров) забирают файлы из оперативной памяти сессии через `session.get_files(session_id)`.
|
||||
- В `upload-platform` Слой 3 эмулируется тестовым callback/эндпоинтом, фиксирующим успешный приём файла из сессии для дальнейшей обработки.
|
||||
|
||||
---
|
||||
|
||||
## 3. Файловая структура Слоя 2 в `upload-platform`
|
||||
|
||||
```
|
||||
upload-platform/
|
||||
├── upload/
|
||||
│ ├── frontend/
|
||||
│ │ ├── index.js # Слой 1 (initFilePicker)
|
||||
│ │ ├── table/ # UI Слоя 1
|
||||
│ │ ├── zip/ # fflate
|
||||
│ │ └── upload/ # Слой 2 (Фронтенд):
|
||||
│ │ ├── put_to_vm.js # XHR PUT одного файла на буфер с прогрессом и abort
|
||||
│ │ └── upload_via_vm.js # Пофайловый транзит (PUT -> upload_refs -> repeat)
|
||||
│ └── backend/ # Слой 2 (Бэкенд):
|
||||
│ ├── __init__.py
|
||||
│ ├── upload_refs/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── blueprint.py # Flask Blueprint (POST /api/upload_refs)
|
||||
│ │ ├── pull_file.py # Исходящий потоковый GET с ретраями (httpx)
|
||||
│ │ ├── safe_name.py # Санитизация имен файлов
|
||||
│ │ └── config.py # Конфигурация по умолчанию
|
||||
│ └── session/
|
||||
│ ├── __init__.py
|
||||
│ ├── state.py # In-memory хранилище сессий
|
||||
│ ├── create_session.py
|
||||
│ ├── add_file.py
|
||||
│ ├── get_files.py
|
||||
│ ├── store_result.py
|
||||
│ ├── store_csv.py
|
||||
│ ├── ttl.py
|
||||
│ ├── cancel.py
|
||||
│ └── cleanup.py
|
||||
├── site/
|
||||
│ ├── app.py # Flask demo + local RAM mock buffer
|
||||
│ └── templates/index.html
|
||||
├── tests/ # pytest + browser smoke
|
||||
├── LAYER2-RESUME.md # Техническое задание и сводка
|
||||
├── build.mjs
|
||||
├── package.json
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Зафиксированные факты инфраструктуры
|
||||
1. **ВМ `5.172.178.213`**: Nginx WebDAV настроен на сброс на диск (`/var/www/drhider-upload/`, `/var/www/contracts-upload/`) с cron-очисткой каждые 5 мин (`find ... -mmin +30 -delete`). Для целевого прода требуется перевод в RAM/in-memory буфер.
|
||||
2. **Кластер k8s**: неймспейсы `20a75175-a58c-49cb-b8fa-e86367b1a8dc` (`drhider`), `b4523aba-b5e6-40f1-be56-bb4d2509357c` (`contractor`), `0e108526-5bb2-4757-9499-3f0bac4f0e83` (`uploader-dev`).
|
||||
@@ -0,0 +1,145 @@
|
||||
# РЕЗЮМЕ И ТЕХНИЧЕСКОЕ ЗАДАНИЕ: СЛОЙ 2 (ЗАГРУЗКА ЧЕРЕЗ ВМ-БУФЕР)
|
||||
|
||||
Документ составлен 2026-09-06 для старта нового рабочего контекста в репозитории [upload-platform](upload-platform).
|
||||
|
||||
---
|
||||
|
||||
## 1. ЦЕЛЬ И АРХИТЕКТУРНЫЕ ГРАНИЦЫ
|
||||
|
||||
В репозитории [upload-platform](upload-platform) разрабатывается и изолированно тестируется переиспользуемый встраиваемый модуль, состоящий из двух независимых слоёв:
|
||||
|
||||
1. **Слой 1 (Выбор файлов / File Picker)** — **ГОТОВ И ПРОТЕСТИРОВАН (v0.1.13)**:
|
||||
- Фронтенд-модуль на Vanilla JS + `fflate`.
|
||||
- Поддерживает выбор отдельных файлов, выбор папок (`webkitdirectory`), распаковку ZIP и вложенных архивов.
|
||||
- Дедупликация, фильтрация по расширениям, древовидный UI, лимиты (`maxEntries`, `maxEntryBytes`, `maxTotalBytes`, `maxDepth`).
|
||||
- Возвращает чистый плоский массив выбранных объектов `File` через метод `picker.getFiles()`. Не зависит от способа передачи данных.
|
||||
|
||||
2. **Слой 2 (Транзитная загрузка файлов через ВМ-буфер)** — **ТЕКУЩАЯ ЗАДАЧА**:
|
||||
- Задача слоя: доставить файлы из браузера в память сессии бэкенд-приложения (Flask).
|
||||
- **Почему нужен буфер**: шлюз ingress managed-кластера Kubernetes (`pythonk8s`) обрывает входящие HTTP-запросы с телом более 64 КБ. Прямой `POST /upload` больших файлов в кластер невозможен. При этом исходящий трафик (egress) из кластера не ограничен.
|
||||
- **Решение**: Браузер загружает файлы во внешний буфер на ВМ, а бэкенд вытягивает их исходящими `GET`-запросами (pull) в оперативную память своей сессии.
|
||||
- **Потребители модуля**: сервис анонимизации `drhider` и сервис сверки договоров (`contractor`). Оба сервиса встраивают Слой 1 + Слой 2 как подпапку `upload/` без изменения исходного кода модулей (только собственный конфиг) и поверх накладывают свою бизнес-логику (Слой 3: LLM, обфускация, парсинг, diff).
|
||||
|
||||
---
|
||||
|
||||
## 2. ФАКТИЧЕСКОЕ СОСТОЯНИЕ ИНФРАСТРУКТУРЫ (ПРОВЕРЕНО НА ВМ И В K8S)
|
||||
|
||||
Проверено 2026-09-06 прямыми командами на ВМ и в кластере:
|
||||
|
||||
1. **ВМ `5.172.178.213` (contracts.kube5s.ru)**:
|
||||
- Доступ: `ssh -i ~/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213`.
|
||||
- В `nginx` настроены WebDAV-буферы:
|
||||
- `/drhider-upload/` $\rightarrow$ `alias /var/www/drhider-upload/;`, CORS: `https://drhider.pythonk8s.dev.nubes.ru`.
|
||||
- `/contracts-upload/` $\rightarrow$ `alias /var/www/contracts-upload/;`, CORS: `https://contractor.pythonk8s.dev.nubes.ru`.
|
||||
- **Проблема текущей реализации на ВМ**:
|
||||
- Nginx WebDAV пишет файлы на диск (корневой раздел).
|
||||
- Cron каждые 5 минут удаляет файлы старше 30 минут: `find /var/www/drhider-upload -type f -mmin +30 -delete`.
|
||||
- По требованию безопасности файлы не должны оседать на диске — обработка и буферизация должны быть строго в оперативной памяти (RAM).
|
||||
|
||||
2. **Kubernetes-кластер (`iot-naeel`)**:
|
||||
- Под `drhider`: namespace `20a75175-a58c-49cb-b8fa-e86367b1a8dc`, хост `drhider.pythonk8s.dev.nubes.ru`.
|
||||
- Под `contractor`: namespace `b4523aba-b5e6-40f1-be56-bb4d2509357c`, хост `contractor.pythonk8s.dev.nubes.ru`.
|
||||
- Под `uploader-dev` (полигон): namespace `0e108526-5bb2-4757-9499-3f0bac4f0e83`, хост `uploader-dev.pythonk8s.dev.nubes.ru` (на нём крутится `upload-platform` 0.1.13).
|
||||
|
||||
---
|
||||
|
||||
## 3. ПРИНЦИПИАЛЬНЫЕ АРХИТЕКТУРНЫЕ ИЗМЕНЕНИЯ СЛОЯ 2
|
||||
|
||||
### Изменение 1: Пофайловый транзит (Streaming / Per-File Transit) вместо батча
|
||||
- **Как было раньше**: Браузер в цикле заливал все файлы пачки в буфер на ВМ, и только в самом конце отправлял во Flask один общий запрос `/api/upload_refs` со списком всех ссылок. Буфер был вынужден одновременно хранить все файлы пачки (до сотен мегабайт).
|
||||
- **Как должно быть**:
|
||||
1. Браузер берет **один файл** $\rightarrow$ делает `PUT` в буфер на ВМ.
|
||||
2. Браузер сразу делает `POST /api/upload_refs` для **этого одного файла**.
|
||||
3. Flask вытягивает его исходящим потоковым `GET` прямо в память сессии (`site/session.py`) и сразу отправляет `DELETE` на ВМ.
|
||||
4. Файл на ВМ удален. Браузер переходит к следующему файлу.
|
||||
- **Результат**: В буфере на ВМ в любой момент времени находится максимум один файл. Общий объем памяти буфера минимален.
|
||||
|
||||
### Изменение 2: Хранение строго в памяти (RAM)
|
||||
- Никаких временных файлов на диске.
|
||||
- На бэкенде Flask файлы хранятся в RAM-структуре сессии (`upload/backend/session/state.py`).
|
||||
- На ВМ для буфера не должен использоваться сброс на диск (на стороне Nginx требуется проксирование в память или легковесный in-memory микросервис).
|
||||
|
||||
### Изменение 3: Полная параметризация (Zero Hardcode)
|
||||
- Прямой IP из браузера поверх HTTPS невозможен (Mixed Content, отсутствие доверенного TLS-сертификата).
|
||||
- В коде не должно быть зашитых хостов (`contracts.kube5s.ru` и т.д.).
|
||||
- Все адреса передаются через конфигурацию:
|
||||
- Фронтенд: `vmUploadUrl` (например, `https://contracts.kube5s.ru/drhider-upload/` или локальный URL).
|
||||
- Бэкенд: `vmUploadPrefix` (для обязательной SSRF-валидации входящих URL перед pull).
|
||||
|
||||
### Изменение 4: Автономный mock-буфер для тестов в `upload-platform`
|
||||
- Тесты в [upload-platform](upload-platform) не должны зависеть от внешней сети или чужих боевых ВМ.
|
||||
- В тестовый стенд и dev-сервер [site/app.py](upload-platform/site/app.py) встраивается локальный легковесный in-memory mock WebDAV (обработка `PUT`, `GET`, `DELETE`), что позволяет гонять Playwright- и Python-тесты полностью оффлайн.
|
||||
|
||||
---
|
||||
|
||||
## 4. СТРУКТУРА МОДУЛЯ В `upload-platform`
|
||||
|
||||
```
|
||||
upload-platform/
|
||||
├── upload/
|
||||
│ ├── frontend/
|
||||
│ │ ├── index.js # Точка входа Слоя 1 (initFilePicker)
|
||||
│ │ ├── table/ # Логика и UI Слоя 1
|
||||
│ │ ├── zip/ # Распаковка архивов (fflate)
|
||||
│ │ └── upload/ # СЛОЙ 2 (ФРОНТЕНД):
|
||||
│ │ ├── put_to_vm.js # XHR PUT одного файла на буфер с прогрессом и abort
|
||||
│ │ └── upload_via_vm.js # Пофайловый транзит (PUT -> upload_refs -> repeat)
|
||||
│ └── backend/ # СЛОЙ 2 (БЭКЕНД):
|
||||
│ ├── __init__.py
|
||||
│ ├── upload_refs/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── blueprint.py # Flask Blueprint (POST /api/upload_refs)
|
||||
│ │ ├── pull_file.py # Исходящий потоковый GET с ретраями (httpx)
|
||||
│ │ ├── safe_name.py # Санитизация имен (защита от path traversal)
|
||||
│ │ └── config.py # Дефолтные параметры (retries, delay)
|
||||
│ └── session/
|
||||
│ ├── __init__.py
|
||||
│ ├── state.py # In-memory хранилище сессий (_sessions, lock, TTL)
|
||||
│ ├── create_session.py # Создание сессии (UUID)
|
||||
│ ├── add_file.py # Добавление файла с проверкой лимита памяти
|
||||
│ ├── get_files.py # Получение списка файлов
|
||||
│ ├── store_result.py # Сохранение ZIP-результата
|
||||
│ ├── store_csv.py # Сохранение CSV-результата
|
||||
│ ├── ttl.py # touch, pause_ttl, resume_ttl
|
||||
│ ├── cancel.py # Мягкая отмена сессии (Event)
|
||||
│ └── cleanup.py # Очистка сессии
|
||||
├── site/
|
||||
│ ├── app.py # Flask demo (Слой 1 + Слой 2 + local mock buffer)
|
||||
│ └── templates/index.html # Demo UI со сквозным сценарием выбора и загрузки
|
||||
├── tests/ # Тесты Слоя 2 (pytest + browser smoke)
|
||||
├── build.mjs # Сборка бандлов (ESM / IIFE)
|
||||
├── package.json # Версионирование платформы
|
||||
└── requirements.txt # Зависимости Python (Flask, httpx, pytest)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. ПОШАГОВЫЙ ПЛАН РЕАЛИЗАЦИИ В НОВОМ ЧАТЕ
|
||||
|
||||
1. **Шаг 1. Перенос и адаптация бэкенда Слоя 2**:
|
||||
- Перенести `backend/session/` и `backend/upload_refs/` из [upload/backend](upload/backend) в [upload-platform/upload/backend](upload-platform/upload/backend).
|
||||
- В `upload-platform/requirements.txt` зафиксировать `httpx>=0.27.0`.
|
||||
- Проверить чистоту импортов и типизацию.
|
||||
|
||||
2. **Шаг 2. Перенос и адаптация фронтенда Слоя 2 на пофайловый транзит**:
|
||||
- Создать [upload-platform/upload/frontend/upload/put_to_vm.js](upload-platform/upload/frontend/upload/put_to_vm.js) (XHR PUT с прогрессом и возможностью `abort`).
|
||||
- Создать [upload-platform/upload/frontend/upload/upload_via_vm.js](upload-platform/upload/frontend/upload/upload_via_vm.js) с реализацией **пофайловой передачи**:
|
||||
- цикл по файлам;
|
||||
- загрузка файла $k$ на буфер;
|
||||
- немедленный вызов `/api/upload_refs` для файла $k$;
|
||||
- обновление прогресса в UI;
|
||||
- переход к файлу $k+1$.
|
||||
|
||||
3. **Шаг 3. Локальный mock-буфер и интеграция в `site/app.py`**:
|
||||
- Встроить в [upload-platform/site/app.py](upload-platform/site/app.py) тестовый Blueprint mock-буфера (`PUT`, `GET`, `DELETE` в оперативной памяти).
|
||||
- Подключить `create_upload_refs_blueprint` к приложению.
|
||||
- Обновить [upload-platform/site/templates/index.html](upload-platform/site/templates/index.html), добавив кнопку «Загрузить» и отображение прогресса пофайловой передачи.
|
||||
|
||||
4. **Шаг 4. Написание автоматических тестов**:
|
||||
- Python unit-тесты (`pytest`): `safe_name`, `session` (TTL, лимиты, add/get), `upload_refs` (SSRF-блокировка, pull с mock-буфера, удаление после pull).
|
||||
- Playwright browser-тесты: сквозной сценарий от выбора файлов в `initFilePicker` до их успешного появления в сессии Flask.
|
||||
|
||||
5. **Шаг 5. Сборка, bump версии и документация**:
|
||||
- Обновить `build.mjs` и `package.json` (bump версии).
|
||||
- Написать подробный [upload-platform/README.md](upload-platform/README.md) с инструкцией для агентов, как встроить оба слоя в сторонний сервис за 2 минуты.
|
||||
- Фиксация коммитом и пуш.
|
||||
Reference in New Issue
Block a user