diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..6001fa4 --- /dev/null +++ b/install.sh @@ -0,0 +1,174 @@ +#!/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/ + --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 [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 +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[@]}" "$@"