Files
contracts-flask/site/routes/upload_bp.py
T

101 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Upload-модуль: конвертация .doc→.docx + хранение/парсинг (sink для VM-буфера)."""
import base64
import hashlib
import httpx
from services.parse import parse_file
from db import documents
import config
def _convert(filename: str, data: bytes) -> bytes:
""".doc → .docx через внешний libreoffice-сервис с fallback на публичный endpoint.
Назначение:
Преобразование устаревшего бинарного формата Word 97-2003 (.doc) в современный .docx,
так как встроенный парсер документов работает исключительно со структурой OpenXML.
Почему реализован fallback:
Внутри Kubernetes-кластера сервис доступен по внутреннему DNS имени
(http://containerk8s.df36c8af-1a95-4623-b551-0d37b731ccca.svc.cluster.local:5000),
однако при запуске вне кластера (локальный сервер разработки, standalone-тесты)
внутреннее доменное имя не резолвится ([Errno -2] Name or service not known).
В таком случае запрос автоматически повторяется к публичному ingress-эндпоинту
https://liberta.containerk8s.dev.nubes.ru, гарантируя прозрачную работу в любых средах.
"""
urls = [config.CONVERT_SERVICE_URL]
fallback = "https://liberta.containerk8s.dev.nubes.ru"
if fallback not in urls:
urls.append(fallback)
last_err = None
for base_url in urls:
if not base_url:
continue
try:
resp = httpx.post(
base_url.rstrip("/") + "/convert",
files={"file": (filename, data, "application/msword")},
timeout=120,
)
if resp.status_code == 200:
return resp.content
try:
last_err = resp.json().get("error", "conversion failed")
except Exception:
last_err = f"HTTP {resp.status_code}"
except httpx.TimeoutException:
last_err = "conversion timeout"
except Exception as e:
last_err = str(e)
continue
raise Exception(last_err or "conversion failed")
def _store_and_parse(filename: str, data: bytes, batch_id, contract_id, zip_source=None, mime_type="application/octet-stream"):
"""Общая логика: дедуп → insert в БД → авто-парсинг. Возвращает dict-результат."""
content_hash = hashlib.sha256(data).hexdigest()[:16]
# Дедупликация по хешу
if batch_id:
existing = documents.get_by_hash(batch_id, content_hash)
if existing:
return {"ok": False, "error": "duplicate", "doc_id": existing["id"], "duplicate_of": True}
doc = documents.insert(
filename=filename,
mime_type=mime_type,
original_bytes=base64.b64encode(data).decode(),
batch_id=batch_id,
zip_source=zip_source,
content_hash=content_hash,
)
# Авто-парсинг
try:
result = parse_file(filename, data)
if result["status"] == "parsed":
documents.set_parsed(doc["id"], result["elements"])
parsed = {"status": "parsed", "element_count": result.get("element_count", 0)}
else:
documents.set_error(doc["id"], result.get("error", "parse failed"))
parsed = {"status": "error", "error": result.get("error", "parse failed")}
except Exception as e:
documents.set_error(doc["id"], str(e))
parsed = {"status": "error", "error": str(e)}
return {"ok": True, "doc_id": doc["id"], "contract_id": contract_id, "parsed": parsed}
def contracts_upload_sink(name, content, batch_id=None, contract_id=None, zip_source=None):
"""Sink для переиспользуемого модуля upload: вставить файл в documents + авто-парсинг.
Вызывается модулем create_upload_refs_blueprint(cfg, sink=...) на каждый
вытянутый с ВМ файл. .doc конвертируется в .docx через внешний LibreOffice
(парсер умеет только .docx). Возвращает dict ok/doc_id/contract_id/parsed.
"""
if name.lower().endswith(".doc"):
content = _convert(name, content)
name = name[:-4] + ".docx"
return _store_and_parse(name, content, batch_id, contract_id, zip_source)