Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit a126708

Browse files
committed
media/{download,thumbnail}: support timeout_ms parameter
Signed-off-by: Sumner Evans <[email protected]>
1 parent 28fefba commit a126708

File tree

5 files changed

+154
-43
lines changed

5 files changed

+154
-43
lines changed

synapse/api/errors.py

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class Codes(str, Enum):
8080
WEAK_PASSWORD = "M_WEAK_PASSWORD"
8181
INVALID_SIGNATURE = "M_INVALID_SIGNATURE"
8282
USER_DEACTIVATED = "M_USER_DEACTIVATED"
83+
NOT_YET_UPLOADED = "M_NOT_YET_UPLOADED"
8384

8485
# Part of MSC3848
8586
# https://github.com/matrix-org/matrix-spec-proposals/pull/3848

synapse/media/_base.py

+6
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
"text/xml",
5151
]
5252

53+
# Default timeout_ms for download and thumbnail requests
54+
DEFAULT_MAX_TIMEOUT_MS = 20_000
55+
56+
# Maximum allowed timeout_ms for download and thumbnail requests
57+
MAXIMUM_ALLOWED_MAX_TIMEOUT_MS = 60_000
58+
5359

5460
def parse_media_id(request: Request) -> Tuple[str, str, Optional[str]]:
5561
"""Parses the server name, media ID and optional file name from the request URI

synapse/media/media_repository.py

+83-15
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import os
1818
import shutil
1919
from io import BytesIO
20-
from typing import IO, TYPE_CHECKING, Dict, List, Optional, Set, Tuple
20+
from typing import IO, TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
2121

2222
from matrix_common.types.mxc_uri import MXCUri
2323

@@ -32,8 +32,10 @@
3232
NotFoundError,
3333
RequestSendFailed,
3434
SynapseError,
35+
cs_error,
3536
)
3637
from synapse.config.repository import ThumbnailRequirement
38+
from synapse.http.server import respond_with_json
3739
from synapse.http.site import SynapseRequest
3840
from synapse.logging.context import defer_to_thread
3941
from synapse.media._base import (
@@ -301,8 +303,62 @@ async def create_content(
301303

302304
return MXCUri(self.server_name, media_id)
303305

306+
def respond_not_yet_uploaded(self, request: SynapseRequest) -> None:
307+
respond_with_json(
308+
request,
309+
404,
310+
cs_error("Media has not been uploaded yet", code=Codes.NOT_YET_UPLOADED),
311+
send_cors=True,
312+
)
313+
314+
async def get_local_media_info(
315+
self, request: SynapseRequest, media_id: str, max_timeout_ms: int
316+
) -> Optional[Dict[str, Any]]:
317+
"""Gets the info dictionary for given local media ID. If the media has
318+
not been uploaded yet, this function will wait up to ``max_timeout_ms``
319+
milliseconds for the media to be uploaded.
320+
Args:
321+
request: The incoming request.
322+
media_id: The media ID of the content. (This is the same as
323+
the file_id for local content.)
324+
max_timeout_ms: the maximum number of milliseconds to wait for the
325+
media to be uploaded.
326+
Returns:
327+
Either the info dictionary for the given local media ID or
328+
``None``. If ``None``, then no further processing is necessary as
329+
this function will send the necessary JSON response.
330+
"""
331+
wait_until = self.clock.time_msec() + max_timeout_ms
332+
while True:
333+
# Get the info for the media
334+
media_info = await self.store.get_local_media(media_id)
335+
if not media_info:
336+
respond_404(request)
337+
return None
338+
339+
if media_info["quarantined_by"]:
340+
logger.info("Media is quarantined")
341+
respond_404(request)
342+
return None
343+
344+
# The file has been uploaded, so stop looping
345+
if media_info.get("media_length") is not None:
346+
return media_info
347+
348+
if self.clock.time_msec() >= wait_until:
349+
break
350+
351+
await self.clock.sleep(0.5)
352+
353+
self.respond_not_yet_uploaded(request)
354+
return None
355+
304356
async def get_local_media(
305-
self, request: SynapseRequest, media_id: str, name: Optional[str]
357+
self,
358+
request: SynapseRequest,
359+
media_id: str,
360+
name: Optional[str],
361+
max_timeout_ms: int,
306362
) -> None:
307363
"""Responds to requests for local media, if exists, or returns 404.
308364
@@ -312,13 +368,14 @@ async def get_local_media(
312368
the file_id for local content.)
313369
name: Optional name that, if specified, will be used as
314370
the filename in the Content-Disposition header of the response.
371+
max_timeout_ms: the maximum number of milliseconds to wait for the
372+
media to be uploaded.
315373
316374
Returns:
317375
Resolves once a response has successfully been written to request
318376
"""
319-
media_info = await self.store.get_local_media(media_id)
320-
if not media_info or media_info["quarantined_by"]:
321-
respond_404(request)
377+
media_info = await self.get_local_media_info(request, media_id, max_timeout_ms)
378+
if not media_info:
322379
return
323380

324381
self.mark_recently_accessed(None, media_id)
@@ -343,6 +400,7 @@ async def get_remote_media(
343400
server_name: str,
344401
media_id: str,
345402
name: Optional[str],
403+
max_timeout_ms: int,
346404
) -> None:
347405
"""Respond to requests for remote media.
348406
@@ -352,6 +410,8 @@ async def get_remote_media(
352410
media_id: The media ID of the content (as defined by the remote server).
353411
name: Optional name that, if specified, will be used as
354412
the filename in the Content-Disposition header of the response.
413+
max_timeout_ms: the maximum number of milliseconds to wait for the
414+
media to be uploaded.
355415
356416
Returns:
357417
Resolves once a response has successfully been written to request
@@ -377,27 +437,31 @@ async def get_remote_media(
377437
key = (server_name, media_id)
378438
async with self.remote_media_linearizer.queue(key):
379439
responder, media_info = await self._get_remote_media_impl(
380-
server_name, media_id
440+
server_name, media_id, max_timeout_ms
381441
)
382442

383443
# We deliberately stream the file outside the lock
384-
if responder:
444+
if responder and media_info:
385445
media_type = media_info["media_type"]
386446
media_length = media_info["media_length"]
387447
upload_name = name if name else media_info["upload_name"]
388448
await respond_with_responder(
389449
request, responder, media_type, media_length, upload_name
390450
)
391451
else:
392-
respond_404(request)
452+
self.respond_not_yet_uploaded(request)
393453

394-
async def get_remote_media_info(self, server_name: str, media_id: str) -> dict:
454+
async def get_remote_media_info(
455+
self, server_name: str, media_id: str, max_timeout_ms: int
456+
) -> dict:
395457
"""Gets the media info associated with the remote file, downloading
396458
if necessary.
397459
398460
Args:
399461
server_name: Remote server_name where the media originated.
400462
media_id: The media ID of the content (as defined by the remote server).
463+
max_timeout_ms: the maximum number of milliseconds to wait for the
464+
media to be uploaded.
401465
402466
Returns:
403467
The media info of the file
@@ -413,7 +477,7 @@ async def get_remote_media_info(self, server_name: str, media_id: str) -> dict:
413477
key = (server_name, media_id)
414478
async with self.remote_media_linearizer.queue(key):
415479
responder, media_info = await self._get_remote_media_impl(
416-
server_name, media_id
480+
server_name, media_id, max_timeout_ms
417481
)
418482

419483
# Ensure we actually use the responder so that it releases resources
@@ -424,7 +488,7 @@ async def get_remote_media_info(self, server_name: str, media_id: str) -> dict:
424488
return media_info
425489

426490
async def _get_remote_media_impl(
427-
self, server_name: str, media_id: str
491+
self, server_name: str, media_id: str, max_timeout_ms: int
428492
) -> Tuple[Optional[Responder], dict]:
429493
"""Looks for media in local cache, if not there then attempt to
430494
download from remote server.
@@ -433,6 +497,8 @@ async def _get_remote_media_impl(
433497
server_name: Remote server_name where the media originated.
434498
media_id: The media ID of the content (as defined by the
435499
remote server).
500+
max_timeout_ms: the maximum number of milliseconds to wait for the
501+
media to be uploaded.
436502
437503
Returns:
438504
A tuple of responder and the media info of the file.
@@ -463,8 +529,7 @@ async def _get_remote_media_impl(
463529

464530
try:
465531
media_info = await self._download_remote_file(
466-
server_name,
467-
media_id,
532+
server_name, media_id, max_timeout_ms
468533
)
469534
except SynapseError:
470535
raise
@@ -497,6 +562,7 @@ async def _download_remote_file(
497562
self,
498563
server_name: str,
499564
media_id: str,
565+
max_timeout_ms: int,
500566
) -> dict:
501567
"""Attempt to download the remote file from the given server name,
502568
using the given file_id as the local id.
@@ -506,7 +572,8 @@ async def _download_remote_file(
506572
media_id: The media ID of the content (as defined by the
507573
remote server). This is different than the file_id, which is
508574
locally generated.
509-
file_id: Local file ID
575+
max_timeout_ms: the maximum number of milliseconds to wait for the
576+
media to be uploaded.
510577
511578
Returns:
512579
The media info of the file.
@@ -530,7 +597,8 @@ async def _download_remote_file(
530597
# tell the remote server to 404 if it doesn't
531598
# recognise the server_name, to make sure we don't
532599
# end up with a routing loop.
533-
"allow_remote": "false"
600+
"allow_remote": "false",
601+
"timeout_ms": str(max_timeout_ms),
534602
},
535603
)
536604
except RequestSendFailed as e:

synapse/rest/media/download_resource.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@
2020
set_corp_headers,
2121
set_cors_headers,
2222
)
23-
from synapse.http.servlet import parse_boolean
23+
from synapse.http.servlet import parse_boolean, parse_integer
2424
from synapse.http.site import SynapseRequest
25-
from synapse.media._base import parse_media_id, respond_404
25+
from synapse.media._base import (
26+
DEFAULT_MAX_TIMEOUT_MS,
27+
MAXIMUM_ALLOWED_MAX_TIMEOUT_MS,
28+
parse_media_id,
29+
respond_404,
30+
)
2631

2732
if TYPE_CHECKING:
2833
from synapse.media.media_repository import MediaRepository
@@ -54,13 +59,17 @@ async def _async_render_GET(self, request: SynapseRequest) -> None:
5459
)
5560
# Limited non-standard form of CSP for IE11
5661
request.setHeader(b"X-Content-Security-Policy", b"sandbox;")
57-
request.setHeader(
58-
b"Referrer-Policy",
59-
b"no-referrer",
60-
)
62+
request.setHeader(b"Referrer-Policy", b"no-referrer")
6163
server_name, media_id, name = parse_media_id(request)
64+
max_timeout_ms = parse_integer(
65+
request, "timeout_ms", default=DEFAULT_MAX_TIMEOUT_MS
66+
)
67+
max_timeout_ms = min(max_timeout_ms, MAXIMUM_ALLOWED_MAX_TIMEOUT_MS)
68+
6269
if self._is_mine_server_name(server_name):
63-
await self.media_repo.get_local_media(request, media_id, name)
70+
await self.media_repo.get_local_media(
71+
request, media_id, name, max_timeout_ms
72+
)
6473
else:
6574
allow_remote = parse_boolean(request, "allow_remote", default=True)
6675
if not allow_remote:
@@ -72,4 +81,6 @@ async def _async_render_GET(self, request: SynapseRequest) -> None:
7281
respond_404(request)
7382
return
7483

75-
await self.media_repo.get_remote_media(request, server_name, media_id, name)
84+
await self.media_repo.get_remote_media(
85+
request, server_name, media_id, name, max_timeout_ms
86+
)

0 commit comments

Comments
 (0)