135 lines
5.6 KiB
Python
135 lines
5.6 KiB
Python
#!/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)
|