v2.0.14: этап 4 — .doc→.docx в sink, удалены /api/unzip_refs и /api/convert_refs
Deploy contracts-flask / validate (push) Canceled after 0s

This commit is contained in:
“Naeel”
2026-08-26 11:40:03 +03:00
parent d2894ad7c5
commit 5b49e88f1e
4 changed files with 16 additions and 126 deletions
+6 -111
View File
@@ -1,5 +1,5 @@
"""Upload blueprint — загрузка, конвертация, распаковка."""
import io, os, time, base64, hashlib, zipfile
import io, os, base64, hashlib, zipfile
import httpx
from flask import Blueprint, request, jsonify, send_file
from services.parse import parse_file
@@ -18,59 +18,6 @@ def _check_ext(filename: str) -> str | None:
return None
def _safe_name(name: str) -> str:
"""Санитизация имени файла: только basename, защита от path traversal."""
name = (name or "").replace("\\", "/").rsplit("/", 1)[-1].strip()
if not name or name in (".", ".."):
return "file.bin"
return name[:255]
def _pull_with_retries(url: str, timeout: int = 120) -> bytes:
"""Скачать файл с ВМ-буфера с ретраями (egress, без лимита шлюза)."""
last: Exception | None = None
for attempt in range(1, config.PULL_RETRIES + 1):
try:
resp = httpx.get(url, timeout=timeout, follow_redirects=False)
if resp.status_code == 200:
return resp.content
last = Exception(f"HTTP {resp.status_code}")
except Exception as e:
last = e
if attempt < config.PULL_RETRIES:
time.sleep(config.PULL_RETRY_DELAY)
raise last or Exception("pull failed")
def _delete_from_vm(url: str) -> None:
"""Best-effort удаление файла с ВМ-буфера."""
try:
httpx.delete(url, timeout=30)
except Exception:
pass
def _pull_from_ref(ref):
"""SSRF-проверка → лимит → pull с ретраями → DELETE с ВМ. Возвращает (name, content)."""
name = _safe_name(str(ref.get("name", "") or ""))
size = int(ref.get("size") or 0)
url = str(ref.get("url", "") or "")
if not url.startswith(config.VM_UPLOAD_PREFIX):
raise Exception("invalid url (SSRF guard)")
if size > config.VM_UPLOAD_MAX_BYTES:
_delete_from_vm(url)
raise Exception(f"file too large: {size} bytes (max {config.VM_UPLOAD_MAX_BYTES})")
content = _pull_with_retries(url)
if len(content) > config.VM_UPLOAD_MAX_BYTES:
_delete_from_vm(url)
raise Exception("file too large after pull")
_delete_from_vm(url)
return name, content
def _unzip(data: bytes):
"""Распаковать ZIP → (ok, files, error)."""
MAX_FILES = 500
@@ -183,49 +130,15 @@ def contracts_upload_sink(name, content, batch_id=None, contract_id=None, zip_so
"""Sink для переиспользуемого модуля upload: вставить файл в documents + авто-парсинг.
Вызывается модулем create_upload_refs_blueprint(cfg, sink=...) на каждый
вытянутый с ВМ файл. Возвращает dict с ключами ok/doc_id/contract_id/parsed.
вытянутый с ВМ файл. .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)
@upload_bp.route("/convert-doc", methods=["POST"])
def convert_doc():
""".doc → .docx через внешний libreoffice-сервис (прямой multipart)."""
f = request.files.get("files")
if not f:
return jsonify(ok=False, error="no file"), 400
try:
content = _convert(f.filename, f.read())
except Exception as e:
return jsonify(ok=False, error=str(e)), 500
return send_file(
io.BytesIO(content),
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
@upload_bp.route("/api/convert_refs", methods=["POST"])
def convert_refs():
""".doc → .docx через ВМ-буфер (паттерн drhider): pull .doc с ВМ → конвертация → docx."""
data = request.get_json(silent=True) or {}
files = data.get("files") or []
if not files:
return jsonify(ok=False, error="no files"), 400
ref = files[0]
try:
name, content = _pull_from_ref(ref)
except Exception as e:
return jsonify(ok=False, error=str(e)), 400
try:
docx = _convert(name, content)
except Exception as e:
return jsonify(ok=False, error=str(e)), 500
return send_file(
io.BytesIO(docx),
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
@upload_bp.route("/unzip-upload", methods=["POST"])
def unzip_upload():
"""Распаковать ZIP → список файлов (base64 для фронтенда, прямой multipart)."""
@@ -236,21 +149,3 @@ def unzip_upload():
if not ok:
return jsonify(ok=False, error=err), 400
return jsonify(ok=True, files=files)
@upload_bp.route("/api/unzip_refs", methods=["POST"])
def unzip_refs():
"""Распаковать ZIP через ВМ-буфер (паттерн drhider): pull ZIP с ВМ → распаковка."""
data = request.get_json(silent=True) or {}
files = data.get("files") or []
if not files:
return jsonify(ok=False, error="no files"), 400
ref = files[0]
try:
name, content = _pull_from_ref(ref)
except Exception as e:
return jsonify(ok=False, error=str(e)), 400
ok, unzipped, err = _unzip(content)
if not ok:
return jsonify(ok=False, error=err), 400
return jsonify(ok=True, files=unzipped)