From e3405556b5d3428a360e7c50b83cec92f113496e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 13 Jul 2026 10:55:43 +0400 Subject: [PATCH] =?UTF-8?q?v0.0.7:=20SSE=20per-file=20progress=20=E2=80=94?= =?UTF-8?q?=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B0=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=B0=D0=B6=D0=B4=D0=BE=D0=BC=D1=83=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/app.py | 2 +- site/routes/api_bp.py | 62 +++++++++++++++++++++++++++++++++++---- site/templates/index.html | 35 ++++++++++++++++------ 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/site/app.py b/site/app.py index cef2906..a7a9a89 100644 --- a/site/app.py +++ b/site/app.py @@ -20,7 +20,7 @@ if _sys_path_root not in sys.path: sys.path.insert(0, _sys_path_root) # Версия приложения (меняется при изменениях) -VERSION = "0.0.6" +VERSION = "0.0.7" def create_app(): diff --git a/site/routes/api_bp.py b/site/routes/api_bp.py index a00f648..ddd8944 100644 --- a/site/routes/api_bp.py +++ b/site/routes/api_bp.py @@ -1,18 +1,20 @@ """ Blueprint: API DrHider. -Four endpoints: -- POST /api/upload — upload one file -> {session_id} -- POST /api/process/{sid} — process all session files -> {status:"done"} -- GET /api/download/{sid} — download ZIP (with timestamp name) -- GET /api/csv/{sid} — download CSV separately +Five endpoints: +- POST /api/upload — upload one file -> {session_id} +- GET /api/process_stream/ — SSE: process files, per-file progress +- POST /api/process/ — process all session files -> {status:"done"} (legacy) +- GET /api/download/ — download ZIP (with timestamp name) +- GET /api/csv/ — download CSV separately """ import io +import json import zipfile import traceback 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.builder import build_zip, build_mapping_csv @@ -40,6 +42,54 @@ def upload(): return jsonify({"ok": True, "session": sid, "count": file_count(sid)}) +@api_bp.route("/process_stream/", 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/", methods=["POST"]) def process(sid): """Process all session files one by one -> ZIP.""" diff --git a/site/templates/index.html b/site/templates/index.html index 41bad6f..6e8f59f 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -203,7 +203,7 @@ async function uploadFiles() { } } - // Фаза 2: обработка + // Фаза 2: обработка (SSE — прогресс по каждому файлу) for (let i = 0; i < total; i++) ss(i, ''); st.className = 'status progress'; st.textContent = 'Обработка...'; @@ -214,14 +214,31 @@ async function uploadFiles() { }, 1000); try { - const resp = await fetch('/api/process/' + currentSid, { method: 'POST' }); - const data = await resp.json(); - clearInterval(ptimer); - if (!data.ok) throw new Error(data.error); - for (let i = 0; i < total; i++) ss(i, ''); - st.className = 'status done'; - st.textContent = '✅ Обработано ' + data.files + ' файлов за ' + ((performance.now() - t0) / 1000).toFixed(1) + 'с'; - db.classList.add('show'); + await new Promise((resolve, reject) => { + const es = new EventSource('/api/process_stream/' + currentSid); + es.addEventListener('start', function(e) { + const d = JSON.parse(e.data); + ss(d.idx, '⏳ обработка'); + }); + es.addEventListener('done', function(e) { + const d = JSON.parse(e.data); + ss(d.idx, ''); + }); + es.addEventListener('complete', function(e) { + es.close(); + clearInterval(ptimer); + const d = JSON.parse(e.data); + st.className = 'status done'; + st.textContent = '✅ Обработано ' + d.total + ' файлов за ' + ((performance.now() - t0) / 1000).toFixed(1) + 'с'; + db.classList.add('show'); + resolve(); + }); + es.onerror = function() { + es.close(); + clearInterval(ptimer); + reject(new Error('SSE connection failed')); + }; + }); } catch (err) { clearInterval(ptimer); st.className = 'status error';