Files
truenas-truecloud-patch/patch/apply.sh
T
flan 6a8ed7fa67 Fix four audit findings
- patch/apply.sh: replace sed with Python+env-var for PATCH_DIR
  substitution into sitecustomize.py; sed's & and | metacharacters
  silently corrupt or truncate the output for paths containing those
  chars; Python str.replace has no metacharacter issues; also write to
  a tmp file and mv atomically so a failed substitution never leaves
  an empty sitecustomize.py at the destination
- recover.sh: fix re-enable hint from $PATCH_DIR/apply.sh to
  $PATCH_DIR/patch/apply.sh (apply.sh moved into patch/ subdirectory)
- install.sh + uninstall.sh: match PREINIT hook on comment field
  ("TrueCloud provider patch (S3/B2)") instead of exact script path;
  exact-path match breaks when the repo is moved after install —
  uninstall leaves the stale hook registered (fires on every boot),
  and reinstall creates a duplicate entry; install.sh now also updates
  the script path on re-run so a moved repo self-corrects
2026-06-15 17:36:58 +00:00

132 lines
5.7 KiB
Bash
Executable File

#!/bin/bash
# patch/apply.sh — registered as a TrueNAS PREINIT initshutdownscript.
#
# Runs on every boot BEFORE middlewared starts, so patches land before
# the first Python process for middlewared is created.
#
# TrueNAS updates replace /usr/ entirely; this script re-applies two patches:
#
# 1. sitecustomize.py — Python executes this automatically at startup.
# Monkey-patches B2 restic support and fixes the
# URL builder for empty-host providers.
#
# 2. Angular JS bundle — Widens the TrueCloud Backup credential dropdown
# from Storj-only to include S3 and B2.
#
# Design principle: every step is independently fail-safe.
# A failed patch logs a warning and continues; middlewared always starts.
# Never use `set -e` in a PREINIT script.
# Derive PATCH_DIR from this script's location (parent of the patch/ directory).
PATCH_DIR="$(cd "$(dirname "$0")/.." && pwd)"
LOG="$PATCH_DIR/apply.log"
# Rotate log at 512 KB to avoid unbounded growth on a system volume.
# Keep two prior generations (.1 and .2) so the last three boots are always available.
if [ -f "$LOG" ] && [ "$(wc -c < "$LOG")" -gt 524288 ]; then
[ -f "${LOG}.1" ] && mv "${LOG}.1" "${LOG}.2"
mv "$LOG" "${LOG}.1"
fi
exec >> "$LOG" 2>&1
echo "=== $(date -Iseconds) ==="
# Kill switch: if this file exists, skip all patching and exit cleanly.
# Recovery: touch "$PATCH_DIR/disabled" (then reboot or restart middlewared).
if [ -f "$PATCH_DIR/disabled" ]; then
echo "Kill switch active ($PATCH_DIR/disabled exists) — patch not applied."
echo "To re-enable: rm $PATCH_DIR/disabled"
echo "=== done ==="
exit 0
fi
# 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).
find_mw_python() {
local py="python3"
local shebang=""
if [ -x /usr/bin/middlewared ]; then
# Read the first line safely (max 256 bytes) — avoids reading a binary ELF
shebang=$(dd if=/usr/bin/middlewared bs=256 count=1 2>/dev/null | head -1 || true)
if [[ "$shebang" =~ ^'#!'(/[^[:space:]]+python[^[:space:]]*) ]]; then
py="${BASH_REMATCH[1]}"
elif [[ "$shebang" =~ ^'#!/usr/bin/env '(python[^[:space:]]*) ]]; then
py=$(command -v "${BASH_REMATCH[1]}" 2>/dev/null || echo "python3")
fi
fi
# Verify the chosen interpreter can actually import middlewared.
# Use >&2 so this message goes to stderr, not captured by $(...) substitution.
if ! "$py" -c "import middlewared" 2>/dev/null; then
echo "WARNING: '$py' cannot import middlewared; falling back to python3" >&2
py="python3"
if ! "$py" -c "import middlewared" 2>/dev/null; then
echo "WARNING: 'python3' also cannot import middlewared; sitecustomize.py may be installed in the wrong location" >&2
fi
fi
echo "$py"
}
# ── Step 1: sitecustomize.py ──────────────────────────────────────────────────
echo "--- backend patch ---"
PYTHON=$(find_mw_python)
echo "Using Python: $PYTHON"
SITE_PKG=$("$PYTHON" -c "import site; print(site.getsitepackages()[0])" 2>/dev/null || true)
if [ -z "$SITE_PKG" ]; then
echo "WARNING: Cannot determine site-packages directory; skipping backend patch."
echo " Run: $PYTHON -c \"import site; print(site.getsitepackages())\""
else
# Back up any pre-existing sitecustomize.py that isn't ours.
_can_install=true
if [ -f "$SITE_PKG/sitecustomize.py" ] && \
! grep -q "truecloud-patch" "$SITE_PKG/sitecustomize.py" 2>/dev/null; then
if cp "$SITE_PKG/sitecustomize.py" \
"$SITE_PKG/sitecustomize.py.pre-truecloud-patch"; then
echo "OK: Backed up existing sitecustomize.py"
else
echo "WARNING: Could not back up existing sitecustomize.py; skipping install to avoid data loss."
_can_install=false
fi
fi
if [ "$_can_install" = true ]; then
# Substitute PATCH_DIR into the source so sitecustomize.py knows where
# to write hook_status.json and check the kill switch at runtime.
# Pass PATCH_DIR via env var so arbitrary path characters don't break
# the substitution (sed metacharacters & and | are unsafe in shell-
# interpolated replacement strings). Write to a temp file first so a
# failed substitution never truncates the existing sitecustomize.py.
_sc_tmp="$SITE_PKG/sitecustomize.py.truecloud-tmp"
if TRUECLOUD_PATCH_DIR="$PATCH_DIR" \
"$PYTHON" -c "
import os, sys
d = os.environ['TRUECLOUD_PATCH_DIR']
with open(d + '/patch/sitecustomize.py', encoding='utf-8') as fh:
sys.stdout.write(fh.read().replace('/data/truecloud-patch', d))
" > "$_sc_tmp" && mv "$_sc_tmp" "$SITE_PKG/sitecustomize.py"; then
echo "OK: Installed sitecustomize.py → $SITE_PKG/sitecustomize.py"
else
rm -f "$_sc_tmp"
echo "WARNING: Failed to write $SITE_PKG/sitecustomize.py (permission error?)"
fi
fi
fi
# ── Step 2: Angular bundle ────────────────────────────────────────────────────
echo "--- UI patch ---"
"$PYTHON" "$PATCH_DIR/patch/patch_ui.py" || echo "WARNING: patch_ui.py exited non-zero; UI dropdown may still show Storj only."
# ── Done ──────────────────────────────────────────────────────────────────────
echo "=== done ==="