backends: route office formats through dispatch(); diagnostic unknown-extension errors
Synchronous dispatch() now converts office documents via OnlyOffice (asyncio.run around OOConversionManager.convert), refusing to run inside a live event loop and pointing async callers at pool.generate_office_preview(). The office import stays lazy so the base install works without the 'office' extra. Unknown file types now name the offending extension (or its absence) instead of a generic 'preview unsupported'.
This commit is contained in:
@@ -5,6 +5,7 @@ plus quality/size parameters and returning `(avif_bytes, PreviewResponse)`.
|
|||||||
`dispatch` picks the right backend for a path.
|
`dispatch` picks the right backend for a path.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ from mediapreview.backends.image import (
|
|||||||
from mediapreview.backends.pdf import process_pdf
|
from mediapreview.backends.pdf import process_pdf
|
||||||
from mediapreview.backends.video import process_video
|
from mediapreview.backends.video import process_video
|
||||||
from mediapreview.exceptions import PreviewError, backend_error
|
from mediapreview.exceptions import PreviewError, backend_error
|
||||||
from mediapreview.formats import DOC_PREVIEW_SUFFIXES
|
from mediapreview.formats import DOC_PREVIEW_SUFFIXES, OFFICE_PREVIEW_SUFFIXES
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"dispatch",
|
"dispatch",
|
||||||
@@ -42,6 +43,42 @@ def dispatch(path, quality, maxsize, maxzoom, data=None):
|
|||||||
if suffix in DOC_PREVIEW_SUFFIXES:
|
if suffix in DOC_PREVIEW_SUFFIXES:
|
||||||
backend = "pdf"
|
backend = "pdf"
|
||||||
return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom)
|
return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom)
|
||||||
|
if suffix in OFFICE_PREVIEW_SUFFIXES:
|
||||||
|
backend = "onlyoffice"
|
||||||
|
try:
|
||||||
|
from mediapreview.office import ( # noqa: PLC0415
|
||||||
|
close_oo_client,
|
||||||
|
get_oo_manager,
|
||||||
|
)
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(
|
||||||
|
"Office document previews require the 'office' extra:"
|
||||||
|
" pip install mediapreview[office]"
|
||||||
|
) from e
|
||||||
|
try:
|
||||||
|
asyncio.get_running_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
pass # no event loop, asyncio.run() is safe
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Office preview via dispatch() cannot be called inside a running"
|
||||||
|
" event loop; use mediapreview.pool.generate_office_preview() instead"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _convert_office() -> bytes:
|
||||||
|
manager = get_oo_manager()
|
||||||
|
try:
|
||||||
|
return await manager.convert(path)
|
||||||
|
finally:
|
||||||
|
await close_oo_client()
|
||||||
|
|
||||||
|
png_bytes = asyncio.run(_convert_office())
|
||||||
|
result, resp = process_image_buffer(
|
||||||
|
png_bytes, quality=quality, maxsize=maxsize, maxzoom=maxzoom
|
||||||
|
)
|
||||||
|
if resp is not None:
|
||||||
|
resp.backend = "onlyoffice+" + (resp.backend or "vips")
|
||||||
|
return result, resp
|
||||||
mime_type, _ = mimetypes.guess_type(path.name)
|
mime_type, _ = mimetypes.guess_type(path.name)
|
||||||
if mime_type and mime_type.startswith("video/"):
|
if mime_type and mime_type.startswith("video/"):
|
||||||
backend = "video"
|
backend = "video"
|
||||||
@@ -62,4 +99,6 @@ def dispatch(path, quality, maxsize, maxzoom, data=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Preview dispatch failed for %s", path)
|
logger.exception("Preview dispatch failed for %s", path)
|
||||||
raise backend_error(backend, str(e)) from e
|
raise backend_error(backend, str(e)) from e
|
||||||
raise backend_error(backend, "preview unsupported")
|
if not suffix:
|
||||||
|
raise backend_error(backend, "unknown file type: no file extension")
|
||||||
|
raise backend_error(backend, f"unknown file extension: {suffix!r}")
|
||||||
|
|||||||
@@ -98,7 +98,11 @@ async def test_generate_office_preview_raises_structured_error(monkeypatch):
|
|||||||
async def fake_convert(_filepath: Path, request_timeout: float = 5.0) -> bytes:
|
async def fake_convert(_filepath: Path, request_timeout: float = 5.0) -> bytes:
|
||||||
raise onlyoffice_error_from_code("-8")
|
raise onlyoffice_error_from_code("-8")
|
||||||
|
|
||||||
|
async def fake_available() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
monkeypatch.setattr(office, "convert_to_png_async", fake_convert)
|
monkeypatch.setattr(office, "convert_to_png_async", fake_convert)
|
||||||
|
monkeypatch.setattr(office, "is_available_cached", fake_available)
|
||||||
|
|
||||||
with pytest.raises(OnlyOfficeError) as exc_info:
|
with pytest.raises(OnlyOfficeError) as exc_info:
|
||||||
await generate_office_preview(
|
await generate_office_preview(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from mediapreview.backends.image import (
|
|||||||
)
|
)
|
||||||
from mediapreview.backends.pdf import process_pdf
|
from mediapreview.backends.pdf import process_pdf
|
||||||
from mediapreview.backends.video import process_video
|
from mediapreview.backends.video import process_video
|
||||||
|
from mediapreview.exceptions import PreviewBackendError
|
||||||
from mediapreview.office import is_available_async
|
from mediapreview.office import is_available_async
|
||||||
from mediapreview.pool import generate_office_preview
|
from mediapreview.pool import generate_office_preview
|
||||||
|
|
||||||
@@ -159,6 +160,51 @@ def test_dispatch(
|
|||||||
assert resp.height == expected_height
|
assert resp.height == expected_height
|
||||||
|
|
||||||
|
|
||||||
|
def test_dispatch_office(monkeypatch) -> None:
|
||||||
|
"""dispatch() converts office documents via OnlyOffice when called directly."""
|
||||||
|
fake_png = (FILES / "Landscape_1.jpg").read_bytes()
|
||||||
|
|
||||||
|
class _FakeManager:
|
||||||
|
async def convert(self, filepath: Path) -> bytes:
|
||||||
|
assert filepath == FILES / "file-sample_100kB.docx"
|
||||||
|
return fake_png
|
||||||
|
|
||||||
|
async def _noop() -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr("mediapreview.office.get_oo_manager", _FakeManager)
|
||||||
|
monkeypatch.setattr("mediapreview.office.close_oo_client", _noop)
|
||||||
|
|
||||||
|
data, resp = dispatch(
|
||||||
|
FILES / "file-sample_100kB.docx",
|
||||||
|
quality=60,
|
||||||
|
maxsize=512,
|
||||||
|
maxzoom=2.0,
|
||||||
|
)
|
||||||
|
_assert_ok(data, resp)
|
||||||
|
assert resp.backend == "onlyoffice+vips"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dispatch_unknown_extension(tmp_path: Path) -> None:
|
||||||
|
"""Unsupported extensions produce a diagnostic naming the extension."""
|
||||||
|
path = tmp_path / "unknown-file.xyz"
|
||||||
|
path.write_text("not a previewable file")
|
||||||
|
with pytest.raises(PreviewBackendError) as exc_info:
|
||||||
|
dispatch(path, quality=60, maxsize=512, maxzoom=2.0)
|
||||||
|
assert "unknown file extension: '.xyz'" in str(exc_info.value)
|
||||||
|
assert exc_info.value.backend == "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dispatch_no_extension(tmp_path: Path) -> None:
|
||||||
|
"""Files without an extension produce a diagnostic saying so."""
|
||||||
|
path = tmp_path / "unknown-file-no-ext"
|
||||||
|
path.write_text("not a previewable file")
|
||||||
|
with pytest.raises(PreviewBackendError) as exc_info:
|
||||||
|
dispatch(path, quality=60, maxsize=512, maxzoom=2.0)
|
||||||
|
assert "unknown file type: no file extension" in str(exc_info.value)
|
||||||
|
assert exc_info.value.backend == "unknown"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Office previews via OnlyOffice
|
# Office previews via OnlyOffice
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user