v2.2.1 - Intégration de LibreOffice Headless pour les exports PDF natifs (Étapes 3 & 4)
Build and Push Docker Image / build-and-push (push) Failing after 27s
Build and Push Docker Image / build-and-push (push) Failing after 27s
This commit is contained in:
1 parent
b0f8a4e62d
commit
626ec138e4
3 files changed
+264
-79
No files matched your search
+116
-4
@@ -1,16 +1,43 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, Response
|
||||
from pydantic import BaseModel
|
||||
import requests
|
||||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
import json
|
||||
import subprocess
|
||||
import urllib3
|
||||
import socket
|
||||
import config
|
||||
|
||||
# Forçage IPv4
|
||||
old_getaddrinfo = socket.getaddrinfo
|
||||
def force_ipv4_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
|
||||
return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)
|
||||
socket.getaddrinfo = force_ipv4_getaddrinfo
|
||||
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
app = FastAPI(title="AETHAS38 API")
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
FRONTEND_DIR = os.path.join(BASE_DIR, "frontend")
|
||||
SESSIONS_DIR = os.path.join(BASE_DIR, "sessions")
|
||||
MEDIA_DIR = os.path.join(SESSIONS_DIR, "media")
|
||||
TEMP_DIR = os.path.join(SESSIONS_DIR, "temp")
|
||||
|
||||
os.makedirs(MEDIA_DIR, exist_ok=True)
|
||||
os.makedirs(TEMP_DIR, exist_ok=True)
|
||||
|
||||
# Montage dynamique
|
||||
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
|
||||
app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
|
||||
|
||||
class ExportRequest(BaseModel):
|
||||
content: str
|
||||
format: str # pdf, txt, md, json
|
||||
type: str # prompt ou response
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def serve_admin():
|
||||
@@ -25,4 +52,89 @@ async def get_config():
|
||||
@app.post("/api/config")
|
||||
async def update_config(data: dict):
|
||||
config.save_config(data)
|
||||
return {"status": "success", "message": "Configuration sauvegardée avec succès."}
|
||||
return {"status": "success", "message": "Configuration sauvegardée avec succès."}
|
||||
|
||||
@app.post("/api/export")
|
||||
async def export_content(req: ExportRequest):
|
||||
filename_base = f"aethas38_{req.type}_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
if req.format == "txt":
|
||||
return Response(
|
||||
content=req.content,
|
||||
media_type="text/plain",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename_base}.txt"}
|
||||
)
|
||||
|
||||
elif req.format == "md":
|
||||
return Response(
|
||||
content=f"# AETHAS38 Export ({req.type.upper()})\n\n{req.content}",
|
||||
media_type="text/markdown",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename_base}.md"}
|
||||
)
|
||||
|
||||
elif req.format == "json":
|
||||
json_data = json.dumps({"type": req.type, "content": req.content}, indent=4, ensure_ascii=False)
|
||||
return Response(
|
||||
content=json_data,
|
||||
media_type="application/json",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename_base}.json"}
|
||||
)
|
||||
|
||||
elif req.format == "pdf":
|
||||
# Utilisation de LibreOffice en mode headless
|
||||
html_filename = os.path.join(TEMP_DIR, f"{filename_base}.html")
|
||||
pdf_filename = os.path.join(TEMP_DIR, f"{filename_base}.pdf")
|
||||
|
||||
# Création d'un HTML propre pour la conversion
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; margin: 40px; color: #333; }}
|
||||
h1 {{ color: #00e5ff; border-bottom: 2px solid #ddd; padding-bottom: 10px; }}
|
||||
pre, p {{ white-space: pre-wrap; line-height: 1.6; background: #f4f4f4; padding: 15px; border-radius: 5px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AETHAS38 - Export {req.type.upper()}</h1>
|
||||
<p>{req.content}</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
with open(html_filename, "w", encoding="utf-8") as f:
|
||||
f.write(html_content)
|
||||
|
||||
try:
|
||||
# Commande LibreOffice headless pour convertir le HTML en PDF
|
||||
# Note: selon l'OS (Windows vs Linux Docker), la commande peut s'appeler 'soffice' ou 'libreoffice'
|
||||
cmd = ["soffice", "--headless", "--convert-to", "pdf", "--outdir", TEMP_DIR, html_filename]
|
||||
|
||||
# Exécution synchrone dans un thread pour ne pas bloquer l'API
|
||||
def run_libreoffice():
|
||||
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
await asyncio.to_thread(run_libreoffice)
|
||||
|
||||
if not os.path.exists(pdf_filename):
|
||||
raise Exception("La génération du PDF par LibreOffice a échoué.")
|
||||
|
||||
with open(pdf_filename, "rb") as pdf_file:
|
||||
pdf_bytes = pdf_file.read()
|
||||
|
||||
# Nettoyage des fichiers temporaires
|
||||
os.remove(html_filename)
|
||||
os.remove(pdf_filename)
|
||||
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename_base}.pdf"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Erreur de génération PDF via LibreOffice : {str(e)}")
|
||||
|
||||
raise HTTPException(status_code=400, detail="Format d'export non supporté.")
|
||||
@@ -8,4 +8,5 @@ openpyxl
|
||||
fastapi
|
||||
uvicorn
|
||||
pydantic
|
||||
python-dotenv
|
||||
python-dotenv
|
||||
httpx
|
||||
Reference in new issue
Block a user