#!/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, '', 'backups'); // apiscp-borg $templateClass->create_link('Borg', '/apps/borg', true, '', '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, '', 'backups'); // apiscp-borg $templateClass->create_link('Borg', '/apps/myborgbackups', true, '', '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/.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" </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