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

Commit 298355c

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

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 (
@@ -300,8 +302,62 @@ async def create_content(
300302

301303
return MXCUri(self.server_name, media_id)
302304

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

323380
self.mark_recently_accessed(None, media_id)
@@ -342,6 +399,7 @@ async def get_remote_media(
342399
server_name: str,
343400
media_id: str,
344401
name: Optional[str],
402+
max_timeout_ms: int,
345403
) -> None:
346404
"""Respond to requests for remote media.
347405
@@ -351,6 +409,8 @@ async def get_remote_media(
351409
media_id: The media ID of the content (as defined by the remote server).
352410
name: Optional name that, if specified, will be used as
353411
the filename in the Content-Disposition header of the response.
412+
max_timeout_ms: the maximum number of milliseconds to wait for the
413+
media to be uploaded.
354414
355415
Returns:
356416
Resolves once a response has successfully been written to request
@@ -368,27 +428,31 @@ async def get_remote_media(
368428
key = (server_name, media_id)
369429
async with self.remote_media_linearizer.queue(key):
370430
responder, media_info = await self._get_remote_media_impl(
371-
server_name, media_id
431+
server_name, media_id, max_timeout_ms
372432
)
373433

374434
# We deliberately stream the file outside the lock
375-
if responder:
435+
if responder and media_info:
376436
media_type = media_info["media_type"]
377437
media_length = media_info["media_length"]
378438
upload_name = name if name else media_info["upload_name"]
379439
await respond_with_responder(
380440
request, responder, media_type, media_length, upload_name
381441
)
382442
else:
383-
respond_404(request)
443+
self.respond_not_yet_uploaded(request)
384444

385-
async def get_remote_media_info(self, server_name: str, media_id: str) -> dict:
445+
async def get_remote_media_info(
446+
self, server_name: str, media_id: str, max_timeout_ms: int
447+
) -> dict:
386448
"""Gets the media info associated with the remote file, downloading
387449
if necessary.
388450
389451
Args:
390452
server_name: Remote server_name where the media originated.
391453
media_id: The media ID of the content (as defined by the remote server).
454+
max_timeout_ms: the maximum number of milliseconds to wait for the
455+
media to be uploaded.
392456
393457
Returns:
394458
The media info of the file
@@ -404,7 +468,7 @@ async def get_remote_media_info(self, server_name: str, media_id: str) -> dict:
404468
key = (server_name, media_id)
405469
async with self.remote_media_linearizer.queue(key):
406470
responder, media_info = await self._get_remote_media_impl(
407-
server_name, media_id
471+
server_name, media_id, max_timeout_ms
408472
)
409473

410474
# Ensure we actually use the responder so that it releases resources
@@ -415,7 +479,7 @@ async def get_remote_media_info(self, server_name: str, media_id: str) -> dict:
415479
return media_info
416480

417481
async def _get_remote_media_impl(
418-
self, server_name: str, media_id: str
482+
self, server_name: str, media_id: str, max_timeout_ms: int
419483
) -> Tuple[Optional[Responder], dict]:
420484
"""Looks for media in local cache, if not there then attempt to
421485
download from remote server.
@@ -424,6 +488,8 @@ async def _get_remote_media_impl(
424488
server_name: Remote server_name where the media originated.
425489
media_id: The media ID of the content (as defined by the
426490
remote server).
491+
max_timeout_ms: the maximum number of milliseconds to wait for the
492+
media to be uploaded.
427493
428494
Returns:
429495
A tuple of responder and the media info of the file.
@@ -454,8 +520,7 @@ async def _get_remote_media_impl(
454520

455521
try:
456522
media_info = await self._download_remote_file(
457-
server_name,
458-
media_id,
523+
server_name, media_id, max_timeout_ms
459524
)
460525
except SynapseError:
461526
raise
@@ -488,6 +553,7 @@ async def _download_remote_file(
488553
self,
489554
server_name: str,
490555
media_id: str,
556+
max_timeout_ms: int,
491557
) -> dict:
492558
"""Attempt to download the remote file from the given server name,
493559
using the given file_id as the local id.
@@ -497,7 +563,8 @@ async def _download_remote_file(
497563
media_id: The media ID of the content (as defined by the
498564
remote server). This is different than the file_id, which is
499565
locally generated.
500-
file_id: Local file ID
566+
max_timeout_ms: the maximum number of milliseconds to wait for the
567+
media to be uploaded.
501568
502569
Returns:
503570
The media info of the file.
@@ -521,7 +588,8 @@ async def _download_remote_file(
521588
# tell the remote server to 404 if it doesn't
522589
# recognise the server_name, to make sure we don't
523590
# end up with a routing loop.
524-
"allow_remote": "false"
591+
"allow_remote": "false",
592+
"timeout_ms": str(max_timeout_ms),
525593
},
526594
)
527595
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 server_name == self.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)