Move preview error handling to mediapreview.
This commit is contained in:
+1
-1
@@ -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)."""
|
"""Build and run the patched OnlyOffice Docker image (via mediapreview)."""
|
||||||
if confdir is not None:
|
if confdir is not None:
|
||||||
os.environ["CISTA_HOME"] = confdir.as_posix()
|
os.environ["CISTA_HOME"] = confdir.as_posix()
|
||||||
|
|||||||
+18
-71
@@ -6,22 +6,19 @@ Sanic with auth, etag negotiation and the in-memory response cache.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from pathlib import PurePosixPath
|
from pathlib import PurePosixPath
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
from wsgiref.handlers import format_date_time
|
from wsgiref.handlers import format_date_time
|
||||||
|
|
||||||
import httpx
|
|
||||||
from mediapreview import CachedPreview, PreviewCache, is_previewable_path
|
from mediapreview import CachedPreview, PreviewCache, is_previewable_path
|
||||||
from mediapreview.formats import OFFICE_PREVIEW_SUFFIXES
|
from mediapreview.exceptions import (
|
||||||
from mediapreview.formats import expected_backend as _expected_preview_backend
|
PreviewBackendError,
|
||||||
from mediapreview.office import onlyoffice_error_short_text
|
PreviewCancelledError,
|
||||||
from mediapreview.pool import (
|
|
||||||
PREVIEW_TIMEOUT,
|
|
||||||
PreviewError,
|
PreviewError,
|
||||||
PreviewPoolClosedError,
|
)
|
||||||
PreviewTimeoutError,
|
from mediapreview.formats import OFFICE_PREVIEW_SUFFIXES
|
||||||
|
from mediapreview.pool import (
|
||||||
generate_office_preview,
|
generate_office_preview,
|
||||||
run_preview,
|
run_preview,
|
||||||
)
|
)
|
||||||
@@ -39,21 +36,6 @@ bp = Blueprint("preview", url_prefix="/preview")
|
|||||||
_preview_cache = PreviewCache(capacity=500)
|
_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
|
@bp.on_request
|
||||||
async def verify_preview(request):
|
async def verify_preview(request):
|
||||||
"""Verify access to preview routes."""
|
"""Verify access to preview routes."""
|
||||||
@@ -101,57 +83,22 @@ async def preview(req, path):
|
|||||||
# Generate preview
|
# Generate preview
|
||||||
try:
|
try:
|
||||||
if filepath.suffix.lower() in OFFICE_PREVIEW_SUFFIXES:
|
if filepath.suffix.lower() in OFFICE_PREVIEW_SUFFIXES:
|
||||||
img, preview_resp = await asyncio.wait_for(
|
img, preview_resp = await generate_office_preview(
|
||||||
generate_office_preview(filepath, quality, maxsize, maxzoom),
|
filepath, quality, maxsize, maxzoom
|
||||||
timeout=PREVIEW_TIMEOUT,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
img, preview_resp = await asyncio.wait_for(
|
img, preview_resp = await run_preview(filepath, quality, maxsize, maxzoom)
|
||||||
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)
|
|
||||||
except PreviewError as e:
|
except PreviewError as e:
|
||||||
detail = str(e)
|
# mediapreview is responsible for backend-specific diagnostics; cista only
|
||||||
if detail == "preview worker error" and e.stderr:
|
# needs the backend name, a short access-log reason, and a response status.
|
||||||
captured = e.stderr.strip()
|
if isinstance(e, PreviewCancelledError):
|
||||||
if captured:
|
req.ctx.log_extra = e.short or "preview cancelled"
|
||||||
detail = captured.splitlines()[0]
|
raise asyncio.CancelledError from e
|
||||||
# The worker already logged the failure (with traceback where the
|
status = 422 if isinstance(e, PreviewBackendError) else 503
|
||||||
# error occurred) — annotate the access log instead of re-logging,
|
req.ctx.log_extra = f"{e.backend}: {e.short}" if e.backend else e.short
|
||||||
# with a shortened reason. In dev mode, print the full error too.
|
|
||||||
backend = e.backend or _expected_preview_backend(filepath)
|
|
||||||
if req.app.debug:
|
if req.app.debug:
|
||||||
full = detail
|
logger.warning("%s", str(e))
|
||||||
if e.stderr and e.stderr.strip() not in detail:
|
return empty(status)
|
||||||
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)
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
# Server shutdown or client disconnect: the connection is being torn
|
# Server shutdown or client disconnect: the connection is being torn
|
||||||
# down, so responding is impossible — just annotate the access log.
|
# down, so responding is impossible — just annotate the access log.
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ dependencies = [
|
|||||||
"html5tagger>=1.3.0",
|
"html5tagger>=1.3.0",
|
||||||
"httpx>=0.28.0",
|
"httpx>=0.28.0",
|
||||||
"inotify>=0.2.12",
|
"inotify>=0.2.12",
|
||||||
"mediapreview[standard]",
|
"mediapreview[standard]>=0.2.0",
|
||||||
"msgspec>=0.19.0",
|
"msgspec>=0.19.0",
|
||||||
"natsort>=8.4.0",
|
"natsort>=8.4.0",
|
||||||
"numpy>=2.3.2",
|
"numpy>=2.3.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user