# Инструкция: перенос UI и загрузки файлов с ВМ на Flask (Штурвал) **Дата:** 2026-07-14 **Основано на:** drhider v0.0.26 — проверено в бою --- ## Архитектура загрузки (drhider как образец) ``` Браузер (index.html) │ JS: uploadFiles() — цикл по файлам │ XHR POST /api/upload (FormData, по одному файлу) ▼ Flask (api_bp.py) │ upload(): сохраняет в сессию → {session_id} │ process_stream(): SSE — обработка с прогрессом ▼ Python (drhider/*.py) │ obfuscate_files() — основная логика ▼ Flask → ZIP + CSV → браузеру ``` ## Файлы, отвечающие за UI и загрузку ### 1. `site/templates/index.html` — весь фронтенд **Ключевые элементы:** ```html
``` **Ключевые JS-функции:** | Функция | Что делает | |---------|------------| | `resetAll()` | Сброс всего состояния (F5, между загрузками) | | `rr()` | Перерисовка таблицы файлов | | `uploadFiles()` | **Главная** — цикл загрузки + SSE-обработка | | `ss(idx, html)` | Обновление ячейки статуса в таблице | | `downloadZip()` / `downloadCsv()` | Скачивание результатов | **Критичные исправления (v0.0.26):** ```javascript // 1. Прогрев upstream при загрузке страницы — обязательно! window.addEventListener('load', () => { sf = []; fi.value = ''; rr(); fetch('/health').catch(() => {}); // ← вот это }); // 2. Abort предыдущих запросов перед новым let activeES = null; // активный EventSource let activeXHR = null; // активный XHR // 3. Защита от F5 во время загрузки window.addEventListener('beforeunload', () => resetAll()); // 4. Сохранение списка файлов до очистки const files = sf.slice(); // копия перед resetAll() // 5. Цикл загрузки (по одному файлу) for (let i = 0; i < total; i++) { const fd = new FormData(); fd.append('files', f, f.name); if (currentSid) fd.append('session', currentSid); const resp = await fetch('/api/upload?_=' + Date.now(), { method: 'POST', body: fd }); const data = await resp.json(); currentSid = data.session; } // 6. SSE-обработка activeES = new EventSource('/api/process_stream/' + currentSid); activeES.addEventListener('start', ...); // файл начат activeES.addEventListener('done', ...); // файл готов activeES.addEventListener('complete', ...); // всё готово ``` ### 2. `site/routes/api_bp.py` — API эндпоинты ``` POST /api/upload — загрузка одного файла → {session_id} GET /api/process_stream/ — SSE: обработка с прогрессом POST /api/process/ — обработка без SSE (legacy) GET /api/download/ — скачать ZIP GET /api/csv/ — скачать CSV отдельно ``` **Критичные заголовки для SSE:** ```python return Response( stream_with_context(generate()), content_type="text/event-stream", headers={ "Cache-Control": "no-cache", "X-Accel-Buffering": "no" # ← без этого nginx буферизует SSE } ) ``` **Критично: GeneratorExit в SSE-генераторе:** ```python def generate(): for idx, (fname, content) in enumerate(files): try: yield f"event: start\ndata: ...\n\n" except GeneratorExit: return # клиент отключился — не обрабатываем дальше # ... обработка файла ... try: yield f"event: done\ndata: ...\n\n" except GeneratorExit: return ``` ### 3. `site/routes/health_bp.py` — liveness probe ```python @health_bp.route("/health") def health(): return jsonify({"ok": True, "version": "0.0.1"}) ``` **Обязательно для Штурвала.** ### 4. `site/app.py` — точка входа **Критичные настройки:** ```python VERSION = "0.0.1" def create_app(): app = Flask(__name__) app.config["VERSION"] = VERSION app.config["MAX_CONTENT_LENGTH"] = 200 * 1024 * 1024 # 200 MB from routes import register_routes register_routes(app) @app.after_request def no_cache(response): response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" return response return app app = create_app() ``` --- ## Пошаговая инструкция миграции ### Шаг 1: Скопировать скелет UI Скопировать `site/templates/index.html` из drhider как основу. Заменить: - Заголовок (``, `.title`, `.card-header`) - Текст описания - `accept` в `<input type="file">` если другие форматы - Логику в `uploadFiles()` — вызов своего API вместо `obfuscate_files` ### Шаг 2: Адаптировать API Скопировать `site/routes/api_bp.py`, заменить: - Название blueprint'а - Логику в `process_stream()` — вызов своей функции вместо `obfuscate_files()` - Формат SSE-событий если нужен другой ### Шаг 3: Health endpoint Скопировать `site/routes/health_bp.py` как есть. Только версию поменять. ### Шаг 4: app.py Скопировать `site/app.py`, заменить: - `VERSION` - `MAX_CONTENT_LENGTH` если нужен другой лимит - Импорт своих blueprint'ов ### Шаг 5: session.py Скопировать `site/session.py` как есть. Это in-memory хранилище загруженных файлов и результатов. ### Шаг 6: Интеграция бизнес-логики Твоя функция обработки должна принимать тот же интерфейс что и `obfuscate_files`: ```python def твоя_функция(files, **kwargs): """ Args: files: list of (filename: str, content: bytes, mimetype: str) Returns: zip_bytes: bytes — ZIP-архив с результатами csv_str: str — CSV с маппингом (или "") """ ``` --- ## Чек-лист перед деплоем - [ ] `/health` возвращает `{"ok": true}` - [ ] `MAX_CONTENT_LENGTH` достаточен для файлов - [ ] `no_cache` after_request есть - [ ] `X-Accel-Buffering: no` на SSE-эндпоинте - [ ] `GeneratorExit` в каждом `yield` SSE-генератора - [ ] `stream_with_context` оборачивает генератор - [ ] `fetch('/health')` при загрузке страницы - [ ] `beforeunload` → `resetAll()` - [ ] `activeXHR` и `activeES` очищаются перед новым запуском - [ ] `Connection: close` — **НЕ ставить** (Waitress/PEP 3333 запрещает hop-by-hop) --- ## Ссылки на файлы-образцы | Что | Где в drhider | |-----|---------------| | HTML/JS/CSS фронтенд | `site/templates/index.html` | | API (upload + SSE + download) | `site/routes/api_bp.py` | | Health probe | `site/routes/health_bp.py` | | Точка входа Flask | `site/app.py` | | In-memory сессии | `site/session.py` | | Полная архитектура | `docs/ARCHITECTURE.md` | | Решённые проблемы кластера | `PROBLEM-AND-SOLUTION.md` |