docs: актуализация README/ARCHITECTURE + план паттерна ВМ-буфер+pull (снимок перед ВМ-инкапсуляцией загрузки)
Deploy drhider / validate (push) Canceled after 0s

This commit is contained in:
“Naeel”
2026-08-21 15:35:45 +03:00
parent a9d4139afb
commit 705b39ab7d
3 changed files with 182 additions and 75 deletions
@@ -0,0 +1,124 @@
# ПЛАН: перенос паттерна «ВМ-буфер + pull» в drhider
_Дата: 2026-08-21. Основа: `contracts/loadtest/History/2026-08-21-pattern-vm-buffer-pull.md`._
_Статус: план, код НЕ менялся. Писать будет DeepSeek Flash._
---
## Цель
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/<token>/<name>, {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.