Fix B2 restic URL: use colon separator (b2:bucket:path) for restic 0.16.x
restic 0.16.x changed the B2 URL format to use a colon between bucket and path (b2:bucket:prefix) instead of a slash. The middlewared URL builder produces b2:/bucket/path; restic then validates the full string after 'b2:' as a bucket name, which fails because the slash is not in [a-z0-9-]. Fix the restic.py wrapper to strip the leading slash and replace the first slash with a colon: b2:/bucket/path -> b2:bucket:path. Also fix the hasattr bug in sitecustomize.py _patch_b2: hasattr() returns True for methods inherited from the base class (which raises NotImplementedError), causing the patch to be silently skipped. Use 'get_restic_config' not in cls.__dict__ instead.
This commit is contained in:
@@ -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 for providers with no hostname component (`b2:bucket/path` vs the broken `b2:/bucket/path`). | `sitecustomize.py` — Python's standard startup hook; no middleware files are modified on disk |
|
||||
| **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) |
|
||||
| **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
|
||||
@@ -202,7 +202,7 @@ You need three things from the task you created:
|
||||
export B2_ACCOUNT_ID="your-key-id"
|
||||
export B2_ACCOUNT_KEY="your-application-key"
|
||||
export RESTIC_PASSWORD="your-repo-password"
|
||||
REPO="b2:your-bucket/your-folder"
|
||||
REPO="b2:your-bucket:your-folder" # restic 0.16.x uses colon, not slash
|
||||
```
|
||||
|
||||
**S3-compatible (AWS S3, Wasabi, Cloudflare R2, MinIO, etc.):**
|
||||
|
||||
+28
-18
@@ -239,25 +239,35 @@ def get_restic_config(cloud_backup):
|
||||
cmd = list(result.cmd)
|
||||
for i, part in enumerate(cmd):
|
||||
if part.startswith("--repo=") or part.startswith("--repository="):
|
||||
prefix, _, url = part.partition("=")
|
||||
prefix += "="
|
||||
scheme, sep, rest = url.partition(":")
|
||||
if sep and rest.startswith("/") and not rest.startswith("//"):
|
||||
cmd[i] = f"{prefix}{scheme}:{rest[1:]}"
|
||||
try:
|
||||
return _dc.replace(result, cmd=cmd)
|
||||
except TypeError:
|
||||
return result._replace(cmd=cmd)
|
||||
break
|
||||
if i and cmd[i - 1] in ("-r", "--repo", "--repository"):
|
||||
scheme, sep, rest = part.partition(":")
|
||||
if sep and rest.startswith("/") and not rest.startswith("//"):
|
||||
cmd[i] = f"{scheme}:{rest[1:]}"
|
||||
try:
|
||||
return _dc.replace(result, cmd=cmd)
|
||||
except TypeError:
|
||||
return result._replace(cmd=cmd)
|
||||
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
|
||||
|
||||
+35
-30
@@ -7,13 +7,14 @@ Hooks two middlewared module imports using the find_spec / exec_module API
|
||||
|
||||
middlewared.rclone.remote.b2
|
||||
Adds get_restic_config() so the native restic B2 backend works.
|
||||
Restic repo URL: b2:<bucket>/<folder>
|
||||
Restic repo URL: b2:<bucket>:<folder> (colon separator, restic 0.16.x)
|
||||
Auth: B2_ACCOUNT_ID, B2_ACCOUNT_KEY
|
||||
|
||||
middlewared.plugins.cloud_backup.restic
|
||||
Fixes the URL builder for providers with no hostname component.
|
||||
Stock code: f"{rclone_type}:{url}/{remote_path}" → "b2:/bucket/path" (broken)
|
||||
Patched: "b2:bucket/path" when url == ""
|
||||
Fixes the URL builder for providers with no hostname component, and
|
||||
converts the slash separator to a colon for B2 (restic 0.16.x format):
|
||||
Stock code: f"{rclone_type}:{url}/{remote_path}" → "b2:/bucket/path"
|
||||
Patched: "b2:bucket:path" (leading slash stripped, / → : for B2)
|
||||
|
||||
Both patches are no-ops if the module already provides the functionality
|
||||
(i.e. a future TrueNAS version adds native support). All errors are caught
|
||||
@@ -134,7 +135,7 @@ class _Loader:
|
||||
def _patch_b2(module):
|
||||
cls = module.B2RcloneRemote
|
||||
|
||||
if hasattr(cls, "get_restic_config"):
|
||||
if "get_restic_config" in cls.__dict__:
|
||||
# A future TrueNAS version already added native B2 restic support.
|
||||
_record_status("middlewared.rclone.remote.b2", ok=True,
|
||||
detail="native support present; patch not needed")
|
||||
@@ -168,42 +169,46 @@ def _patch_restic(module):
|
||||
_orig = module.get_restic_config
|
||||
|
||||
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.
|
||||
# Call the original — it handles cache, RESTIC_PASSWORD, env, etc.
|
||||
result = _orig(cloud_backup)
|
||||
|
||||
# Fix stray leading slash in repo URL: "b2:/bucket" → "b2:bucket".
|
||||
# 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.
|
||||
# Covers all flag forms restic accepts (--repository is a documented synonym):
|
||||
# -r <url> --repo <url> --repo=<url>
|
||||
# --repository <url> --repository=<url>
|
||||
cmd = list(result.cmd)
|
||||
for i, part in enumerate(cmd):
|
||||
if part.startswith("--repo=") or part.startswith("--repository="):
|
||||
prefix, _, url = part.partition("=")
|
||||
prefix += "="
|
||||
scheme, sep, rest = url.partition(":")
|
||||
if sep and rest.startswith("/") and not rest.startswith("//"):
|
||||
cmd[i] = f"{prefix}{scheme}:{rest[1:]}"
|
||||
try:
|
||||
return dataclasses.replace(result, cmd=cmd)
|
||||
except TypeError:
|
||||
return result._replace(cmd=cmd)
|
||||
break
|
||||
if i and cmd[i - 1] in ("-r", "--repo", "--repository"):
|
||||
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)
|
||||
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
|
||||
|
||||
get_restic_config._truecloud_patched = True
|
||||
module.get_restic_config = get_restic_config
|
||||
sys.stderr.write("[truecloud-patch] restic URL fix applied\n")
|
||||
sys.stderr.write("[truecloud-patch] restic B2 URL fix applied (b2:bucket:path)\n")
|
||||
_record_status("middlewared.plugins.cloud_backup.restic", ok=True)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user