From 338cb2f37209bf69aa513efb9f3755f885002112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Tue, 14 Jul 2026 22:01:43 +0400 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D1=8F=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20UI=20=D0=B8=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B8=20=D1=81=20=D0=92=D0=9C=20=D0=BD=D0=B0=20Fla?= =?UTF-8?q?sk=20(=D0=B4=D0=BB=D1=8F=20=D0=A1=D0=B2=D0=B5=D1=80=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=9A=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=D0=BE=D0=B2?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/MIGRATION-GUIDE.md | 253 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 docs/MIGRATION-GUIDE.md diff --git a/docs/MIGRATION-GUIDE.md b/docs/MIGRATION-GUIDE.md new file mode 100644 index 0000000..458a365 --- /dev/null +++ b/docs/MIGRATION-GUIDE.md @@ -0,0 +1,253 @@ +# Инструкция: перенос 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` |