storage: Rework static avatar files hashing logic.

Previously, the hashing logic for static avatar files hashed the default
and medium files separately, which didn’t match how user-uploaded
avatars work—where you just add the "-medium.png" suffix to get the
medium version. Since we don’t have clear documentation for avatars yet,
this caused some issues for the mobile apps.

This commit makes sure the default and its medium variation share the
same hash.
This commit is contained in:
PieterCK 2024-10-24 14:23:34 +07:00 committed by Tim Abbott
parent 2109d3d1ab
commit c3017e55d2
1 changed files with 56 additions and 23 deletions

View File

@ -24,26 +24,62 @@ else:
return os.path.join(settings.STATIC_ROOT, path) return os.path.join(settings.STATIC_ROOT, path)
def reformat_medium_filename(hashed_name: str) -> str:
"""
Because the protocol for getting medium-size avatar URLs
was never fully documented, the mobile apps use a
substitution of the form s/.png/-medium.png/ to get the
medium-size avatar URLs. Thus, we must ensure the hashed
filenames for system bot avatars follow this naming convention.
"""
name_parts = hashed_name.rsplit(".", 1)
base_name = name_parts[0]
if len(name_parts) != 2 or "-medium" not in base_name:
return hashed_name
extension = name_parts[1].replace("png", "medium.png")
base_name = base_name.replace("-medium", "")
return f"{base_name}-{extension}"
class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage): class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage):
hashed_static_avatar_file_map: dict[str, str] = {}
def process_static_avatars_name(
self,
name: str,
content: Optional["File[bytes]"] = None,
filename: str | None = None,
) -> str:
"""
Because the protocol for getting medium-size avatar URLs
was never fully documented, the mobile apps use a
substitution of the form s/.png/-medium.png/ to get the
medium-size avatar URLs.
This function hashes system bots' avatar files in a way
that follows the pattern used for user-uploaded avatars.
It ensures the following:
* Hashed filenames for system bot avatars follow this
naming convention:
- avatar.png -> avatar-medium.png
* The system bots' default avatar file and its medium
version share the same hash:
- bot.36f721bad3d0.png -> bot.36f721bad3d0-medium.png
"""
def reformat_medium_filename(hashed_name: str) -> str:
name_parts = hashed_name.rsplit(".", 1)
base_name = name_parts[0]
if len(name_parts) != 2 or "-medium" not in base_name:
return hashed_name
extension = name_parts[1].replace("png", "medium.png")
base_name = base_name.replace("-medium", "")
return f"{base_name}-{extension}"
if name.endswith("-medium.png"):
# This logic relies on the fact that the medium files will
# be hashed first due to the "-medium" string, which places
# them earlier in the naming order. So, adhering to the
# medium file naming convention is crucial for this logic.
hashed_medium_file: str = reformat_medium_filename(
super().hashed_name(name, content, filename)
)
default_file = name.replace("-medium.png", ".png")
hashed_default_file = hashed_medium_file.replace("-medium.png", ".png")
self.hashed_static_avatar_file_map[default_file] = hashed_default_file
return hashed_medium_file
assert name in self.hashed_static_avatar_file_map
return self.hashed_static_avatar_file_map[name]
@override @override
def hashed_name( def hashed_name(
self, name: str, content: Optional["File[bytes]"] = None, filename: str | None = None self, name: str, content: Optional["File[bytes]"] = None, filename: str | None = None
@ -64,11 +100,8 @@ class IgnoreBundlesManifestStaticFilesStorage(ManifestStaticFilesStorage):
# so they can hit our Nginx caching block for static files. # so they can hit our Nginx caching block for static files.
# We don't need to worry about stale caches since these are # We don't need to worry about stale caches since these are
# only used by the system bots. # only used by the system bots.
if not name.endswith("-medium.png"): return self.process_static_avatars_name(name, content, filename)
return super().hashed_name(name, content, filename)
hashed_medium_file = super().hashed_name(name, content, filename)
return reformat_medium_filename(hashed_medium_file)
if name == "generated/emoji/emoji_api.json": if name == "generated/emoji/emoji_api.json":
# Unlike most .json files, we do want to hash this file; # Unlike most .json files, we do want to hash this file;
# its hashed URL is returned as part of the API. See # its hashed URL is returned as part of the API. See