Return a 404 if no valid thumbnail is found. (#9163)
If no thumbnail of the requested type exists, return a 404 instead of erroring. This doesn't quite match the spec (which does not define what happens if no thumbnail can be found), but is consistent with what Synapse already does.erikj/test_old_dep_postgres
parent
31c5382d7a
commit
a7882f9887
|
@ -0,0 +1 @@
|
||||||
|
Fix a long-standing bug where Synapse would return a 500 error when a thumbnail did not exist (and auto-generation of thumbnails was not enabled).
|
|
@ -300,6 +300,7 @@ class FileInfo:
|
||||||
thumbnail_height (int)
|
thumbnail_height (int)
|
||||||
thumbnail_method (str)
|
thumbnail_method (str)
|
||||||
thumbnail_type (str): Content type of thumbnail, e.g. image/png
|
thumbnail_type (str): Content type of thumbnail, e.g. image/png
|
||||||
|
thumbnail_length (int): The size of the media file, in bytes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -312,6 +313,7 @@ class FileInfo:
|
||||||
thumbnail_height=None,
|
thumbnail_height=None,
|
||||||
thumbnail_method=None,
|
thumbnail_method=None,
|
||||||
thumbnail_type=None,
|
thumbnail_type=None,
|
||||||
|
thumbnail_length=None,
|
||||||
):
|
):
|
||||||
self.server_name = server_name
|
self.server_name = server_name
|
||||||
self.file_id = file_id
|
self.file_id = file_id
|
||||||
|
@ -321,6 +323,7 @@ class FileInfo:
|
||||||
self.thumbnail_height = thumbnail_height
|
self.thumbnail_height = thumbnail_height
|
||||||
self.thumbnail_method = thumbnail_method
|
self.thumbnail_method = thumbnail_method
|
||||||
self.thumbnail_type = thumbnail_type
|
self.thumbnail_type = thumbnail_type
|
||||||
|
self.thumbnail_length = thumbnail_length
|
||||||
|
|
||||||
|
|
||||||
def get_filename_from_headers(headers: Dict[bytes, List[bytes]]) -> Optional[str]:
|
def get_filename_from_headers(headers: Dict[bytes, List[bytes]]) -> Optional[str]:
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||||
|
|
||||||
from twisted.web.http import Request
|
from twisted.web.http import Request
|
||||||
|
|
||||||
|
@ -106,31 +106,17 @@ class ThumbnailResource(DirectServeJsonResource):
|
||||||
return
|
return
|
||||||
|
|
||||||
thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
|
thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
|
||||||
|
await self._select_and_respond_with_thumbnail(
|
||||||
if thumbnail_infos:
|
request,
|
||||||
thumbnail_info = self._select_thumbnail(
|
width,
|
||||||
width, height, method, m_type, thumbnail_infos
|
height,
|
||||||
)
|
method,
|
||||||
|
m_type,
|
||||||
file_info = FileInfo(
|
thumbnail_infos,
|
||||||
server_name=None,
|
media_id,
|
||||||
file_id=media_id,
|
url_cache=media_info["url_cache"],
|
||||||
url_cache=media_info["url_cache"],
|
server_name=None,
|
||||||
thumbnail=True,
|
)
|
||||||
thumbnail_width=thumbnail_info["thumbnail_width"],
|
|
||||||
thumbnail_height=thumbnail_info["thumbnail_height"],
|
|
||||||
thumbnail_type=thumbnail_info["thumbnail_type"],
|
|
||||||
thumbnail_method=thumbnail_info["thumbnail_method"],
|
|
||||||
)
|
|
||||||
|
|
||||||
t_type = file_info.thumbnail_type
|
|
||||||
t_length = thumbnail_info["thumbnail_length"]
|
|
||||||
|
|
||||||
responder = await self.media_storage.fetch_media(file_info)
|
|
||||||
await respond_with_responder(request, responder, t_type, t_length)
|
|
||||||
else:
|
|
||||||
logger.info("Couldn't find any generated thumbnails")
|
|
||||||
respond_404(request)
|
|
||||||
|
|
||||||
async def _select_or_generate_local_thumbnail(
|
async def _select_or_generate_local_thumbnail(
|
||||||
self,
|
self,
|
||||||
|
@ -276,26 +262,64 @@ class ThumbnailResource(DirectServeJsonResource):
|
||||||
thumbnail_infos = await self.store.get_remote_media_thumbnails(
|
thumbnail_infos = await self.store.get_remote_media_thumbnails(
|
||||||
server_name, media_id
|
server_name, media_id
|
||||||
)
|
)
|
||||||
|
await self._select_and_respond_with_thumbnail(
|
||||||
|
request,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
method,
|
||||||
|
m_type,
|
||||||
|
thumbnail_infos,
|
||||||
|
media_info["filesystem_id"],
|
||||||
|
url_cache=None,
|
||||||
|
server_name=server_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _select_and_respond_with_thumbnail(
|
||||||
|
self,
|
||||||
|
request: Request,
|
||||||
|
desired_width: int,
|
||||||
|
desired_height: int,
|
||||||
|
desired_method: str,
|
||||||
|
desired_type: str,
|
||||||
|
thumbnail_infos: List[Dict[str, Any]],
|
||||||
|
file_id: str,
|
||||||
|
url_cache: Optional[str] = None,
|
||||||
|
server_name: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Respond to a request with an appropriate thumbnail from the previously generated thumbnails.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The incoming request.
|
||||||
|
desired_width: The desired width, the returned thumbnail may be larger than this.
|
||||||
|
desired_height: The desired height, the returned thumbnail may be larger than this.
|
||||||
|
desired_method: The desired method used to generate the thumbnail.
|
||||||
|
desired_type: The desired content-type of the thumbnail.
|
||||||
|
thumbnail_infos: A list of dictionaries of candidate thumbnails.
|
||||||
|
file_id: The ID of the media that a thumbnail is being requested for.
|
||||||
|
url_cache: The URL cache value.
|
||||||
|
server_name: The server name, if this is a remote thumbnail.
|
||||||
|
"""
|
||||||
if thumbnail_infos:
|
if thumbnail_infos:
|
||||||
thumbnail_info = self._select_thumbnail(
|
file_info = self._select_thumbnail(
|
||||||
width, height, method, m_type, thumbnail_infos
|
desired_width,
|
||||||
|
desired_height,
|
||||||
|
desired_method,
|
||||||
|
desired_type,
|
||||||
|
thumbnail_infos,
|
||||||
|
file_id,
|
||||||
|
url_cache,
|
||||||
|
server_name,
|
||||||
)
|
)
|
||||||
file_info = FileInfo(
|
if not file_info:
|
||||||
server_name=server_name,
|
logger.info("Couldn't find a thumbnail matching the desired inputs")
|
||||||
file_id=media_info["filesystem_id"],
|
respond_404(request)
|
||||||
thumbnail=True,
|
return
|
||||||
thumbnail_width=thumbnail_info["thumbnail_width"],
|
|
||||||
thumbnail_height=thumbnail_info["thumbnail_height"],
|
|
||||||
thumbnail_type=thumbnail_info["thumbnail_type"],
|
|
||||||
thumbnail_method=thumbnail_info["thumbnail_method"],
|
|
||||||
)
|
|
||||||
|
|
||||||
t_type = file_info.thumbnail_type
|
|
||||||
t_length = thumbnail_info["thumbnail_length"]
|
|
||||||
|
|
||||||
responder = await self.media_storage.fetch_media(file_info)
|
responder = await self.media_storage.fetch_media(file_info)
|
||||||
await respond_with_responder(request, responder, t_type, t_length)
|
await respond_with_responder(
|
||||||
|
request, responder, file_info.thumbnail_type, file_info.thumbnail_length
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("Failed to find any generated thumbnails")
|
logger.info("Failed to find any generated thumbnails")
|
||||||
respond_404(request)
|
respond_404(request)
|
||||||
|
@ -306,67 +330,117 @@ class ThumbnailResource(DirectServeJsonResource):
|
||||||
desired_height: int,
|
desired_height: int,
|
||||||
desired_method: str,
|
desired_method: str,
|
||||||
desired_type: str,
|
desired_type: str,
|
||||||
thumbnail_infos,
|
thumbnail_infos: List[Dict[str, Any]],
|
||||||
) -> dict:
|
file_id: str,
|
||||||
|
url_cache: Optional[str],
|
||||||
|
server_name: Optional[str],
|
||||||
|
) -> Optional[FileInfo]:
|
||||||
|
"""
|
||||||
|
Choose an appropriate thumbnail from the previously generated thumbnails.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
desired_width: The desired width, the returned thumbnail may be larger than this.
|
||||||
|
desired_height: The desired height, the returned thumbnail may be larger than this.
|
||||||
|
desired_method: The desired method used to generate the thumbnail.
|
||||||
|
desired_type: The desired content-type of the thumbnail.
|
||||||
|
thumbnail_infos: A list of dictionaries of candidate thumbnails.
|
||||||
|
file_id: The ID of the media that a thumbnail is being requested for.
|
||||||
|
url_cache: The URL cache value.
|
||||||
|
server_name: The server name, if this is a remote thumbnail.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The thumbnail which best matches the desired parameters.
|
||||||
|
"""
|
||||||
|
desired_method = desired_method.lower()
|
||||||
|
|
||||||
|
# The chosen thumbnail.
|
||||||
|
thumbnail_info = None
|
||||||
|
|
||||||
d_w = desired_width
|
d_w = desired_width
|
||||||
d_h = desired_height
|
d_h = desired_height
|
||||||
|
|
||||||
if desired_method.lower() == "crop":
|
if desired_method == "crop":
|
||||||
|
# Thumbnails that match equal or larger sizes of desired width/height.
|
||||||
crop_info_list = []
|
crop_info_list = []
|
||||||
|
# Other thumbnails.
|
||||||
crop_info_list2 = []
|
crop_info_list2 = []
|
||||||
for info in thumbnail_infos:
|
for info in thumbnail_infos:
|
||||||
|
# Skip thumbnails generated with different methods.
|
||||||
|
if info["thumbnail_method"] != "crop":
|
||||||
|
continue
|
||||||
|
|
||||||
t_w = info["thumbnail_width"]
|
t_w = info["thumbnail_width"]
|
||||||
t_h = info["thumbnail_height"]
|
t_h = info["thumbnail_height"]
|
||||||
t_method = info["thumbnail_method"]
|
aspect_quality = abs(d_w * t_h - d_h * t_w)
|
||||||
if t_method == "crop":
|
min_quality = 0 if d_w <= t_w and d_h <= t_h else 1
|
||||||
aspect_quality = abs(d_w * t_h - d_h * t_w)
|
|
||||||
min_quality = 0 if d_w <= t_w and d_h <= t_h else 1
|
|
||||||
size_quality = abs((d_w - t_w) * (d_h - t_h))
|
|
||||||
type_quality = desired_type != info["thumbnail_type"]
|
|
||||||
length_quality = info["thumbnail_length"]
|
|
||||||
if t_w >= d_w or t_h >= d_h:
|
|
||||||
crop_info_list.append(
|
|
||||||
(
|
|
||||||
aspect_quality,
|
|
||||||
min_quality,
|
|
||||||
size_quality,
|
|
||||||
type_quality,
|
|
||||||
length_quality,
|
|
||||||
info,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
crop_info_list2.append(
|
|
||||||
(
|
|
||||||
aspect_quality,
|
|
||||||
min_quality,
|
|
||||||
size_quality,
|
|
||||||
type_quality,
|
|
||||||
length_quality,
|
|
||||||
info,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if crop_info_list:
|
|
||||||
return min(crop_info_list)[-1]
|
|
||||||
else:
|
|
||||||
return min(crop_info_list2)[-1]
|
|
||||||
else:
|
|
||||||
info_list = []
|
|
||||||
info_list2 = []
|
|
||||||
for info in thumbnail_infos:
|
|
||||||
t_w = info["thumbnail_width"]
|
|
||||||
t_h = info["thumbnail_height"]
|
|
||||||
t_method = info["thumbnail_method"]
|
|
||||||
size_quality = abs((d_w - t_w) * (d_h - t_h))
|
size_quality = abs((d_w - t_w) * (d_h - t_h))
|
||||||
type_quality = desired_type != info["thumbnail_type"]
|
type_quality = desired_type != info["thumbnail_type"]
|
||||||
length_quality = info["thumbnail_length"]
|
length_quality = info["thumbnail_length"]
|
||||||
if t_method == "scale" and (t_w >= d_w or t_h >= d_h):
|
if t_w >= d_w or t_h >= d_h:
|
||||||
|
crop_info_list.append(
|
||||||
|
(
|
||||||
|
aspect_quality,
|
||||||
|
min_quality,
|
||||||
|
size_quality,
|
||||||
|
type_quality,
|
||||||
|
length_quality,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
crop_info_list2.append(
|
||||||
|
(
|
||||||
|
aspect_quality,
|
||||||
|
min_quality,
|
||||||
|
size_quality,
|
||||||
|
type_quality,
|
||||||
|
length_quality,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if crop_info_list:
|
||||||
|
thumbnail_info = min(crop_info_list)[-1]
|
||||||
|
elif crop_info_list2:
|
||||||
|
thumbnail_info = min(crop_info_list2)[-1]
|
||||||
|
elif desired_method == "scale":
|
||||||
|
# Thumbnails that match equal or larger sizes of desired width/height.
|
||||||
|
info_list = []
|
||||||
|
# Other thumbnails.
|
||||||
|
info_list2 = []
|
||||||
|
|
||||||
|
for info in thumbnail_infos:
|
||||||
|
# Skip thumbnails generated with different methods.
|
||||||
|
if info["thumbnail_method"] != "scale":
|
||||||
|
continue
|
||||||
|
|
||||||
|
t_w = info["thumbnail_width"]
|
||||||
|
t_h = info["thumbnail_height"]
|
||||||
|
size_quality = abs((d_w - t_w) * (d_h - t_h))
|
||||||
|
type_quality = desired_type != info["thumbnail_type"]
|
||||||
|
length_quality = info["thumbnail_length"]
|
||||||
|
if t_w >= d_w or t_h >= d_h:
|
||||||
info_list.append((size_quality, type_quality, length_quality, info))
|
info_list.append((size_quality, type_quality, length_quality, info))
|
||||||
elif t_method == "scale":
|
else:
|
||||||
info_list2.append(
|
info_list2.append(
|
||||||
(size_quality, type_quality, length_quality, info)
|
(size_quality, type_quality, length_quality, info)
|
||||||
)
|
)
|
||||||
if info_list:
|
if info_list:
|
||||||
return min(info_list)[-1]
|
thumbnail_info = min(info_list)[-1]
|
||||||
else:
|
elif info_list2:
|
||||||
return min(info_list2)[-1]
|
thumbnail_info = min(info_list2)[-1]
|
||||||
|
|
||||||
|
if thumbnail_info:
|
||||||
|
return FileInfo(
|
||||||
|
file_id=file_id,
|
||||||
|
url_cache=url_cache,
|
||||||
|
server_name=server_name,
|
||||||
|
thumbnail=True,
|
||||||
|
thumbnail_width=thumbnail_info["thumbnail_width"],
|
||||||
|
thumbnail_height=thumbnail_info["thumbnail_height"],
|
||||||
|
thumbnail_type=thumbnail_info["thumbnail_type"],
|
||||||
|
thumbnail_method=thumbnail_info["thumbnail_method"],
|
||||||
|
thumbnail_length=thumbnail_info["thumbnail_length"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# No matching thumbnail was found.
|
||||||
|
return None
|
||||||
|
|
|
@ -202,7 +202,6 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
config = self.default_config()
|
config = self.default_config()
|
||||||
config["media_store_path"] = self.media_store_path
|
config["media_store_path"] = self.media_store_path
|
||||||
config["thumbnail_requirements"] = {}
|
|
||||||
config["max_image_pixels"] = 2000000
|
config["max_image_pixels"] = 2000000
|
||||||
|
|
||||||
provider_config = {
|
provider_config = {
|
||||||
|
@ -313,15 +312,39 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
||||||
self.assertEqual(headers.getRawHeaders(b"Content-Disposition"), None)
|
self.assertEqual(headers.getRawHeaders(b"Content-Disposition"), None)
|
||||||
|
|
||||||
def test_thumbnail_crop(self):
|
def test_thumbnail_crop(self):
|
||||||
|
"""Test that a cropped remote thumbnail is available."""
|
||||||
self._test_thumbnail(
|
self._test_thumbnail(
|
||||||
"crop", self.test_image.expected_cropped, self.test_image.expected_found
|
"crop", self.test_image.expected_cropped, self.test_image.expected_found
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_thumbnail_scale(self):
|
def test_thumbnail_scale(self):
|
||||||
|
"""Test that a scaled remote thumbnail is available."""
|
||||||
self._test_thumbnail(
|
self._test_thumbnail(
|
||||||
"scale", self.test_image.expected_scaled, self.test_image.expected_found
|
"scale", self.test_image.expected_scaled, self.test_image.expected_found
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_invalid_type(self):
|
||||||
|
"""An invalid thumbnail type is never available."""
|
||||||
|
self._test_thumbnail("invalid", None, False)
|
||||||
|
|
||||||
|
@unittest.override_config(
|
||||||
|
{"thumbnail_sizes": [{"width": 32, "height": 32, "method": "scale"}]}
|
||||||
|
)
|
||||||
|
def test_no_thumbnail_crop(self):
|
||||||
|
"""
|
||||||
|
Override the config to generate only scaled thumbnails, but request a cropped one.
|
||||||
|
"""
|
||||||
|
self._test_thumbnail("crop", None, False)
|
||||||
|
|
||||||
|
@unittest.override_config(
|
||||||
|
{"thumbnail_sizes": [{"width": 32, "height": 32, "method": "crop"}]}
|
||||||
|
)
|
||||||
|
def test_no_thumbnail_scale(self):
|
||||||
|
"""
|
||||||
|
Override the config to generate only cropped thumbnails, but request a scaled one.
|
||||||
|
"""
|
||||||
|
self._test_thumbnail("scale", None, False)
|
||||||
|
|
||||||
def _test_thumbnail(self, method, expected_body, expected_found):
|
def _test_thumbnail(self, method, expected_body, expected_found):
|
||||||
params = "?width=32&height=32&method=" + method
|
params = "?width=32&height=32&method=" + method
|
||||||
channel = make_request(
|
channel = make_request(
|
||||||
|
|
Loading…
Reference in New Issue