mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 22:13:26 +00:00
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.
134 lines
5.6 KiB
Python
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))
|