Skip to content
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

Support MSC3916 by adding a federation /download endpoint #17172

Merged
merged 19 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions synapse/media/media_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@
ThumbnailInfo,
get_filename_from_headers,
respond_404,
respond_with_multipart_responder,
respond_with_responder,
)
from synapse.media.filepath import MediaFilePaths
from synapse.media.media_storage import MediaStorage
from synapse.media.media_storage import MediaStorage, MultipartResponder
from synapse.media.storage_provider import StorageProviderWrapper
from synapse.media.thumbnailer import Thumbnailer, ThumbnailError
from synapse.media.url_previewer import UrlPreviewer
Expand All @@ -70,6 +71,7 @@
if TYPE_CHECKING:
from synapse.server import HomeServer


logger = logging.getLogger(__name__)

# How often to run the background job to update the "recently accessed"
Expand Down Expand Up @@ -422,6 +424,7 @@ async def get_local_media(
media_id: str,
name: Optional[str],
max_timeout_ms: int,
federation: bool = False,
) -> None:
"""Responds to requests for local media, if exists, or returns 404.

Expand All @@ -433,6 +436,7 @@ async def get_local_media(
the filename in the Content-Disposition header of the response.
max_timeout_ms: the maximum number of milliseconds to wait for the
media to be uploaded.
federation: whether the local media being fetched is for a federation request

Returns:
Resolves once a response has successfully been written to request
Expand All @@ -452,10 +456,17 @@ async def get_local_media(

file_info = FileInfo(None, media_id, url_cache=bool(url_cache))

responder = await self.media_storage.fetch_media(file_info)
await respond_with_responder(
request, responder, media_type, media_length, upload_name
responder = await self.media_storage.fetch_media(
file_info, media_info, federation
)
if federation:
# this really should be a Multipart responder but just in case
assert isinstance(responder, MultipartResponder)
await respond_with_multipart_responder(request, responder, media_info)
else:
await respond_with_responder(
request, responder, media_type, media_length, upload_name
)

async def get_remote_media(
self,
Expand Down
40 changes: 35 additions & 5 deletions synapse/media/storage_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@
import os
import shutil
from typing import TYPE_CHECKING, Callable, Optional
from uuid import uuid4

from synapse.config._base import Config
from synapse.logging.context import defer_to_thread, run_in_background
from synapse.logging.opentracing import start_active_span, trace_with_opname
from synapse.util.async_helpers import maybe_awaitable

from ..storage.databases.main.media_repository import LocalMedia
from ._base import FileInfo, Responder
from .media_storage import FileResponder
from .media_storage import FileResponder, MultipartResponder

logger = logging.getLogger(__name__)

Expand All @@ -55,13 +57,21 @@ async def store_file(self, path: str, file_info: FileInfo) -> None:
"""

@abc.abstractmethod
async def fetch(self, path: str, file_info: FileInfo) -> Optional[Responder]:
async def fetch(
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
self,
path: str,
file_info: FileInfo,
media_info: Optional[LocalMedia] = None,
federation: bool = False,
) -> Optional[Responder]:
"""Attempt to fetch the file described by file_info and stream it
into writer.

Args:
path: Relative path of file in local cache
file_info: The metadata of the file.
media_info: metadata of the media item
federation: Whether the requested media is for a federation request

Returns:
Returns a Responder if the provider has the file, otherwise returns None.
Expand Down Expand Up @@ -124,15 +134,23 @@ async def store() -> None:
run_in_background(store)

@trace_with_opname("StorageProviderWrapper.fetch")
async def fetch(self, path: str, file_info: FileInfo) -> Optional[Responder]:
async def fetch(
self,
path: str,
file_info: FileInfo,
media_info: Optional[LocalMedia] = None,
federation: bool = False,
) -> Optional[Responder]:
if file_info.url_cache:
# Files in the URL preview cache definitely aren't stored here,
# so avoid any potentially slow I/O or network access.
return None

# store_file is supposed to return an Awaitable, but guard
# against improper implementations.
return await maybe_awaitable(self.backend.fetch(path, file_info))
return await maybe_awaitable(
self.backend.fetch(path, file_info, media_info, federation)
)


class FileStorageProviderBackend(StorageProvider):
Expand Down Expand Up @@ -172,11 +190,23 @@ async def store_file(self, path: str, file_info: FileInfo) -> None:
)

@trace_with_opname("FileStorageProviderBackend.fetch")
async def fetch(self, path: str, file_info: FileInfo) -> Optional[Responder]:
async def fetch(
self,
path: str,
file_info: FileInfo,
media_info: Optional[LocalMedia] = None,
federation: bool = False,
) -> Optional[Responder]:
"""See StorageProvider.fetch"""

backup_fname = os.path.join(self.base_directory, path)
if os.path.isfile(backup_fname):
if federation:
assert media_info is not None
boundary = uuid4().hex.encode("ascii")
return MultipartResponder(
open(backup_fname, "rb"), media_info, boundary
)
return FileResponder(open(backup_fname, "rb"))

return None
Expand Down