RAG/docker/soffice/app.py

131 lines
4.9 KiB
Python

import os
import tempfile
import subprocess
import shutil
import glob
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.responses import FileResponse
from pydantic import BaseModel
from starlette.concurrency import run_in_threadpool
from starlette.background import BackgroundTask
app = FastAPI()
@app.get('/health')
async def health():
return {'status': 'ok'}
@app.get('/diagnose')
async def diagnose():
info = {}
try:
info['soffice_path'] = shutil.which('soffice')
except Exception as e:
info['soffice_path_error'] = str(e)
try:
proc = subprocess.run(['soffice', '--version'], capture_output=True, text=True, timeout=5)
info['soffice_version'] = proc.stdout.strip() or proc.stderr.strip()
except Exception as e:
info['soffice_version_error'] = str(e)
try:
info['libreoffice_program_list'] = os.listdir('/usr/lib/libreoffice/program')[:50]
except Exception as e:
info['libreoffice_program_list_error'] = str(e)
return info
@app.post('/convert')
async def convert(file: UploadFile = File(...)):
"""Accept a file upload, convert it to .docx via soffice, and return the resulting file."""
# Save upload to a temp file
tmpdir = tempfile.mkdtemp(prefix='soffice_upload_')
# print("Created temp dir", tmpdir)
try:
original_name = file.filename or 'uploaded'
# print(original_name)
_, ext = os.path.splitext(original_name)
in_path = os.path.join(tmpdir, 'input' + (ext or ''))
with open(in_path, 'wb') as f:
content = await file.read()
f.write(content)
# print("Saved uploaded file to", in_path)
# ========== 修改点 1: 添加文件存在检查 ==========
if not os.path.exists(in_path):
raise HTTPException(status_code=500, detail="Uploaded file was not saved properly")
# If already .docx, return it directly
if in_path.lower().endswith('.docx'):
# ========== 修改点 2: 为 .docx 文件添加背景清理任务 ==========
return FileResponse(
in_path,
media_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
filename=os.path.basename(in_path),
background=BackgroundTask(lambda: cleanup_temp_dir(tmpdir)) # 使用 BackgroundTask
)
def run_conv():
return subprocess.run(
['soffice', '--headless', '--convert-to', 'docx', '--outdir', tmpdir, in_path],
check=True, capture_output=True, text=True, timeout=120
)
try:
await run_in_threadpool(run_conv)
except subprocess.CalledProcessError as e:
stderr = (e.stderr or '').strip()
raise HTTPException(status_code=500, detail=f'soffice conversion failed: {stderr}')
except subprocess.TimeoutExpired:
raise HTTPException(status_code=504, detail='conversion timeout')
candidates = glob.glob(os.path.join(tmpdir, '*.docx'))
# print("Conversion produced candidates:", candidates)
if not candidates:
raise HTTPException(status_code=500, detail='soffice produced no .docx')
out_file = candidates[0]
# ========== 修改点 3: 添加输出文件存在检查 ==========
if not os.path.exists(out_file):
raise HTTPException(status_code=500, detail="Converted file was not created properly")
out_fname = os.path.splitext(original_name)[0] + '.docx'
# print("Returning converted file", out_file, "as", out_fname)
# ========== 修改点 4: 主要修改 - 使用 BackgroundTask 延迟清理 ==========
return FileResponse(
out_file,
media_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
filename=out_fname,
background=BackgroundTask(lambda: cleanup_temp_dir(tmpdir)) # 使用 BackgroundTask
)
# ========== 修改点 5: 移除 finally 块中的立即清理,改为只在异常时清理 ==========
except Exception as e:
# 只有在发生异常时才立即清理
cleanup_temp_dir(tmpdir)
# 重新抛出异常
if isinstance(e, HTTPException):
raise e
else:
raise HTTPException(status_code=500, detail=f"Conversion error: {str(e)}")
# ========== 修改点 6: 添加清理函数 ==========
def cleanup_temp_dir(tmpdir: str):
"""清理临时目录,忽略错误"""
try:
if tmpdir and os.path.exists(tmpdir):
shutil.rmtree(tmpdir, ignore_errors=True)
print(f"Cleaned up temp dir: {tmpdir}")
except Exception as e:
print(f"Warning: Failed to cleanup temp dir {tmpdir}: {e}")
if __name__ == '__main__':
import uvicorn
port = int(os.environ.get('SOFFICE_PORT') or 8003)
uvicorn.run('app:app', host='0.0.0.0', port=port)