media_repository.py 27 KB

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