Files
drhider/History/2026-08-21-pattern-vm-buffer-pull-plan.md
T

7.4 KiB
Raw Blame History

ПЛАН: перенос паттерна «ВМ-буфер + 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): по одному файлу в цикле → FormDataPOST /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 + статическая отдача:

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 -tsystemctl reload nginx.

2. Flask — новый endpoint pull (site/routes/api_bp.py)

@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.