# ПЛАН: перенос паттерна «ВМ-буфер + pull» в drhider _Дата: 2026-08-21. Основа: `contracts/loadtest/History/2026-08-21-pattern-vm-buffer-pull.md`._ _Статус: РЕАЛИЗОВАНО 2026-08-21 (v0.0.58), см. `2026-08-21-vm-upload-implemented.md`._ --- ## Цель drhider (Managed Flask, Штурвал, `drhider.pythonk8s.dev.nubes.ru`) получает файлы через `POST /api/upload` (multipart). Входной шлюз managed-кластера обрывает тела >~64КБ (HTTP 000 ~10с). Решение — загружать файл на ВМ напрямую, Flask тянет сам (egress). ## Директива (обязательно) **ВСЕ файловые операции загрузки в Flask (из браузера или вообще снаружи) — ТОЛЬКО через ВМ.** Никаких прямых `POST /api/upload` с телом файла во Flask. ## Среда (уточнено 2026-08-21) - `iot-naeel` — СОБСТВЕННЫЙ кластер, параметры/аннотации правились через kubectl. Пока тестируем/смотрим поведение на нём (лимит 64КБ там может не воспроизводиться). - В дальнейшем деплой — на ОБЫЧНЫЙ managed-кластер (лимит 64КБ будет реальным). - ВМ — ОДНА (`5.172.178.213`, `contracts.kube5s.ru`), та же, что для loadtest. Location `/drhider-upload/` добавляется в существующий nginx рядом с `/lt-serve/`. - Вывод: VM-путь строим СЕЙЧАС, независимо от поведения iot-naeel — прод всё равно на обычном. ## Текущий поток `uploadFiles()` (index.html ~413): по одному файлу в цикле → `FormData` → `POST /api/upload`. Бэк: `MAX_CONTENT_LENGTH=200МБ`, сессия 500МБ. HTTP-клиент — **httpx** (не requests). ## Целевой поток ``` Браузер ──(PUT, файл целиком)──▶ ВМ nginx (без лимита 64КБ) │ └─ получил URL + токен │ └──▶ Flask POST /api/upload_refs (JSON: [{name,size,url}], <64КБ) │ ▼ Flask egress GET с ВМ ──▶ читает файл ──▶ add_file() ──▶ дальше как сейчас ``` ## Изменения (4 места) ### 1. ВМ `5.172.178.213`, `nginx-contracts.conf` — приём загрузки Новый location (рядом с `/lt-serve/`), приём через PUT + статическая отдача: ```nginx location /drhider-upload/ { alias /var/www/drhider-upload/; dav_methods PUT; create_full_put_path on; client_max_body_size 1024m; # CORS: браузер с drhider.pythonk8s.dev.nubes.ru шлёт PUT (не simple → preflight) add_header Access-Control-Allow-Origin https://drhider.pythonk8s.dev.nubes.ru always; add_header Access-Control-Allow-Methods 'PUT, GET, OPTIONS, DELETE' always; add_header Access-Control-Allow-Headers 'Content-Type' always; if ($request_method = OPTIONS) { return 204; } } ``` - Каталог `/var/www/drhider-upload/` (владелец `www-data`), НЕ `/home/naeel` (403). - Порядок: backup → правка → `nginx -t` → `systemctl reload nginx`. ### 2. Flask — новый endpoint pull (`site/routes/api_bp.py`) ```python @api_bp.route("/upload_refs", methods=["POST"]) def upload_refs(): data = request.get_json(silent=True) or {} sid = data.get("session") or create_session() refs = data.get("files") or [] if not refs: return jsonify({"ok": False, "error": "No files"}), 400 added = 0 with httpx.Client(timeout=120, follow_redirects=True) as client: for ref in refs: name, url = ref.get("name"), ref.get("url") if not name or not url: continue content = b"".join(client.stream("GET", url).iter_bytes()) # egress, по частям if not add_file(sid, name, content): return jsonify({"ok": False, "error": "Session not found"}), 404 added += 1 return jsonify({"ok": True, "session": sid, "count": file_count(sid)}) ``` - Импорт `httpx` (уже в requirements.txt) и `file_count` из session. - `stream(...).iter_bytes()` — НЕ `content` целиком (OOM при 100МБ+). - Обработка ошибок egress (таймаут/403/404) → вернуть 502 с именем файла. ### 3. Фронт (`site/templates/index.html`, `uploadFiles()`) Разбить «Фазу 1» на два шага: - **Шаг 1a:** каждый файл → `fetch(<ВМ>/drhider-upload//, {method:'PUT', body:file})`, собирать `[{name, size, url}]`. Прогресс — `xhr.upload.onprogress` заменить на `fetch` + `ReadableStream` (или оставить XHR, но на PUT к ВМ). - **Шаг 1b:** один `POST /api/upload_refs` с JSON `{session, files:[...]}` (<64КБ). - Токен: `crypto.randomUUID()`. URL ВМ — константа/`data-атрибут`. - Показывать прогресс «загрузка на ВМ» отдельно от «загрузка в Flask». ### 4. `requirements.txt` — без изменений (httpx уже есть). ## Безопасность и жизненный цикл - Токен в пути URL — только `crypto.randomUUID()`, не переиспользуется. - Flask после `add_file` делает `DELETE` на URL ВМ (или ВМ чистит по TTL — cron/systemd-timer `find /var/www/drhider-upload -mmin +30 -delete`). - ВМ отдаёт файл только по валидному URL с токеном (нет открытого листинга: `autoindex off`). ## Грабли (из pattern-дока + drhider) - CORS: PUT — не simple-метод → браузер шлёт OPTIONS preflight; nginx обязан отвечать 204. - `site/` конфликтует со stdlib `site.py` (для loadtest; у drhider — Штурвал, без gunicorn). - egress через `httpx` с `stream`, timeout 120; большие ОТВЕТЫ проходят (проверено 50МБ). - OOM: не читать файл в память целиком; сессия 500МБ уже ограничивает. - Локальные curl на Krupski идут через прокси `172.17.192.1:10808` → для теста ВМ `--noproxy '*'`. ## Порядок работ + проверка 1. ВМ nginx (location + каталог + CORS) → проверить `curl --noproxy '*' -X PUT`. 2. Flask `/api/upload_refs` → проверить `python3 -c "import py_compile; py_compile.compile(...)"`. 3. Фронт `uploadFiles()` → `node -c` нет (это EJS/HTML+JS внутри шаблона) — ручная проверка. 4. Сквозной тест: файл 10МБ → ВМ → Flask → обфускация → ZIP. 5. Commit + push (правило: после правки + bump версии `VERSION` в `site/app.py`). ## Решения (закрыты) - Кластер: сейчас `iot-naeel` (тест), прод — обычный managed. VM-путь обязателен. - ВМ: одна, `contracts.kube5s.ru` (`5.172.178.213`), общая с loadtest — добавляем location.