Files
truenas-truecloud-patch/patch/patch_ui.py
T
flan 78f943a71b Fix kill switch not cleared on reinstall; guard midclt delete; tighten sitecustomize imports
install.sh: running install.sh after a recover.sh left /data/truecloud-patch/disabled
in place, so apply.sh silently skipped all patching and middlewared restarted
without the patch. Clear the kill switch file before running apply.sh.

uninstall.sh: midclt initshutdownscript.delete was unguarded under set -euo pipefail,
so a delete failure (already-removed entry, transient API error) aborted the script
before sitecustomize.py was cleaned up or /data/truecloud-patch/ was removed. Now
guarded with an if/else that warns and continues.

sitecustomize.py: move importlib.machinery/.util imports inside the if block in
find_spec so they only execute when intercepting our two target modules, not on
every module import across the whole process. In _install(), import os before
importlib.util so the kill switch check (cheap) runs before the importlib import
(slightly heavier on first use). Remove the unused orig variable in _patch_restic.

patch_ui.py: add explicit encoding="utf-8" to both open() calls. errors="replace"
on read so malformed bytes in a bundle don't silently skip a candidate file.
2026-06-15 02:39:34 +00:00

134 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""
Patches the TrueNAS webui Angular bundle to show S3 and B2 credentials
in the TrueCloud Backup task form, instead of Storj only.
Angular's Ivy compiler inlines TypeScript string enum values as literals in
the compiled bundle, so the template binding:
[filterByProviders]="[CloudSyncProviderName.Storj]"
appears verbatim in the minified JS as:
"filterByProviders",["STORJ_IX"]
We replace that array to include S3 and B2. The file is backed up before
modification so uninstall.sh can restore it.
Safe to run multiple times — a marker string detects an already-patched file.
Exits 0 in all cases (warnings are printed to stdout and logged by apply.sh).
"""
import os
import re
import shutil
import sys
WEBUI_CANDIDATES = [
"/usr/share/truenas/webui",
"/usr/share/truenas-ui",
"/var/www/truenas",
]
# Angular's Ivy template compiler serialises the Storj-only filter as this
# exact substring in every production build we've observed.
FIND = re.compile(r'("filterByProviders",)\["STORJ_IX"\]')
REPLACE = r'\1["STORJ_IX","S3","B2"]'
# A patched file contains both "S3" and "B2" next to "STORJ_IX" in this form.
# This string is specific enough not to appear elsewhere in the bundle.
MARKER = '"STORJ_IX","S3","B2"'
def find_webui():
for d in WEBUI_CANDIDATES:
if os.path.isdir(d):
return d
return None
def find_bundle(webui):
"""
Walk the webui directory looking for the JS file that contains the
filterByProviders binding. Returns (path, content) or (None, None).
Only .js files are read; binary files and permission errors are skipped.
"""
matches = []
for root, _dirs, names in os.walk(webui):
for name in sorted(names): # deterministic order
if not name.endswith(".js"):
continue
path = os.path.join(root, name)
try:
with open(path, encoding="utf-8", errors="replace") as fh:
content = fh.read()
if FIND.search(content):
matches.append((path, content))
except (UnicodeDecodeError, PermissionError, OSError):
continue
if not matches:
return None, None
if len(matches) > 1:
# Unexpected — log all matches so the operator can investigate.
print(
f"[truecloud-patch] WARNING: filterByProviders pattern found in "
f"{len(matches)} files; patching only the first."
)
for p, _ in matches:
print(f"[truecloud-patch] {p}")
return matches[0]
def main():
webui = find_webui()
if not webui:
print(
"[truecloud-patch] WARNING: webui directory not found; skipping UI patch.\n"
"[truecloud-patch] Searched: " + ", ".join(WEBUI_CANDIDATES)
)
return
path, content = find_bundle(webui)
if path is None:
print(
"[truecloud-patch] WARNING: filterByProviders pattern not found in any JS bundle.\n"
"[truecloud-patch] The TrueNAS webui may have been restructured in this version.\n"
"[truecloud-patch] File an issue at https://github.com/sudolulo/truenas-truecloud-patch\n"
f"[truecloud-patch] TrueNAS version info: {_tnversion()}"
)
return
if MARKER in content:
print(f"[truecloud-patch] UI already patched: {path}")
return
backup = path + ".pre-truecloud-patch"
if not os.path.exists(backup):
shutil.copy2(path, backup)
patched, count = FIND.subn(REPLACE, content)
try:
with open(path, "w", encoding="utf-8") as fh:
fh.write(patched)
except OSError as exc:
print(f"[truecloud-patch] ERROR: Could not write {path}: {exc}")
return
print(f"[truecloud-patch] UI bundle patched ({count} replacement(s)): {path}")
def _tnversion():
try:
with open("/etc/version") as fh:
return fh.read().strip()
except OSError:
return "unknown"
if __name__ == "__main__":
main()