123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- # -*- coding: utf-8 -*-
- # Copyright 2014 OpenMarket Ltd
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- from .thumbnailer import Thumbnailer
- from synapse.http.server import respond_with_json
- from synapse.util.stringutils import random_string
- from synapse.api.errors import (
- cs_exception, CodeMessageException, cs_error, Codes, SynapseError
- )
- from twisted.internet import defer
- from twisted.web.resource import Resource
- from twisted.protocols.basic import FileSender
- import os
- import logging
- logger = logging.getLogger(__name__)
- class BaseMediaResource(Resource):
- isLeaf = True
- def __init__(self, hs, filepaths):
- Resource.__init__(self)
- self.auth = hs.get_auth()
- self.client = hs.get_http_client()
- self.clock = hs.get_clock()
- self.server_name = hs.hostname
- self.store = hs.get_datastore()
- self.max_upload_size = hs.config.max_upload_size
- self.max_image_pixels = hs.config.max_image_pixels
- self.filepaths = filepaths
- self.downloads = {}
- @staticmethod
- def catch_errors(request_handler):
- @defer.inlineCallbacks
- def wrapped_request_handler(self, request):
- try:
- yield request_handler(self, request)
- except CodeMessageException as e:
- logger.exception(e)
- respond_with_json(
- request, e.code, cs_exception(e), send_cors=True
- )
- except:
- logger.exception(
- "Failed handle request %s.%s on %r",
- request_handler.__module__,
- request_handler.__name__,
- self,
- )
- respond_with_json(
- request,
- 500,
- {"error": "Internal server error"},
- send_cors=True
- )
- return wrapped_request_handler
- @staticmethod
- def _parse_media_id(request):
- try:
- server_name, media_id = request.postpath
- return (server_name, media_id)
- except:
- raise SynapseError(
- 404,
- "Invalid media id token %r" % (request.postpath,),
- Codes.UNKKOWN,
- )
- @staticmethod
- def _parse_integer(request, arg_name, default=None):
- try:
- if default is None:
- return int(request.args[arg_name][0])
- else:
- return int(request.args.get(arg_name, [default])[0])
- except:
- raise SynapseError(
- 400,
- "Missing integer argument %r" % (arg_name,),
- Codes.UNKNOWN,
- )
- @staticmethod
- def _parse_string(request, arg_name, default=None):
- try:
- if default is None:
- return request.args[arg_name][0]
- else:
- return request.args.get(arg_name, [default])[0]
- except:
- raise SynapseError(
- 400,
- "Missing string argument %r" % (arg_name,),
- Codes.UNKNOWN,
- )
- def _respond_404(self, request):
- respond_with_json(
- request, 404,
- cs_error(
- "Not found %r" % (request.postpath,),
- code=Codes.NOT_FOUND,
- ),
- send_cors=True
- )
- @staticmethod
- def _makedirs(filepath):
- dirname = os.path.dirname(filepath)
- if not os.path.exists(dirname):
- os.makedirs(dirname)
- def _get_remote_media(self, server_name, media_id):
- key = (server_name, media_id)
- download = self.downloads.get(key)
- if download is None:
- download = self._get_remote_media_impl(server_name, media_id)
- self.downloads[key] = download
- @download.addBoth
- def callback(media_info):
- del self.downloads[key]
- return download
- @defer.inlineCallbacks
- def _get_remote_media_impl(self, server_name, media_id):
- media_info = yield self.store.get_cached_remote_media(
- server_name, media_id
- )
- if not media_info:
- media_info = yield self._download_remote_file(
- server_name, media_id
- )
- defer.returnValue(media_info)
- @defer.inlineCallbacks
- def _download_remote_file(self, server_name, media_id):
- file_id = random_string(24)
- fname = self.filepaths.remote_media_filepath(
- server_name, file_id
- )
- self._makedirs(fname)
- try:
- with open(fname, "wb") as f:
- request_path = "/".join((
- "/_matrix/media/v1/download", server_name, media_id,
- ))
- length, headers = yield self.client.get_file(
- server_name, request_path, output_stream=f,
- max_size=self.max_upload_size,
- )
- media_type = headers["Content-Type"][0]
- time_now_ms = self.clock.time_msec()
- yield self.store.store_cached_remote_media(
- origin=server_name,
- media_id=media_id,
- media_type=media_type,
- time_now_ms=self.clock.time_msec(),
- upload_name=None,
- media_length=length,
- filesystem_id=file_id,
- )
- except:
- os.remove(fname)
- raise
- media_info = {
- "media_type": media_type,
- "media_length": length,
- "upload_name": None,
- "created_ts": time_now_ms,
- "filesystem_id": file_id,
- }
- yield self._generate_remote_thumbnails(
- server_name, media_id, media_info
- )
- defer.returnValue(media_info)
- @defer.inlineCallbacks
- def _respond_with_file(self, request, media_type, file_path):
- logger.debug("Responding with %r", file_path)
- if os.path.isfile(file_path):
- request.setHeader(b"Content-Type", media_type.encode("UTF-8"))
- # cache for at least a day.
- # XXX: we might want to turn this off for data we don't want to
- # recommend caching as it's sensitive or private - or at least
- # select private. don't bother setting Expires as all our
- # clients are smart enough to be happy with Cache-Control
- request.setHeader(
- b"Cache-Control", b"public,max-age=86400,s-maxage=86400"
- )
- with open(file_path, "rb") as f:
- yield FileSender().beginFileTransfer(f, request)
- request.finish()
- else:
- self._respond_404()
- def _get_thumbnail_requirements(self, media_type):
- if media_type == "image/jpeg":
- return (
- (32, 32, "crop", "image/jpeg"),
- (96, 96, "crop", "image/jpeg"),
- (320, 240, "scale", "image/jpeg"),
- (640, 480, "scale", "image/jpeg"),
- )
- elif (media_type == "image/png") or (media_type == "image/gif"):
- return (
- (32, 32, "crop", "image/png"),
- (96, 96, "crop", "image/png"),
- (320, 240, "scale", "image/png"),
- (640, 480, "scale", "image/png"),
- )
- else:
- return ()
- @defer.inlineCallbacks
- def _generate_local_thumbnails(self, media_id, media_info):
- media_type = media_info["media_type"]
- requirements = self._get_thumbnail_requirements(media_type)
- if not requirements:
- return
- input_path = self.filepaths.local_media_filepath(media_id)
- thumbnailer = Thumbnailer(input_path)
- m_width = thumbnailer.width
- m_height = thumbnailer.height
- if m_width * m_height >= self.max_image_pixels:
- logger.info(
- "Image too large to thumbnail %r x %r > %r",
- m_width, m_height, self.max_image_pixels
- )
- return
- scales = set()
- crops = set()
- for r_width, r_height, r_method, r_type in requirements:
- if r_method == "scale":
- t_width, t_height = thumbnailer.aspect(r_width, r_height)
- scales.add((
- min(m_width, t_width), min(m_height, t_height), r_type,
- ))
- elif r_method == "crop":
- crops.add((r_width, r_height, r_type))
- for t_width, t_height, t_type in scales:
- t_method = "scale"
- t_path = self.filepaths.local_media_thumbnail(
- media_id, t_width, t_height, t_type, t_method
- )
- self._makedirs(t_path)
- t_len = thumbnailer.scale(t_path, t_width, t_height, t_type)
- yield self.store.store_local_thumbnail(
- media_id, t_width, t_height, t_type, t_method, t_len
- )
- for t_width, t_height, t_type in crops:
- if (t_width, t_height, t_type) in scales:
- # If the aspect ratio of the cropped thumbnail matches a purely
- # scaled one then there is no point in calculating a separate
- # thumbnail.
- continue
- t_method = "crop"
- t_path = self.filepaths.local_media_thumbnail(
- media_id, t_width, t_height, t_type, t_method
- )
- self._makedirs(t_path)
- t_len = thumbnailer.crop(t_path, t_width, t_height, t_type)
- yield self.store.store_local_thumbnail(
- media_id, t_width, t_height, t_type, t_method, t_len
- )
- defer.returnValue({
- "width": m_width,
- "height": m_height,
- })
- @defer.inlineCallbacks
- def _generate_remote_thumbnails(self, server_name, media_id, media_info):
- media_type = media_info["media_type"]
- file_id = media_info["filesystem_id"]
- requirements = self._get_thumbnail_requirements(media_type)
- if not requirements:
- return
- input_path = self.filepaths.remote_media_filepath(server_name, file_id)
- thumbnailer = Thumbnailer(input_path)
- m_width = thumbnailer.width
- m_height = thumbnailer.height
- if m_width * m_height >= self.max_image_pixels:
- logger.info(
- "Image too large to thumbnail %r x %r > %r",
- m_width, m_height, self.max_image_pixels
- )
- return
- scales = set()
- crops = set()
- for r_width, r_height, r_method, r_type in requirements:
- if r_method == "scale":
- t_width, t_height = thumbnailer.aspect(r_width, r_height)
- scales.add((
- min(m_width, t_width), min(m_height, t_height), r_type,
- ))
- elif r_method == "crop":
- crops.add((r_width, r_height, r_type))
- for t_width, t_height, t_type in scales:
- t_method = "scale"
- t_path = self.filepaths.remote_media_thumbnail(
- server_name, file_id, t_width, t_height, t_type, t_method
- )
- self._makedirs(t_path)
- t_len = thumbnailer.scale(t_path, t_width, t_height, t_type)
- yield self.store.store_remote_media_thumbnail(
- server_name, media_id, file_id,
- t_width, t_height, t_type, t_method, t_len
- )
- for t_width, t_height, t_type in crops:
- if (t_width, t_height, t_type) in scales:
- # If the aspect ratio of the cropped thumbnail matches a purely
- # scaled one then there is no point in calculating a separate
- # thumbnail.
- continue
- t_method = "crop"
- t_path = self.filepaths.remote_media_thumbnail(
- server_name, file_id, t_width, t_height, t_type, t_method
- )
- self._makedirs(t_path)
- t_len = thumbnailer.crop(t_path, t_width, t_height, t_type)
- yield self.store.store_remote_media_thumbnail(
- server_name, media_id, file_id,
- t_width, t_height, t_type, t_method, t_len
- )
- defer.returnValue({
- "width": m_width,
- "height": m_height,
- })
|