Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

188 linhas
6.5 KiB

  1. # Copyright 2018-2021 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import abc
  15. import logging
  16. import os
  17. import shutil
  18. from typing import TYPE_CHECKING, Callable, Optional
  19. from synapse.config._base import Config
  20. from synapse.logging.context import defer_to_thread, run_in_background
  21. from synapse.logging.opentracing import start_active_span, trace_with_opname
  22. from synapse.util.async_helpers import maybe_awaitable
  23. from ._base import FileInfo, Responder
  24. from .media_storage import FileResponder
  25. logger = logging.getLogger(__name__)
  26. if TYPE_CHECKING:
  27. from synapse.server import HomeServer
  28. class StorageProvider(metaclass=abc.ABCMeta):
  29. """A storage provider is a service that can store uploaded media and
  30. retrieve them.
  31. """
  32. @abc.abstractmethod
  33. async def store_file(self, path: str, file_info: FileInfo) -> None:
  34. """Store the file described by file_info. The actual contents can be
  35. retrieved by reading the file in file_info.upload_path.
  36. Args:
  37. path: Relative path of file in local cache
  38. file_info: The metadata of the file.
  39. """
  40. @abc.abstractmethod
  41. async def fetch(self, path: str, file_info: FileInfo) -> Optional[Responder]:
  42. """Attempt to fetch the file described by file_info and stream it
  43. into writer.
  44. Args:
  45. path: Relative path of file in local cache
  46. file_info: The metadata of the file.
  47. Returns:
  48. Returns a Responder if the provider has the file, otherwise returns None.
  49. """
  50. class StorageProviderWrapper(StorageProvider):
  51. """Wraps a storage provider and provides various config options
  52. Args:
  53. backend: The storage provider to wrap.
  54. store_local: Whether to store new local files or not.
  55. store_synchronous: Whether to wait for file to be successfully
  56. uploaded, or todo the upload in the background.
  57. store_remote: Whether remote media should be uploaded
  58. """
  59. def __init__(
  60. self,
  61. backend: StorageProvider,
  62. store_local: bool,
  63. store_synchronous: bool,
  64. store_remote: bool,
  65. ):
  66. self.backend = backend
  67. self.store_local = store_local
  68. self.store_synchronous = store_synchronous
  69. self.store_remote = store_remote
  70. def __str__(self) -> str:
  71. return "StorageProviderWrapper[%s]" % (self.backend,)
  72. @trace_with_opname("StorageProviderWrapper.store_file")
  73. async def store_file(self, path: str, file_info: FileInfo) -> None:
  74. if not file_info.server_name and not self.store_local:
  75. return None
  76. if file_info.server_name and not self.store_remote:
  77. return None
  78. if file_info.url_cache:
  79. # The URL preview cache is short lived and not worth offloading or
  80. # backing up.
  81. return None
  82. if self.store_synchronous:
  83. # store_file is supposed to return an Awaitable, but guard
  84. # against improper implementations.
  85. await maybe_awaitable(self.backend.store_file(path, file_info)) # type: ignore
  86. else:
  87. # TODO: Handle errors.
  88. async def store() -> None:
  89. try:
  90. return await maybe_awaitable(
  91. self.backend.store_file(path, file_info)
  92. )
  93. except Exception:
  94. logger.exception("Error storing file")
  95. run_in_background(store)
  96. @trace_with_opname("StorageProviderWrapper.fetch")
  97. async def fetch(self, path: str, file_info: FileInfo) -> Optional[Responder]:
  98. if file_info.url_cache:
  99. # Files in the URL preview cache definitely aren't stored here,
  100. # so avoid any potentially slow I/O or network access.
  101. return None
  102. # store_file is supposed to return an Awaitable, but guard
  103. # against improper implementations.
  104. return await maybe_awaitable(self.backend.fetch(path, file_info))
  105. class FileStorageProviderBackend(StorageProvider):
  106. """A storage provider that stores files in a directory on a filesystem.
  107. Args:
  108. hs
  109. config: The config returned by `parse_config`.
  110. """
  111. def __init__(self, hs: "HomeServer", config: str):
  112. self.hs = hs
  113. self.cache_directory = hs.config.media.media_store_path
  114. self.base_directory = config
  115. def __str__(self) -> str:
  116. return "FileStorageProviderBackend[%s]" % (self.base_directory,)
  117. @trace_with_opname("FileStorageProviderBackend.store_file")
  118. async def store_file(self, path: str, file_info: FileInfo) -> None:
  119. """See StorageProvider.store_file"""
  120. primary_fname = os.path.join(self.cache_directory, path)
  121. backup_fname = os.path.join(self.base_directory, path)
  122. dirname = os.path.dirname(backup_fname)
  123. os.makedirs(dirname, exist_ok=True)
  124. # mypy needs help inferring the type of the second parameter, which is generic
  125. shutil_copyfile: Callable[[str, str], str] = shutil.copyfile
  126. with start_active_span("shutil_copyfile"):
  127. await defer_to_thread(
  128. self.hs.get_reactor(),
  129. shutil_copyfile,
  130. primary_fname,
  131. backup_fname,
  132. )
  133. @trace_with_opname("FileStorageProviderBackend.fetch")
  134. async def fetch(self, path: str, file_info: FileInfo) -> Optional[Responder]:
  135. """See StorageProvider.fetch"""
  136. backup_fname = os.path.join(self.base_directory, path)
  137. if os.path.isfile(backup_fname):
  138. return FileResponder(open(backup_fname, "rb"))
  139. return None
  140. @staticmethod
  141. def parse_config(config: dict) -> str:
  142. """Called on startup to parse config supplied. This should parse
  143. the config and raise if there is a problem.
  144. The returned value is passed into the constructor.
  145. In this case we only care about a single param, the directory, so let's
  146. just pull that out.
  147. """
  148. return Config.ensure_directory(config["directory"])