Files
contracts-flask/site/routes/upload_bp.py
T
2026-07-16 10:18:17 +04:00

137 lines
4.5 KiB
Python

"""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)