v0.0.7: SSE per-file progress — таблица обновляется по каждому файлу
Deploy drhider / validate (push) Waiting to run
Deploy drhider / validate (push) Waiting to run
This commit is contained in:
+1
-1
@@ -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
@@ -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."""
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user