"""PDF 미리보기 생성 워커 — LibreOffice Headless로 문서→PDF 변환""" import subprocess import shutil from datetime import datetime, timezone from pathlib import Path from sqlalchemy.ext.asyncio import AsyncSession from core.config import settings from core.utils import setup_logger logger = setup_logger("preview_worker") # PDF 변환 대상 포맷 CONVERTIBLE_FORMATS = { "docx", "xlsx", "pptx", "odt", "ods", "odp", # 안정 지원 "odoc", "osheet", "hwp", "hwpx", # 검증 필요 } # 이미 PDF이거나 변환 불필요한 포맷 NATIVE_PDF = {"pdf"} NATIVE_IMAGE = {"jpg", "jpeg", "png", "gif", "bmp", "tiff"} TEXT_FORMATS = {"md", "txt", "csv", "json", "xml", "html"} PREVIEW_DIR_NAME = "PKM/.preview" TIMEOUT_SECONDS = 60 async def process(document_id: int, session: AsyncSession) -> None: """문서 PDF 미리보기 생성""" from models.document import Document doc = await session.get(Document, document_id) if not doc: logger.error(f"[preview] document_id={document_id} 없음") return fmt = doc.file_format.lower() # PDF/이미지/텍스트는 변환 불필요 if fmt in NATIVE_PDF or fmt in NATIVE_IMAGE or fmt in TEXT_FORMATS: doc.preview_status = "ready" if fmt in NATIVE_PDF else "none" doc.preview_at = datetime.now(timezone.utc) await session.commit() return if fmt not in CONVERTIBLE_FORMATS: doc.preview_status = "none" await session.commit() logger.info(f"[preview] {doc.title} — 변환 불가 포맷: {fmt}") return # 원본 파일 경로 source = Path(settings.nas_mount_path) / doc.file_path if not source.exists(): doc.preview_status = "failed" await session.commit() logger.error(f"[preview] 원본 없음: {source}") return # 미리보기 디렉토리 preview_dir = Path(settings.nas_mount_path) / PREVIEW_DIR_NAME preview_dir.mkdir(parents=True, exist_ok=True) output_path = preview_dir / f"{document_id}.pdf" doc.preview_status = "processing" await session.commit() # LibreOffice 변환 try: tmp_dir = Path("/tmp/preview_work") tmp_dir.mkdir(exist_ok=True) result = subprocess.run( [ "libreoffice", "--headless", "--convert-to", "pdf", "--outdir", str(tmp_dir), str(source), ], capture_output=True, text=True, timeout=TIMEOUT_SECONDS, ) if result.returncode != 0: raise RuntimeError(f"LibreOffice 변환 실패: {result.stderr[:200]}") # 변환 결과 찾기 converted = tmp_dir / f"{source.stem}.pdf" if not converted.exists(): raise RuntimeError(f"변환 결과물 없음: {converted}") # 캐시로 이동 shutil.move(str(converted), str(output_path)) doc.preview_status = "ready" doc.preview_hash = doc.file_hash doc.preview_at = datetime.now(timezone.utc) await session.commit() logger.info(f"[preview] {doc.title} → PDF 변환 완료") except subprocess.TimeoutExpired: doc.preview_status = "failed" await session.commit() logger.error(f"[preview] {doc.title} — 변환 timeout ({TIMEOUT_SECONDS}s)") except Exception as e: doc.preview_status = "failed" await session.commit() logger.error(f"[preview] {doc.title} — 변환 실패: {e}")