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

Commit 447f353

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 b8632ff commit 447f353

File tree

6 files changed

+158
-19
lines changed

6 files changed

+158
-19
lines changed

flake.nix

+3-18
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
libxml2
105105
libxslt
106106
sqlite
107+
gcc
107108

108109
# Native dependencies for unit tests (SyTest also requires OpenSSL).
109110
openssl
@@ -152,7 +153,7 @@
152153

153154
# Postgres is needed to run Synapse with postgres support and
154155
# to run certain unit tests that require postgres.
155-
services.postgres.enable = true;
156+
services.postgres.enable = false;
156157

157158
# On the first invocation of `devenv up`, create a database for
158159
# Synapse to store data in.
@@ -168,7 +169,7 @@
168169
'';
169170

170171
# Redis is needed in order to run Synapse in worker mode.
171-
services.redis.enable = true;
172+
services.redis.enable = false;
172173

173174
# Configure and start Synapse. Before starting Synapse, this shell code:
174175
# * generates a default homeserver.yaml config file if one does not exist, and
@@ -178,22 +179,6 @@
178179
process.before = ''
179180
python -m synapse.app.homeserver -c homeserver.yaml --generate-config --server-name=synapse.dev --report-stats=no
180181
mkdir -p homeserver-config-overrides.d
181-
cat > homeserver-config-overrides.d/database.yaml << EOF
182-
## Do not edit this file. This file is generated by flake.nix
183-
database:
184-
name: psycopg2
185-
args:
186-
user: synapse_user
187-
database: synapse
188-
host: $PGHOST
189-
cp_min: 5
190-
cp_max: 10
191-
EOF
192-
cat > homeserver-config-overrides.d/redis.yaml << EOF
193-
## Do not edit this file. This file is generated by flake.nix
194-
redis:
195-
enabled: true
196-
EOF
197182
'';
198183
# Start synapse when `devenv up` is run.
199184
processes.synapse.exec = "poetry run python -m synapse.app.homeserver -c homeserver.yaml -c homeserver-config-overrides.d";

synapse/media/media_repository.py

+24
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,29 @@ 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+
unused_expires_at = now + self.unused_expiration_time
202+
await self.store.store_local_media_id(
203+
media_id=media_id,
204+
time_now_ms=now,
205+
user_id=auth_user,
206+
unused_expires_at=unused_expires_at,
207+
)
208+
return f"mxc://{self.server_name}/{media_id}", unused_expires_at
209+
186210
@trace
187211
async def create_content(
188212
self,

synapse/rest/media/create_resource.py

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

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)