flan cc43b2831d Fix five quality findings from second re-review
sitecustomize.py: when find_spec resolves real_spec as None (module absent
after a TrueNAS update), record a FAIL status and mark the module done so
hook_status.json is still written and cmd_verify shows a diagnostic FAIL
instead of the ambiguous "no status file found".

sitecustomize.py: the AttributeError fallback in the URL-fix wrapper now
writes a WARNING to stderr before returning the unmodified result, making
the unexpected ResticConfig type visible in journalctl.

apply.sh: after falling back to bare python3, verify that python3 can also
import middlewared; if not, emit a second warning so the operator knows the
backend patch may be installed in the wrong site-packages directory.

patch_ui.py: abort (return without writing) when FIND.subn produces a count
other than 1, instead of committing a doubly-patched bundle and having
subsequent runs silently accept it via the MARKER check.

uninstall.sh: when a vendor sitecustomize.py backup exists, use mv to
atomically overwrite our file rather than rm-then-mv; eliminates the window
where a read-only /usr causes rm to fail under set -e, aborting before the
backup is restored.
2026-06-15 04:07:16 +00:00

truenas-truecloud-patch

Extends TrueNAS SCALE's TrueCloud Backup feature to work with S3-compatible providers and native Backblaze B2, instead of Storj only.


Why this exists

In 2026, Storj raised the price of their TrueNAS-integrated storage tier from $5/month to $50/month — a 10× increase. For many home lab and small-office users, the TrueCloud Backup feature became unaffordable overnight.

TrueCloud Backup is the only native TrueNAS mechanism that provides:

  • Integrated ZFS snapshot support before each backup
  • Restic-based incremental deduplication
  • Scheduled tasks with progress and log tracking in the UI
  • Dataset lock integration

Running restic manually is possible but loses all of the above. This patch restores access to the TrueCloud Backup feature for users who need a provider other than Storj, with storage they already pay for or that costs a fraction of the new Storj price.


⚠ Disclaimer — please read before installing

This project is unofficial, unsupported, and not affiliated with iXsystems or the TrueNAS project in any way.

By installing this patch you accept the following:

  • Unsupported configuration. TrueNAS support staff are not obligated to help with any issue on a system running this patch. If you file a bug report, remove the patch first and reproduce the issue on an unmodified system.

  • May break on TrueNAS updates. The patch targets internal middleware APIs that are not part of any public contract. They can change at any time. When they do, the patch silently degrades to Storj-only behaviour rather than breaking TrueNAS — but you should check the log after each update.

  • Your backups are your responsibility. Verify that your backup jobs complete successfully and that restores work before relying on them for disaster recovery.

  • 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.


What is actually patched

Nothing in TrueNAS's persistent database or configuration is modified. The patch operates on two files that live in /usr/ (which TrueNAS replaces 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
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 bundle; original is backed up

Both changes are fail-safe: if a patch cannot be applied (e.g. TrueNAS restructured the relevant code), middlewared starts normally with Storj-only support and the reason is logged to /data/truecloud-patch/apply.log.

Supported providers after patching

Provider Credential type in TrueNAS
Backblaze B2 (native B2 API) B2
AWS S3, Wasabi, Cloudflare R2, MinIO, and any S3-compatible endpoint S3
Storj (unchanged) STORJ_IX

How persistence works

TrueNAS SCALE updates replace /usr/ entirely. The patch survives by storing all scripts in /data/truecloud-patch/ (a persistent ZFS dataset) and registering a PREINIT initshutdownscript in the TrueNAS database. This causes apply.sh to run on every boot before middlewared starts, placing sitecustomize.py in the correct site-packages directory and re-patching the UI bundle.

Python version compatibility

sitecustomize.py uses the find_spec / exec_module import hook API (introduced in Python 3.4, required from Python 3.12 onwards). This covers:

  • TrueNAS SCALE 24.x — Debian 12, Python 3.11 ✓
  • TrueNAS SCALE 25.x — Debian 13, Python 3.12 ✓

Install

Run on your TrueNAS box as root, with the system fully booted:

