Eliminate URL-fix duplication; document native-support behaviour

URL-fix logic now lives once in sitecustomize._tc_fix_restic_cmd.
apply.sh's restic.py BLOCK delegates to it instead of repeating
the ~40-line implementation.

Additional safety: the BLOCK now guards its get_restic_config
reference with try/except NameError, so a future TrueNAS that
restructures restic.py won't cause an import error.

README: updated Backend table and disclaimer to reflect graceful
degradation; added "If TrueNAS adds native support" section that
covers all five upgrade scenarios (safe pass-through, base-class
shadowing risk, schema-change risk, etc.).
This commit is contained in:
2026-06-16 17:11:08 +00:00
parent e8b0f961c7
commit 8c0ffab166
3 changed files with 75 additions and 78 deletions
+17 -39
View File
@@ -231,46 +231,24 @@ import sys
BLOCK = """
# TRUECLOUD_PATCH — added by truenas-truecloud-patch/patch/apply.sh
_tc_orig_get_restic_config = get_restic_config
# URL fix logic lives in sitecustomize._tc_fix_restic_cmd (single source of truth).
try:
_tc_orig_get_restic_config = get_restic_config
except NameError:
pass # get_restic_config not in this module; TrueNAS restructured restic.py
else:
def get_restic_config(cloud_backup):
import dataclasses as _dc
result = _tc_orig_get_restic_config(cloud_backup)
try:
import sitecustomize as _sc
return _sc._tc_fix_restic_cmd(result, _dc)
except Exception as _e:
import sys as _sys
_sys.stderr.write(f"[truecloud-patch] restic URL fix failed: {_e}\n")
return result
def get_restic_config(cloud_backup):
import dataclasses as _dc
result = _tc_orig_get_restic_config(cloud_backup)
cmd = list(result.cmd)
for i, part in enumerate(cmd):
if part.startswith("--repo=") or part.startswith("--repository="):
pfx, _, url = part.partition("=")
pfx += "="
elif i and cmd[i - 1] in ("-r", "--repo", "--repository"):
pfx = None
url = part
else:
continue
scheme, sep, rest = url.partition(":")
if not sep:
break
changed = False
# Strip stray leading slash: b2:/bucket -> b2:bucket
if rest.startswith("/") and not rest.startswith("//"):
rest = rest[1:]
changed = True
# restic 0.16.x B2 uses colon to separate bucket from path:
# b2:bucket:prefix (not b2:bucket/prefix)
# middlewared builds the slash form; fix the separator.
if scheme == "b2" and "/" in rest:
rest = rest.replace("/", ":", 1)
changed = True
if changed:
new_url = scheme + ":" + rest
cmd[i] = pfx + new_url if pfx is not None else new_url
try:
return _dc.replace(result, cmd=cmd)
except TypeError:
return result._replace(cmd=cmd)
break
return result
get_restic_config._truecloud_patched = True
get_restic_config._truecloud_patched = True
"""
path = sys.argv[1]
+34 -36
View File
@@ -132,6 +132,39 @@ class _Loader:
# ── Patch functions ───────────────────────────────────────────────────────────
def _tc_fix_restic_cmd(result, dataclasses):
"""Fix the restic repo URL: b2:/bucket/path → b2:bucket:path (restic 0.16.x)."""
cmd = list(result.cmd)
for i, part in enumerate(cmd):
if part.startswith("--repo=") or part.startswith("--repository="):
pfx, _, url = part.partition("=")
pfx += "="
elif i and cmd[i - 1] in ("-r", "--repo", "--repository"):
pfx = None
url = part
else:
continue
scheme, sep, rest = url.partition(":")
if not sep:
break
changed = False
if rest.startswith("/") and not rest.startswith("//"):
rest = rest[1:]
changed = True
if scheme == "b2" and "/" in rest:
rest = rest.replace("/", ":", 1)
changed = True
if changed:
new_url = scheme + ":" + rest
cmd[i] = pfx + new_url if pfx is not None else new_url
try:
return dataclasses.replace(result, cmd=cmd)
except TypeError:
return result._replace(cmd=cmd)
break
return result
def _patch_b2(module):
cls = module.B2RcloneRemote
@@ -169,42 +202,7 @@ def _patch_restic(module):
_orig = module.get_restic_config
def get_restic_config(cloud_backup):
# Call the original — it handles cache, RESTIC_PASSWORD, env, etc.
result = _orig(cloud_backup)
# Fix the repo URL in the restic command.
# Stock middlewared builds: b2:/bucket/path
# restic 0.16.x B2 expects: b2:bucket:path (colon separator, no leading slash)
# "scheme://path" (Storj) must not be touched.
cmd = list(result.cmd)
for i, part in enumerate(cmd):
if part.startswith("--repo=") or part.startswith("--repository="):
pfx, _, url = part.partition("=")
pfx += "="
elif i and cmd[i - 1] in ("-r", "--repo", "--repository"):
pfx = None
url = part
else:
continue
scheme, sep, rest = url.partition(":")
if not sep:
break
changed = False
if rest.startswith("/") and not rest.startswith("//"):
rest = rest[1:]
changed = True
if scheme == "b2" and "/" in rest:
rest = rest.replace("/", ":", 1)
changed = True
if changed:
new_url = scheme + ":" + rest
cmd[i] = pfx + new_url if pfx is not None else new_url
try:
return dataclasses.replace(result, cmd=cmd)
except TypeError:
return result._replace(cmd=cmd)
break
return result
return _tc_fix_restic_cmd(_orig(cloud_backup), dataclasses)
get_restic_config._truecloud_patched = True
module.get_restic_config = get_restic_config