release: nexus btpanel session fix and app-v2
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remove orphaned files under web/app/assets/ not referenced by current Vite build.
|
||||
|
||||
Vite/Rolldown minified bundles reference chunks by bare filename (e.g. ServersPage-xxx.js)
|
||||
without import() paths — scan file contents for known asset basenames.
|
||||
|
||||
Orphans newer than --retention-days are kept so long-lived browser tabs can still load
|
||||
their lazy chunks after a deploy (see docs/design/specs/2026-06-02-keep-session-no-refresh-design.md).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# Leading `_` required for Vite chunks like `_plugin-vue_export-helper-*.js`
|
||||
ASSET_NAME_RE = re.compile(
|
||||
r"[_A-Za-z0-9][A-Za-z0-9_.-]*\.(?:js|css|woff2?|ttf|eot)"
|
||||
)
|
||||
URL_RE = re.compile(r"""url\(["']?([^"')]+)["']?\)""")
|
||||
DEFAULT_RETENTION_DAYS = 14
|
||||
|
||||
|
||||
def _parse_args(argv: list[str]) -> tuple[bool, int, Path]:
|
||||
dry_run = "--dry-run" in argv
|
||||
retention_days = DEFAULT_RETENTION_DAYS
|
||||
positional: list[str] = []
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
arg = argv[i]
|
||||
if arg == "--dry-run":
|
||||
i += 1
|
||||
continue
|
||||
if arg.startswith("--retention-days="):
|
||||
retention_days = int(arg.split("=", 1)[1])
|
||||
i += 1
|
||||
continue
|
||||
if arg == "--retention-days":
|
||||
if i + 1 >= len(argv):
|
||||
raise SystemExit("--retention-days requires a value")
|
||||
retention_days = int(argv[i + 1])
|
||||
i += 2
|
||||
continue
|
||||
positional.append(arg)
|
||||
i += 1
|
||||
if retention_days < 0:
|
||||
raise SystemExit("--retention-days must be >= 0")
|
||||
app_dir = Path(positional[0] if positional else "/www/wwwroot/api.synaglobal.vip/web/app")
|
||||
return dry_run, retention_days, app_dir
|
||||
|
||||
|
||||
def _referenced_basenames(text: str, known: set[str]) -> set[str]:
|
||||
found: set[str] = set()
|
||||
for m in ASSET_NAME_RE.finditer(text):
|
||||
name = m.group(0)
|
||||
if name in known:
|
||||
found.add(name)
|
||||
for raw in URL_RE.findall(text):
|
||||
name = Path(raw).name
|
||||
if name in known:
|
||||
found.add(name)
|
||||
return found
|
||||
|
||||
|
||||
def collect(app_dir: Path) -> set[Path]:
|
||||
assets = app_dir / "assets"
|
||||
index = app_dir / "index.html"
|
||||
if not index.is_file():
|
||||
raise SystemExit(f"missing {index}")
|
||||
|
||||
all_names = {p.name for p in assets.iterdir() if p.is_file()}
|
||||
keep_names: set[str] = set()
|
||||
|
||||
for m in re.finditer(r"/app/assets/([^\s\"']+)", index.read_text(encoding="utf-8")):
|
||||
name = m.group(1).split("/")[-1]
|
||||
if name in all_names:
|
||||
keep_names.add(name)
|
||||
|
||||
if not keep_names:
|
||||
raise SystemExit("no entry assets found in index.html")
|
||||
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for name in list(keep_names):
|
||||
path = assets / name
|
||||
if not path.is_file():
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||
for ref in _referenced_basenames(text, all_names):
|
||||
if ref not in keep_names:
|
||||
keep_names.add(ref)
|
||||
changed = True
|
||||
|
||||
return {assets / n for n in keep_names}
|
||||
|
||||
|
||||
def _partition_orphans(
|
||||
orphan: list[Path],
|
||||
retention_days: int,
|
||||
) -> tuple[list[Path], list[Path]]:
|
||||
"""Split orphans into delete-now vs keep-for-open-tabs (mtime within window)."""
|
||||
if retention_days <= 0:
|
||||
return orphan, []
|
||||
cutoff = time.time() - retention_days * 86400
|
||||
to_remove: list[Path] = []
|
||||
retained: list[Path] = []
|
||||
for p in orphan:
|
||||
if p.stat().st_mtime >= cutoff:
|
||||
retained.append(p)
|
||||
else:
|
||||
to_remove.append(p)
|
||||
return to_remove, retained
|
||||
|
||||
|
||||
def main() -> int:
|
||||
dry_run, retention_days, app_dir = _parse_args(sys.argv[1:])
|
||||
assets = app_dir / "assets"
|
||||
if not assets.is_dir():
|
||||
raise SystemExit(f"not a directory: {assets}")
|
||||
|
||||
keep = collect(app_dir)
|
||||
all_files = {p for p in assets.iterdir() if p.is_file()}
|
||||
orphan = sorted(all_files - keep, key=lambda p: p.name)
|
||||
to_remove, retained = _partition_orphans(orphan, retention_days)
|
||||
|
||||
if dry_run:
|
||||
print(
|
||||
f"dry-run retention_days={retention_days} "
|
||||
f"referenced_kept={len(keep)} "
|
||||
f"would_remove={len(to_remove)} "
|
||||
f"retained_orphans={len(retained)} "
|
||||
f"total={len(all_files)}"
|
||||
)
|
||||
return 0
|
||||
|
||||
for p in to_remove:
|
||||
p.unlink()
|
||||
print(
|
||||
f"kept={len(keep)} removed={len(to_remove)} "
|
||||
f"retained_orphans={len(retained)} retention_days={retention_days} "
|
||||
f"total_before={len(all_files)}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user