diff --git a/patch/apply.sh b/patch/apply.sh index bf6dcb5..fb9fa41 100755 --- a/patch/apply.sh +++ b/patch/apply.sh @@ -38,11 +38,6 @@ if [ -f "$PATCH_DIR/disabled" ]; then exit 0 fi -# ── Helpers ─────────────────────────────────────────────────────────────────── - -warn() { echo "WARNING: $*"; } -ok() { echo "OK: $*"; } - # Find the Python interpreter that middlewared actually uses. # On TrueNAS SCALE, /usr/bin/middlewared is usually a Python entry-point script # with a shebang pointing at the right interpreter (system or venv). @@ -81,21 +76,21 @@ echo "Using Python: $PYTHON" SITE_PKG=$("$PYTHON" -c "import site; print(site.getsitepackages()[0])" 2>/dev/null || true) if [ -z "$SITE_PKG" ]; then - warn "Cannot determine site-packages directory; skipping backend patch." - warn "Verify that '$PYTHON -c \"import site; print(site.getsitepackages())\"' works." + echo "WARNING: Cannot determine site-packages directory; skipping backend patch." + echo "WARNING: Verify that '$PYTHON -c \"import site; print(site.getsitepackages())\"' works." else # Back up any pre-existing sitecustomize.py that isn't ours. if [ -f "$SITE_PKG/sitecustomize.py" ] && \ ! grep -q "truecloud-patch" "$SITE_PKG/sitecustomize.py" 2>/dev/null; then cp "$SITE_PKG/sitecustomize.py" \ "$SITE_PKG/sitecustomize.py.pre-truecloud-patch" - ok "Backed up existing sitecustomize.py" + echo "OK: Backed up existing sitecustomize.py" fi if cp "$PATCH_DIR/sitecustomize.py" "$SITE_PKG/sitecustomize.py" 2>/dev/null; then - ok "Installed sitecustomize.py → $SITE_PKG/sitecustomize.py" + echo "OK: Installed sitecustomize.py → $SITE_PKG/sitecustomize.py" else - warn "Failed to write $SITE_PKG/sitecustomize.py (permission error?)" + echo "WARNING: Failed to write $SITE_PKG/sitecustomize.py (permission error?)" fi fi @@ -103,11 +98,7 @@ fi echo "--- UI patch ---" -if "$PYTHON" "$PATCH_DIR/patch_ui.py"; then - : # patch_ui.py prints its own status -else - warn "patch_ui.py exited non-zero; UI dropdown may still show Storj only." -fi +"$PYTHON" "$PATCH_DIR/patch_ui.py" || echo "WARNING: patch_ui.py exited non-zero; UI dropdown may still show Storj only." # ── Done ────────────────────────────────────────────────────────────────────── diff --git a/patch/create_task.py b/patch/create_task.py index c26379d..107e78b 100755 --- a/patch/create_task.py +++ b/patch/create_task.py @@ -83,7 +83,7 @@ def make_client(host, api_key, insecure=False): # ── Sub-commands ────────────────────────────────────────────────────────────── -def cmd_verify(_client, _args): +def cmd_verify(): """Print the hook status written by sitecustomize.py at middlewared startup.""" if not os.path.exists(_STATUS_FILE): print("No hook status file found.") @@ -233,19 +233,19 @@ def main(): args = p.parse_args() if args.cmd == "verify": - cmd_verify(None, args) + cmd_verify() return if not args.host or not args.api_key: p.error("--host and --api-key are required for this command") client = make_client(args.host, args.api_key, args.insecure) - dispatch = { - "list-credentials": cmd_list_credentials, - "list-tasks": cmd_list_tasks, - "create": cmd_create, - } - dispatch[args.cmd](client, args) + if args.cmd == "list-credentials": + cmd_list_credentials(client, args) + elif args.cmd == "list-tasks": + cmd_list_tasks(client, args) + elif args.cmd == "create": + cmd_create(client, args) if __name__ == "__main__": diff --git a/patch/sitecustomize.py b/patch/sitecustomize.py index 220053a..f034c5f 100644 --- a/patch/sitecustomize.py +++ b/patch/sitecustomize.py @@ -106,7 +106,10 @@ class _Loader: # exec_module succeeded — apply our patch. try: - _PATCHES[fullname](module) + if fullname == "middlewared.rclone.remote.b2": + _patch_b2(module) + elif fullname == "middlewared.plugins.cloud_backup.restic": + _patch_restic(module) except Exception as exc: sys.stderr.write( f"[truecloud-patch] patch failed for {fullname}: {exc}\n" @@ -125,15 +128,11 @@ def _patch_b2(module): detail="native support present; patch not needed") return - @staticmethod - def get_restic_config(task): + def _b2_restic_config(task): p = task["credentials"]["provider"] - return "", { - "B2_ACCOUNT_ID": p["account"], - "B2_ACCOUNT_KEY": p["key"], - } + return "", {"B2_ACCOUNT_ID": p["account"], "B2_ACCOUNT_KEY": p["key"]} - cls.get_restic_config = get_restic_config + cls.get_restic_config = staticmethod(_b2_restic_config) cls.restic = True sys.stderr.write("[truecloud-patch] B2 restic support enabled\n") _record_status("middlewared.rclone.remote.b2", ok=True) @@ -146,37 +145,29 @@ def _patch_restic(module): return import dataclasses - import re _orig = module.get_restic_config - # Matches "scheme:/path" — the broken form the stock URL builder produces - # when a provider has no hostname component (url == ""). - # Does NOT match "scheme://path" (Storj and similar legitimately use ://). - _broken_url = re.compile(r'^(\w[\w+.-]*):/(?!/)(.+)$') - def get_restic_config(cloud_backup): # Call the original — it handles transfer_setting, cache, RESTIC_PASSWORD, # env construction, and everything else we don't own. result = _orig(cloud_backup) # Scan the built command for the -r argument and fix the URL if - # it contains a stray leading slash: "b2:/bucket/path" → "b2:bucket/path". + # it has a stray leading slash: "b2:/bucket/path" → "b2:bucket/path". + # "scheme://path" is intentional (Storj) and must not be touched. cmd = list(result.cmd) for i, part in enumerate(cmd): if i and cmd[i - 1] == "-r": - m = _broken_url.match(part) - if m: - cmd[i] = f"{m.group(1)}:{m.group(2)}" - # Prefer dataclasses.replace (passes unknown future fields - # through automatically). Fall back to NamedTuple._replace - # in case ResticConfig is refactored. + scheme, sep, rest = part.partition(":") + if sep and rest.startswith("/") and not rest.startswith("//"): + cmd[i] = f"{scheme}:{rest[1:]}" try: return dataclasses.replace(result, cmd=cmd) except TypeError: return result._replace(cmd=cmd) - break # -r arg present and already correct - return result # URL was fine; return original unchanged + break + return result get_restic_config._truecloud_patched = True module.get_restic_config = get_restic_config @@ -184,11 +175,6 @@ def _patch_restic(module): _record_status("middlewared.plugins.cloud_backup.restic", ok=True) -_PATCHES = { - "middlewared.rclone.remote.b2": _patch_b2, - "middlewared.plugins.cloud_backup.restic": _patch_restic, -} - _STATUS_FILE = "/data/truecloud-patch/hook_status.json" _hook_status: dict = {} @@ -201,8 +187,6 @@ def _record_status(fullname: str, ok: bool, detail: str = "") -> None: if fullname in _hook_status: return # idempotent: first call wins _hook_status[fullname] = {"ok": ok, "detail": detail} - if len(_hook_status) < len(_PATCHES): - return # wait until all patches have reported payload = { "patched_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),