convert-service: веб-морда для конвертации .doc → .docx
This commit is contained in:
+104
-17
@@ -1,20 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""LibreOffice converter — Flask HTTP service. POST .doc → .docx."""
|
||||
"""LibreOffice converter — Flask HTTP service. .doc → .docx with UI + API."""
|
||||
import subprocess, tempfile, os
|
||||
from flask import Flask, request, send_file
|
||||
from flask import Flask, request, send_file, render_template_string
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/convert", methods=["POST"])
|
||||
def convert():
|
||||
if "file" not in request.files:
|
||||
return {"ok": False, "error": "no file"}, 400
|
||||
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>"""
|
||||
|
||||
f = request.files["file"]
|
||||
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(
|
||||
@@ -23,15 +88,9 @@ def convert():
|
||||
)
|
||||
docx_files = [x for x in os.listdir(tmpdir) if x.endswith(".docx")]
|
||||
if not docx_files:
|
||||
return {"ok": False, "error": "conversion produced no .docx"}, 500
|
||||
return send_file(os.path.join(tmpdir, docx_files[0]),
|
||||
mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
as_attachment=True,
|
||||
download_name=os.path.splitext(f.filename)[0] + ".docx")
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"ok": False, "error": "conversion timeout"}, 500
|
||||
except subprocess.CalledProcessError as e:
|
||||
return {"ok": False, "error": f"libreoffice: {e.stderr.decode() if e.stderr else 'unknown'}"}, 500
|
||||
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)
|
||||
@@ -39,9 +98,37 @@ def convert():
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user