0a9588abb7
- Add pagination support to findAll (page, limit query params) - Add findByTemplateId method to service - Add GET /by-template/:templateId endpoint to controller - Service already includes CRUD for QuestionBank and QuestionBankItem
192 lines
6.5 KiB
Python
192 lines
6.5 KiB
Python
import io
|
|
import os
|
|
import subprocess
|
|
import time
|
|
from typing import Optional
|
|
|
|
from fastapi import FastAPI, File, HTTPException, UploadFile
|
|
from fastapi.responses import FileResponse, RedirectResponse
|
|
from PIL import Image # Pillow library for image processing
|
|
from pydantic import BaseModel
|
|
|
|
# Response models
|
|
class ConvertResponse(BaseModel):
|
|
pdf_path: str
|
|
converted: bool
|
|
original: Optional[str] = None
|
|
file_size: Optional[int] = None
|
|
error: Optional[str] = None
|
|
|
|
class HealthResponse(BaseModel):
|
|
status: str
|
|
service: str
|
|
version: str
|
|
uptime: float
|
|
|
|
# FastAPI Application
|
|
app = FastAPI(
|
|
title="LibreOffice Document Conversion Service",
|
|
description="Convert Word/PPT/Excel/PDF to PDF and support mixed content document processing",
|
|
version="1.0.0",
|
|
docs_url="/docs",
|
|
redoc_url="/redoc"
|
|
)
|
|
|
|
start_time = time.time()
|
|
|
|
@app.get("/", include_in_schema=False)
|
|
async def root():
|
|
"""Redirect to documentation page"""
|
|
return RedirectResponse(url="/docs")
|
|
|
|
@app.get("/health", response_model=HealthResponse)
|
|
async def health():
|
|
"""Health check interface"""
|
|
return HealthResponse(
|
|
status="healthy",
|
|
service="libreoffice-converter",
|
|
version="1.0.0",
|
|
uptime=time.time() - start_time
|
|
)
|
|
|
|
@app.post("/convert")
|
|
async def convert(file: UploadFile = File(...)):
|
|
"""
|
|
Document conversion interface
|
|
Returns: PDF file stream
|
|
"""
|
|
try:
|
|
# File format validation
|
|
allowed_extensions = [
|
|
'.pdf', '.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx',
|
|
'.md', '.txt', '.rtf', '.odt', '.ods', '.odp',
|
|
'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'
|
|
]
|
|
file_ext = os.path.splitext(file.filename)[1].lower()
|
|
|
|
if file_ext not in allowed_extensions:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Unsupported file format: {file_ext}. Supported formats: {', '.join(allowed_extensions)}"
|
|
)
|
|
|
|
# Check uploads directory existence
|
|
upload_dir = "/app/uploads" if os.path.exists("/app/uploads") else "./uploads"
|
|
os.makedirs(upload_dir, exist_ok=True)
|
|
|
|
# Save uploaded file
|
|
filepath = os.path.join(upload_dir, file.filename)
|
|
with open(filepath, "wb") as buffer:
|
|
content = await file.read()
|
|
buffer.write(content)
|
|
|
|
# For PDF files, return directly without conversion
|
|
if file_ext == '.pdf':
|
|
return FileResponse(filepath, filename=file.filename, media_type='application/pdf')
|
|
|
|
if file_ext == '.md':
|
|
# Use Node.js script to render Markdown to PDF
|
|
expected_pdf = filepath.rsplit('.', 1)[0] + '.pdf'
|
|
cmd = [
|
|
'node',
|
|
'/app/md_to_pdf.js',
|
|
filepath,
|
|
expected_pdf
|
|
]
|
|
elif file_ext in ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp']:
|
|
# For image files, use Pillow to convert to PDF
|
|
expected_pdf = filepath.rsplit('.', 1)[0] + '.pdf'
|
|
|
|
# Open image and save as PDF
|
|
with Image.open(filepath) as img:
|
|
# Convert RGBA mode to RGB (support for transparent images)
|
|
if img.mode in ('RGBA', 'LA', 'P'):
|
|
# Convert to white background
|
|
background = Image.new('RGB', img.size, (255, 255, 255))
|
|
if img.mode == 'P':
|
|
img = img.convert('RGBA')
|
|
background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
|
|
img = background
|
|
elif img.mode != 'RGB':
|
|
img = img.convert('RGB')
|
|
|
|
# Save as PDF
|
|
img.save(expected_pdf, 'PDF', resolution=100.0, save_all=False)
|
|
|
|
# Verify PDF generation completed
|
|
if not os.path.exists(expected_pdf):
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Image to PDF conversion succeeded but output file not found"
|
|
)
|
|
|
|
# Image conversion completed, return PDF file
|
|
filename_base = os.path.splitext(file.filename)[0]
|
|
return FileResponse(expected_pdf, filename=f"{filename_base}.pdf", media_type='application/pdf')
|
|
else:
|
|
# Conversion using LibreOffice
|
|
cmd = [
|
|
'soffice',
|
|
'--headless',
|
|
'--convert-to', 'pdf',
|
|
'--outdir', upload_dir,
|
|
filepath
|
|
]
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=600, # Extended to 10 minutes to support complex Markdown conversion
|
|
)
|
|
|
|
# Combine stdout and stderr for error reporting since capture_output uses PIPE
|
|
combined_output = result.stdout if result.stdout else ""
|
|
if result.stderr:
|
|
combined_output += "\n" + result.stderr
|
|
|
|
# Display Node.js script output for debugging
|
|
print(f"Node.js script output: {combined_output}")
|
|
|
|
if result.returncode != 0:
|
|
print(f"Subprocess failed with return code: {result.returncode}")
|
|
|
|
# Combine stdout and stderr for error reporting
|
|
combined_output = result.stdout if result.stdout else ""
|
|
if result.stderr:
|
|
combined_output += "\n" + result.stderr
|
|
|
|
print(f"Subprocess output: {combined_output}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Conversion failed: {combined_output}"
|
|
)
|
|
|
|
# Verify output file
|
|
expected_pdf = filepath.rsplit('.', 1)[0] + '.pdf'
|
|
if not os.path.exists(expected_pdf):
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Conversion succeeded but output file not found"
|
|
)
|
|
|
|
filename_base = os.path.splitext(file.filename)[0]
|
|
return FileResponse(expected_pdf, filename=f"{filename_base}.pdf", media_type='application/pdf')
|
|
|
|
except HTTPException:
|
|
raise
|
|
except subprocess.TimeoutExpired:
|
|
raise HTTPException(status_code=504, detail="Conversion timeout (300 seconds)")
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@app.get("/version")
|
|
async def version():
|
|
"""Version information"""
|
|
return {
|
|
"service": "libreoffice-converter",
|
|
"version": "1.0.0",
|
|
"framework": "FastAPI",
|
|
"libreoffice": "7.x"
|
|
}
|