# РЕЗЮМЕ И ТЕХНИЧЕСКОЕ ЗАДАНИЕ: СЛОЙ 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 минуты. - Фиксация коммитом и пуш.