Skip to content

Commit b792a25

Browse files
authored
MSC3911: ap? automatic pending media deletion (#130)
# Linked Media MSC3911 AP?: Automatic media deletion [#3365](famedly/product-management#3365) Background job to automatically delete unattached media. This should likely use the same 24h as asynchronous media uploads. # Acceptance criteria - [x] Background job to delete pending media after 24h
2 parents cf795a5 + 595e5ed commit b792a25

23 files changed

Lines changed: 463 additions & 167 deletions

docker/complement/conf/workers-shared-extra.yaml.j2

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ experimental_features:
130130
# Invite filtering
131131
msc4155_enabled: true
132132
# Media Attachment
133-
msc3911_enabled: true
133+
msc3911:
134+
enabled: true
135+
block_unrestricted_media_upload: false
136+
purge_pending_unattached_media: true
137+
pending_media_cleanup_interval_ms: 86400000 # 1 day
134138

135139
server_notices:
136140
system_mxid_localpart: _server

synapse/config/experimental.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,22 @@ class MSC3866Config:
365365
require_approval_for_new_accounts: bool = False
366366

367367

368+
@attr.s(auto_attribs=True, frozen=True, slots=True)
369+
class MSC3911Config:
370+
"""Configuration for MSC3911 (Linking Media to Events)"""
371+
372+
# MSC3911 is enabled
373+
enabled: bool = False
374+
375+
# Disable the current media create and upload endpoints
376+
block_unrestricted_media_upload: bool = False
377+
378+
# Delete pending media that is older than certain interval and not attached to any events.
379+
purge_pending_unattached_media: bool = False
380+
# This configures how often the cleanup loop run in milliseconds
381+
pending_media_cleanup_interval_ms: int = 24 * 60 * 60 * 1000 # 1 day
382+
383+
368384
class ExperimentalConfig(Config):
369385
"""Config section for enabling experimental features"""
370386

@@ -590,9 +606,5 @@ def read_config(
590606
self.msc4306_enabled: bool = experimental.get("msc4306_enabled", False)
591607

592608
# MSC3911: Linking Media to Events
593-
self.msc3911_enabled: bool = experimental.get("msc3911_enabled", False)
594-
595-
# Disable the current media create and upload endpoints
596-
self.msc3911_unrestricted_media_upload_disabled: bool = experimental.get(
597-
"msc3911_unrestricted_media_upload_disabled", False
598-
)
609+
raw_msc3911_config = experimental.get("msc3911", {})
610+
self.msc3911 = MSC3911Config(**raw_msc3911_config)

synapse/federation/transport/server/federation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def __init__(
7979
):
8080
super().__init__(hs, authenticator, ratelimiter, server_name)
8181
self.handler = hs.get_federation_server()
82-
self.enable_restricted_media = hs.config.experimental.msc3911_enabled
82+
self.enable_restricted_media = hs.config.experimental.msc3911.enabled
8383

8484

8585
class FederationSendServlet(BaseFederationServerServlet):

synapse/handlers/profile.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ def __init__(self, hs: "HomeServer"):
8080

8181
self._third_party_rules = hs.get_module_api_callbacks().third_party_event_rules
8282

83-
self.enable_restricted_media = hs.config.experimental.msc3911_enabled
84-
self.disable_unrestricted_media = (
85-
hs.config.experimental.msc3911_unrestricted_media_upload_disabled
83+
self.enable_restricted_media = hs.config.experimental.msc3911.enabled
84+
self.block_unrestricted_media = (
85+
hs.config.experimental.msc3911.block_unrestricted_media_upload
8686
)
8787

8888
async def get_profile(self, user_id: str, ignore_backoff: bool = True) -> JsonDict:
@@ -334,7 +334,7 @@ async def validate_avatar_url(self, avatar_url: str, requester: Requester) -> No
334334
# event, it will either be copied/passed along/dropped depending on the
335335
# above circumstances
336336
if not media_info:
337-
if self.disable_unrestricted_media:
337+
if self.block_unrestricted_media:
338338
# The user should have done a COPY on this media previous to this
339339
# attempt to set
340340
raise SynapseError(
@@ -345,7 +345,7 @@ async def validate_avatar_url(self, avatar_url: str, requester: Requester) -> No
345345
# For backwards compatible behavior, treat the media as unrestricted
346346
return
347347

348-
if self.disable_unrestricted_media and not media_info.restricted:
348+
if self.block_unrestricted_media and not media_info.restricted:
349349
raise SynapseError(
350350
HTTPStatus.BAD_REQUEST,
351351
f"The media attachment request is invalid as the media '{mxc_uri.media_id}' is not restricted",

synapse/handlers/room.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,7 @@ async def create_room(
12321232
room_avatar = initial_state.get((EventTypes.RoomAvatar, ""))
12331233
# It is unfortunate that this conditional block has to be run twice: once here
12341234
# to validate the data and again to actually attach the media reference.
1235-
if room_avatar is not None and self.config.experimental.msc3911_enabled:
1235+
if room_avatar is not None and self.config.experimental.msc3911.enabled:
12361236
# this should be an mxc, but the spec does not specifically say it has to be
12371237
extracted_media_id: Optional[str] = room_avatar.get("url")
12381238
# It may be that "url" is set to either an empty string or None. Accept
@@ -1550,7 +1550,7 @@ async def create_event(
15501550

15511551
mxc_restrictions = None
15521552
if (
1553-
self.config.experimental.msc3911_enabled
1553+
self.config.experimental.msc3911.enabled
15541554
and etype == EventTypes.RoomAvatar
15551555
):
15561556
# this should be an mxc, but the spec does not specifically say it has to be

synapse/handlers/room_member.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ def __init__(self, hs: "HomeServer"):
109109
self.account_data_handler = hs.get_account_data_handler()
110110
self.event_auth_handler = hs.get_event_auth_handler()
111111
self._worker_lock_handler = hs.get_worker_locks_handler()
112-
self.enable_restricted_media = hs.config.experimental.msc3911_enabled
112+
self.enable_restricted_media = hs.config.experimental.msc3911.enabled
113113
self.allow_legacy_media = (
114-
not hs.config.experimental.msc3911_unrestricted_media_upload_disabled
114+
not hs.config.experimental.msc3911.block_unrestricted_media_upload
115115
)
116116

117117
self._membership_types_to_include_profile_data_in = {

synapse/media/media_repository.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def __init__(self, hs: "HomeServer"):
112112
self.server_name = hs.hostname
113113
self.store = hs.get_datastores().main
114114
self._is_mine_server_name = hs.is_mine_server_name
115-
self.enable_media_restriction = self.hs.config.experimental.msc3911_enabled
115+
self.enable_media_restriction = self.hs.config.experimental.msc3911.enabled
116116

117117
@trace
118118
async def create_media_id_without_expiration(
@@ -643,6 +643,16 @@ def __init__(self, hs: "HomeServer"):
643643
key=lambda limit: limit.time_period_ms, reverse=True
644644
)
645645

646+
if (
647+
self.hs.config.experimental.msc3911.purge_pending_unattached_media
648+
and hs.config.media.media_instance_running_background_jobs
649+
== hs.config.worker.worker_name
650+
):
651+
self.clock.looping_call(
652+
self._pending_media_cleanup,
653+
self.hs.config.experimental.msc3911.pending_media_cleanup_interval_ms,
654+
)
655+
646656
def _start_update_recently_accessed(self) -> Deferred:
647657
return run_as_background_process(
648658
"update_recently_accessed_media", self._update_recently_accessed
@@ -664,6 +674,13 @@ async def _update_recently_accessed(self) -> None:
664674
local_media, remote_media, self.clock.time_msec()
665675
)
666676

677+
async def _pending_media_cleanup(self) -> None:
678+
pending_media_ids = await self.store.get_pending_media_ids(
679+
self.hs.config.experimental.msc3911.pending_media_cleanup_interval_ms
680+
)
681+
if pending_media_ids:
682+
await self.delete_local_media_ids(pending_media_ids)
683+
667684
def mark_recently_accessed(self, server_name: Optional[str], media_id: str) -> None:
668685
"""Mark the given media as recently accessed.
669686

synapse/media/thumbnailer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def __init__(
271271
self.media_storage = media_storage
272272
self.store = hs.get_datastores().main
273273
self.dynamic_thumbnails = hs.config.media.dynamic_thumbnails
274-
self.enable_media_restriction = self.hs.config.experimental.msc3911_enabled
274+
self.enable_media_restriction = self.hs.config.experimental.msc3911.enabled
275275

276276
async def respond_local_thumbnail(
277277
self,

synapse/rest/client/media.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
410410
MediaConfigResource(hs).register(http_server)
411411
ThumbnailResource(hs, media_repo, media_repo.media_storage).register(http_server)
412412
DownloadResource(hs, media_repo).register(http_server)
413-
if hs.config.experimental.msc3911_enabled:
413+
if hs.config.experimental.msc3911.enabled:
414414
CreateResource(hs, media_repo, restricted=True).register(http_server)
415415
UploadRestrictedResource(hs, media_repo).register(http_server)
416416
CopyResource(hs, media_repo).register(http_server)

synapse/rest/client/profile.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def __init__(self, hs: "HomeServer"):
109109
self.hs = hs
110110
self.profile_handler = hs.get_profile_handler()
111111
self.auth = hs.get_auth()
112-
self.enable_restricted_media = hs.config.experimental.msc3911_enabled
112+
self.enable_restricted_media = hs.config.experimental.msc3911.enabled
113113

114114
async def on_GET(
115115
self, request: SynapseRequest, user_id: str, field_name: str
@@ -178,7 +178,7 @@ async def on_PUT(
178178

179179
content = parse_json_object_from_request(request)
180180
try:
181-
new_value = content[field_name]
181+
input_value = content[field_name]
182182
except KeyError:
183183
raise SynapseError(
184184
400, f"Missing key '{field_name}'", errcode=Codes.MISSING_PARAM
@@ -200,24 +200,24 @@ async def on_PUT(
200200
)
201201
if field_name == ProfileFields.DISPLAYNAME:
202202
await self.profile_handler.set_displayname(
203-
user, requester, new_value, is_admin, propagate=propagate
203+
user, requester, input_value, is_admin, propagate=propagate
204204
)
205205
elif field_name == ProfileFields.AVATAR_URL:
206-
if self.enable_restricted_media and new_value:
206+
if self.enable_restricted_media and input_value:
207207
current_avatar_url = (
208208
await self.profile_handler.store.get_profile_avatar_url(
209209
requester.user
210210
)
211211
)
212212
# If new_value is the same as existing one, keep the function idempotent
213-
if current_avatar_url and str(current_avatar_url) == new_value:
213+
if current_avatar_url and str(current_avatar_url) == input_value:
214214
return 200, {}
215215
await self.profile_handler.set_avatar_url(
216-
user, requester, new_value, is_admin, propagate=propagate
216+
user, requester, input_value, is_admin, propagate=propagate
217217
)
218218
else:
219219
await self.profile_handler.set_profile_field(
220-
user, requester, field_name, new_value, is_admin
220+
user, requester, field_name, input_value, is_admin
221221
)
222222

223223
return 200, {}

0 commit comments

Comments
 (0)