-
Notifications
You must be signed in to change notification settings - Fork 424
Implement synapse issue #16751: Treat local_media_directory as optional storage provider #19204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 1 commit
2e78aa8
7cc0aeb
8eed314
b6ffef6
ccc047d
5d272af
646a051
94c4358
f63c91a
99eb251
af958d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Made the local media directory optional by treating it as a storage provider. This allows off-site media storage without local cache, where media is stored directly to remote providers only, with temporary files used for thumbnail generation when needed. Contributed by Patrice Brend'amour @dr.allgood. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,16 +50,16 @@ | |
| from synapse.api.errors import NotFoundError | ||
| from synapse.logging.context import defer_to_thread, run_in_background | ||
| from synapse.logging.opentracing import start_active_span, trace, trace_with_opname | ||
| from synapse.media._base import ThreadedFileSender | ||
| from synapse.media.storage_provider import FileStorageProviderBackend | ||
| from synapse.util.clock import Clock | ||
| from synapse.util.file_consumer import BackgroundFileConsumer | ||
|
|
||
| from ..types import JsonDict | ||
| from ._base import FileInfo, Responder | ||
| from ._base import FileInfo, Responder, ThreadedFileSender | ||
| from .filepath import MediaFilePaths | ||
|
|
||
| if TYPE_CHECKING: | ||
| from synapse.media.storage_provider import StorageProvider | ||
| from synapse.media.storage_provider import StorageProviderWrapper | ||
| from synapse.server import HomeServer | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
@@ -162,7 +162,7 @@ def __init__( | |
| self, | ||
| hs: "HomeServer", | ||
| filepaths: MediaFilePaths, | ||
| storage_providers: Sequence["StorageProvider"], | ||
| storage_providers: Sequence["StorageProviderWrapper"], | ||
drallgood marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ): | ||
| self.hs = hs | ||
| self.reactor = hs.get_reactor() | ||
|
|
@@ -228,7 +228,7 @@ async def store_into_file( | |
| path = self._file_info_to_path(file_info) | ||
|
|
||
| if self.local_provider: | ||
| fname = os.path.join(self.local_media_directory, path) | ||
| fname = os.path.join(self.local_media_directory, path) # type: ignore[arg-type] | ||
| dirname = os.path.dirname(fname) | ||
drallgood marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| os.makedirs(dirname, exist_ok=True) | ||
|
|
||
|
|
@@ -237,11 +237,11 @@ async def store_into_file( | |
| with open(fname, "wb") as f: | ||
| yield f, fname | ||
|
|
||
| with start_active_span("spam checking and writing to other storage providers"): | ||
| spam_check = ( | ||
| await self._spam_checker_module_callbacks.check_media_file_for_spam( | ||
| ReadableFileWrapper(self.clock, fname), file_info | ||
| ) | ||
| with start_active_span( | ||
| "spam checking and writing to other storage providers" | ||
| ): | ||
| spam_check = await self._spam_checker_module_callbacks.check_media_file_for_spam( | ||
| ReadableFileWrapper(self.clock, fname), file_info | ||
| ) | ||
| if spam_check != self._spam_checker_module_callbacks.NOT_SPAM: | ||
| logger.info("Blocking media due to spam checker") | ||
|
|
@@ -268,14 +268,14 @@ async def store_into_file( | |
| # No local provider, write to temp file | ||
| with tempfile.NamedTemporaryFile(delete=False) as f: | ||
| fname = f.name | ||
| yield f, fname | ||
| yield cast(BinaryIO, f), fname | ||
|
|
||
| try: | ||
| with start_active_span("spam checking and writing to storage providers"): | ||
| spam_check = ( | ||
| await self._spam_checker_module_callbacks.check_media_file_for_spam( | ||
| ReadableFileWrapper(self.clock, fname), file_info | ||
| ) | ||
| with start_active_span( | ||
| "spam checking and writing to storage providers" | ||
| ): | ||
| spam_check = await self._spam_checker_module_callbacks.check_media_file_for_spam( | ||
| ReadableFileWrapper(self.clock, fname), file_info | ||
| ) | ||
| if spam_check != self._spam_checker_module_callbacks.NOT_SPAM: | ||
| logger.info("Blocking media due to spam checker") | ||
|
|
@@ -302,6 +302,18 @@ async def fetch_media(self, file_info: FileInfo) -> Responder | None: | |
| Returns: | ||
| Returns a Responder if the file was found, otherwise None. | ||
| """ | ||
| # URL cache files are stored locally and should not go through storage providers | ||
| if file_info.url_cache: | ||
| path = self._file_info_to_path(file_info) | ||
| if self.local_provider: | ||
| local_path = os.path.join(self.local_media_directory, path) | ||
| if os.path.isfile(local_path): | ||
| # Import here to avoid circular import | ||
| from .media_storage import FileResponder | ||
|
|
||
| return FileResponder(self.hs, open(local_path, "rb")) | ||
| return None | ||
|
|
||
| paths = [self._file_info_to_path(file_info)] | ||
|
|
||
| # fallback for remote thumbnails with no method in the filename | ||
|
|
@@ -339,7 +351,7 @@ async def ensure_media_is_in_local_cache(self, file_info: FileInfo) -> str: | |
| """ | ||
| path = self._file_info_to_path(file_info) | ||
| if self.local_provider: | ||
| local_path = os.path.join(self.local_media_directory, path) | ||
| local_path = os.path.join(self.local_media_directory, path) # type: ignore[arg-type] | ||
| if os.path.exists(local_path): | ||
| return local_path | ||
|
|
||
|
|
@@ -353,7 +365,10 @@ async def ensure_media_is_in_local_cache(self, file_info: FileInfo) -> str: | |
| height=file_info.thumbnail.height, | ||
| content_type=file_info.thumbnail.type, | ||
| ) | ||
| legacy_local_path = os.path.join(self.local_media_directory, legacy_path) | ||
| legacy_local_path = os.path.join( | ||
| self.local_media_directory, # type: ignore[arg-type] | ||
| legacy_path, | ||
| ) | ||
| if os.path.exists(legacy_local_path): | ||
| return legacy_local_path | ||
|
|
||
|
|
@@ -363,13 +378,13 @@ async def ensure_media_is_in_local_cache(self, file_info: FileInfo) -> str: | |
| for provider in self.storage_providers: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could make use of the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not really. They have a different purpose. The ensure_media_is_in_local_cache is used to ensure a local copy so a thumbnail can be generated. While the fetch_media returns a Responder (to stream the file). |
||
| if provider is self.local_provider: | ||
| continue | ||
| res: Any = await provider.fetch(path, file_info) | ||
| if res: | ||
| with res: | ||
| remote_res: Any = await provider.fetch(path, file_info) | ||
| if remote_res: | ||
| with remote_res: | ||
| consumer = BackgroundFileConsumer( | ||
| open(local_path, "wb"), self.reactor | ||
| ) | ||
| await res.write_to_consumer(consumer) | ||
| await remote_res.write_to_consumer(consumer) | ||
| await consumer.wait() | ||
| return local_path | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.