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:
@@ -46,8 +46,8 @@ By installing this patch you accept the following:
|
||||
|
||||
- **No warranty.** This software is provided as-is. See the LICENSE file.
|
||||
|
||||
If TrueNAS ever adds native support for additional providers in TrueCloud
|
||||
Backup, uninstall this patch immediately.
|
||||
If TrueNAS adds native B2 or S3 support to TrueCloud Backup, the patch
|
||||
detects it and degrades gracefully — see [Native support](#if-truenas-adds-native-support) below.
|
||||
|
||||
---
|
||||
|
||||
@@ -59,7 +59,7 @@ on every update) and are therefore re-applied automatically on every boot.
|
||||
|
||||
| Layer | What changes | Technique |
|
||||
|---|---|---|
|
||||
| **Backend** | `B2RcloneRemote` gains `get_restic_config()`. `restic.py` URL builder is fixed: strips the stray leading slash and converts the slash separator to a colon (`b2:bucket:path`), which is the format restic 0.16.x expects. | Direct file patch in the overlay (primary) + `sitecustomize.py` import hook (belt-and-suspenders) |
|
||||
| **Backend** | `B2RcloneRemote` gains `get_restic_config()` — skipped automatically if TrueNAS already provides one on the class. `restic.py` URL builder is fixed: strips the stray leading slash and converts the slash separator to a colon (`b2:bucket:path`), which is the format restic 0.16.x expects. URL wrapper is a no-op if the URL is already correctly formed. | Direct file patch in the overlay (primary, delegates URL logic to `sitecustomize.py`) + `sitecustomize.py` import hook (belt-and-suspenders) |
|
||||
| **UI** | The Angular bundle's `filterByProviders` binding is widened from `["STORJ_IX"]` to `["STORJ_IX","S3","B2"]` | In-place text replacement in the compiled JS chunk; original is backed up |
|
||||
|
||||
Both changes are **fail-safe**: if a patch cannot be applied (e.g. TrueNAS
|
||||
@@ -265,6 +265,27 @@ restic -r "$REPO" ls latest
|
||||
|
||||
---
|
||||
|
||||
## If TrueNAS adds native support
|
||||
|
||||
When a TrueNAS update ships native B2 or S3 support in TrueCloud Backup, the
|
||||
patch handles each component as follows:
|
||||
|
||||
| Component | What happens | Action needed |
|
||||
|---|---|---|
|
||||
| **B2 `get_restic_config`** added directly to `B2RcloneRemote` | `__dict__` guard detects it; our method is **not attached** | None — native version used automatically |
|
||||
| **restic.py URL builder** fixed to emit `b2:bucket:path` directly | Our wrapper sees no `/` to fix; it becomes a **no-op** | None — correct URL passes through unchanged |
|
||||
| **`get_restic_config` moved** out of `restic.py` entirely | `NameError` guard in the patched file catches it; wrapper silently does nothing | None — but run `verify` to confirm state |
|
||||
| **B2 credential schema changed** (e.g. `provider["account"]` renamed) | Our B2 config function raises `KeyError`; backup task fails | Uninstall or update the patch |
|
||||
| **B2 `get_restic_config`** added to a **base class** (not `B2RcloneRemote`) | `__dict__` check misses it; our method is attached and **shadows** the native one | Uninstall the patch |
|
||||
|
||||
**Recommended check after any TrueNAS update that adds TrueCloud provider
|
||||
support**: run `python3 /mnt/tank/truenas-truecloud-patch/patch/create_task.py verify`
|
||||
and attempt a B2 backup. If both pass, the patch is coexisting correctly. If
|
||||
the backup fails with a credential or URL error that worked before the update,
|
||||
uninstall the patch — TrueNAS has shipped a conflicting implementation.
|
||||
|
||||
---
|
||||
|
||||
## After a TrueNAS update
|
||||
|
||||
1. Check the log: `cat /mnt/tank/truenas-truecloud-patch/apply.log | tail -30`
|
||||
|
||||
+17
-39
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user