Files
contracts/convert-service/app.py
T

135 lines
5.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.
#!/usr/bin/env python3
"""LibreOffice converter — Flask HTTP service. .doc → .docx with UI + API."""
import subprocess, tempfile, os
from flask import Flask, request, send_file, render_template_string
app = Flask(__name__)
HTML = """<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Конвертер .doc → .docx</title>
<style>
body{font-family:system-ui,sans-serif;max-width:600px;margin:40px auto;padding:0 16px;color:#1a1a1a}
h1{font-size:20px;margin-bottom:8px}
.sub{color:#888;font-size:13px;margin-bottom:24px}
form{background:#f5f5f5;padding:24px;border-radius:8px}
input[type=file]{margin-bottom:16px;display:block}
button{background:#0066cc;color:#fff;border:none;padding:10px 24px;border-radius:6px;cursor:pointer;font-size:14px}
button:hover{background:#0052a3}
button:disabled{opacity:0.5;cursor:not-allowed}
.result{margin-top:20px;padding:16px;border-radius:8px}
.ok{background:#e6ffe6;border:1px solid #b3e6b3}
.err{background:#ffe6e6;border:1px solid #e6b3b3}
.info{background:#e6f0ff;border:1px solid #b3d4e6;margin:16px 0;padding:12px;border-radius:6px;font-size:13px}
code{background:#eee;padding:1px 4px;border-radius:3px;font-size:12px}
.spinner{display:inline-block;width:14px;height:14px;border:2px solid #ccc;border-top-color:#0066cc;border-radius:50%;animation:spin 0.8s linear infinite;margin-right:6px;vertical-align:middle}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body>
<h1>📄 Конвертер .doc → .docx</h1>
<p class="sub">LibreOffice в headless-режиме. Поддерживает .doc, .rtf, .odt.</p>
<div class="info">
<strong>API:</strong> <code>POST /convert</code> с <code>multipart/form-data</code>, поле <code>file</code>.<br>
Ответ: файл .docx или <code>{"ok":false,"error":"..."}</code>.<br>
<strong>Health:</strong> <code>GET /health</code> → <code>{"ok":true}</code>
</div>
<hr style="margin:24px 0;border:none;border-top:1px solid #e0e0e0">
<h2 style="font-size:16px;margin-bottom:12px">Конвертировать файл</h2>
<form id="convForm" enctype="multipart/form-data">
<input type="file" name="file" accept=".doc,.rtf,.odt" required id="fileInput">
<button type="submit" id="submitBtn">Конвертировать</button>
</form>
<div id="result"></div>
<script>
document.getElementById('convForm').onsubmit = async function(e) {
e.preventDefault();
var btn = document.getElementById('submitBtn');
var result = document.getElementById('result');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span>Конвертирую...';
result.innerHTML = '';
try {
var fd = new FormData();
fd.append('file', document.getElementById('fileInput').files[0]);
var resp = await fetch('/convert', { method: 'POST', body: fd });
if (resp.ok && resp.headers.get('Content-Type').includes('application/vnd')) {
var blob = await resp.blob();
var url = URL.createObjectURL(blob);
var fn = document.getElementById('fileInput').files[0].name.replace(/\\.[^.]+$/,'') + '.docx';
result.innerHTML = '<div class="result ok">✅ Готово! <a href="'+url+'" download="'+fn+'">Скачать '+fn+'</a></div>';
} else {
var err = await resp.json();
result.innerHTML = '<div class="result err">❌ ' + (err.error || 'Ошибка') + '</div>';
}
} catch(ex) {
result.innerHTML = '<div class="result err">❌ Сеть: ' + ex.message + '</div>';
}
btn.disabled = false;
btn.textContent = 'Конвертировать';
};
</script>
</body>
</html>"""
def _convert_file(f):
"""Convert one file to .docx, return (bytes, download_name) or raise."""
with tempfile.NamedTemporaryFile(suffix=".doc", delete=False) as tmp:
f.save(tmp)
doc_path = tmp.name
tmpdir = tempfile.mkdtemp()
try:
subprocess.run(
["libreoffice", "--headless", "--convert-to", "docx", "--outdir", tmpdir, doc_path],
timeout=120, capture_output=True, check=True
)
docx_files = [x for x in os.listdir(tmpdir) if x.endswith(".docx")]
if not docx_files:
raise ValueError("conversion produced no .docx")
with open(os.path.join(tmpdir, docx_files[0]), "rb") as out:
return out.read(), os.path.splitext(f.filename)[0] + ".docx"
finally:
if os.path.exists(doc_path):
os.unlink(doc_path)
for x in os.listdir(tmpdir):
os.unlink(os.path.join(tmpdir, x))
os.rmdir(tmpdir)
@app.route("/", methods=["GET"])
def index():
return render_template_string(HTML)
@app.route("/convert", methods=["POST"])
def convert():
if "file" not in request.files:
return {"ok": False, "error": "no file"}, 400
f = request.files["file"]
if not f.filename:
return {"ok": False, "error": "empty filename"}, 400
try:
data, name = _convert_file(f)
return send_file(
io.BytesIO(data),
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
as_attachment=True,
download_name=name,
)
except subprocess.TimeoutExpired:
return {"ok": False, "error": "conversion timeout"}, 500
except subprocess.CalledProcessError as e:
err = e.stderr.decode() if e.stderr else "unknown"
return {"ok": False, "error": f"libreoffice: {err}"}, 500
except ValueError as e:
return {"ok": False, "error": str(e)}, 500
@app.route("/health", methods=["GET"])
def health():
return {"ok": True}
if __name__ == "__main__":
import io
app.run(host="0.0.0.0", port=5000)