"""Upload blueprint — загрузка, конвертация, распаковка.""" import io, os, base64, hashlib, zipfile, tempfile, subprocess from flask import Blueprint, request, jsonify, send_file from site.services.parse import parse_file from site.db import documents from site.config import MAX_CONTENT_LENGTH 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 (без сохранения на диск).""" f = request.files.get("files") if not f: return jsonify(ok=False, error="no file"), 400 data = f.read() doc_path = None tmpdir = None try: with tempfile.NamedTemporaryFile(suffix=".doc", delete=False) as tmp: tmp.write(data) doc_path = tmp.name tmpdir = tempfile.mkdtemp() subprocess.run( ["libreoffice", "--headless", "--convert-to", "docx", "--outdir", tmpdir, doc_path], timeout=30, capture_output=True, ) docx_files = [x for x in os.listdir(tmpdir) if x.endswith(".docx")] if docx_files: with open(os.path.join(tmpdir, docx_files[0]), "rb") as out: return send_file( io.BytesIO(out.read()), mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document", ) return jsonify(ok=False, error="conversion produced no output"), 500 finally: if doc_path and os.path.exists(doc_path): os.unlink(doc_path) if tmpdir and os.path.exists(tmpdir): for x in os.listdir(tmpdir): os.unlink(os.path.join(tmpdir, x)) os.rmdir(tmpdir) @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)