git clone https://github.com/sudolulo/truenas-truecloud-patch.git
cd truenas-truecloud-patch
bash install.sh

Refresh your browser. S3 and B2 credentials now appear in the Data Protection → TrueCloud Backup → Add credential dropdown.

Creating a credential

Before creating a backup task, add a credential in the TrueNAS UI:

Data Protection → TrueCloud Backup → Add → (create new credential)

Or use Credentials → Backup Credentials → Cloud Credentials → Add and select B2 or Amazon S3.

For S3-compatible providers, select Amazon S3, then set a custom endpoint in the credential's advanced settings (e.g. https://s3.wasabisys.com).

Creating a task via CLI

If the UI still shows only Storj after refreshing (e.g. the JS bundle pattern changed in a new TrueNAS version), create tasks directly via the REST API:

# List your cloud credentials to find the right ID
python3 /data/truecloud-patch/create_task.py \
    --host 192.168.1.1 --api-key <key> list-credentials

# Create a task with a B2 credential (id=3)
python3 /data/truecloud-patch/create_task.py \
    --host 192.168.1.1 --api-key <key> create \
    --name "tank-to-b2" \
    --path /mnt/tank/data \
    --credential 3 \
    --bucket my-bucket \
    --folder backups/tank \
    --password "restic-repo-password" \
    --keep-last 14

Get an API key from System → API Keys → Add.


Uninstall

bash /path/to/truenas-truecloud-patch/uninstall.sh

Removes the PREINIT hook, sitecustomize.py, and restores the original UI bundle from backup. The backend changes vanish on the next middlewared restart.


Restoring from a TrueCloud Backup

TrueCloud Backup uses restic under the hood. Restores are done with the restic command directly — TrueNAS does not yet expose a restore UI for TrueCloud Backup tasks.

1. Find the restic binary

which restic 2>/dev/null || find /usr -name restic -type f 2>/dev/null | head -1

Use that path in the commands below (referred to as restic).

2. Gather your repository details

You need three things from the task you created:

Detail Where to find it
Bucket and folder TrueNAS UI → Data Protection → TrueCloud Backup → edit the task → Attributes
Credentials (key ID + secret) TrueNAS UI → Credentials → Backup Credentials → edit the credential
Repository password The --password value you supplied when creating the task

3. Set environment variables

Backblaze B2:

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"

S3-compatible (AWS S3, Wasabi, Cloudflare R2, MinIO, etc.):

export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export RESTIC_PASSWORD="your-repo-password"
# Use just the hostname as the endpoint — no https:// prefix:
REPO="s3:s3.wasabisys.com/your-bucket/your-folder"   # Wasabi example
# REPO="s3:s3.amazonaws.com/your-bucket/your-folder" # AWS S3
# REPO="s3:<account>.r2.cloudflarestorage.com/your-bucket/your-folder" # R2

4. List snapshots

restic -r "$REPO" snapshots

Output example:

ID        Time                 Host        Tags  Paths
──────────────────────────────────────────────────────────
a1b2c3d4  2026-06-01 02:00:05  truenas           /mnt/tank/data
e5f6a7b8  2026-06-08 02:00:07  truenas           /mnt/tank/data

5. Restore files

Restore everything from the latest snapshot to a temporary location:

restic -r "$REPO" restore latest --target /mnt/tank/restore-tmp

Restore a specific snapshot by ID:

restic -r "$REPO" restore a1b2c3d4 --target /mnt/tank/restore-tmp

Restore only specific paths from within a snapshot:

restic -r "$REPO" restore latest \
    --include /mnt/tank/data/important-dir \
    --target /mnt/tank/restore-tmp

Browse a snapshot without extracting (useful for finding the right file):

restic -r "$REPO" ls latest

6. Notes

  • Restore to a different path first, then move files into place after verifying. Restoring directly over a live dataset can cause data loss if the snapshot is incomplete or from the wrong point in time.
  • If you created the task with --snapshot (ZFS snapshot before each run), the restic snapshot captures the dataset at a consistent point in time.
  • Use restic check -r "$REPO" periodically to verify repository integrity.

