content_repository.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-2016 OpenMarket Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import base64
  16. import logging
  17. import os
  18. import re
  19. from canonicaljson import json
  20. from twisted.protocols.basic import FileSender
  21. from twisted.web import resource, server
  22. from synapse.api.errors import Codes, cs_error
  23. from synapse.http.server import finish_request, respond_with_json_bytes
  24. logger = logging.getLogger(__name__)
  25. class ContentRepoResource(resource.Resource):
  26. """Provides file uploading and downloading.
  27. Uploads are POSTed to wherever this Resource is linked to. This resource
  28. returns a "content token" which can be used to GET this content again. The
  29. token is typically a path, but it may not be. Tokens can expire, be
  30. one-time uses, etc.
  31. In this case, the token is a path to the file and contains 3 interesting
  32. sections:
  33. - User ID base64d (for namespacing content to each user)
  34. - random 24 char string
  35. - Content type base64d (so we can return it when clients GET it)
  36. """
  37. isLeaf = True
  38. def __init__(self, hs, directory):
  39. resource.Resource.__init__(self)
  40. self.hs = hs
  41. self.directory = directory
  42. def render_GET(self, request):
  43. # no auth here on purpose, to allow anyone to view, even across home
  44. # servers.
  45. # TODO: A little crude here, we could do this better.
  46. filename = request.path.decode("ascii").split("/")[-1]
  47. # be paranoid
  48. filename = re.sub("[^0-9A-z.-_]", "", filename)
  49. file_path = self.directory + "/" + filename
  50. logger.debug("Searching for %s", file_path)
  51. if os.path.isfile(file_path):
  52. # filename has the content type
  53. base64_contentype = filename.split(".")[1]
  54. content_type = base64.urlsafe_b64decode(base64_contentype)
  55. logger.info("Sending file %s", file_path)
  56. f = open(file_path, "rb")
  57. request.setHeader("Content-Type", content_type)
  58. # cache for at least a day.
  59. # XXX: we might want to turn this off for data we don't want to
  60. # recommend caching as it's sensitive or private - or at least
  61. # select private. don't bother setting Expires as all our matrix
  62. # clients are smart enough to be happy with Cache-Control (right?)
  63. request.setHeader(b"Cache-Control", b"public,max-age=86400,s-maxage=86400")
  64. d = FileSender().beginFileTransfer(f, request)
  65. # after the file has been sent, clean up and finish the request
  66. def cbFinished(ignored):
  67. f.close()
  68. finish_request(request)
  69. d.addCallback(cbFinished)
  70. else:
  71. respond_with_json_bytes(
  72. request,
  73. 404,
  74. json.dumps(cs_error("Not found", code=Codes.NOT_FOUND)),
  75. send_cors=True,
  76. )
  77. return server.NOT_DONE_YET
  78. def render_OPTIONS(self, request):
  79. respond_with_json_bytes(request, 200, {}, send_cors=True)
  80. return server.NOT_DONE_YET