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