103 lines
		
	
	
		
			3.6 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			103 lines
		
	
	
		
			3.6 KiB
		
	
	
	
		
			Python
		
	
	
# -*- coding: utf-8 -*-
 | 
						|
# Copyright 2014-2016 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.
 | 
						|
 | 
						|
import base64
 | 
						|
import logging
 | 
						|
import os
 | 
						|
import re
 | 
						|
 | 
						|
from canonicaljson import json
 | 
						|
 | 
						|
from twisted.protocols.basic import FileSender
 | 
						|
from twisted.web import resource, server
 | 
						|
 | 
						|
from synapse.api.errors import Codes, cs_error
 | 
						|
from synapse.http.server import finish_request, respond_with_json_bytes
 | 
						|
 | 
						|
logger = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
class ContentRepoResource(resource.Resource):
 | 
						|
    """Provides file uploading and downloading.
 | 
						|
 | 
						|
    Uploads are POSTed to wherever this Resource is linked to. This resource
 | 
						|
    returns a "content token" which can be used to GET this content again. The
 | 
						|
    token is typically a path, but it may not be. Tokens can expire, be
 | 
						|
    one-time uses, etc.
 | 
						|
 | 
						|
    In this case, the token is a path to the file and contains 3 interesting
 | 
						|
    sections:
 | 
						|
        - User ID base64d (for namespacing content to each user)
 | 
						|
        - random 24 char string
 | 
						|
        - Content type base64d (so we can return it when clients GET it)
 | 
						|
 | 
						|
    """
 | 
						|
    isLeaf = True
 | 
						|
 | 
						|
    def __init__(self, hs, directory):
 | 
						|
        resource.Resource.__init__(self)
 | 
						|
        self.hs = hs
 | 
						|
        self.directory = directory
 | 
						|
 | 
						|
    def render_GET(self, request):
 | 
						|
        # no auth here on purpose, to allow anyone to view, even across home
 | 
						|
        # servers.
 | 
						|
 | 
						|
        # TODO: A little crude here, we could do this better.
 | 
						|
        filename = request.path.decode('ascii').split('/')[-1]
 | 
						|
        # be paranoid
 | 
						|
        filename = re.sub("[^0-9A-z.-_]", "", filename)
 | 
						|
 | 
						|
        file_path = self.directory + "/" + filename
 | 
						|
 | 
						|
        logger.debug("Searching for %s", file_path)
 | 
						|
 | 
						|
        if os.path.isfile(file_path):
 | 
						|
            # filename has the content type
 | 
						|
            base64_contentype = filename.split(".")[1]
 | 
						|
            content_type = base64.urlsafe_b64decode(base64_contentype)
 | 
						|
            logger.info("Sending file %s", file_path)
 | 
						|
            f = open(file_path, 'rb')
 | 
						|
            request.setHeader('Content-Type', content_type)
 | 
						|
 | 
						|
            # 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 matrix
 | 
						|
            # clients are smart enough to be happy with Cache-Control (right?)
 | 
						|
            request.setHeader(
 | 
						|
                b"Cache-Control", b"public,max-age=86400,s-maxage=86400"
 | 
						|
            )
 | 
						|
 | 
						|
            d = FileSender().beginFileTransfer(f, request)
 | 
						|
 | 
						|
            # after the file has been sent, clean up and finish the request
 | 
						|
            def cbFinished(ignored):
 | 
						|
                f.close()
 | 
						|
                finish_request(request)
 | 
						|
            d.addCallback(cbFinished)
 | 
						|
        else:
 | 
						|
            respond_with_json_bytes(
 | 
						|
                request,
 | 
						|
                404,
 | 
						|
                json.dumps(cs_error("Not found", code=Codes.NOT_FOUND)),
 | 
						|
                send_cors=True)
 | 
						|
 | 
						|
        return server.NOT_DONE_YET
 | 
						|
 | 
						|
    def render_OPTIONS(self, request):
 | 
						|
        respond_with_json_bytes(request, 200, {}, send_cors=True)
 | 
						|
        return server.NOT_DONE_YET
 |