Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Do not error when thumbnailing invalid files #8236

Merged
merged 11 commits into from Sep 9, 2020
66 changes: 58 additions & 8 deletions synapse/rest/media/v1/media_repository.py
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

# Source not found, cannot return a 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
Contributor 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
Contributor 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

# Source not found, cannot return a 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
14 changes: 12 additions & 2 deletions synapse/rest/media/v1/thumbnailer.py
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(object):

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
35 changes: 25 additions & 10 deletions tests/rest/media/v1/test_media_storage.py
Expand Up @@ -120,6 +120,11 @@ 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)


with open("test.gif", "rb") as f:
data = f.read()


@parameterized_class(
Expand Down Expand Up @@ -161,6 +166,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 +310,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 +336,15 @@ def _test_thumbnail(self, method, expected_body):
)
self.pump()

self.assertEqual(channel.code, 200)
if expected_body is not None:
self.assertEqual(
channel.result["body"], expected_body, channel.result["body"]
)
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:
# ensure that the result is at least some valid image
Image.open(BytesIO(channel.result["body"]))
# A 404 with no body.
self.assertEqual(channel.code, 404)
clokep marked this conversation as resolved.
Show resolved Hide resolved