v0.0.7: SSE per-file progress — таблица обновляется по каждому файлу
Deploy drhider / validate (push) Waiting to run

This commit is contained in:
2026-07-13 10:55:43 +04:00
parent 46d8ced0dd
commit e3405556b5
3 changed files with 83 additions and 16 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ if _sys_path_root not in sys.path:
sys.path.insert(0, _sys_path_root) sys.path.insert(0, _sys_path_root)
# Версия приложения (меняется при изменениях) # Версия приложения (меняется при изменениях)
VERSION = "0.0.6" VERSION = "0.0.7"
def create_app(): def create_app():
+55 -5
View File
@@ -1,18 +1,20 @@
""" """
Blueprint: API DrHider. Blueprint: API DrHider.
Four endpoints: Five endpoints:
- POST /api/upload — upload one file -> {session_id} - POST /api/upload — upload one file -> {session_id}
- POST /api/process/{sid} — process all session files -> {status:"done"} - GET /api/process_stream/<sid> — SSE: process files, per-file progress
- GET /api/download/{sid} — download ZIP (with timestamp name) - POST /api/process/<sid> — process all session files -> {status:"done"} (legacy)
- GET /api/csv/{sid} — download CSV separately - GET /api/download/<sid> — download ZIP (with timestamp name)
- GET /api/csv/<sid> — download CSV separately
""" """
import io import io
import json
import zipfile import zipfile
import traceback import traceback
from datetime import datetime from datetime import datetime
from flask import Blueprint, request, send_file, jsonify from flask import Blueprint, request, send_file, jsonify, Response, stream_with_context
from drhider import obfuscate_files, LLMClient from drhider import obfuscate_files, LLMClient
from drhider.builder import build_zip, build_mapping_csv from drhider.builder import build_zip, build_mapping_csv
@@ -40,6 +42,54 @@ def upload():
return jsonify({"ok": True, "session": sid, "count": file_count(sid)}) return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
@api_bp.route("/process_stream/<sid>", methods=["GET"])
def process_stream(sid):
"""SSE: process all session files one by one, streaming per-file progress."""
files = get_files(sid)
if files is None:
return jsonify({"ok": False, "error": "Session not found"}), 404
if not files:
return jsonify({"ok": False, "error": "No files"}), 400
total = len(files)
def generate():
llm = LLMClient()
all_results = []
all_mapping = {}
for idx, (fname, content) in enumerate(files):
# Отправляем: начали файл
yield f"data: {json.dumps({'event': 'start', 'idx': idx, 'name': fname, 'total': total})}\n\n"
zip_data, _csv = obfuscate_files([(fname, content, "")], llm_client=llm)
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
for name in zf.namelist():
if name == "mapping.csv":
csv_text = zf.read(name).decode("utf-8")
lines = csv_text.strip().split("\n")
if len(lines) > 1:
for line in lines[1:]:
parts = line.split(",", 2)
if len(parts) >= 3:
all_mapping[parts[1]] = parts[2]
elif not name.endswith("/"):
all_results.append((name, zf.read(name)))
# Отправляем: закончили файл
yield f"data: {json.dumps({'event': 'done', 'idx': idx, 'name': fname, 'total': total})}\n\n"
csv_str = build_mapping_csv(all_mapping) if all_mapping else ""
final_zip = build_zip(all_results)
store_result(sid, final_zip)
if csv_str:
store_csv(sid, csv_str)
yield f"data: {json.dumps({'event': 'complete', 'total': len(all_results)})}\n\n"
return Response(
stream_with_context(generate()),
content_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
)
@api_bp.route("/process/<sid>", methods=["POST"]) @api_bp.route("/process/<sid>", methods=["POST"])
def process(sid): def process(sid):
"""Process all session files one by one -> ZIP.""" """Process all session files one by one -> ZIP."""
+23 -6
View File
@@ -203,7 +203,7 @@ async function uploadFiles() {
} }
} }
// Фаза 2: обработка // Фаза 2: обработка (SSE — прогресс по каждому файлу)
for (let i = 0; i < total; i++) ss(i, '<span style="color:#2563eb">⏳</span>'); for (let i = 0; i < total; i++) ss(i, '<span style="color:#2563eb">⏳</span>');
st.className = 'status progress'; st.className = 'status progress';
st.textContent = 'Обработка...'; st.textContent = 'Обработка...';
@@ -214,14 +214,31 @@ async function uploadFiles() {
}, 1000); }, 1000);
try { try {
const resp = await fetch('/api/process/' + currentSid, { method: 'POST' }); await new Promise((resolve, reject) => {
const data = await resp.json(); const es = new EventSource('/api/process_stream/' + currentSid);
es.addEventListener('start', function(e) {
const d = JSON.parse(e.data);
ss(d.idx, '<span style="color:#2563eb">⏳ обработка</span>');
});
es.addEventListener('done', function(e) {
const d = JSON.parse(e.data);
ss(d.idx, '<span style="color:#22c55e">✓</span>');
});
es.addEventListener('complete', function(e) {
es.close();
clearInterval(ptimer); clearInterval(ptimer);
if (!data.ok) throw new Error(data.error); const d = JSON.parse(e.data);
for (let i = 0; i < total; i++) ss(i, '<span style="color:#22c55e">✓</span>');
st.className = 'status done'; st.className = 'status done';
st.textContent = '✅ Обработано ' + data.files + ' файлов за ' + ((performance.now() - t0) / 1000).toFixed(1) + 'с'; st.textContent = '✅ Обработано ' + d.total + ' файлов за ' + ((performance.now() - t0) / 1000).toFixed(1) + 'с';
db.classList.add('show'); db.classList.add('show');
resolve();
});
es.onerror = function() {
es.close();
clearInterval(ptimer);
reject(new Error('SSE connection failed'));
};
});
} catch (err) { } catch (err) {
clearInterval(ptimer); clearInterval(ptimer);
st.className = 'status error'; st.className = 'status error';