stativ hash
This commit is contained in:
@@ -190,34 +190,68 @@ def validate_offline_assets() -> None:
|
||||
)
|
||||
|
||||
|
||||
_STATIC_HASH_CACHE: dict[tuple[str, int], str] = {}
|
||||
_STATIC_HASH_VALUE = "dev"
|
||||
_STATIC_HASH_READY = False
|
||||
|
||||
def static_hash(static_root: Path | None = None) -> str:
|
||||
"""Return one short hash for all app static files.
|
||||
|
||||
Note: This value is used as the shared browser-cache version, so any static file
|
||||
change invalidates app.js imports, CSS and local frontend assets together.
|
||||
def _versioned_static_files(root: Path) -> list[Path]:
|
||||
"""Return static files that should invalidate frontend JS/CSS caches.
|
||||
|
||||
Note: Only JavaScript and CSS affect the executable frontend version. Images,
|
||||
favicons and user-provided tracker icons stay outside this lightweight hash.
|
||||
"""
|
||||
return [
|
||||
path
|
||||
for path in root.rglob("*")
|
||||
if path.is_file()
|
||||
and path.suffix.lower() in {".js", ".css"}
|
||||
and "tracker_favicons" not in path.parts
|
||||
]
|
||||
|
||||
|
||||
def compute_static_hash(static_root: Path | None = None) -> str:
|
||||
"""Compute one short startup hash for frontend JavaScript and CSS files.
|
||||
|
||||
Note: This function reads JS/CSS files and should be called during app
|
||||
startup, not from frequent request handlers.
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
root = static_root or (BASE_DIR / "pytorrent" / "static")
|
||||
files = [path for path in root.rglob("*") if path.is_file() and "tracker_favicons" not in path.parts]
|
||||
fingerprint = f"{root}:{sum(path.stat().st_mtime_ns for path in files)}:{sum(path.stat().st_size for path in files)}"
|
||||
cached = _STATIC_HASH_CACHE.get((fingerprint, len(files)))
|
||||
if cached:
|
||||
return cached
|
||||
digest = hashlib.sha256()
|
||||
for path in sorted(files):
|
||||
files = sorted(_versioned_static_files(root), key=lambda item: item.as_posix())
|
||||
for path in files:
|
||||
rel = path.relative_to(root).as_posix()
|
||||
stat = path.stat()
|
||||
digest.update(rel.encode("utf-8"))
|
||||
digest.update(str(stat.st_size).encode("ascii"))
|
||||
digest.update(str(stat.st_mtime_ns).encode("ascii"))
|
||||
try:
|
||||
digest.update(path.read_bytes())
|
||||
stat = path.stat()
|
||||
content = path.read_bytes()
|
||||
except OSError:
|
||||
continue
|
||||
digest.update(rel.encode("utf-8"))
|
||||
digest.update(str(stat.st_size).encode("ascii"))
|
||||
digest.update(content)
|
||||
value = digest.hexdigest()[:16]
|
||||
_STATIC_HASH_CACHE.clear()
|
||||
_STATIC_HASH_CACHE[(fingerprint, len(files))] = value
|
||||
return value
|
||||
return value or "dev"
|
||||
|
||||
|
||||
def initialize_static_hash(static_root: Path | None = None) -> str:
|
||||
"""Compute and store the frontend static hash once for this process.
|
||||
|
||||
Note: The API endpoint and template helpers only return this in-memory value,
|
||||
which keeps mobile version checks ultra-light.
|
||||
"""
|
||||
global _STATIC_HASH_VALUE, _STATIC_HASH_READY
|
||||
_STATIC_HASH_VALUE = compute_static_hash(static_root)
|
||||
_STATIC_HASH_READY = True
|
||||
return _STATIC_HASH_VALUE
|
||||
|
||||
|
||||
def static_hash(static_root: Path | None = None) -> str:
|
||||
"""Return the startup frontend static hash without rescanning files.
|
||||
|
||||
Note: The optional argument is kept for compatibility with existing callers;
|
||||
it is only used for a lazy fallback before app startup initialization.
|
||||
"""
|
||||
if not _STATIC_HASH_READY:
|
||||
return initialize_static_hash(static_root)
|
||||
return _STATIC_HASH_VALUE
|
||||
|
||||
Reference in New Issue
Block a user