Skip to content

Commit ee47db4

Browse files
authored
MSC3911: Deduplication - save file with sha256 path (#155)
# Linked Media MSC3911 AP?: Deduplication- file path with sha256 [#3465](famedly/product-management#3465) Fixes: #3465 Currently we are storing the file based on `media_id`. With msc3911 introduced, there are many copy actions and that creates duplicated media. So now we try to save file with sha256 path and avoid duplication. There is a feature flag introduce with `use_sha256_paths`. But even with feature flag is on, we still want to the existing media_id path logic function well, and backward compatible. Feature flag also prevents the existing tests failure. Previously files were saved like this: ``` media/local_content/Ab/cD/... media/local_thumbnails/Ab/cD/.../32-32-image-png-crop media/remote_content/{server_name}/Ab/cD/... ``` But with `use_sha256_paths` enabled, media storage structure would be like this: ``` media/eb/f4/... thumbnails/eb/f4/.../32-32-image-png-crop ```
2 parents b792a25 + cc52826 commit ee47db4

29 files changed

Lines changed: 1806 additions & 275 deletions

rust/src/http_client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ fn get_runtime<'a>(reactor: &Bound<'a, PyAny>) -> PyResult<PyRef<'a, PyTokioRunt
137137
static DEFER: OnceCell<PyObject> = OnceCell::new();
138138

