101 lines
4.6 KiB
Python
101 lines
4.6 KiB
Python
"""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)
|