LibrePortal/install.sh
librelad 9207ec384b feat(install): thin bootstrap installer install.sh (phase B)
curl -fsSL https://get.libreportal.org/install.sh | sudo bash [-s -- FLAGS]

Self-contained bootstrap (downloaded and run before any LibrePortal code exists):
resolves the channel manifest (<base>/<channel>/latest.json), downloads the
release tarball, VERIFIES the sha256 (manifest or .sha256 sidecar) and refuses on
mismatch, extracts to <system>/install, then hands off to init.sh unattended with
the relocatable flags (--system-dir/--containers-dir/--backups-dir/--manager-user/
--allow-home). curl-or-wget, sha256sum-or-shasum; honours LP_RELEASE_BASE_URL for
testing against a local/file server.

Dev modes preserved: --local=PATH and --git-url= (with --git-user/--git-token).
Generates a random manager/WebUI password if none given. --dry-run stages + verifies
without installing.

Verified against a local http server: dry-run resolves→downloads→verifies→extracts
and reports the correct init.sh handoff; a corrupted tarball is refused. The
end-to-end exec needs init.sh release-mode awareness (phase C) + a throwaway box.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-25 18:04:13 +01:00

175 lines
7.8 KiB
Bash

#!/usr/bin/env bash
#
# LibrePortal bootstrap installer.
#
# curl -fsSL https://get.libreportal.org/install.sh | sudo bash
# curl -fsSL https://get.libreportal.org/install.sh | sudo bash -s -- \
# --system-dir=/mnt/ssd/lp --backups-dir=/mnt/usb/lp-backups --manager-user=admin
#
# Fetches a versioned, checksum-verified release tarball over plain HTTPS (no git,
# no auth), extracts it to the system root, and hands off to init.sh. Git/local
# remain available for development. Self-contained on purpose — it's downloaded and
# run on its own, before any LibrePortal code exists on the box.
set -euo pipefail
# --- defaults ---------------------------------------------------------------
CHANNEL="stable"
VERSION_PIN=""
MODE="release" # release | local | git
LOCAL_SRC=""
GIT_URL="" ; GIT_USER="" ; GIT_TOKEN=""
RELEASE_BASE_URL="${LP_RELEASE_BASE_URL:-https://get.libreportal.org}"
PASSWORD="${LP_PASSWORD:-}"
SYSTEM_DIR="" ; CONTAINERS_DIR="" ; BACKUPS_DIR="" ; MANAGER_USER="" ; ALLOW_HOME=""
DRY_RUN=0
usage() {
sed -n '3,12p' "$0" | sed 's/^# \{0,1\}//'
cat <<'EOF'
Options:
--channel=stable|edge Release channel (default stable)
--version=X.Y.Z Pin a specific version (else latest in channel)
--system-dir=PATH Manager-owned control plane (default /libreportal-system)
--containers-dir=PATH Live app data (default /libreportal-containers)
--backups-dir=PATH Backup repos (default /libreportal-backups)
--manager-user=NAME Control-plane user (default libreportal)
--allow-home Permit containers/backups inside /home/<user>
--password=PASS Manager/WebUI password (else a random one is generated)
--base-url=URL Override the release host (also: LP_RELEASE_BASE_URL)
--local=PATH Dev: install from a local source folder
--git-url=URL Dev: install by cloning a git repo
--git-user=U --git-token=T Dev: git credentials
--dry-run Fetch + verify + stage, but don't install
EOF
}
# --- parse flags ------------------------------------------------------------
for arg in "$@"; do
case "$arg" in
--channel=*) CHANNEL="${arg#*=}" ;;
--version=*) VERSION_PIN="${arg#*=}" ;;
--system-dir=*) SYSTEM_DIR="${arg#*=}" ;;
--containers-dir=*) CONTAINERS_DIR="${arg#*=}" ;;
--backups-dir=*) BACKUPS_DIR="${arg#*=}" ;;
--manager-user=*) MANAGER_USER="${arg#*=}" ;;
--allow-home) ALLOW_HOME="--allow-home" ;;
--password=*) PASSWORD="${arg#*=}" ;;
--base-url=*) RELEASE_BASE_URL="${arg#*=}" ;;
--local=*) MODE="local"; LOCAL_SRC="${arg#*=}" ;;
--git-url=*) MODE="git"; GIT_URL="${arg#*=}" ;;
--git-user=*) GIT_USER="${arg#*=}" ;;
--git-token=*) GIT_TOKEN="${arg#*=}" ;;
--dry-run) DRY_RUN=1 ;;
-h|--help) usage; exit 0 ;;
*) echo "install.sh: unknown option '$arg' (try --help)" >&2; exit 2 ;;
esac
done
err() { echo "install.sh: $*" >&2; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || err "missing required tool: $1"; }
[[ $DRY_RUN -eq 1 || $EUID -eq 0 ]] || err "must run as root (try: sudo). Use --dry-run to test fetch only."
# Resolve the system root (honour env; the default matches paths.sh).
SYSTEM_DIR="${SYSTEM_DIR:-${LP_SYSTEM_DIR:-/libreportal-system}}"
INSTALL_DIR="$SYSTEM_DIR/install"
need tar
if command -v curl >/dev/null 2>&1; then DL=curl
elif command -v wget >/dev/null 2>&1; then DL=wget
else err "need curl or wget"; fi
SHACMD=""
command -v sha256sum >/dev/null 2>&1 && SHACMD="sha256sum"
[[ -z "$SHACMD" ]] && command -v shasum >/dev/null 2>&1 && SHACMD="shasum -a 256"
[[ -n "$SHACMD" || "$MODE" != "release" ]] || err "need sha256sum or shasum to verify the release"
fetch() { # fetch <url> [outfile] (outfile omitted -> stdout)
if [[ "$DL" == curl ]]; then
[[ -n "${2:-}" ]] && curl -fsSL "$1" -o "$2" || curl -fsSL "$1"
else
[[ -n "${2:-}" ]] && wget -qO "$2" "$1" || wget -qO- "$1"
fi
}
json_str() { printf '%s' "$1" | grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed -E 's/.*"([^"]*)"$/\1/'; }
STAGE="$(mktemp -d)"; trap 'rm -rf "$STAGE"' EXIT
SRC=""
# --- obtain the code --------------------------------------------------------
case "$MODE" in
release)
local_ver="$VERSION_PIN" tarname="" want_sha=""
if [[ -z "$local_ver" ]]; then
echo "Resolving $CHANNEL channel from $RELEASE_BASE_URL ..."
manifest="$(fetch "$RELEASE_BASE_URL/$CHANNEL/latest.json")" || err "cannot fetch channel manifest"
local_ver="$(json_str "$manifest" version)"
tarname="$(json_str "$manifest" url)"
want_sha="$(json_str "$manifest" sha256)"
fi
[[ -n "$local_ver" ]] || err "could not determine version"
[[ -n "$tarname" ]] || tarname="libreportal-${local_ver}.tar.gz"
tarurl="$RELEASE_BASE_URL/$CHANNEL/$tarname"
echo "Downloading $tarurl ..."
fetch "$tarurl" "$STAGE/$tarname" || err "download failed"
if [[ -z "$want_sha" ]]; then
fetch "$tarurl.sha256" "$STAGE/$tarname.sha256" || err "no checksum available for $tarname"
want_sha="$(cut -d' ' -f1 < "$STAGE/$tarname.sha256")"
fi
got_sha="$($SHACMD "$STAGE/$tarname" | cut -d' ' -f1)"
[[ "$got_sha" == "$want_sha" ]] || err "CHECKSUM MISMATCH — refusing to install. expected $want_sha, got $got_sha"
echo "✓ checksum verified ($got_sha)"
mkdir -p "$STAGE/x"
tar xzf "$STAGE/$tarname" -C "$STAGE/x" --strip-components=1
SRC="$STAGE/x"
;;
local)
[[ -d "$LOCAL_SRC" ]] || err "local source folder not found: $LOCAL_SRC"
SRC="$LOCAL_SRC"
;;
git)
need git
[[ -n "$GIT_URL" ]] || err "--git-url is required for a git install"
local_url="$GIT_URL"
[[ -n "$GIT_USER" ]] && local_url="${GIT_URL%://*}://${GIT_USER}:${GIT_TOKEN}@${GIT_URL#*://}"
git clone -q "$local_url" "$STAGE/x" || err "git clone failed"
SRC="$STAGE/x"
;;
esac
[[ -f "$SRC/init.sh" && -d "$SRC/scripts" ]] || err "fetched tree is not a LibrePortal source (missing init.sh/scripts)"
if [[ $DRY_RUN -eq 1 ]]; then
echo "[dry-run] verified $MODE source at: $SRC"
echo "[dry-run] would copy -> $INSTALL_DIR and run init.sh with:"
echo " system=$SYSTEM_DIR containers=${CONTAINERS_DIR:-(default)} backups=${BACKUPS_DIR:-(default)} manager=${MANAGER_USER:-(default)} $ALLOW_HOME"
exit 0
fi
# --- place the code + hand off to init.sh -----------------------------------
[[ -n "$PASSWORD" ]] || { PASSWORD="$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | cut -c1-20)"; GEN_PW=1; }
mkdir -p "$INSTALL_DIR"
cp -a "$SRC/." "$INSTALL_DIR/"
cp -f "$INSTALL_DIR/init.sh" /root/ 2>/dev/null || true
flags=( --unattended )
[[ -n "$SYSTEM_DIR" ]] && flags+=( "--system-dir=$SYSTEM_DIR" )
[[ -n "$CONTAINERS_DIR" ]] && flags+=( "--containers-dir=$CONTAINERS_DIR" )
[[ -n "$BACKUPS_DIR" ]] && flags+=( "--backups-dir=$BACKUPS_DIR" )
[[ -n "$MANAGER_USER" ]] && flags+=( "--manager-user=$MANAGER_USER" )
[[ -n "$ALLOW_HOME" ]] && flags+=( "$ALLOW_HOME" )
# Positional contract: init <password> <git_user> <git_token> <git_url> <unattended> <install_mode>
case "$MODE" in
git) set -- init "$PASSWORD" "${GIT_USER:-empty}" "${GIT_TOKEN:-empty}" "$GIT_URL" true git ;;
local) set -- init "$PASSWORD" empty empty empty true local ;;
*) set -- init "$PASSWORD" empty empty empty true release ;;
esac
[[ "${GEN_PW:-0}" == 1 ]] && echo "Generated manager/WebUI password: $PASSWORD (also recorded in the install log)"
echo "Running installer ..."
cd "$INSTALL_DIR"
exec bash "$INSTALL_DIR/init.sh" "${flags[@]}" "$@"