From 0c9fe7638cc830cba75388ceb50cec4337a61a40 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 13 Aug 2026 04:29:25 +0000 Subject: [PATCH] Move preview error handling to mediapreview. --- cista/onlyoffice.py | 2 +- cista/preview.py | 89 +++++++++------------------------------------ pyproject.toml | 2 +- 3 files changed, 20 insertions(+), 73 deletions(-) diff --git a/cista/onlyoffice.py b/cista/onlyoffice.py index 2bd7f00..dc6d9eb 100644 --- a/cista/onlyoffice.py +++ b/cista/onlyoffice.py @@ -20,7 +20,7 @@ def configure() -> None: ) -def setup_docker(confdir: Path | None = None) -> int: +def setup_docker(confdir: Path | None = None) -> str: """Build and run the patched OnlyOffice Docker image (via mediapreview).""" if confdir is not None: os.environ["CISTA_HOME"] = confdir.as_posix() diff --git a/cista/preview.py b/cista/preview.py index f608938..4d0da7b 100644 --- a/cista/preview.py +++ b/cista/preview.py @@ -6,22 +6,19 @@ Sanic with auth, etag negotiation and the in-memory response cache. """ import asyncio -import re import urllib.parse from pathlib import PurePosixPath from urllib.parse import unquote from wsgiref.handlers import format_date_time -import httpx from mediapreview import CachedPreview, PreviewCache, is_previewable_path -from mediapreview.formats import OFFICE_PREVIEW_SUFFIXES -from mediapreview.formats import expected_backend as _expected_preview_backend -from mediapreview.office import onlyoffice_error_short_text -from mediapreview.pool import ( - PREVIEW_TIMEOUT, +from mediapreview.exceptions import ( + PreviewBackendError, + PreviewCancelledError, PreviewError, - PreviewPoolClosedError, - PreviewTimeoutError, +) +from mediapreview.formats import OFFICE_PREVIEW_SUFFIXES +from mediapreview.pool import ( generate_office_preview, run_preview, ) @@ -39,21 +36,6 @@ bp = Blueprint("preview", url_prefix="/preview") _preview_cache = PreviewCache(capacity=500) -def _shorten_error(detail: str) -> str: - """Shorten an upstream backend error for the single-line access log. - - Backend errors arrive verbatim from ffmpeg/pyvips/pymupdf and often carry - an '[Errno N]' prefix, the input file path, and multi-line library noise — - all redundant with the URL already in the log line. Keep the first line, - drop the bracket prefix, and cut at the first ': ' separator. - """ - lines = detail.splitlines() - if not lines: - return "" - first_line = re.sub(r"^\[[^\]]*\]\s*", "", lines[0].strip()) - return first_line.split(": ", 1)[0] - - @bp.on_request async def verify_preview(request): """Verify access to preview routes.""" @@ -101,57 +83,22 @@ async def preview(req, path): # Generate preview try: if filepath.suffix.lower() in OFFICE_PREVIEW_SUFFIXES: - img, preview_resp = await asyncio.wait_for( - generate_office_preview(filepath, quality, maxsize, maxzoom), - timeout=PREVIEW_TIMEOUT, + img, preview_resp = await generate_office_preview( + filepath, quality, maxsize, maxzoom ) else: - img, preview_resp = await asyncio.wait_for( - run_preview(filepath, quality, maxsize, maxzoom), - timeout=PREVIEW_TIMEOUT, - ) - except TimeoutError: - req.ctx.log_extra = f"{_expected_preview_backend(filepath)} timeout" - return empty(503) - except PreviewTimeoutError as e: - req.ctx.log_extra = ( - f"{(e.backend or _expected_preview_backend(filepath))} timeout" - ) - return empty(503) - except httpx.HTTPStatusError: - req.ctx.log_extra = "onlyoffice N/A" - return empty(503) - except httpx.RequestError: - req.ctx.log_extra = "onlyoffice N/A" - return empty(503) - except RuntimeError as e: - detail = str(e) - if detail.startswith("OnlyOffice"): - req.ctx.log_extra = onlyoffice_error_short_text(detail) - return empty(503) - raise - except PreviewPoolClosedError: - # Server is shutting down; not an error, just a cancelled preview. - req.ctx.log_extra = "preview cancelled" - return empty(503) + img, preview_resp = await run_preview(filepath, quality, maxsize, maxzoom) except PreviewError as e: - detail = str(e) - if detail == "preview worker error" and e.stderr: - captured = e.stderr.strip() - if captured: - detail = captured.splitlines()[0] - # The worker already logged the failure (with traceback where the - # error occurred) — annotate the access log instead of re-logging, - # with a shortened reason. In dev mode, print the full error too. - backend = e.backend or _expected_preview_backend(filepath) + # mediapreview is responsible for backend-specific diagnostics; cista only + # needs the backend name, a short access-log reason, and a response status. + if isinstance(e, PreviewCancelledError): + req.ctx.log_extra = e.short or "preview cancelled" + raise asyncio.CancelledError from e + status = 422 if isinstance(e, PreviewBackendError) else 503 + req.ctx.log_extra = f"{e.backend}: {e.short}" if e.backend else e.short if req.app.debug: - full = detail - if e.stderr and e.stderr.strip() not in detail: - full = f"{detail}\n{e.stderr.strip()}" - logger.warning("[%s] preview failed: %s", backend, full.strip()) - short = _shorten_error(detail) - req.ctx.log_extra = f"{backend}: {short}" if short else backend - return empty(422) + logger.warning("%s", str(e)) + return empty(status) except asyncio.CancelledError: # Server shutdown or client disconnect: the connection is being torn # down, so responding is impossible — just annotate the access log. diff --git a/pyproject.toml b/pyproject.toml index 626e154..7601c40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "html5tagger>=1.3.0", "httpx>=0.28.0", "inotify>=0.2.12", - "mediapreview[standard]", + "mediapreview[standard]>=0.2.0", "msgspec>=0.19.0", "natsort>=8.4.0", "numpy>=2.3.2",