139139
/// Access to the `twisted.internet.defer` module.
140-
fn defer(py: Python<'_>) -> PyResult<&Bound<PyAny>> {
140+
fn defer(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> {
141141
Ok(DEFER
142142
.get_or_try_init(|| py.import("twisted.internet.defer").map(Into::into))?
143143
.bind(py))

synapse/config/repository.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
194194
config.get("media_store_path", "media_store")
195195
)
196196

197+
self.enable_local_media_storage_deduplication = config.get(
198+
"enable_local_media_storage_deduplication", False
199+
)
200+
197201
backup_media_store_path = config.get("backup_media_store_path")
198202

199203
synchronous_backup_media_store = config.get(

synapse/federation/transport/server/federation.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ 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
8382

8483

8584
class FederationSendServlet(BaseFederationServerServlet):

synapse/handlers/profile.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,7 @@ 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.block_unrestricted_media = (
85-
hs.config.experimental.msc3911.block_unrestricted_media_upload
86-
)
83+
self.msc3911_config = hs.config.experimental.msc3911
8784

8885
async def get_profile(self, user_id: str, ignore_backoff: bool = True) -> JsonDict:
8986
"""
@@ -334,7 +331,7 @@ async def validate_avatar_url(self, avatar_url: str, requester: Requester) -> No
334331
# event, it will either be copied/passed along/dropped depending on the
335332
# above circumstances
336333
if not media_info:
337-
if self.block_unrestricted_media:
334+
if self.msc3911_config.block_unrestricted_media_upload:
338335
# The user should have done a COPY on this media previous to this
339336
# attempt to set
340337
raise SynapseError(
@@ -345,7 +342,10 @@ async def validate_avatar_url(self, avatar_url: str, requester: Requester) -> No
345342
# For backwards compatible behavior, treat the media as unrestricted
346343
return
347344

348-
if self.block_unrestricted_media and not media_info.restricted:
345+
if (
346+
self.msc3911_config.block_unrestricted_media_upload
347+
and not media_info.restricted
348+
):
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",
@@ -424,7 +424,7 @@ async def set_avatar_url(
424424
)
425425

426426
# msc3911: Update the media restrictions to include the profile user ID
427-
if self.enable_restricted_media and avatar_url_to_set:
427+
if self.msc3911_config.enabled and avatar_url_to_set:
428428
await self.validate_avatar_url(avatar_url_to_set, requester)
429429
await self.hs.get_datastores().main.set_media_restricted_to_user_profile(
430430
self.hs.config.server.server_name,

synapse/handlers/room_member.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,7 @@ 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
113-
self.allow_legacy_media = (
114-
not hs.config.experimental.msc3911.block_unrestricted_media_upload
115-
)
112+
self.msc3911_config = hs.config.experimental.msc3911
116113

117114
self._membership_types_to_include_profile_data_in = {
118115
Membership.JOIN,
@@ -865,7 +862,7 @@ async def update_membership_locked(
865862
except Exception as e:
866863
logger.info("Failed to get profile information for %r: %s", target, e)
867864

868-
if self.enable_restricted_media and not media_info_for_attachment:
865+
if self.msc3911_config.enabled and not media_info_for_attachment:
869866
# This code path should only be taken for memberships updating an
870867
# avatar url
871868
avatar_url = content.get(EventContentFields.MEMBERSHIP_AVATAR_URL)
@@ -898,7 +895,7 @@ async def update_membership_locked(
898895
# hope that it is not restricted when it finally shows up.
899896
# Guard against other potentials escaping by raising if it
900897
# should occur when unrestricted media begins to be disallowed.
901-
if self.allow_legacy_media:
898+
if not self.msc3911_config.block_unrestricted_media_upload:
902899
logger.debug(
903900
"Ignoring media copy request; the media is unknown and "
904901
"will not be treated as restricted"

synapse/media/_base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,8 @@ class FileInfo:
540540
url_cache: bool = False
541541
# Whether the file is a thumbnail or not.
542542
thumbnail: Optional[ThumbnailInfo] = None
543+
# The sha256 of the media. When enable_local_media_storage_deduplication is True, this will be generating the filepath.
544+
sha256: Optional[str] = None
543545

544546
# The below properties exist to maintain compatibility with third-party modules.
545547
@property

synapse/media/filepath.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ def local_media_filepath_rel(self, media_id: str) -> str:
184184

185185
local_media_filepath = _wrap_in_base_path(local_media_filepath_rel)
186186

187+
@_wrap_with_jail_check(relative=True)
188+
def filepath_sha_rel(self, sha256: str) -> str:
189+
return os.path.join(
190+
_validate_path_component(sha256[0:2]),
191+
_validate_path_component(sha256[2:4]),
192+
_validate_path_component(sha256[4:]),
193+
)
194+
195+
filepath_sha = _wrap_in_base_path(filepath_sha_rel)
196+
187197
@_wrap_with_jail_check(relative=True)
188198
def local_media_thumbnail_rel(
189199
self, media_id: str, width: int, height: int, content_type: str, method: str
@@ -200,6 +210,22 @@ def local_media_thumbnail_rel(
200210

201211
local_media_thumbnail = _wrap_in_base_path(local_media_thumbnail_rel)
202212

213+
@_wrap_with_jail_check(relative=True)
214+
def thumbnail_sha_rel(
215+
self, sha256: str, width: int, height: int, content_type: str, method: str
216+
) -> str:
217+
top_level_type, sub_type = content_type.split("/")
218+
file_name = "%i-%i-%s-%s-%s" % (width, height, top_level_type, sub_type, method)
219+
return os.path.join(
220+
"thumbnails",
221+
_validate_path_component(sha256[0:2]),
222+
_validate_path_component(sha256[2:4]),
223+
_validate_path_component(sha256[4:]),
224+
_validate_path_component(file_name),
225+
)
226+
227+
thumbnail_sha = _wrap_in_base_path(thumbnail_sha_rel)
228+
203229
@_wrap_with_jail_check(relative=False)
204230
def local_media_thumbnail_dir(self, media_id: str) -> str:
205231
"""
@@ -218,6 +244,24 @@ def local_media_thumbnail_dir(self, media_id: str) -> str:
218244
_validate_path_component(media_id[4:]),
219245
)
220246

247+
@_wrap_with_jail_check(relative=False)
248+
def thumbnail_sha_dir(self, sha256: str) -> str:
249+
"""
250+
Retrieve the local store path of thumbnails of a given media_id
251+
252+
Args:
253+
sha256: The sha256 to query.
254+
Returns:
255+
Path of local_thumbnails from sha256
256+
"""
257+
return os.path.join(
258+
self.base_path,
259+
"thumbnails",
260+
_validate_path_component(sha256[0:2]),
261+
_validate_path_component(sha256[2:4]),
262+
_validate_path_component(sha256[4:]),
263+
)
264+
221265
@_wrap_with_jail_check(relative=True)
222266
def remote_media_filepath_rel(self, server_name: str, file_id: str) -> str:
223267
return os.path.join(

0 commit comments

Comments
 (0)