Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8236.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a longstanding bug where files that could not be thumbnailed would result in an Internal Server Error.
66 changes: 58 additions & 8 deletions synapse/rest/media/v1/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
from .preview_url_resource import PreviewUrlResource
from .storage_provider import StorageProviderWrapper
from .thumbnail_resource import ThumbnailResource
from .thumbnailer import Thumbnailer
from .thumbnailer import Thumbnailer, ThumbnailError
from .upload_resource import UploadResource

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -460,13 +460,29 @@ def _generate_thumbnail(self, thumbnailer, t_width, t_height, t_method, t_type):
return t_byte_source

async def generate_local_exact_thumbnail(
self, media_id, t_width, t_height, t_method, t_type, url_cache
):
self,
media_id: str,
t_width: int,
t_height: int,
t_method: str,
t_type: str,
url_cache: str,
) -> Optional[str]:
input_path = await self.media_storage.ensure_media_is_in_local_cache(
FileInfo(None, media_id, url_cache=url_cache)
)

thumbnailer = Thumbnailer(input_path)
try:
thumbnailer = Thumbnailer(input_path)
except ThumbnailError:
logger.warning(
"Unable to generate a thumbnail for local media %s using a method of %s and type of %s",
media_id,
t_method,
t_type,
)
return None

t_byte_source = await defer_to_thread(
self.hs.get_reactor(),
self._generate_thumbnail,
Expand Down Expand Up @@ -506,14 +522,35 @@ async def generate_local_exact_thumbnail(

return output_path

# Could not generate thumbnail.
return None

async def generate_remote_exact_thumbnail(
self, server_name, file_id, media_id, t_width, t_height, t_method, t_type
):
self,
server_name: str,
file_id: str,
media_id: str,
t_width: int,
t_height: int,
t_method: str,
t_type: str,
) -> Optional[str]:
input_path = await self.media_storage.ensure_media_is_in_local_cache(
FileInfo(server_name, file_id, url_cache=False)
)

thumbnailer = Thumbnailer(input_path)
try:
thumbnailer = Thumbnailer(input_path)
except ThumbnailError:
logger.warning(
"Unable to generate a thumbnail for remote media %s from %s using a method of %s and type of %s",
media_id,
server_name,
t_method,
t_type,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want log why as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm....that's a good thought, we'd have to pass the original exception through (I guess using raise from, maybe?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@erikjohnston I'm hoping b5b96ea is what you were thinking?

return None

t_byte_source = await defer_to_thread(
self.hs.get_reactor(),
self._generate_thumbnail,
Expand Down Expand Up @@ -559,6 +596,9 @@ async def generate_remote_exact_thumbnail(

return output_path

# Could not generate thumbnail.
return None

async def _generate_thumbnails(
self,
server_name: Optional[str],
Expand Down Expand Up @@ -590,7 +630,17 @@ async def _generate_thumbnails(
FileInfo(server_name, file_id, url_cache=url_cache)
)

thumbnailer = Thumbnailer(input_path)
try:
thumbnailer = Thumbnailer(input_path)
except ThumbnailError:
logger.warning(
"Unable to generate thumbnails for remote media %s from %s using a method of %s and type of %s",
media_id,
server_name,
media_type,
)
return None

m_width = thumbnailer.width
m_height = thumbnailer.height

Expand Down
5 changes: 3 additions & 2 deletions synapse/rest/media/v1/thumbnail_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import logging

from synapse.api.errors import SynapseError
from synapse.http.server import DirectServeJsonResource, set_cors_headers
from synapse.http.servlet import parse_integer, parse_string

Expand Down Expand Up @@ -173,7 +174,7 @@ async def _select_or_generate_local_thumbnail(
await respond_with_file(request, desired_type, file_path)
else:
logger.warning("Failed to generate thumbnail")
respond_404(request)
raise SynapseError(400, "Failed to generate thumbnail.")

async def _select_or_generate_remote_thumbnail(
self,
Expand Down Expand Up @@ -235,7 +236,7 @@ async def _select_or_generate_remote_thumbnail(
await respond_with_file(request, desired_type, file_path)
else:
logger.warning("Failed to generate thumbnail")
respond_404(request)
raise SynapseError(400, "Failed to generate thumbnail.")

async def _respond_remote_thumbnail(
self, request, server_name, media_id, width, height, method, m_type
Expand Down
14 changes: 12 additions & 2 deletions synapse/rest/media/v1/thumbnailer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import logging
from io import BytesIO

from PIL import Image as Image
from PIL import Image

logger = logging.getLogger(__name__)

Expand All @@ -31,12 +31,22 @@
}


class ThumbnailError(Exception):
"""An error occurred generating a thumbnail."""


class Thumbnailer:

FORMATS = {"image/jpeg": "JPEG", "image/png": "PNG"}

def __init__(self, input_path):
self.image = Image.open(input_path)
try:
self.image = Image.open(input_path)
except OSError:
# If an error occurs opening the image, a thumbnail won't be able to
# be generated.
raise ThumbnailError()

self.width, self.height = self.image.size
self.transpose_method = None
try:
Expand Down
39 changes: 29 additions & 10 deletions tests/rest/media/v1/test_media_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,13 @@ class _TestImage:
extension = attr.ib(type=bytes)
expected_cropped = attr.ib(type=Optional[bytes])
expected_scaled = attr.ib(type=Optional[bytes])
expected_found = attr.ib(default=True, type=bool)


@parameterized_class(
("test_image",),
[
# smol png
# smoll png
(
_TestImage(
unhexlify(
Expand Down Expand Up @@ -161,6 +162,8 @@ class _TestImage:
None,
),
),
# an empty file
(_TestImage(b"", b"image/gif", b".gif", None, None, False,),),
],
)
class MediaRepoTests(unittest.HomeserverTestCase):
Expand Down Expand Up @@ -303,12 +306,16 @@ def test_disposition_none(self):
self.assertEqual(headers.getRawHeaders(b"Content-Disposition"), None)

def test_thumbnail_crop(self):
self._test_thumbnail("crop", self.test_image.expected_cropped)
self._test_thumbnail(
"crop", self.test_image.expected_cropped, self.test_image.expected_found
)

def test_thumbnail_scale(self):
self._test_thumbnail("scale", self.test_image.expected_scaled)
self._test_thumbnail(
"scale", self.test_image.expected_scaled, self.test_image.expected_found
)

def _test_thumbnail(self, method, expected_body):
def _test_thumbnail(self, method, expected_body, expected_found):
params = "?width=32&height=32&method=" + method
request, channel = self.make_request(
"GET", self.media_id + params, shorthand=False
Expand All @@ -325,11 +332,23 @@ def _test_thumbnail(self, method, expected_body):
)
self.pump()

self.assertEqual(channel.code, 200)
if expected_body is not None:
if expected_found:
self.assertEqual(channel.code, 200)
if expected_body is not None:
self.assertEqual(
channel.result["body"], expected_body, channel.result["body"]
)
else:
# ensure that the result is at least some valid image
Image.open(BytesIO(channel.result["body"]))
else:
# A 404 with a JSON body.
self.assertEqual(channel.code, 404)
self.assertEqual(
channel.result["body"], expected_body, channel.result["body"]
channel.json_body,
{
"errcode": "M_NOT_FOUND",
"error": "Not found [b'example.com', b'12345?width=32&height=32&method=%s']"
% method,
},
)
else:
# ensure that the result is at least some valid image
Image.open(BytesIO(channel.result["body"]))