apiscp-borg/install-layer2.sh
Laurence Horrocks-Barlow 5c2da48c64 feat(gui): group menu under a shared 'Backups' category with a per-plugin icon
Move the admin and site menu entries into a shared 'Backups' sidebar category
(created idempotently by each sibling plugin) with a distinct icon per engine,
instead of a top-level link. Each plugin tags its category+link lines with an
apiscp-borg marker so its uninstaller removes only its own, leaving the shared
category for any other backup plugin still installed.
2026-07-26 05:15:15 +01:00

148 lines
7.9 KiB
Bash

#!/bin/sh
#
# install-layer2.sh: install the native ApisCP integration (module + GUI apps +
# menu links + hooks) into their upgrade-safe locations.
#
# Every target is a git-ignored path ApisCP preserves across upgrades:
# - module -> $CP_ROOT/lib/modules/surrogates/borg.php (.gitignore: surrogates/*)
# - admin app -> $CP_ROOT/config/custom/apps/borg/ (config/custom is all-ignored)
# - site app -> $CP_ROOT/config/custom/apps/myborgbackups/ (site-owner edition)
# - admin menu-> $CP_ROOT/config/custom/templates/admin.php
# - site menu -> $CP_ROOT/config/custom/templates/site.php
#
# Run as root on an ApisCP server AFTER install.sh (which installs the Layer 1
# binaries the module calls). Idempotent.
set -eu
[ "$(id -u)" = 0 ] || { echo "run as root" >&2; exit 1; }
SRC=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
# Locate ApisCP.
CP_ROOT="${CP_ROOT:-/usr/local/apnscp}"
[ -d "$CP_ROOT/lib/modules" ] || { echo "ApisCP not found at $CP_ROOT (set CP_ROOT)" >&2; exit 1; }
OWNER=apnscp
echo "CP_ROOT = $CP_ROOT"
# 1. Backend module.
echo "installing module -> lib/modules/surrogates/borg.php"
install -d -o "$OWNER" -g "$OWNER" "$CP_ROOT/lib/modules/surrogates"
install -m 0644 -o "$OWNER" -g "$OWNER" "$SRC/src/modules/borg.php" "$CP_ROOT/lib/modules/surrogates/borg.php"
# 2. GUI app (whole tree).
echo "installing app -> config/custom/apps/borg/"
install -d -o "$OWNER" -g "$OWNER" "$CP_ROOT/config/custom/apps/borg/views"
install -m 0644 -o "$OWNER" -g "$OWNER" "$SRC/src/apps/borg/borg.php" "$CP_ROOT/config/custom/apps/borg/borg.php"
install -m 0644 -o "$OWNER" -g "$OWNER" "$SRC/src/apps/borg/application.yml" "$CP_ROOT/config/custom/apps/borg/application.yml"
install -m 0644 -o "$OWNER" -g "$OWNER" "$SRC/src/apps/borg/views/index.blade.php" "$CP_ROOT/config/custom/apps/borg/views/index.blade.php"
# 2b. Site-owner GUI app (whole tree). Same module, scoped PRIVILEGE_SITE verbs.
echo "installing site-owner app -> config/custom/apps/myborgbackups/"
install -d -o "$OWNER" -g "$OWNER" "$CP_ROOT/config/custom/apps/myborgbackups/views"
install -m 0644 -o "$OWNER" -g "$OWNER" "$SRC/src/apps/myborgbackups/myborgbackups.php" "$CP_ROOT/config/custom/apps/myborgbackups/myborgbackups.php"
install -m 0644 -o "$OWNER" -g "$OWNER" "$SRC/src/apps/myborgbackups/application.yml" "$CP_ROOT/config/custom/apps/myborgbackups/application.yml"
install -m 0644 -o "$OWNER" -g "$OWNER" "$SRC/src/apps/myborgbackups/views/index.blade.php" "$CP_ROOT/config/custom/apps/myborgbackups/views/index.blade.php"
# 3. Menu link. Append to an existing custom admin.php rather than clobber it.
echo "installing menu link -> config/custom/templates/admin.php"
install -d -o "$OWNER" -g "$OWNER" "$CP_ROOT/config/custom/templates"
DEST="$CP_ROOT/config/custom/templates/admin.php"
if [ -f "$DEST" ]; then
if grep -q "/apps/borg" "$DEST"; then
echo " link already present; leaving $DEST untouched"
else
echo " appending create_link() to existing $DEST"
if grep -q '?>' "$DEST"; then
echo " WARNING: $DEST contains a closing ?>; add the create_link() call by hand" >&2
else
cat >> "$DEST" <<'PHP'
$templateClass->create_category('Backups', true, '<svg role="img" fill="currentColor" class="ui-menu-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V7l-4-4zm-5 16a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm3-10H5V5h10v4z"/></svg>', 'backups'); // apiscp-borg
$templateClass->create_link('Borg', '/apps/borg', true, '<svg role="img" fill="currentColor" class="ui-menu-inline-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>', 'backups'); // apiscp-borg
PHP
fi
fi
else
install -m 0644 -o "$OWNER" -g "$OWNER" "$SRC/src/templates/admin.php" "$DEST"
fi
# 3b. Site-owner menu link. Same append-not-clobber logic for the site template.
echo "installing site menu link -> config/custom/templates/site.php"
SITE_DEST="$CP_ROOT/config/custom/templates/site.php"
if [ -f "$SITE_DEST" ]; then
if grep -q "/apps/myborgbackups" "$SITE_DEST"; then
echo " link already present; leaving $SITE_DEST untouched"
elif grep -q '?>' "$SITE_DEST"; then
echo " WARNING: $SITE_DEST contains a closing ?>; add the create_link() call by hand" >&2
else
echo " appending create_link() to existing $SITE_DEST"
cat >> "$SITE_DEST" <<'PHP'
$templateClass->create_category('Backups', true, '<svg role="img" fill="currentColor" class="ui-menu-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V7l-4-4zm-5 16a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm3-10H5V5h10v4z"/></svg>', 'backups'); // apiscp-borg
$templateClass->create_link('Borg', '/apps/myborgbackups', true, '<svg role="img" fill="currentColor" class="ui-menu-inline-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>', 'backups'); // apiscp-borg
PHP
fi
else
install -m 0644 -o "$OWNER" -g "$OWNER" "$SRC/src/templates/site.php" "$SITE_DEST"
fi
# 3c. Account-lifecycle hooks. ApisCP runs config/custom/hooks/<event>.sh after
# create/suspend/delete with the site id as the first argument. Append-safe:
# only install ours if not already present (do not clobber operator hooks).
echo "installing account hooks -> config/custom/hooks/"
install -d -o "$OWNER" -g "$OWNER" "$CP_ROOT/config/custom/hooks"
for _hook in addDomain suspendDomain deleteDomain; do
_dest="$CP_ROOT/config/custom/hooks/$_hook.sh"
if [ -e "$_dest" ] && ! grep -q 'apiscp-borg' "$_dest" 2>/dev/null; then
echo " $_hook.sh exists and is not ours; leaving it (add our call by hand if wanted)"
else
install -m 0755 -o "$OWNER" -g "$OWNER" "$SRC/src/hooks/$_hook.sh" "$_dest"
echo " installed $_hook.sh"
fi
done
# 4. Sudoers. The panel runs the module as the unprivileged ApisCP user, but the
# Borg repository and /home/virtual are root-only. Let that user run ONLY the
# three plugin binaries as root, nothing else. The binaries validate their own
# inputs and are the privilege boundary.
PANEL_USER="${PANEL_USER:-apnscp}"
echo "installing sudoers -> /etc/sudoers.d/apiscp-borg (user: $PANEL_USER)"
_sudo_tmp=$(mktemp)
cat > "$_sudo_tmp" <<EOF
# Installed by apiscp-borg install-layer2.sh. Do not edit by hand.
Defaults:$PANEL_USER !requiretty
Cmnd_Alias APISCP_BORG_CMDS = /usr/local/bin/borg-apiscp-backup, /usr/local/bin/borg-apiscp-restore, /usr/local/bin/borg-apiscp-repo
$PANEL_USER ALL=(root) NOPASSWD: APISCP_BORG_CMDS
EOF
if visudo -cf "$_sudo_tmp" >/dev/null 2>&1; then
install -m 0440 -o root -g root "$_sudo_tmp" /etc/sudoers.d/apiscp-borg
echo " sudoers installed and validated"
else
echo " WARNING: sudoers validation failed; the GUI will not be able to run as root" >&2
visudo -cf "$_sudo_tmp" 2>&1 | sed 's/^/ /' >&2
fi
rm -f "$_sudo_tmp"
# Keep the engine config root-only; it holds the repository passphrase and is
# read/written by the root binaries, never directly by the panel user.
[ -f /etc/apiscp-borg/config ] && chown root:root /etc/apiscp-borg/config && chmod 0600 /etc/apiscp-borg/config
# Restart the panel so it picks up the new apps and menus (module is picked up
# on next CLI/API call without a restart).
echo "restarting ApisCP panel"
systemctl restart apnscp || echo " restart apnscp yourself to see the new menu entry" >&2
cat <<'EOF'
Layer 2 installed. Verify:
cpcmd borg:sites
cpcmd borg:status
Then load the appliance-admin panel; "Borg Backups" appears in the menu.
Site owners get a scoped "My Borg Backups" entry in the site panel (Account
section) backed by the PRIVILEGE_SITE verbs (borg:my_site, my_snapshots,
restore_my_files, my_databases, my_database_backups, restore_my_database,
import_my_database, restore_my_account, run_my_backup). The menu is
session-cached, so a site owner sees it after a fresh login.
EOF