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

Commit c826981

Browse files
committed
media/create: add /create endpoint
See matrix-org/matrix-spec-proposals#2246 for details Signed-off-by: Sumner Evans <[email protected]>
1 parent 5061bf7 commit c826981

File tree

5 files changed

+147
-0
lines changed

5 files changed

+147
-0
lines changed

synapse/media/media_repository.py

+23
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def __init__(self, hs: "HomeServer"):
7777
self.store = hs.get_datastores().main
7878
self.max_upload_size = hs.config.media.max_upload_size
7979
self.max_image_pixels = hs.config.media.max_image_pixels
80+
self.unused_expiration_time = hs.config.media.unused_expiration_time
8081

8182
Thumbnailer.set_limits(self.max_image_pixels)
8283

@@ -175,6 +176,28 @@ def mark_recently_accessed(self, server_name: Optional[str], media_id: str) -> N
175176
else:
176177
self.recently_accessed_locals.add(media_id)
177178

179+
@trace
180+
async def create_media_id(self, auth_user: UserID) -> Tuple[str, int]:
181+
"""Create and store a media ID for a local user and return the MXC URI and its
182+
expiration.
183+
Args:
184+
auth_user: The user_id of the uploader
185+
Returns:
186+
A tuple containing the MXC URI of the stored content and the timestamp at
187+
which the MXC URI expires.
188+
"""
189+
media_id = random_string(24)
190+
now = self.clock.time_msec()
191+
# After the configured amount of time, don't allow the upload to start.
192+
unused_expires_at = now + self.unused_expiration_time
193+
await self.store.store_local_media_id(
194+
media_id=media_id,
195+
time_now_ms=now,
196+
user_id=auth_user,
197+
unused_expires_at=unused_expires_at,
198+
)
199+
return f"mxc://{self.server_name}/{media_id}", unused_expires_at
200+
178201
@trace
179202
async def create_content(
180203
self,

synapse/rest/media/create_resource.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright 2023 Beeper Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
from typing import TYPE_CHECKING
17+
18+
from synapse.api.errors import LimitExceededError
19+
from synapse.api.ratelimiting import Ratelimiter
20+
from synapse.http.server import DirectServeJsonResource, respond_with_json
21+
from synapse.http.site import SynapseRequest
22+
23+
if TYPE_CHECKING:
24+
from synapse.media.media_repository import MediaRepository
25+
from synapse.server import HomeServer
26+
27+
logger = logging.getLogger(__name__)
28+
29+
30+
class CreateResource(DirectServeJsonResource):
31+
isLeaf = True
32+
33+
def __init__(self, hs: "HomeServer", media_repo: "MediaRepository"):
34+
super().__init__()
35+
36+
self.media_repo = media_repo
37+
self.clock = hs.get_clock()
38+
self.auth = hs.get_auth()
39+
40+
# A rate limiter for creating new media IDs.
41+
self._create_media_rate_limiter = Ratelimiter(
42+
store=hs.get_datastores().main,
43+
clock=self.clock,
44+
rate_hz=hs.config.ratelimiting.rc_media_create.per_second,
45+
burst_count=hs.config.ratelimiting.rc_media_create.burst_count,
46+
)
47+
48+
async def _async_render_OPTIONS(self, request: SynapseRequest) -> None:
49+
respond_with_json(request, 200, {}, send_cors=True)
50+
51+
async def _async_render_POST(self, request: SynapseRequest) -> None:
52+
requester = await self.auth.get_user_by_req(request)
53+
54+
# If the create media requests for the user are over the limit, drop them.
55+
allowed, time_allowed = await self._create_media_rate_limiter.can_do_action(
56+
requester
57+
)
58+
if not allowed:
59+
time_now_s = self.clock.time()
60+
raise LimitExceededError(
61+
retry_after_ms=int(1000 * (time_allowed - time_now_s))
62+
)
63+
64+
content_uri, unused_expires_at = await self.media_repo.create_media_id(
65+
requester.user
66+
)
67+
68+
logger.info(
69+
"Created Media URI %r that if unused will expire at %d",
70+
content_uri,
71+
unused_expires_at,
72+
)
73+
respond_with_json(
74+
request,
75+
200,
76+
{
77+
"content_uri": content_uri,
78+
"unused_expires_at": unused_expires_at,
79+
},
80+
send_cors=True,
81+
)

synapse/rest/media/media_repository_resource.py

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from synapse.http.server import UnrecognizedRequestResource
1919

2020
from .config_resource import MediaConfigResource
21+
from .create_resource import CreateResource
2122
from .download_resource import DownloadResource
2223
from .preview_url_resource import PreviewUrlResource
2324
from .thumbnail_resource import ThumbnailResource
@@ -80,6 +81,7 @@ def __init__(self, hs: "HomeServer"):
8081
super().__init__()
8182
media_repo = hs.get_media_repository()
8283

84+
self.putChild(b"create", CreateResource(hs, media_repo))
8385
self.putChild(b"upload", UploadResource(hs, media_repo))
8486
self.putChild(b"download", DownloadResource(hs, media_repo))
8587
self.putChild(

synapse/storage/databases/main/media_repository.py

+19
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,25 @@ def _get_local_media_ids_txn(txn: LoggingTransaction) -> List[str]:
329329
"get_local_media_ids", _get_local_media_ids_txn
330330
)
331331

332+
@trace
333+
async def store_local_media_id(
334+
self,
335+
media_id: str,
336+
time_now_ms: int,
337+
user_id: UserID,
338+
unused_expires_at: int,
339+
) -> None:
340+
await self.db_pool.simple_insert(
341+
"local_media_repository",
342+
{
343+
"media_id": media_id,
344+
"created_ts": time_now_ms,
345+
"user_id": user_id.to_string(),
346+
"unused_expires_at": unused_expires_at,
347+
},
348+
desc="store_local_media_id",
349+
)
350+
332351
@trace
333352
async def store_local_media(
334353
self,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* Copyright 2023 Beeper Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
-- Add new colums to the `local_media_repository` to keep track of when the
17+
-- media ID must be used by. This is to support async uploads (see MSC2246).
18+
19+
ALTER TABLE local_media_repository
20+
ADD COLUMN unused_expires_at BIGINT DEFAULT NULL;
21+
22+
CREATE INDEX CONCURRENTLY ON local_media_repository (unused_expires_at);

0 commit comments

Comments
 (0)