LibrePortal/install.sh
librelad 90663a077a feat(install): release fetch mode + lpFetchSource abstraction (phase C)
scripts/source/fetch.sh (sourced at runtime via files_source.sh):
- lpFetchRelease [ver]: resolve channel manifest -> download tarball -> VERIFY
  sha256 (refuse on mismatch/absence) -> replace the install tree ( is
  code-only now; configs/logs live in the separate system tree, so no backup
  dance). Host/channel from LP_RELEASE_BASE_URL/CFG_RELEASE_BASE_URL + channel.
- lpFetchSource: dispatch release|git|local.
- lpVersionGt: numeric dotted semver compare (used by the updater + badge).

init.sh initGIT is now release-aware: the bootstrap (install.sh) stages+verifies
the code and sets LP_ALREADY_FETCHED=1 (skip re-fetch); a direct release run sources
fetch.sh; a bare /root reinstall is directed to install.sh. install.sh exports
LP_ALREADY_FETCHED + LP_RELEASE_BASE_URL on hand-off. validateUnattended already
accepts release (git-url is gated on git mode).

Config: CFG_INSTALL_MODE default -> release, + CFG_RELEASE_BASE_URL / CFG_RELEASE_CHANNEL
(add-only reconcile preserves existing installs' git/local mode).

Verified: lpVersionGt across cases; lpFetchRelease downloads+verifies+extracts a
clean tree against a local server. The updater + reset/reinstall release paths are
phase D.

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

178 lines
8.0 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"
# Tell init.sh the code is already staged + verified, so initGIT skips re-fetching.
export LP_ALREADY_FETCHED=1
export LP_RELEASE_BASE_URL # carry the (possibly overridden) host through
exec bash "$INSTALL_DIR/init.sh" "${flags[@]}" "$@"