perf(web): use content hash for static asset cache busting
Switch from timestamp-based to MD5 content hash versioning. Cache now only invalidates when file content actually changes.
This commit is contained in:
@@ -13,8 +13,8 @@ Series Identifier Convention:
|
||||
All template helpers that handle series data use `key` for identification and
|
||||
provide `folder` as display metadata only.
|
||||
"""
|
||||
import hashlib
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -27,10 +27,44 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Configure templates directory
|
||||
TEMPLATES_DIR = Path(__file__).parent.parent / "web" / "templates"
|
||||
STATIC_DIR = Path(__file__).parent.parent / "web" / "static"
|
||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
|
||||
# Version token for static asset cache-busting; changes on every server start.
|
||||
STATIC_VERSION: str = str(int(time.time()))
|
||||
# Cache for static file hashes: {file_path: (mtime, hash)}
|
||||
_hash_cache: Dict[str, tuple[float, str]] = {}
|
||||
|
||||
|
||||
def get_static_version(file_path: str) -> str:
|
||||
"""
|
||||
Get cache-busting version for a static file based on content hash.
|
||||
|
||||
Hash is computed once and cached; cache is invalidated when file mtime changes.
|
||||
|
||||
Args:
|
||||
file_path: Relative path to static file (e.g., 'css/styles.css')
|
||||
|
||||
Returns:
|
||||
8-character hex hash of file content, or empty string if file not found
|
||||
"""
|
||||
full_path = STATIC_DIR / file_path
|
||||
|
||||
if not full_path.exists():
|
||||
logger.warning(f"Static file not found: {file_path}")
|
||||
return ""
|
||||
|
||||
current_mtime = full_path.stat().st_mtime
|
||||
|
||||
# Check cache validity
|
||||
if file_path in _hash_cache:
|
||||
cached_mtime, cached_hash = _hash_cache[file_path]
|
||||
if cached_mtime == current_mtime:
|
||||
return cached_hash
|
||||
|
||||
# Compute new hash
|
||||
file_hash = hashlib.md5(full_path.read_bytes()).hexdigest()[:8]
|
||||
_hash_cache[file_path] = (current_mtime, file_hash)
|
||||
|
||||
return file_hash
|
||||
|
||||
|
||||
def get_base_context(
|
||||
@@ -51,7 +85,7 @@ def get_base_context(
|
||||
"title": title,
|
||||
"app_name": "Aniworld Download Manager",
|
||||
"version": APP_VERSION,
|
||||
"static_v": STATIC_VERSION,
|
||||
"static_version": get_static_version,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AniWorld Manager</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_v }}">
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_version('css/styles.css') }}">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<!-- UX Enhancement and Mobile & Accessibility CSS -->
|
||||
<link rel="stylesheet" href="/static/css/ux_features.css?v={{ static_v }}">
|
||||
<link rel="stylesheet" href="/static/css/ux_features.css?v={{ static_version('css/ux_features.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -727,22 +727,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Shared Modules (load in dependency order) -->
|
||||
<script src="/static/js/shared/constants.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/auth.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/api-client.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/theme.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/ui-utils.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/websocket-client.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/constants.js?v={{ static_version('js/shared/constants.js') }}"></script>
|
||||
<script src="/static/js/shared/auth.js?v={{ static_version('js/shared/auth.js') }}"></script>
|
||||
<script src="/static/js/shared/api-client.js?v={{ static_version('js/shared/api-client.js') }}"></script>
|
||||
<script src="/static/js/shared/theme.js?v={{ static_version('js/shared/theme.js') }}"></script>
|
||||
<script src="/static/js/shared/ui-utils.js?v={{ static_version('js/shared/ui-utils.js') }}"></script>
|
||||
<script src="/static/js/shared/websocket-client.js?v={{ static_version('js/shared/websocket-client.js') }}"></script>
|
||||
|
||||
<!-- External modules -->
|
||||
<script src="/static/js/localization.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/user_preferences.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/localization.js?v={{ static_version('js/localization.js') }}"></script>
|
||||
<script src="/static/js/user_preferences.js?v={{ static_version('js/user_preferences.js') }}"></script>
|
||||
|
||||
<!-- Index Page Modules -->
|
||||
<script src="/static/js/index/context-menu.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/index/edit-modal.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/index/series-manager.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/index/selection-manager.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/index/context-menu.js?v={{ static_version('js/index/context-menu.js') }}"></script>
|
||||
<script src="/static/js/index/edit-modal.js?v={{ static_version('js/index/edit-modal.js') }}"></script>
|
||||
<script src="/static/js/index/series-manager.js?v={{ static_version('js/index/series-manager.js') }}"></script>
|
||||
<script src="/static/js/index/selection-manager.js?v={{ static_version('js/index/selection-manager.js') }}"></script>
|
||||
<script src="/static/js/index/search.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/index/scan-manager.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/index/nfo-manager.js?v={{ static_v }}"></script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AniWorld Manager - Initializing</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_v }}">
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_version('css/styles.css') }}">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.loading-container {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AniWorld Manager - Login</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_v }}">
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_version('css/styles.css') }}">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.login-container {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Download Queue - AniWorld Manager</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_v }}">
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_version('css/styles.css') }}">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
@@ -234,19 +234,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Shared Modules (load in dependency order) -->
|
||||
<script src="/static/js/shared/constants.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/auth.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/api-client.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/theme.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/ui-utils.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/websocket-client.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/shared/constants.js?v={{ static_version('js/shared/constants.js') }}"></script>
|
||||
<script src="/static/js/shared/auth.js?v={{ static_version('js/shared/auth.js') }}"></script>
|
||||
<script src="/static/js/shared/api-client.js?v={{ static_version('js/shared/api-client.js') }}"></script>
|
||||
<script src="/static/js/shared/theme.js?v={{ static_version('js/shared/theme.js') }}"></script>
|
||||
<script src="/static/js/shared/ui-utils.js?v={{ static_version('js/shared/ui-utils.js') }}"></script>
|
||||
<script src="/static/js/shared/websocket-client.js?v={{ static_version('js/shared/websocket-client.js') }}"></script>
|
||||
|
||||
<!-- Queue Page Modules -->
|
||||
<script src="/static/js/queue/queue-api.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/queue/queue-renderer.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/queue/progress-handler.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/queue/queue-socket-handler.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/queue/queue-init.js?v={{ static_v }}"></script>
|
||||
<script src="/static/js/queue/queue-api.js?v={{ static_version('js/queue/queue-api.js') }}"></script>
|
||||
<script src="/static/js/queue/queue-renderer.js?v={{ static_version('js/queue/queue-renderer.js') }}"></script>
|
||||
<script src="/static/js/queue/progress-handler.js?v={{ static_version('js/queue/progress-handler.js') }}"></script>
|
||||
<script src="/static/js/queue/queue-socket-handler.js?v={{ static_version('js/queue/queue-socket-handler.js') }}"></script>
|
||||
<script src="/static/js/queue/queue-init.js?v={{ static_version('js/queue/queue-init.js') }}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AniWorld Manager - Setup</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_v }}">
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_version('css/styles.css') }}">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.setup-container {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AniWorld Manager - Resolve Series</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_v }}">
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v={{ static_version('css/styles.css') }}">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.unresolved-container {
|
||||
|
||||
Reference in New Issue
Block a user