After a TrueNAS update

  1. Check the log: cat /data/truecloud-patch/apply.log | tail -30
  2. If you see "WARNING: … pattern not found", the UI patch needs updating. Open an issue with your TrueNAS version number.
  3. The backend patch (B2 support + URL fix) is more stable — check that a B2 backup job still completes successfully after any update.

Emergency recovery

middlewared won't start

Run this from the TrueNAS shell (local console, SSH, or the debug shell in the UI):

bash /data/truecloud-patch/recover.sh

This creates a kill-switch file (/data/truecloud-patch/disabled). sitecustomize.py checks for it at Python startup; if present, the import hook is skipped entirely and middlewared starts clean with Storj-only support. Nothing else on your system is affected.

If you cannot run a script and only have a bare shell prompt:

touch /data/truecloud-patch/disabled
systemctl restart middlewared

If middlewared still won't start after the kill switch is set, the problem is unrelated to this patch. Check:

journalctl -u middlewared -n 50

To re-enable the patch once you have investigated:

rm /data/truecloud-patch/disabled
bash /data/truecloud-patch/apply.sh

Web UI is blank or broken

If the TrueNAS web interface loads blank or shows JavaScript errors, the Angular bundle may have been interrupted mid-write (e.g. power cut during boot). The original bundle is always backed up before patching, so recovery is straightforward:

# Find the backup (the path varies by TrueNAS version):
find /usr/share/truenas /var/www/truenas -name "*.js.pre-truecloud-patch" 2>/dev/null

# Restore it — substitute the actual path from the find output:
mv /usr/share/truenas/webui/main.XXXXXXXX.js.pre-truecloud-patch \
   /usr/share/truenas/webui/main.XXXXXXXX.js

Refresh your browser. The UI will return to normal (Storj-only until the patch re-runs at next reboot, or you run bash /data/truecloud-patch/apply.sh manually).


Backend verify shows FAIL

python3 /data/truecloud-patch/create_task.py verify

If one or more entries show [FAIL]:

  1. Check the apply log for errors during the last boot:
    cat /data/truecloud-patch/apply.log | tail -40
    
  2. Check middlewared's own log for Python tracebacks:
    journalctl -u middlewared -n 50
    
  3. A FAIL is non-fatal. middlewared runs normally; the affected provider falls back to Storj-only. Your existing backups are not at risk.
  4. If the detail says the module doesn't exist, a TrueNAS update renamed or restructured the internal API. Open an issue with your TrueNAS version number and the full verify output.

Troubleshooting

Apply log (check after each reboot or install):

cat /data/truecloud-patch/apply.log

Verify backend patch is loaded (while middlewared is running):

python3 /data/truecloud-patch/create_task.py verify

This reads /data/truecloud-patch/hook_status.json, written by the import hook once both target modules have been loaded by middlewared. If it reports "No hook status file found" immediately after install, restart middlewared and try again — the file is written when middlewared imports the relevant modules, which happens at service start. Does not require --host or --api-key.

Verify the UI patch (should print your TrueNAS version):

grep -c 'STORJ_IX.*S3.*B2' \
    $(find /usr/share/truenas -name '*.js' 2>/dev/null) 2>/dev/null \
    | grep -v ':0'

B2 backup fails with credential error Confirm the credential type is exactly B2 (not S3 with a B2 endpoint). B2's native restic backend uses a different auth path than S3-compatible B2.

S3-compatible backup fails S3 support already existed in the backend — the credential setup is the likely issue. Verify the endpoint URL, access key, secret key, and bucket name in the credential settings. Use --insecure in create_task.py only if you are using a self-signed certificate on your S3 endpoint.

S
Description
Extends TrueNAS SCALE's TrueCloud Backup to Backblaze B2 and S3-compatible providers, and to datasets with children. Survives TrueNAS updates. Requires TrueNAS 24.10+.
Readme MIT
802 KiB
v0.6.1
Latest
2026-07-13 16:42:22 -04:00
Languages
Python 79.6%
Shell 20.4%