146 lines
5.0 KiB
Python
146 lines
5.0 KiB
Python
"""Upload blueprint — загрузка, конвертация, распаковка."""
|
|
import io, os, base64, hashlib, zipfile, tempfile, subprocess
|
|
from flask import Blueprint, request, jsonify, send_file
|
|
from site.services.parse import parse_file
|
|
from site.db import documents
|
|
from site.config import MAX_CONTENT_LENGTH
|
|
|
|
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 (без сохранения на диск)."""
|
|
f = request.files.get("files")
|
|
if not f:
|
|
return jsonify(ok=False, error="no file"), 400
|
|
|
|
data = f.read()
|
|
doc_path = None
|
|
tmpdir = None
|
|
try:
|
|
with tempfile.NamedTemporaryFile(suffix=".doc", delete=False) as tmp:
|
|
tmp.write(data)
|
|
doc_path = tmp.name
|
|
tmpdir = tempfile.mkdtemp()
|
|
subprocess.run(
|
|
["libreoffice", "--headless", "--convert-to", "docx", "--outdir", tmpdir, doc_path],
|
|
timeout=30, capture_output=True,
|
|
)
|
|
docx_files = [x for x in os.listdir(tmpdir) if x.endswith(".docx")]
|
|
if docx_files:
|
|
with open(os.path.join(tmpdir, docx_files[0]), "rb") as out:
|
|
return send_file(
|
|
io.BytesIO(out.read()),
|
|
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
)
|
|
return jsonify(ok=False, error="conversion produced no output"), 500
|
|
finally:
|
|
if doc_path and os.path.exists(doc_path):
|
|
os.unlink(doc_path)
|
|
if tmpdir and os.path.exists(tmpdir):
|
|
for x in os.listdir(tmpdir):
|
|
os.unlink(os.path.join(tmpdir, x))
|
|
os.rmdir(tmpdir)
|
|
|
|
|
|
@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)
|