Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

501 lignes
18 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-2016 OpenMarket Ltd
  3. # Copyright 2020-2021 The Matrix.org Foundation C.I.C.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import logging
  17. from typing import TYPE_CHECKING, Any, Dict, List, Optional
  18. from twisted.web.server import Request
  19. from synapse.api.errors import SynapseError
  20. from synapse.http.server import DirectServeJsonResource, set_cors_headers
  21. from synapse.http.servlet import parse_integer, parse_string
  22. from synapse.rest.media.v1.media_storage import MediaStorage
  23. from ._base import (
  24. FileInfo,
  25. parse_media_id,
  26. respond_404,
  27. respond_with_file,
  28. respond_with_responder,
  29. )
  30. if TYPE_CHECKING:
  31. from synapse.app.homeserver import HomeServer
  32. from synapse.rest.media.v1.media_repository import MediaRepository
  33. logger = logging.getLogger(__name__)
  34. class ThumbnailResource(DirectServeJsonResource):
  35. isLeaf = True
  36. def __init__(
  37. self,
  38. hs: "HomeServer",
  39. media_repo: "MediaRepository",
  40. media_storage: MediaStorage,
  41. ):
  42. super().__init__()
  43. self.store = hs.get_datastore()
  44. self.media_repo = media_repo
  45. self.media_storage = media_storage
  46. self.dynamic_thumbnails = hs.config.dynamic_thumbnails
  47. self.server_name = hs.hostname
  48. async def _async_render_GET(self, request: Request) -> None:
  49. set_cors_headers(request)
  50. server_name, media_id, _ = parse_media_id(request)
  51. width = parse_integer(request, "width", required=True)
  52. height = parse_integer(request, "height", required=True)
  53. method = parse_string(request, "method", "scale")
  54. m_type = parse_string(request, "type", "image/png")
  55. if server_name == self.server_name:
  56. if self.dynamic_thumbnails:
  57. await self._select_or_generate_local_thumbnail(
  58. request, media_id, width, height, method, m_type
  59. )
  60. else:
  61. await self._respond_local_thumbnail(
  62. request, media_id, width, height, method, m_type
  63. )
  64. self.media_repo.mark_recently_accessed(None, media_id)
  65. else:
  66. if self.dynamic_thumbnails:
  67. await self._select_or_generate_remote_thumbnail(
  68. request, server_name, media_id, width, height, method, m_type
  69. )
  70. else:
  71. await self._respond_remote_thumbnail(
  72. request, server_name, media_id, width, height, method, m_type
  73. )
  74. self.media_repo.mark_recently_accessed(server_name, media_id)
  75. async def _respond_local_thumbnail(
  76. self,
  77. request: Request,
  78. media_id: str,
  79. width: int,
  80. height: int,
  81. method: str,
  82. m_type: str,
  83. ) -> None:
  84. media_info = await self.store.get_local_media(media_id)
  85. if not media_info:
  86. respond_404(request)
  87. return
  88. if media_info["quarantined_by"]:
  89. logger.info("Media is quarantined")
  90. respond_404(request)
  91. return
  92. thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
  93. await self._select_and_respond_with_thumbnail(
  94. request,
  95. width,
  96. height,
  97. method,
  98. m_type,
  99. thumbnail_infos,
  100. media_id,
  101. media_id,
  102. url_cache=media_info["url_cache"],
  103. server_name=None,
  104. )
  105. async def _select_or_generate_local_thumbnail(
  106. self,
  107. request: Request,
  108. media_id: str,
  109. desired_width: int,
  110. desired_height: int,
  111. desired_method: str,
  112. desired_type: str,
  113. ) -> None:
  114. media_info = await self.store.get_local_media(media_id)
  115. if not media_info:
  116. respond_404(request)
  117. return
  118. if media_info["quarantined_by"]:
  119. logger.info("Media is quarantined")
  120. respond_404(request)
  121. return
  122. thumbnail_infos = await self.store.get_local_media_thumbnails(media_id)
  123. for info in thumbnail_infos:
  124. t_w = info["thumbnail_width"] == desired_width
  125. t_h = info["thumbnail_height"] == desired_height
  126. t_method = info["thumbnail_method"] == desired_method
  127. t_type = info["thumbnail_type"] == desired_type
  128. if t_w and t_h and t_method and t_type:
  129. file_info = FileInfo(
  130. server_name=None,
  131. file_id=media_id,
  132. url_cache=media_info["url_cache"],
  133. thumbnail=True,
  134. thumbnail_width=info["thumbnail_width"],
  135. thumbnail_height=info["thumbnail_height"],
  136. thumbnail_type=info["thumbnail_type"],
  137. thumbnail_method=info["thumbnail_method"],
  138. )
  139. t_type = file_info.thumbnail_type
  140. t_length = info["thumbnail_length"]
  141. responder = await self.media_storage.fetch_media(file_info)
  142. if responder:
  143. await respond_with_responder(request, responder, t_type, t_length)
  144. return
  145. logger.debug("We don't have a thumbnail of that size. Generating")
  146. # Okay, so we generate one.
  147. file_path = await self.media_repo.generate_local_exact_thumbnail(
  148. media_id,
  149. desired_width,
  150. desired_height,
  151. desired_method,
  152. desired_type,
  153. url_cache=media_info["url_cache"],
  154. )
  155. if file_path:
  156. await respond_with_file(request, desired_type, file_path)
  157. else:
  158. logger.warning("Failed to generate thumbnail")
  159. raise SynapseError(400, "Failed to generate thumbnail.")
  160. async def _select_or_generate_remote_thumbnail(
  161. self,
  162. request: Request,
  163. server_name: str,
  164. media_id: str,
  165. desired_width: int,
  166. desired_height: int,
  167. desired_method: str,
  168. desired_type: str,
  169. ) -> None:
  170. media_info = await self.media_repo.get_remote_media_info(server_name, media_id)
  171. thumbnail_infos = await self.store.get_remote_media_thumbnails(
  172. server_name, media_id
  173. )
  174. file_id = media_info["filesystem_id"]
  175. for info in thumbnail_infos:
  176. t_w = info["thumbnail_width"] == desired_width
  177. t_h = info["thumbnail_height"] == desired_height
  178. t_method = info["thumbnail_method"] == desired_method
  179. t_type = info["thumbnail_type"] == desired_type
  180. if t_w and t_h and t_method and t_type:
  181. file_info = FileInfo(
  182. server_name=server_name,
  183. file_id=media_info["filesystem_id"],
  184. thumbnail=True,
  185. thumbnail_width=info["thumbnail_width"],
  186. thumbnail_height=info["thumbnail_height"],
  187. thumbnail_type=info["thumbnail_type"],
  188. thumbnail_method=info["thumbnail_method"],
  189. )
  190. t_type = file_info.thumbnail_type
  191. t_length = info["thumbnail_length"]
  192. responder = await self.media_storage.fetch_media(file_info)
  193. if responder:
  194. await respond_with_responder(request, responder, t_type, t_length)
  195. return
  196. logger.debug("We don't have a thumbnail of that size. Generating")
  197. # Okay, so we generate one.
  198. file_path = await self.media_repo.generate_remote_exact_thumbnail(
  199. server_name,
  200. file_id,
  201. media_id,
  202. desired_width,
  203. desired_height,
  204. desired_method,
  205. desired_type,
  206. )
  207. if file_path:
  208. await respond_with_file(request, desired_type, file_path)
  209. else:
  210. logger.warning("Failed to generate thumbnail")
  211. raise SynapseError(400, "Failed to generate thumbnail.")
  212. async def _respond_remote_thumbnail(
  213. self,
  214. request: Request,
  215. server_name: str,
  216. media_id: str,
  217. width: int,
  218. height: int,
  219. method: str,
  220. m_type: str,
  221. ) -> None:
  222. # TODO: Don't download the whole remote file
  223. # We should proxy the thumbnail from the remote server instead of
  224. # downloading the remote file and generating our own thumbnails.
  225. media_info = await self.media_repo.get_remote_media_info(server_name, media_id)
  226. thumbnail_infos = await self.store.get_remote_media_thumbnails(
  227. server_name, media_id
  228. )
  229. await self._select_and_respond_with_thumbnail(
  230. request,
  231. width,
  232. height,
  233. method,
  234. m_type,
  235. thumbnail_infos,
  236. media_id,
  237. media_info["filesystem_id"],
  238. url_cache=None,
  239. server_name=server_name,
  240. )
  241. async def _select_and_respond_with_thumbnail(
  242. self,
  243. request: Request,
  244. desired_width: int,
  245. desired_height: int,
  246. desired_method: str,
  247. desired_type: str,
  248. thumbnail_infos: List[Dict[str, Any]],
  249. media_id: str,
  250. file_id: str,
  251. url_cache: Optional[str] = None,
  252. server_name: Optional[str] = None,
  253. ) -> None:
  254. """
  255. Respond to a request with an appropriate thumbnail from the previously generated thumbnails.
  256. Args:
  257. request: The incoming request.
  258. desired_width: The desired width, the returned thumbnail may be larger than this.
  259. desired_height: The desired height, the returned thumbnail may be larger than this.
  260. desired_method: The desired method used to generate the thumbnail.
  261. desired_type: The desired content-type of the thumbnail.
  262. thumbnail_infos: A list of dictionaries of candidate thumbnails.
  263. file_id: The ID of the media that a thumbnail is being requested for.
  264. url_cache: The URL cache value.
  265. server_name: The server name, if this is a remote thumbnail.
  266. """
  267. if thumbnail_infos:
  268. file_info = self._select_thumbnail(
  269. desired_width,
  270. desired_height,
  271. desired_method,
  272. desired_type,
  273. thumbnail_infos,
  274. file_id,
  275. url_cache,
  276. server_name,
  277. )
  278. if not file_info:
  279. logger.info("Couldn't find a thumbnail matching the desired inputs")
  280. respond_404(request)
  281. return
  282. responder = await self.media_storage.fetch_media(file_info)
  283. if responder:
  284. await respond_with_responder(
  285. request,
  286. responder,
  287. file_info.thumbnail_type,
  288. file_info.thumbnail_length,
  289. )
  290. return
  291. # If we can't find the thumbnail we regenerate it. This can happen
  292. # if e.g. we've deleted the thumbnails but still have the original
  293. # image somewhere.
  294. #
  295. # Since we have an entry for the thumbnail in the DB we a) know we
  296. # have have successfully generated the thumbnail in the past (so we
  297. # don't need to worry about repeatedly failing to generate
  298. # thumbnails), and b) have already calculated that appropriate
  299. # width/height/method so we can just call the "generate exact"
  300. # methods.
  301. # First let's check that we do actually have the original image
  302. # still. This will throw a 404 if we don't.
  303. # TODO: We should refetch the thumbnails for remote media.
  304. await self.media_storage.ensure_media_is_in_local_cache(
  305. FileInfo(server_name, file_id, url_cache=url_cache)
  306. )
  307. if server_name:
  308. await self.media_repo.generate_remote_exact_thumbnail(
  309. server_name,
  310. file_id=file_id,
  311. media_id=media_id,
  312. t_width=file_info.thumbnail_width,
  313. t_height=file_info.thumbnail_height,
  314. t_method=file_info.thumbnail_method,
  315. t_type=file_info.thumbnail_type,
  316. )
  317. else:
  318. await self.media_repo.generate_local_exact_thumbnail(
  319. media_id=media_id,
  320. t_width=file_info.thumbnail_width,
  321. t_height=file_info.thumbnail_height,
  322. t_method=file_info.thumbnail_method,
  323. t_type=file_info.thumbnail_type,
  324. url_cache=url_cache,
  325. )
  326. responder = await self.media_storage.fetch_media(file_info)
  327. await respond_with_responder(
  328. request,
  329. responder,
  330. file_info.thumbnail_type,
  331. file_info.thumbnail_length,
  332. )
  333. else:
  334. logger.info("Failed to find any generated thumbnails")
  335. respond_404(request)
  336. def _select_thumbnail(
  337. self,
  338. desired_width: int,
  339. desired_height: int,
  340. desired_method: str,
  341. desired_type: str,
  342. thumbnail_infos: List[Dict[str, Any]],
  343. file_id: str,
  344. url_cache: Optional[str],
  345. server_name: Optional[str],
  346. ) -> Optional[FileInfo]:
  347. """
  348. Choose an appropriate thumbnail from the previously generated thumbnails.
  349. Args:
  350. desired_width: The desired width, the returned thumbnail may be larger than this.
  351. desired_height: The desired height, the returned thumbnail may be larger than this.
  352. desired_method: The desired method used to generate the thumbnail.
  353. desired_type: The desired content-type of the thumbnail.
  354. thumbnail_infos: A list of dictionaries of candidate thumbnails.
  355. file_id: The ID of the media that a thumbnail is being requested for.
  356. url_cache: The URL cache value.
  357. server_name: The server name, if this is a remote thumbnail.
  358. Returns:
  359. The thumbnail which best matches the desired parameters.
  360. """
  361. desired_method = desired_method.lower()
  362. # The chosen thumbnail.
  363. thumbnail_info = None
  364. d_w = desired_width
  365. d_h = desired_height
  366. if desired_method == "crop":
  367. # Thumbnails that match equal or larger sizes of desired width/height.
  368. crop_info_list = []
  369. # Other thumbnails.
  370. crop_info_list2 = []
  371. for info in thumbnail_infos:
  372. # Skip thumbnails generated with different methods.
  373. if info["thumbnail_method"] != "crop":
  374. continue
  375. t_w = info["thumbnail_width"]
  376. t_h = info["thumbnail_height"]
  377. aspect_quality = abs(d_w * t_h - d_h * t_w)
  378. min_quality = 0 if d_w <= t_w and d_h <= t_h else 1
  379. size_quality = abs((d_w - t_w) * (d_h - t_h))
  380. type_quality = desired_type != info["thumbnail_type"]
  381. length_quality = info["thumbnail_length"]
  382. if t_w >= d_w or t_h >= d_h:
  383. crop_info_list.append(
  384. (
  385. aspect_quality,
  386. min_quality,
  387. size_quality,
  388. type_quality,
  389. length_quality,
  390. info,
  391. )
  392. )
  393. else:
  394. crop_info_list2.append(
  395. (
  396. aspect_quality,
  397. min_quality,
  398. size_quality,
  399. type_quality,
  400. length_quality,
  401. info,
  402. )
  403. )
  404. if crop_info_list:
  405. thumbnail_info = min(crop_info_list)[-1]
  406. elif crop_info_list2:
  407. thumbnail_info = min(crop_info_list2)[-1]
  408. elif desired_method == "scale":
  409. # Thumbnails that match equal or larger sizes of desired width/height.
  410. info_list = []
  411. # Other thumbnails.
  412. info_list2 = []
  413. for info in thumbnail_infos:
  414. # Skip thumbnails generated with different methods.
  415. if info["thumbnail_method"] != "scale":
  416. continue
  417. t_w = info["thumbnail_width"]
  418. t_h = info["thumbnail_height"]
  419. size_quality = abs((d_w - t_w) * (d_h - t_h))
  420. type_quality = desired_type != info["thumbnail_type"]
  421. length_quality = info["thumbnail_length"]
  422. if t_w >= d_w or t_h >= d_h:
  423. info_list.append((size_quality, type_quality, length_quality, info))
  424. else:
  425. info_list2.append(
  426. (size_quality, type_quality, length_quality, info)
  427. )
  428. if info_list:
  429. thumbnail_info = min(info_list)[-1]
  430. elif info_list2:
  431. thumbnail_info = min(info_list2)[-1]
  432. if thumbnail_info:
  433. return FileInfo(
  434. file_id=file_id,
  435. url_cache=url_cache,
  436. server_name=server_name,
  437. thumbnail=True,
  438. thumbnail_width=thumbnail_info["thumbnail_width"],
  439. thumbnail_height=thumbnail_info["thumbnail_height"],
  440. thumbnail_type=thumbnail_info["thumbnail_type"],
  441. thumbnail_method=thumbnail_info["thumbnail_method"],
  442. thumbnail_length=thumbnail_info["thumbnail_length"],
  443. )
  444. # No matching thumbnail was found.
  445. return None