Files
zulip/zerver/lib/storage.py
PieterCK c3017e55d2 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.
2024-10-25 09:36:52 -07:00

134 lines
5.6 KiB
Python

# Useful reading is:
# https://zulip.readthedocs.io/en/latest/subsystems/html-css.html#front-end-build-process
import os
from typing import Optional
from django.conf import settings
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
from django.core.files.base import File
from django.core.files.storage import FileSystemStorage
from typing_extensions import override
from zerver.lib.avatar import STATIC_AVATARS_DIR
if settings.DEBUG:
from django.contrib.staticfiles.finders import find
def static_path(path: str) -> str:
return find(path) or "/nonexistent"
else:
def static_path(path: str) -> str:
return os.path.join(settings.STATIC_ROOT, path)
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
def hashed_name(
self, name: str, content: Optional["File[bytes]"] = None, filename: str | None = None
) -> str:
ext = os.path.splitext(name)[1]
if name.startswith("webpack-bundles"):
# Hack to avoid renaming already-hashnamed webpack bundles
# when minifying; this was causing every bundle to have
# two hashes appended to its name, one by webpack and one
# here. We can't just skip processing of these bundles,
# since we do need the Django storage to add these to the
# manifest for django_webpack_loader to work. So, we just
# use a no-op hash function for these already-hashed
# assets.
return name
if name.startswith(STATIC_AVATARS_DIR):
# For these avatar files, we want to make sure they are
# so they can hit our Nginx caching block for static files.
# We don't need to worry about stale caches since these are
# only used by the system bots.
return self.process_static_avatars_name(name, content, filename)
if name == "generated/emoji/emoji_api.json":
# Unlike most .json files, we do want to hash this file;
# its hashed URL is returned as part of the API. See
# data_url() in zerver/lib/emoji.py.
return super().hashed_name(name, content, filename)
if ext in [".png", ".gif", ".jpg", ".svg"]:
# Similarly, don't hash-rename image files; we only serve
# the original file paths (not the hashed file paths), and
# so the only effect of hash-renaming these is to increase
# the size of release tarballs with duplicate copies of these.
#
# One could imagine a future world in which we instead
# used the hashed paths for these; in that case, though,
# we should instead be removing the non-hashed paths.
return name
if ext in [".json", ".po", ".mo", ".mp3", ".ogg", ".html", ".md"]:
# And same story for translation files, sound files, etc.
return name
return super().hashed_name(name, content, filename)
class ZulipStorage(IgnoreBundlesManifestStaticFilesStorage):
# This is a hack to use staticfiles.json from within the
# deployment, rather than a directory under STATIC_ROOT. By doing
# so, we can use a different copy of staticfiles.json for each
# deployment, which ensures that we always use the correct static
# assets for each deployment.
def __init__(self) -> None:
super().__init__(manifest_storage=FileSystemStorage(location=settings.DEPLOY_ROOT))