From e1cc803b0b2295f460abc7d38e626220b135a07d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 12 Nov 2026 12:29:08 +0000 Subject: [PATCH] cache base media URL --- src/backend/InvenTree/InvenTree/helpers.py | 65 +++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/helpers.py b/src/backend/InvenTree/InvenTree/helpers.py index d67d37f23b..478ceb06e3 100644 --- a/src/backend/InvenTree/InvenTree/helpers.py +++ b/src/backend/InvenTree/InvenTree/helpers.py @@ -182,7 +182,70 @@ def getMediaUrl( if name is not None: file = regenerate_imagefile(file, name) - return default_storage.url(file.name) + return get_media_url(file.name) + + +# Request cache key for the default storage base URL (see get_media_url) +MEDIA_BASE_URL_CACHE_KEY = 'media_storage_base_url' + +# Matches '.' or '..' path segments, which urljoin() would resolve +_DOT_SEGMENT_REGEX = re.compile(r'(^|/)\.\.?(/|$)') + + +def _get_filesystem_base_url() -> str: + """Return the base URL of the default storage, if it is a plain FileSystemStorage. + + Returns an empty string for any other storage backend + (e.g. S3, or a subclass which may override url()), + or if the base URL is not in the expected format. + + The result is cached for the duration of the current request. + """ + from django.core.files.storage import FileSystemStorage + + from InvenTree.cache import get_session_cache, set_session_cache + + base_url = get_session_cache(MEDIA_BASE_URL_CACHE_KEY) + + if base_url is None: + base_url = '' + + # Note: default_storage is a LazyObject - __class__ is proxied to the wrapped storage + if default_storage.__class__ is FileSystemStorage: + storage_base_url = default_storage.base_url + + if isinstance(storage_base_url, str) and storage_base_url.endswith('/'): + base_url = storage_base_url + + set_session_cache(MEDIA_BASE_URL_CACHE_KEY, base_url) + + return base_url + + +def get_media_url(name: str) -> str: + """Return the URL for the named file in the default (media) storage. + + Equivalent to default_storage.url(name), but for the default FileSystemStorage + backend the URL is constructed directly, which avoids the overhead of urljoin(). + This is significant when rendering URLs for large API list queries. + + Arguments: + name: The name (path) of the file within the storage + """ + if base_url := _get_filesystem_base_url(): + from django.utils.encoding import filepath_to_uri + + url = filepath_to_uri(name) + + if url is not None: + url = url.lstrip('/') + + # urljoin() resolves '.' and '..' segments, and collapses empty segments ('//') + # Defer to the storage backend for these (rare) cases + if '//' not in url and not _DOT_SEGMENT_REGEX.search(url): + return base_url + url + + return default_storage.url(name) def regenerate_imagefile(_file, _name: str):