stativ hash
This commit is contained in:
@@ -16,7 +16,7 @@ from .config import (
|
||||
SOCKETIO_CORS_ALLOWED_ORIGINS,
|
||||
)
|
||||
from .db import init_db
|
||||
from .services.frontend_assets import asset_path, bootstrap_css_path, static_hash, validate_offline_assets
|
||||
from .services.frontend_assets import asset_path, bootstrap_css_path, initialize_static_hash, static_hash, validate_offline_assets
|
||||
|
||||
socketio = SocketIO(cors_allowed_origins=SOCKETIO_CORS_ALLOWED_ORIGINS, ping_timeout=30, async_mode="threading")
|
||||
|
||||
@@ -56,6 +56,7 @@ def register_error_pages(app: Flask) -> None:
|
||||
def create_app() -> Flask:
|
||||
validate_offline_assets()
|
||||
app = Flask(__name__)
|
||||
initialize_static_hash(Path(app.static_folder or ""))
|
||||
from .logging_config import configure_logging
|
||||
configure_logging(app)
|
||||
if PROXY_FIX_ENABLE:
|
||||
@@ -83,7 +84,7 @@ def create_app() -> Flask:
|
||||
path = Path(app.static_folder or "") / filename
|
||||
try:
|
||||
path.stat()
|
||||
# Note: A single static hash keeps module imports, CSS and local libraries on the same cache version.
|
||||
# Note: A single JS/CSS hash keeps module imports, stylesheets and local libraries on the same cache version.
|
||||
return url_for("static", filename=filename, v=current_static_hash())
|
||||
except OSError:
|
||||
return url_for("static", filename=filename)
|
||||
|
||||
@@ -7499,8 +7499,8 @@
|
||||
"tags": [
|
||||
"System"
|
||||
],
|
||||
"summary": "Get static asset version hash",
|
||||
"description": "Returns the current hash for app static assets. Clients can compare it with window.PYTORRENT.staticHash and reload when it changes.",
|
||||
"summary": "Get current frontend JS/CSS hash",
|
||||
"description": "Returns the startup-computed hash for app JavaScript and CSS assets. The value is kept in memory and returned without scanning static files per request.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Static asset hash",
|
||||
@@ -7513,10 +7513,12 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"static_hash": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Short SHA-256-based hash of frontend JavaScript and CSS files computed once at app startup."
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Alias of static_hash for simple client version checks."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ._shared import *
|
||||
from flask import current_app
|
||||
from pathlib import Path
|
||||
from ..services import operation_logs
|
||||
from ..services.frontend_assets import static_hash
|
||||
|
||||
@@ -51,8 +49,8 @@ def system_status():
|
||||
|
||||
@bp.get("/static_hash")
|
||||
def static_hash_get():
|
||||
# Note: The frontend uses this lightweight version to detect changed static assets on mobile browsers.
|
||||
value = static_hash(Path(current_app.static_folder or ""))
|
||||
# Note: This returns the startup-computed JS/CSS version without scanning files per request.
|
||||
value = static_hash()
|
||||
return ok({"static_hash": value, "version": value})
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -99,9 +99,16 @@ async function loadModuleSources(){
|
||||
return moduleSourcesPromise;
|
||||
}
|
||||
|
||||
function normalizeRuntimeSource(source){
|
||||
const text = String(source || '');
|
||||
// Note: Some generated source chunks may end with a literal \\n marker;
|
||||
// normalize only this trailing marker to avoid invalid Function() source.
|
||||
return text.endsWith('\\n') ? `${text.slice(0, -2)}\n` : text;
|
||||
}
|
||||
|
||||
export async function buildRuntimeSource(){
|
||||
const sources = await loadModuleSources();
|
||||
return `(() => {\n${sources.join('\n')}\n})();\n`;
|
||||
return `(() => {\n${sources.map(normalizeRuntimeSource).join('\n')}\n})();\n`;
|
||||
}
|
||||
|
||||
export async function startApp(){
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const bootstrapRuntimeSource = " async function checkStaticAssetVersion(){ try{ const r=await fetch('/api/static_hash',{cache:'no-store'}); const j=await r.json(); const current=String(window.PYTORRENT?.staticHash||''); const next=String(j.static_hash||j.version||''); if(current && next && current!==next){ window.PYTORRENT.staticHash=next; toast('A new frontend version is available. Reloading...','info'); setTimeout(()=>window.location.reload(), 600); } }catch(e){} }\n setInterval(checkStaticAssetVersion, 120000);\n window.addEventListener('focus', checkStaticAssetVersion);\n updateSortHeaders(); setupColumnResizers(); applyColumnVisibility(); renderColumnManager(); restoreFooterStatusCache(); refreshFooterStatusNow(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); updateBrowserSpeedTitle(); setupTorrentDropZone(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); ensureDashboardToolsUI(); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); if(hasActiveProfile) refreshUserDiskUsage(true).catch(()=>{}); scheduleTrackerSummary(true);\\n";
|
||||
export const bootstrapRuntimeSource = " let lastStaticAssetVersionCheck=0;\n async function checkStaticAssetVersion(force=false){ const now=Date.now(); if(!force && now-lastStaticAssetVersionCheck<60000) return; lastStaticAssetVersionCheck=now; try{ const r=await fetch('/api/static_hash',{cache:'no-store'}); const j=await r.json(); const current=String(window.PYTORRENT?.staticHash||''); const next=String(j.static_hash||j.version||''); if(current && next && current!==next){ window.PYTORRENT.staticHash=next; toast('A new frontend version is available. Reloading...','info'); setTimeout(()=>window.location.reload(), 600); } }catch(e){} }\n setInterval(()=>checkStaticAssetVersion(true), 900000);\n window.addEventListener('focus',()=>checkStaticAssetVersion(false));\n updateSortHeaders(); setupColumnResizers(); applyColumnVisibility(); renderColumnManager(); restoreFooterStatusCache(); refreshFooterStatusNow(); renderFooterPreferences(); applyFooterPreferences(); updateFooterClock(); updateBrowserSpeedTitle(); setupTorrentDropZone(); setInterval(updateFooterClock,1000); scheduleRender(true); if(!hasActiveProfile) renderNoProfileState(); loadLabels().catch(()=>{}); loadRatios().catch(()=>{}); loadSmartQueue().catch(()=>{}); loadAutomations().catch(()=>{}); ensureDashboardToolsUI(); if(portCheckEnabled) loadPortCheck(false); else renderPortCheck({status:'disabled',enabled:false}); if(hasActiveProfile) applyDefaultDownloadPath(false).catch(()=>{}); if(hasActiveProfile) refreshUserDiskUsage(true).catch(()=>{}); scheduleTrackerSummary(true);\n";
|
||||
|
||||
Reference in New Issue
Block a user