media_repository.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-2016 OpenMarket Ltd
  3. # Copyright 2018 New Vector Ltd
  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 errno
  17. import logging
  18. import os
  19. import shutil
  20. from six import iteritems
  21. import twisted.internet.error
  22. import twisted.web.http
  23. from twisted.internet import defer
  24. from twisted.web.resource import Resource
  25. from synapse.api.errors import (
  26. FederationDeniedError,
  27. HttpResponseException,
  28. NotFoundError,
  29. RequestSendFailed,
  30. SynapseError,
  31. )
  32. from synapse.metrics.background_process_metrics import run_as_background_process
  33. from synapse.util import logcontext
  34. from synapse.util.async_helpers import Linearizer
  35. from synapse.util.retryutils import NotRetryingDestination
  36. from synapse.util.stringutils import random_string
  37. from ._base import (
  38. FileInfo,
  39. get_filename_from_headers,
  40. respond_404,
  41. respond_with_responder,
  42. )
  43. from .config_resource import MediaConfigResource
  44. from .download_resource import DownloadResource
  45. from .filepath import MediaFilePaths
  46. from .media_storage import MediaStorage
  47. from .preview_url_resource import PreviewUrlResource
  48. from .storage_provider import StorageProviderWrapper
  49. from .thumbnail_resource import ThumbnailResource
  50. from .thumbnailer import Thumbnailer
  51. from .upload_resource import UploadResource
  52. logger = logging.getLogger(__name__)
  53. UPDATE_RECENTLY_ACCESSED_TS = 60 * 1000
  54. class MediaRepository(object):
  55. def __init__(self, hs):
  56. self.hs = hs
  57. self.auth = hs.get_auth()
  58. self.client = hs.get_http_client()
  59. self.clock = hs.get_clock()
  60. self.server_name = hs.hostname
  61. self.store = hs.get_datastore()
  62. self.max_upload_size = hs.config.max_upload_size
  63. self.max_image_pixels = hs.config.max_image_pixels
  64. self.primary_base_path = hs.config.media_store_path
  65. self.filepaths = MediaFilePaths(self.primary_base_path)
  66. self.dynamic_thumbnails = hs.config.dynamic_thumbnails
  67. self.thumbnail_requirements = hs.config.thumbnail_requirements
  68. self.remote_media_linearizer = Linearizer(name="media_remote")
  69. self.recently_accessed_remotes = set()
  70. self.recently_accessed_locals = set()
  71. self.federation_domain_whitelist = hs.config.federation_domain_whitelist
  72. # List of StorageProviders where we should search for media and
  73. # potentially upload to.
  74. storage_providers = []
  75. for clz, provider_config, wrapper_config in hs.config.media_storage_providers:
  76. backend = clz(hs, provider_config)
  77. provider = StorageProviderWrapper(
  78. backend,
  79. store_local=wrapper_config.store_local,
  80. store_remote=wrapper_config.store_remote,
  81. store_synchronous=wrapper_config.store_synchronous,
  82. )
  83. storage_providers.append(provider)
  84. self.media_storage = MediaStorage(
  85. self.hs, self.primary_base_path, self.filepaths, storage_providers,
  86. )
  87. self.clock.looping_call(
  88. self._start_update_recently_accessed,
  89. UPDATE_RECENTLY_ACCESSED_TS,
  90. )
  91. def _start_update_recently_accessed(self):
  92. return run_as_background_process(
  93. "update_recently_accessed_media", self._update_recently_accessed,
  94. )
  95. @defer.inlineCallbacks
  96. def _update_recently_accessed(self):
  97. remote_media = self.recently_accessed_remotes
  98. self.recently_accessed_remotes = set()
  99. local_media = self.recently_accessed_locals
  100. self.recently_accessed_locals = set()
  101. yield self.store.update_cached_last_access_time(
  102. local_media, remote_media, self.clock.time_msec()
  103. )
  104. def mark_recently_accessed(self, server_name, media_id):
  105. """Mark the given media as recently accessed.
  106. Args:
  107. server_name (str|None): Origin server of media, or None if local
  108. media_id (str): The media ID of the content
  109. """
  110. if server_name:
  111. self.recently_accessed_remotes.add((server_name, media_id))
  112. else:
  113. self.recently_accessed_locals.add(media_id)
  114. @defer.inlineCallbacks
  115. def create_content(self, media_type, upload_name, content, content_length,
  116. auth_user):
  117. """Store uploaded content for a local user and return the mxc URL
  118. Args:
  119. media_type(str): The content type of the file
  120. upload_name(str): The name of the file
  121. content: A file like object that is the content to store
  122. content_length(int): The length of the content
  123. auth_user(str): The user_id of the uploader
  124. Returns:
  125. Deferred[str]: The mxc url of the stored content
  126. """
  127. media_id = random_string(24)
  128. file_info = FileInfo(
  129. server_name=None,
  130. file_id=media_id,
  131. )
  132. fname = yield self.media_storage.store_file(content, file_info)
  133. logger.info("Stored local media in file %r", fname)
  134. yield self.store.store_local_media(
  135. media_id=media_id,
  136. media_type=media_type,
  137. time_now_ms=self.clock.time_msec(),
  138. upload_name=upload_name,
  139. media_length=content_length,
  140. user_id=auth_user,
  141. )
  142. yield self._generate_thumbnails(
  143. None, media_id, media_id, media_type,
  144. )
  145. defer.returnValue("mxc://%s/%s" % (self.server_name, media_id))
  146. @defer.inlineCallbacks
  147. def get_local_media(self, request, media_id, name):
  148. """Responds to reqests for local media, if exists, or returns 404.
  149. Args:
  150. request(twisted.web.http.Request)
  151. media_id (str): The media ID of the content. (This is the same as
  152. the file_id for local content.)
  153. name (str|None): Optional name that, if specified, will be used as
  154. the filename in the Content-Disposition header of the response.
  155. Returns:
  156. Deferred: Resolves once a response has successfully been written
  157. to request
  158. """
  159. media_info = yield self.store.get_local_media(media_id)
  160. if not media_info or media_info["quarantined_by"]:
  161. respond_404(request)
  162. return
  163. self.mark_recently_accessed(None, media_id)
  164. media_type = media_info["media_type"]
  165. media_length = media_info["media_length"]
  166. upload_name = name if name else media_info["upload_name"]
  167. url_cache = media_info["url_cache"]
  168. file_info = FileInfo(
  169. None, media_id,
  170. url_cache=url_cache,
  171. )
  172. responder = yield self.media_storage.fetch_media(file_info)
  173. yield respond_with_responder(
  174. request, responder, media_type, media_length, upload_name,
  175. )
  176. @defer.inlineCallbacks
  177. def get_remote_media(self, request, server_name, media_id, name):
  178. """Respond to requests for remote media.
  179. Args:
  180. request(twisted.web.http.Request)
  181. server_name (str): Remote server_name where the media originated.
  182. media_id (str): The media ID of the content (as defined by the
  183. remote server).
  184. name (str|None): Optional name that, if specified, will be used as
  185. the filename in the Content-Disposition header of the response.
  186. Returns:
  187. Deferred: Resolves once a response has successfully been written
  188. to request
  189. """
  190. if (
  191. self.federation_domain_whitelist is not None and
  192. server_name not in self.federation_domain_whitelist
  193. ):
  194. raise FederationDeniedError(server_name)
  195. self.mark_recently_accessed(server_name, media_id)
  196. # We linearize here to ensure that we don't try and download remote
  197. # media multiple times concurrently
  198. key = (server_name, media_id)
  199. with (yield self.remote_media_linearizer.queue(key)):
  200. responder, media_info = yield self._get_remote_media_impl(
  201. server_name, media_id,
  202. )
  203. # We deliberately stream the file outside the lock
  204. if responder:
  205. media_type = media_info["media_type"]
  206. media_length = media_info["media_length"]
  207. upload_name = name if name else media_info["upload_name"]
  208. yield respond_with_responder(
  209. request, responder, media_type, media_length, upload_name,
  210. )
  211. else:
  212. respond_404(request)
  213. @defer.inlineCallbacks
  214. def get_remote_media_info(self, server_name, media_id):
  215. """Gets the media info associated with the remote file, downloading
  216. if necessary.
  217. Args:
  218. server_name (str): Remote server_name where the media originated.
  219. media_id (str): The media ID of the content (as defined by the
  220. remote server).
  221. Returns:
  222. Deferred[dict]: The media_info of the file
  223. """
  224. if (
  225. self.federation_domain_whitelist is not None and
  226. server_name not in self.federation_domain_whitelist
  227. ):
  228. raise FederationDeniedError(server_name)
  229. # We linearize here to ensure that we don't try and download remote
  230. # media multiple times concurrently
  231. key = (server_name, media_id)
  232. with (yield self.remote_media_linearizer.queue(key)):
  233. responder, media_info = yield self._get_remote_media_impl(
  234. server_name, media_id,
  235. )
  236. # Ensure we actually use the responder so that it releases resources
  237. if responder:
  238. with responder:
  239. pass
  240. defer.returnValue(media_info)
  241. @defer.inlineCallbacks
  242. def _get_remote_media_impl(self, server_name, media_id):
  243. """Looks for media in local cache, if not there then attempt to
  244. download from remote server.
  245. Args:
  246. server_name (str): Remote server_name where the media originated.
  247. media_id (str): The media ID of the content (as defined by the
  248. remote server).
  249. Returns:
  250. Deferred[(Responder, media_info)]
  251. """
  252. media_info = yield self.store.get_cached_remote_media(
  253. server_name, media_id
  254. )
  255. # file_id is the ID we use to track the file locally. If we've already
  256. # seen the file then reuse the existing ID, otherwise genereate a new
  257. # one.
  258. if media_info:
  259. file_id = media_info["filesystem_id"]
  260. else:
  261. file_id = random_string(24)
  262. file_info = FileInfo(server_name, file_id)
  263. # If we have an entry in the DB, try and look for it
  264. if media_info:
  265. if media_info["quarantined_by"]:
  266. logger.info("Media is quarantined")
  267. raise NotFoundError()
  268. responder = yield self.media_storage.fetch_media(file_info)
  269. if responder:
  270. defer.returnValue((responder, media_info))
  271. # Failed to find the file anywhere, lets download it.
  272. media_info = yield self._download_remote_file(
  273. server_name, media_id, file_id
  274. )
  275. responder = yield self.media_storage.fetch_media(file_info)
  276. defer.returnValue((responder, media_info))
  277. @defer.inlineCallbacks
  278. def _download_remote_file(self, server_name, media_id, file_id):
  279. """Attempt to download the remote file from the given server name,
  280. using the given file_id as the local id.
  281. Args:
  282. server_name (str): Originating server
  283. media_id (str): The media ID of the content (as defined by the
  284. remote server). This is different than the file_id, which is
  285. locally generated.
  286. file_id (str): Local file ID
  287. Returns:
  288. Deferred[MediaInfo]
  289. """
  290. file_info = FileInfo(
  291. server_name=server_name,
  292. file_id=file_id,
  293. )
  294. with self.media_storage.store_into_file(file_info) as (f, fname, finish):
  295. request_path = "/".join((
  296. "/_matrix/media/v1/download", server_name, media_id,
  297. ))
  298. try:
  299. length, headers = yield self.client.get_file(
  300. server_name, request_path, output_stream=f,
  301. max_size=self.max_upload_size, args={
  302. # tell the remote server to 404 if it doesn't
  303. # recognise the server_name, to make sure we don't
  304. # end up with a routing loop.
  305. "allow_remote": "false",
  306. }
  307. )
  308. except RequestSendFailed as e:
  309. logger.warn("Request failed fetching remote media %s/%s: %r",
  310. server_name, media_id, e)
  311. raise SynapseError(502, "Failed to fetch remote media")
  312. except HttpResponseException as e:
  313. logger.warn("HTTP error fetching remote media %s/%s: %s",
  314. server_name, media_id, e.response)
  315. if e.code == twisted.web.http.NOT_FOUND:
  316. raise e.to_synapse_error()
  317. raise SynapseError(502, "Failed to fetch remote media")
  318. except SynapseError:
  319. logger.exception("Failed to fetch remote media %s/%s",
  320. server_name, media_id)
  321. raise
  322. except NotRetryingDestination:
  323. logger.warn("Not retrying destination %r", server_name)
  324. raise SynapseError(502, "Failed to fetch remote media")
  325. except Exception:
  326. logger.exception("Failed to fetch remote media %s/%s",
  327. server_name, media_id)
  328. raise SynapseError(502, "Failed to fetch remote media")
  329. yield finish()
  330. media_type = headers[b"Content-Type"][0].decode('ascii')
  331. upload_name = get_filename_from_headers(headers)
  332. time_now_ms = self.clock.time_msec()
  333. logger.info("Stored remote media in file %r", fname)
  334. yield self.store.store_cached_remote_media(
  335. origin=server_name,
  336. media_id=media_id,
  337. media_type=media_type,
  338. time_now_ms=self.clock.time_msec(),
  339. upload_name=upload_name,
  340. media_length=length,
  341. filesystem_id=file_id,
  342. )
  343. media_info = {
  344. "media_type": media_type,
  345. "media_length": length,
  346. "upload_name": upload_name,
  347. "created_ts": time_now_ms,
  348. "filesystem_id": file_id,
  349. }
  350. yield self._generate_thumbnails(
  351. server_name, media_id, file_id, media_type,
  352. )
  353. defer.returnValue(media_info)
  354. def _get_thumbnail_requirements(self, media_type):
  355. return self.thumbnail_requirements.get(media_type, ())
  356. def _generate_thumbnail(self, thumbnailer, t_width, t_height,
  357. t_method, t_type):
  358. m_width = thumbnailer.width
  359. m_height = thumbnailer.height
  360. if m_width * m_height >= self.max_image_pixels:
  361. logger.info(
  362. "Image too large to thumbnail %r x %r > %r",
  363. m_width, m_height, self.max_image_pixels
  364. )
  365. return
  366. if t_method == "crop":
  367. t_byte_source = thumbnailer.crop(t_width, t_height, t_type)
  368. elif t_method == "scale":
  369. t_width, t_height = thumbnailer.aspect(t_width, t_height)
  370. t_width = min(m_width, t_width)
  371. t_height = min(m_height, t_height)
  372. t_byte_source = thumbnailer.scale(t_width, t_height, t_type)
  373. else:
  374. t_byte_source = None
  375. return t_byte_source
  376. @defer.inlineCallbacks
  377. def generate_local_exact_thumbnail(self, media_id, t_width, t_height,
  378. t_method, t_type, url_cache):
  379. input_path = yield self.media_storage.ensure_media_is_in_local_cache(FileInfo(
  380. None, media_id, url_cache=url_cache,
  381. ))
  382. thumbnailer = Thumbnailer(input_path)
  383. t_byte_source = yield logcontext.defer_to_thread(
  384. self.hs.get_reactor(),
  385. self._generate_thumbnail,
  386. thumbnailer, t_width, t_height, t_method, t_type
  387. )
  388. if t_byte_source:
  389. try:
  390. file_info = FileInfo(
  391. server_name=None,
  392. file_id=media_id,
  393. url_cache=url_cache,
  394. thumbnail=True,
  395. thumbnail_width=t_width,
  396. thumbnail_height=t_height,
  397. thumbnail_method=t_method,
  398. thumbnail_type=t_type,
  399. )
  400. output_path = yield self.media_storage.store_file(
  401. t_byte_source, file_info,
  402. )
  403. finally:
  404. t_byte_source.close()
  405. logger.info("Stored thumbnail in file %r", output_path)
  406. t_len = os.path.getsize(output_path)
  407. yield self.store.store_local_thumbnail(
  408. media_id, t_width, t_height, t_type, t_method, t_len
  409. )
  410. defer.returnValue(output_path)
  411. @defer.inlineCallbacks
  412. def generate_remote_exact_thumbnail(self, server_name, file_id, media_id,
  413. t_width, t_height, t_method, t_type):
  414. input_path = yield self.media_storage.ensure_media_is_in_local_cache(FileInfo(
  415. server_name, file_id, url_cache=False,
  416. ))
  417. thumbnailer = Thumbnailer(input_path)
  418. t_byte_source = yield logcontext.defer_to_thread(
  419. self.hs.get_reactor(),
  420. self._generate_thumbnail,
  421. thumbnailer, t_width, t_height, t_method, t_type
  422. )
  423. if t_byte_source:
  424. try:
  425. file_info = FileInfo(
  426. server_name=server_name,
  427. file_id=media_id,
  428. thumbnail=True,
  429. thumbnail_width=t_width,
  430. thumbnail_height=t_height,
  431. thumbnail_method=t_method,
  432. thumbnail_type=t_type,
  433. )
  434. output_path = yield self.media_storage.store_file(
  435. t_byte_source, file_info,
  436. )
  437. finally:
  438. t_byte_source.close()
  439. logger.info("Stored thumbnail in file %r", output_path)
  440. t_len = os.path.getsize(output_path)
  441. yield self.store.store_remote_media_thumbnail(
  442. server_name, media_id, file_id,
  443. t_width, t_height, t_type, t_method, t_len
  444. )
  445. defer.returnValue(output_path)
  446. @defer.inlineCallbacks
  447. def _generate_thumbnails(self, server_name, media_id, file_id, media_type,
  448. url_cache=False):
  449. """Generate and store thumbnails for an image.
  450. Args:
  451. server_name (str|None): The server name if remote media, else None if local
  452. media_id (str): The media ID of the content. (This is the same as
  453. the file_id for local content)
  454. file_id (str): Local file ID
  455. media_type (str): The content type of the file
  456. url_cache (bool): If we are thumbnailing images downloaded for the URL cache,
  457. used exclusively by the url previewer
  458. Returns:
  459. Deferred[dict]: Dict with "width" and "height" keys of original image
  460. """
  461. requirements = self._get_thumbnail_requirements(media_type)
  462. if not requirements:
  463. return
  464. input_path = yield self.media_storage.ensure_media_is_in_local_cache(FileInfo(
  465. server_name, file_id, url_cache=url_cache,
  466. ))
  467. thumbnailer = Thumbnailer(input_path)
  468. m_width = thumbnailer.width
  469. m_height = thumbnailer.height
  470. if m_width * m_height >= self.max_image_pixels:
  471. logger.info(
  472. "Image too large to thumbnail %r x %r > %r",
  473. m_width, m_height, self.max_image_pixels
  474. )
  475. return
  476. # We deduplicate the thumbnail sizes by ignoring the cropped versions if
  477. # they have the same dimensions of a scaled one.
  478. thumbnails = {}
  479. for r_width, r_height, r_method, r_type in requirements:
  480. if r_method == "crop":
  481. thumbnails.setdefault((r_width, r_height, r_type), r_method)
  482. elif r_method == "scale":
  483. t_width, t_height = thumbnailer.aspect(r_width, r_height)
  484. t_width = min(m_width, t_width)
  485. t_height = min(m_height, t_height)
  486. thumbnails[(t_width, t_height, r_type)] = r_method
  487. # Now we generate the thumbnails for each dimension, store it
  488. for (t_width, t_height, t_type), t_method in iteritems(thumbnails):
  489. # Generate the thumbnail
  490. if t_method == "crop":
  491. t_byte_source = yield logcontext.defer_to_thread(
  492. self.hs.get_reactor(),
  493. thumbnailer.crop,
  494. t_width, t_height, t_type,
  495. )
  496. elif t_method == "scale":
  497. t_byte_source = yield logcontext.defer_to_thread(
  498. self.hs.get_reactor(),
  499. thumbnailer.scale,
  500. t_width, t_height, t_type,
  501. )
  502. else:
  503. logger.error("Unrecognized method: %r", t_method)
  504. continue
  505. if not t_byte_source:
  506. continue
  507. try:
  508. file_info = FileInfo(
  509. server_name=server_name,
  510. file_id=file_id,
  511. thumbnail=True,
  512. thumbnail_width=t_width,
  513. thumbnail_height=t_height,
  514. thumbnail_method=t_method,
  515. thumbnail_type=t_type,
  516. url_cache=url_cache,
  517. )
  518. output_path = yield self.media_storage.store_file(
  519. t_byte_source, file_info,
  520. )
  521. finally:
  522. t_byte_source.close()
  523. t_len = os.path.getsize(output_path)
  524. # Write to database
  525. if server_name:
  526. yield self.store.store_remote_media_thumbnail(
  527. server_name, media_id, file_id,
  528. t_width, t_height, t_type, t_method, t_len
  529. )
  530. else:
  531. yield self.store.store_local_thumbnail(
  532. media_id, t_width, t_height, t_type, t_method, t_len
  533. )
  534. defer.returnValue({
  535. "width": m_width,
  536. "height": m_height,
  537. })
  538. @defer.inlineCallbacks
  539. def delete_old_remote_media(self, before_ts):
  540. old_media = yield self.store.get_remote_media_before(before_ts)
  541. deleted = 0
  542. for media in old_media:
  543. origin = media["media_origin"]
  544. media_id = media["media_id"]
  545. file_id = media["filesystem_id"]
  546. key = (origin, media_id)
  547. logger.info("Deleting: %r", key)
  548. # TODO: Should we delete from the backup store
  549. with (yield self.remote_media_linearizer.queue(key)):
  550. full_path = self.filepaths.remote_media_filepath(origin, file_id)
  551. try:
  552. os.remove(full_path)
  553. except OSError as e:
  554. logger.warn("Failed to remove file: %r", full_path)
  555. if e.errno == errno.ENOENT:
  556. pass
  557. else:
  558. continue
  559. thumbnail_dir = self.filepaths.remote_media_thumbnail_dir(
  560. origin, file_id
  561. )
  562. shutil.rmtree(thumbnail_dir, ignore_errors=True)
  563. yield self.store.delete_remote_media(origin, media_id)
  564. deleted += 1
  565. defer.returnValue({"deleted": deleted})
  566. class MediaRepositoryResource(Resource):
  567. """File uploading and downloading.
  568. Uploads are POSTed to a resource which returns a token which is used to GET
  569. the download::
  570. => POST /_matrix/media/v1/upload HTTP/1.1
  571. Content-Type: <media-type>
  572. Content-Length: <content-length>
  573. <media>
  574. <= HTTP/1.1 200 OK
  575. Content-Type: application/json
  576. { "content_uri": "mxc://<server-name>/<media-id>" }
  577. => GET /_matrix/media/v1/download/<server-name>/<media-id> HTTP/1.1
  578. <= HTTP/1.1 200 OK
  579. Content-Type: <media-type>
  580. Content-Disposition: attachment;filename=<upload-filename>
  581. <media>
  582. Clients can get thumbnails by supplying a desired width and height and
  583. thumbnailing method::
  584. => GET /_matrix/media/v1/thumbnail/<server_name>
  585. /<media-id>?width=<w>&height=<h>&method=<m> HTTP/1.1
  586. <= HTTP/1.1 200 OK
  587. Content-Type: image/jpeg or image/png
  588. <thumbnail>
  589. The thumbnail methods are "crop" and "scale". "scale" trys to return an
  590. image where either the width or the height is smaller than the requested
  591. size. The client should then scale and letterbox the image if it needs to
  592. fit within a given rectangle. "crop" trys to return an image where the
  593. width and height are close to the requested size and the aspect matches
  594. the requested size. The client should scale the image if it needs to fit
  595. within a given rectangle.
  596. """
  597. def __init__(self, hs):
  598. Resource.__init__(self)
  599. media_repo = hs.get_media_repository()
  600. self.putChild(b"upload", UploadResource(hs, media_repo))
  601. self.putChild(b"download", DownloadResource(hs, media_repo))
  602. self.putChild(b"thumbnail", ThumbnailResource(
  603. hs, media_repo, media_repo.media_storage,
  604. ))
  605. if hs.config.url_preview_enabled:
  606. self.putChild(b"preview_url", PreviewUrlResource(
  607. hs, media_repo, media_repo.media_storage,
  608. ))
  609. self.putChild(b"config", MediaConfigResource(hs))