Files
drhider/docs/MIGRATION-GUIDE.md
T

289 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Инструкция: перенос 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
<!-- Файловый инпут — множественный выбор -->
<input type="file" id="fileInput" multiple accept=".docx,.pdf,.txt,.zip">
<!-- Таблица выбранных файлов -->
<table>
<tbody id="fileList"></tbody>
</table>
<!-- Статус-бар -->
<div class="status" id="status"></div>
<!-- Кнопки скачивания (скрыты до готовности) -->
<div class="dl-btns" id="dlBtns">
<button onclick="downloadZip()">📦 Скачать ZIP</button>
<button onclick="downloadCsv()">📋 Скачать CSV</button>
</div>
```
**Ключевые JS-функции:**
| Функция | Что делает |
|---------|------------|
| `resetAll()` | Сброс всего состояния (F5, между загрузками) |
| `rr()` | Перерисовка таблицы файлов |
| `uploadFiles()` | **Главная** — цикл загрузки + SSE-обработка |
| `ss(idx, html)` | Обновление ячейки статуса в таблице |
| `downloadZip()` / `downloadCsv()` | Скачивание результатов |
**Критичные исправления (v0.0.26 — v0.0.29):**
```javascript
// 1. Прогрев upstream при загрузке страницы — обязательно!
window.addEventListener('load', () => {
sf = []; fi.value = ''; rr();
fetch('/health').catch(() => {}); // ← обязательно! греет upstream
});
// 2. Abort предыдущих запросов перед новым
let activeES = null; // активный EventSource
let activeXHR = null; // активный XHR
// 3. Защита от F5 во время загрузки
window.addEventListener('beforeunload', () => resetAll());
// 4. Сохранение списка файлов до очистки
const files = sf.slice(); // копия перед resetAll()
// 5. Выбор файлов — ДОБАВЛЯЕТ в таблицу (не стирает, с дедупом)
fi.addEventListener('change', () => {
const incoming = Array.from(fi.files);
const seen = new Set(sf.map(f => f.name + '|' + f.size));
for (const f of incoming) {
if (!seen.has(f.name + '|' + f.size)) sf.push(f);
}
const d = new DataTransfer();
sf.forEach(f => d.items.add(f));
fi.files = d.files;
rr();
});
// 6. Цикл загрузки (fetch, не XHR — XHR давал RST на первом запросе)
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(), { // ← fetch, не XHR
method: 'POST', body: fd
});
const data = await resp.json();
currentSid = data.session;
}
// 7. SSE-обработка
activeES = new EventSource('/api/process_stream/' + currentSid);
activeES.addEventListener('start', ...); // файл начат
activeES.addEventListener('done', ...); // файл готов
activeES.addEventListener('complete', ...); // всё готово
// 8. Скачивание ZIP/CSV — через fetch+Blob (не window.location!)
function downloadZip() {
if (!currentSid) return;
fetch('/api/download/' + currentSid)
.then(r => {
const disp = r.headers.get('Content-Disposition');
const m = disp && disp.match(/filename="?(.+?)"?$/);
return Promise.all([r.blob(), m ? m[1] : 'result.zip']);
})
.then(([blob, fname]) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = fname;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
});
}
```
### 2. `site/routes/api_bp.py` — API эндпоинты
```
POST /api/upload — загрузка одного файла → {session_id}
GET /api/process_stream/<sid> — SSE: обработка с прогрессом
POST /api/process/<sid> — обработка без SSE (legacy)
GET /api/download/<sid> — скачать ZIP
GET /api/csv/<sid> — скачать 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>`, `.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` очищаются перед новым запуском
- [ ] `fetch` вместо XHR (XHR на первом запросе даёт RST)
- [ ] Выбор файлов — добавляет, не стирает (с дедупом)
- [ ] `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` |