Cleanup in js #16
@@ -16,7 +16,7 @@ from .config import (
|
|||||||
SOCKETIO_CORS_ALLOWED_ORIGINS,
|
SOCKETIO_CORS_ALLOWED_ORIGINS,
|
||||||
)
|
)
|
||||||
from .db import init_db
|
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")
|
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:
|
def create_app() -> Flask:
|
||||||
validate_offline_assets()
|
validate_offline_assets()
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
initialize_static_hash(Path(app.static_folder or ""))
|
||||||
from .logging_config import configure_logging
|
from .logging_config import configure_logging
|
||||||
configure_logging(app)
|
configure_logging(app)
|
||||||
if PROXY_FIX_ENABLE:
|
if PROXY_FIX_ENABLE:
|
||||||
@@ -83,7 +84,7 @@ def create_app() -> Flask:
|
|||||||
path = Path(app.static_folder or "") / filename
|
path = Path(app.static_folder or "") / filename
|
||||||
try:
|
try:
|
||||||
path.stat()
|
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())
|
return url_for("static", filename=filename, v=current_static_hash())
|
||||||
except OSError:
|
except OSError:
|
||||||
return url_for("static", filename=filename)
|
return url_for("static", filename=filename)
|
||||||
|
|||||||
@@ -7499,8 +7499,8 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"System"
|
"System"
|
||||||
],
|
],
|
||||||
"summary": "Get static asset version hash",
|
"summary": "Get current frontend JS/CSS hash",
|
||||||
"description": "Returns the current hash for app static assets. Clients can compare it with window.PYTORRENT.staticHash and reload when it changes.",
|
"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": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Static asset hash",
|
"description": "Static asset hash",
|
||||||
@@ -7513,10 +7513,12 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"static_hash": {
|
"static_hash": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Short SHA-256-based hash of frontend JavaScript and CSS files computed once at app startup."
|
||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"description": "Alias of static_hash for simple client version checks."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from ._shared import *
|
from ._shared import *
|
||||||
from flask import current_app
|
|
||||||
from pathlib import Path
|
|
||||||
from ..services import operation_logs
|
from ..services import operation_logs
|
||||||
from ..services.frontend_assets import static_hash
|
from ..services.frontend_assets import static_hash
|
||||||
|
|
||||||
@@ -51,8 +49,8 @@ def system_status():
|
|||||||
|
|
||||||
@bp.get("/static_hash")
|
@bp.get("/static_hash")
|
||||||
def static_hash_get():
|
def static_hash_get():
|
||||||
# Note: The frontend uses this lightweight version to detect changed static assets on mobile browsers.
|
# Note: This returns the startup-computed JS/CSS version without scanning files per request.
|
||||||
value = static_hash(Path(current_app.static_folder or ""))
|
value = static_hash()
|
||||||
return ok({"static_hash": value, "version": value})
|
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
|
def _versioned_static_files(root: Path) -> list[Path]:
|
||||||
change invalidates app.js imports, CSS and local frontend assets together.
|
"""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
|
import hashlib
|
||||||
|
|
||||||
root = static_root or (BASE_DIR / "pytorrent" / "static")
|
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()
|
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()
|
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:
|
try:
|
||||||
digest.update(path.read_bytes())
|
stat = path.stat()
|
||||||
|
content = path.read_bytes()
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
|
digest.update(rel.encode("utf-8"))
|
||||||
|
digest.update(str(stat.st_size).encode("ascii"))
|
||||||
|
digest.update(content)
|
||||||
value = digest.hexdigest()[:16]
|
value = digest.hexdigest()[:16]
|
||||||
_STATIC_HASH_CACHE.clear()
|
return value or "dev"
|
||||||
_STATIC_HASH_CACHE[(fingerprint, len(files))] = value
|
|
||||||
return value
|
|
||||||
|
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;
|
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(){
|
export async function buildRuntimeSource(){
|
||||||
const sources = await loadModuleSources();
|
const sources = await loadModuleSources();
|
||||||
return `(() => {\n${sources.join('\n')}\n})();\n`;
|
return `(() => {\n${sources.map(normalizeRuntimeSource).join('\n')}\n})();\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startApp(){
|
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