"""Upload blueprint — загрузка, конвертация, распаковка.""" import io, os, base64, hashlib, zipfile import httpx from flask import Blueprint, request, jsonify, send_file from services.parse import parse_file from db import documents import config upload_bp = Blueprint("upload", __name__) ALLOWED = {"pdf", "docx", "doc", "zip"} def _check_ext(filename: str) -> str | None: ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "" if ext not in ALLOWED: return f"unsupported format: .{ext} (allowed: {', '.join(sorted(ALLOWED))})" return None @upload_bp.route("/upload", methods=["POST"]) def upload(): """Загрузка одного файла + авто-парсинг → БД.""" f = request.files.get("files") if not f: return jsonify(ok=False, error="no file"), 400 err = _check_ext(f.filename) if err: return jsonify(ok=False, error=err), 400 data = f.read() content_hash = hashlib.sha256(data).hexdigest()[:16] batch_id = request.form.get("batch_id") zip_source = request.form.get("zip_source") # Дедупликация по хешу if batch_id: existing = documents.get_by_hash(batch_id, content_hash) if existing: return jsonify(ok=False, error="duplicate", doc_id=existing["id"]) doc = documents.insert( filename=f.filename, mime_type=f.content_type or "application/octet-stream", original_bytes=base64.b64encode(data).decode(), batch_id=batch_id, zip_source=zip_source, content_hash=content_hash, ) # Авто-парсинг try: result = parse_file(f.filename, data) if result["status"] == "parsed": documents.set_parsed(doc["id"], result["elements"]) else: documents.set_error(doc["id"], result.get("error", "parse failed")) except Exception as e: documents.set_error(doc["id"], str(e)) result = {"status": "error", "error": str(e)} contract_id = request.form.get("contract_id") return jsonify( ok=True, doc_id=doc["id"], contract_id=contract_id, parsed={"status": result["status"], "element_count": result.get("element_count", 0)}, ) @upload_bp.route("/convert-doc", methods=["POST"]) def convert_doc(): """.doc → .docx через внешний libreoffice-сервис (HTTP).""" f = request.files.get("files") if not f: return jsonify(ok=False, error="no file"), 400 try: resp = httpx.post( config.CONVERT_SERVICE_URL + "/convert", files={"file": (f.filename, f.read(), f.content_type or "application/msword")}, timeout=120, ) if resp.status_code == 200: return send_file( io.BytesIO(resp.content), mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document", ) data = resp.json() return jsonify(ok=False, error=data.get("error", "conversion failed")), 500 except httpx.TimeoutException: return jsonify(ok=False, error="conversion timeout"), 500 except Exception as e: return jsonify(ok=False, error=str(e)), 500 @upload_bp.route("/unzip-upload", methods=["POST"]) def unzip_upload(): """Распаковать ZIP → список файлов (base64 для фронтенда).""" f = request.files.get("files") if not f: return jsonify(ok=False, error="no file"), 400 data = f.read() MAX_FILES = 500 MAX_UNCOMPRESSED = 500 * 1024 * 1024 # 500 MB files = [] total = 0 with zipfile.ZipFile(io.BytesIO(data)) as zf: if len(zf.namelist()) > MAX_FILES: return jsonify(ok=False, error=f"too many files in ZIP (max {MAX_FILES})"), 400 for info in zf.infolist(): if info.is_dir(): continue name = os.path.basename(info.filename) if not name or ".." in name or "/" in name or "\\" in name: continue raw = zf.read(info) total += len(raw) if total > MAX_UNCOMPRESSED: return jsonify(ok=False, error="total uncompressed size exceeds 500 MB"), 400 ext = name.rsplit(".", 1)[-1].lower() if "." in name else "" files.append({ "filename": name, "ext": ext, "size": len(raw), "data_b64": base64.b64encode(raw).decode(), }) return jsonify(ok=True, files=files)