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

Commit e673172

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 da4aa7f commit e673172

File tree

5 files changed

+155
-1
lines changed

5 files changed

+155
-1
lines changed

synapse/media/media_repository.py

+25
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def __init__(self, hs: "HomeServer"):
7878
self.store = hs.get_datastores().main
7979
self.max_upload_size = hs.config.media.max_upload_size
8080
self.max_image_pixels = hs.config.media.max_image_pixels
81+
self.unused_expiration_time = hs.config.media.unused_expiration_time
8182

8283
Thumbnailer.set_limits(self.max_image_pixels)
8384

@@ -183,6 +184,30 @@ def mark_recently_accessed(self, server_name: Optional[str], media_id: str) -> N
183184
else:
184185
self.recently_accessed_locals.add(media_id)
185186

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

synapse/rest/media/create_resource.py

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
import re
17+
from typing import TYPE_CHECKING
18+
19+
from synapse.api.errors import LimitExceededError
20+
from synapse.api.ratelimiting import Ratelimiter
21+
from synapse.http.server import DirectServeJsonResource, respond_with_json
22+
from synapse.http.site import SynapseRequest
23+
24+
if TYPE_CHECKING:
25+
from synapse.media.media_repository import MediaRepository
26+
from synapse.server import HomeServer
27+
28+
logger = logging.getLogger(__name__)
29+
30+
31+
class CreateResource(DirectServeJsonResource):
32+
PATTERNS = [re.compile("/_matrix/media/v1/create")]
33+
isLeaf = True
34+
35+
def __init__(self, hs: "HomeServer", media_repo: "MediaRepository"):
36+
super().__init__()
37+
38+
self.media_repo = media_repo
39+
self.clock = hs.get_clock()
40+
self.auth = hs.get_auth()
41+
42+
# A rate limiter for creating new media IDs.
43+
self._create_media_rate_limiter = Ratelimiter(
44+
store=hs.get_datastores().main,
45+
clock=self.clock,
46+
cfg=hs.config.ratelimiting.rc_media_create,
47+
)
48+
49+
async def _async_render_OPTIONS(self, request: SynapseRequest) -> None:
50+
respond_with_json(request, 200, {}, send_cors=True)
51+
52+
async def _async_render_POST(self, request: SynapseRequest) -> None:
53+
requester = await self.auth.get_user_by_req(request)
54+
55+
# If the create media requests for the user are over the limit, drop them.
56+
await self._create_media_rate_limiter.ratelimit(requester)
57+
58+
(
59+
reached_pending_limit,
60+
first_expiration_ts,
61+
) = await self.media_repo.reached_pending_media_limit(
62+
requester.user, self.max_pending_media_uploads
63+
)
64+
if reached_pending_limit:
65+
raise LimitExceededError(
66+
limiter_name="rc_media_create",
67+
retry_after_ms=first_expiration_ts - self.clock.time_msec(),
68+
)
69+
70+
content_uri, unused_expires_at = await self.media_repo.create_media_id(
71+
requester.user
72+
)
73+
74+
logger.info(
75+
"Created Media URI %r that if unused will expire at %d",
76+
content_uri,
77+
unused_expires_at,
78+
)
79+
respond_with_json(
80+
request,
81+
200,
82+
{
83+
"content_uri": content_uri,
84+
"unused_expires_at": unused_expires_at,
85+
},
86+
send_cors=True,
87+
)

synapse/rest/media/media_repository_resource.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from synapse.http.server import HttpServer, JsonResource
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
@@ -91,7 +92,7 @@ def register_servlets(http_server: HttpServer, hs: "HomeServer") -> None:
9192

9293
# Note that many of these should not exist as v1 endpoints, but empirically
9394
# a lot of traffic still goes to them.
94-
95+
CreateResource(hs, media_repo).register(http_server)
9596
UploadResource(hs, media_repo).register(http_server)
9697
DownloadResource(hs, media_repo).register(http_server)
9798
ThumbnailResource(hs, media_repo, media_repo.media_storage).register(

synapse/storage/databases/main/media_repository.py

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

333+
@trace
334+
async def store_local_media_id(
335+
self,
336+
media_id: str,
337+
time_now_ms: int,
338+
user_id: UserID,
339+
unused_expires_at: int,
340+
) -> None:
341+
await self.db_pool.simple_insert(
342+
"local_media_repository",
343+
{
344+
"media_id": media_id,
345+
"created_ts": time_now_ms,
346+
"user_id": user_id.to_string(),
347+
"unused_expires_at": unused_expires_at,
348+
},
349+
desc="store_local_media_id",
350+
)
351+
333352
@trace
334353
async def store_local_media(
335354
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)