|
25 | 25 | import urllib
|
26 | 26 | from abc import ABC, abstractmethod
|
27 | 27 | from types import TracebackType
|
28 |
| -from typing import Awaitable, Dict, Generator, List, Optional, Tuple, Type |
| 28 | +from typing import ( |
| 29 | + TYPE_CHECKING, |
| 30 | + Awaitable, |
| 31 | + Dict, |
| 32 | + Generator, |
| 33 | + List, |
| 34 | + Optional, |
| 35 | + Tuple, |
| 36 | + Type, |
| 37 | +) |
29 | 38 |
|
30 | 39 | import attr
|
31 | 40 |
|
|
37 | 46 | from synapse.http.server import finish_request, respond_with_json
|
38 | 47 | from synapse.http.site import SynapseRequest
|
39 | 48 | from synapse.logging.context import make_deferred_yieldable
|
| 49 | +from synapse.util import Clock |
40 | 50 | from synapse.util.stringutils import is_ascii
|
41 | 51 |
|
| 52 | +if TYPE_CHECKING: |
| 53 | + from synapse.storage.databases.main.media_repository import LocalMedia |
| 54 | + |
| 55 | + |
42 | 56 | logger = logging.getLogger(__name__)
|
43 | 57 |
|
44 | 58 | # list all text content types that will have the charset default to UTF-8 when
|
@@ -260,6 +274,68 @@ def _can_encode_filename_as_token(x: str) -> bool:
|
260 | 274 | return True
|
261 | 275 |
|
262 | 276 |
|
| 277 | +async def respond_with_multipart_responder( |
| 278 | + clock: Clock, |
| 279 | + request: SynapseRequest, |
| 280 | + responder: "Optional[Responder]", |
| 281 | + media_info: "LocalMedia", |
| 282 | +) -> None: |
| 283 | + """ |
| 284 | + Responds to requests originating from the federation media `/download` endpoint by |
| 285 | + streaming a multipart/mixed response |
| 286 | +
|
| 287 | + Args: |
| 288 | + clock: |
| 289 | + request: the federation request to respond to |
| 290 | + responder: the responder which will send the response |
| 291 | + media_info: metadata about the media item |
| 292 | + """ |
| 293 | + if not responder: |
| 294 | + respond_404(request) |
| 295 | + return |
| 296 | + |
| 297 | + # If we have a responder we *must* use it as a context manager. |
| 298 | + with responder: |
| 299 | + if request._disconnected: |
| 300 | + logger.warning( |
| 301 | + "Not sending response to request %s, already disconnected.", request |
| 302 | + ) |
| 303 | + return |
| 304 | + |
| 305 | + from synapse.media.media_storage import MultipartFileConsumer |
| 306 | + |
| 307 | + # note that currently the json_object is just {}, this will change when linked media |
| 308 | + # is implemented |
| 309 | + multipart_consumer = MultipartFileConsumer( |
| 310 | + clock, request, media_info.media_type, {}, media_info.media_length |
| 311 | + ) |
| 312 | + |
| 313 | + logger.debug("Responding to media request with responder %s", responder) |
| 314 | + if media_info.media_length is not None: |
| 315 | + content_length = multipart_consumer.content_length() |
| 316 | + assert content_length is not None |
| 317 | + request.setHeader(b"Content-Length", b"%d" % (content_length,)) |
| 318 | + |
| 319 | + request.setHeader( |
| 320 | + b"Content-Type", |
| 321 | + b"multipart/mixed; boundary=%s" % multipart_consumer.boundary, |
| 322 | + ) |
| 323 | + |
| 324 | + try: |
| 325 | + await responder.write_to_consumer(multipart_consumer) |
| 326 | + except Exception as e: |
| 327 | + # The majority of the time this will be due to the client having gone |
| 328 | + # away. Unfortunately, Twisted simply throws a generic exception at us |
| 329 | + # in that case. |
| 330 | + logger.warning("Failed to write to consumer: %s %s", type(e), e) |
| 331 | + |
| 332 | + # Unregister the producer, if it has one, so Twisted doesn't complain |
| 333 | + if request.producer: |
| 334 | + request.unregisterProducer() |
| 335 | + |
| 336 | + finish_request(request) |
| 337 | + |
| 338 | + |
263 | 339 | async def respond_with_responder(
|
264 | 340 | request: SynapseRequest,
|
265 | 341 | responder: "Optional[Responder]",
|
|
0 commit comments