9.8 KiB
9.8 KiB
Инструкция: перенос 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 — весь фронтенд
Ключевые элементы:
<!-- Файловый инпут — множественный выбор -->
<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):
// 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:
return Response(
stream_with_context(generate()),
content_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no" # ← без этого nginx буферизует SSE
}
)
Критично: GeneratorExit в SSE-генераторе:
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
@health_bp.route("/health")
def health():
return jsonify({"ok": True, "version": "0.0.1"})
Обязательно для Штурвала.
4. site/app.py — точка входа
Критичные настройки:
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, заменить:
VERSIONMAX_CONTENT_LENGTHесли нужен другой лимит- Импорт своих blueprint'ов
Шаг 5: session.py
Скопировать site/session.py как есть. Это in-memory хранилище загруженных файлов и результатов.
Шаг 6: Интеграция бизнес-логики
Твоя функция обработки должна принимать тот же интерфейс что и obfuscate_files:
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_cacheafter_request естьX-Accel-Buffering: noна SSE-эндпоинтеGeneratorExitв каждомyieldSSE-генератора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 |