#!/usr/bin/env bash # peardata interactive installer # # One-liner (from this repo on Gitea): # curl -fsSL https://git.ssh.surf/snxraven/peardata/raw/branch/main/scripts/install.sh | bash # # Optional install host (if you mirror the script): # curl -fsSL https://install.peardata.boats | bash # # Non-interactive: # curl -fsSL …/install.sh | bash -s -- --server --yes # curl -fsSL …/install.sh | bash -s -- --client --yes # PEARDATA_ROLE=server bash scripts/install.sh --yes # # Env overrides: # PEARDATA_ROLE=server|client|both # PEARDATA_VERSION / PEARDATA_TAG=rolling # PEARDATA_GITEA_URL=https://git.ssh.surf # PEARDATA_OWNER=snxraven PEARDATA_REPO=peardata # PEARDATA_SERVER_DIR=/opt/peardata # PEARDATA_CLIENT_DIR=... (default: ~/.local/share/peardata or ~/Applications) # PEARDATA_YES=1 — assume defaults / non-interactive where possible # set -euo pipefail # ─── defaults ─────────────────────────────────────────────────────────────── GITEA_URL="${PEARDATA_GITEA_URL:-https://git.ssh.surf}" OWNER="${PEARDATA_OWNER:-snxraven}" REPO="${PEARDATA_REPO:-peardata}" TAG="${PEARDATA_TAG:-rolling}" VERSION="${PEARDATA_VERSION:-}" # empty → resolve from release assets ROLE="${PEARDATA_ROLE:-}" ASSUME_YES="${PEARDATA_YES:-0}" SERVER_DIR="${PEARDATA_SERVER_DIR:-/opt/peardata}" TMPDIR_ROOT="${TMPDIR:-/tmp}" INSTALL_TMP="" # ─── colors / UI ──────────────────────────────────────────────────────────── if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then C_TEAL="$(tput setaf 6)" C_BOLD="$(tput bold)" C_DIM="$(tput dim)" C_RED="$(tput setaf 1)" C_GREEN="$(tput setaf 2)" C_YELLOW="$(tput setaf 3)" C_RESET="$(tput sgr0)" else C_TEAL="" C_BOLD="" C_DIM="" C_RED="" C_GREEN="" C_YELLOW="" C_RESET="" fi log() { printf '%s[peardata]%s %s\n' "$C_TEAL" "$C_RESET" "$*"; } ok() { printf '%s[peardata]%s %s✓%s %s\n' "$C_TEAL" "$C_RESET" "$C_GREEN" "$C_RESET" "$*"; } warn() { printf '%s[peardata]%s %s!%s %s\n' "$C_TEAL" "$C_RESET" "$C_YELLOW" "$C_RESET" "$*" >&2; } err() { printf '%s[peardata]%s %s✗%s %s\n' "$C_TEAL" "$C_RESET" "$C_RED" "$C_RESET" "$*" >&2; } die() { err "$*"; exit 1; } banner() { cat </dev/null 2>&1 || die "Required command not found: $1" } # ─── detect host ──────────────────────────────────────────────────────────── detect_os() { local u u="$(uname -s 2>/dev/null || echo unknown)" case "$u" in Linux*) echo linux ;; Darwin*) echo darwin ;; MINGW*|MSYS*|CYGWIN*) echo win32 ;; *) echo "unknown" ;; esac } detect_arch() { local m m="$(uname -m 2>/dev/null || echo unknown)" case "$m" in x86_64|amd64) echo x64 ;; aarch64|arm64) echo arm64 ;; armv7*|armhf) die "32-bit / armv7 is not supported (64-bit only)" ;; i386|i686) die "32-bit x86 is not supported (64-bit only)" ;; *) die "Unsupported architecture: $m" ;; esac } host_triple() { local os arch os="$(detect_os)" arch="$(detect_arch)" [[ "$os" == "unknown" ]] && die "Unsupported OS: $(uname -s)" [[ "$os" == "win32" ]] && die "Windows install via bash is limited — download a .tar.gz from the rolling release instead." echo "${os}-${arch}" } # First existing Docker Engine unix socket (empty if none). detect_docker_socket() { local p for p in \ "${PEARDATA_DOCKER_SOCKET:-}" \ /var/run/docker.sock \ /run/docker.sock \ /var/run/podman/podman.sock \ /run/podman/podman.sock do [[ -n "$p" && -S "$p" ]] || continue printf '%s\n' "$p" return 0 done return 1 } # ─── args ─────────────────────────────────────────────────────────────────── parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --server|-s) ROLE=server; shift ;; --client|-c) ROLE=client; shift ;; --both|-b) ROLE=both; shift ;; --yes|-y) ASSUME_YES=1; shift ;; --tag) TAG="${2:-}"; shift 2 ;; --version) VERSION="${2:-}"; shift 2 ;; --server-dir) SERVER_DIR="${2:-}"; shift 2 ;; --help|-h) cat </dev/tty else printf '%s: ' "$prompt" >/dev/tty fi IFS= read -r reply /dev/null 2>&1 } run_root() { if [[ "$(id -u)" -eq 0 ]]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else die "Need root privileges for: $*" fi } download() { local url="$1" out="$2" log "Downloading $(basename "$url")…" if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 -o "$out" "$url" \ || die "Download failed: $url" elif command -v wget >/dev/null 2>&1; then wget -q -O "$out" "$url" || die "Download failed: $url" else die "Need curl or wget" fi } json_get_assets() { local api="${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}/releases/tags/${TAG}" local json json="$(curl -fsSL --connect-timeout 15 "$api" 2>/dev/null || true)" if [[ -z "$json" ]]; then return 1 fi printf '%s' "$json" | tr ',' '\n' | sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | grep -E '^peardata-(server|client)-' || true } resolve_version() { if [[ -n "$VERSION" ]]; then echo "$VERSION" return fi local assets name assets="$(json_get_assets || true)" if [[ -n "$assets" ]]; then name="$(printf '%s\n' "$assets" | grep -E "^peardata-server-[0-9]" | head -1 || true)" if [[ -z "$name" ]]; then name="$(printf '%s\n' "$assets" | grep -E "^peardata-client-[0-9]" | head -1 || true)" fi if [[ -n "$name" ]]; then # peardata-server-0.1.0-linux-x64.tar.gz echo "$name" | sed -E 's/^peardata-(server|client)-([0-9][^/]*)-(linux|darwin|win32)-.*/\2/' return fi fi local ver ver="$(curl -fsSL "${GITEA_URL}/${OWNER}/${REPO}/raw/branch/main/package.json" 2>/dev/null \ | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1 || true)" if [[ -n "$ver" ]]; then echo "$ver" return fi die "Could not resolve release version — set PEARDATA_VERSION=x.y.z" } asset_url() { local name="$1" echo "${GITEA_URL}/${OWNER}/${REPO}/releases/download/${TAG}/${name}" } # ─── server install (Linux Bare binary + systemd) ─────────────────────────── install_server() { local host="$1" version="$2" local os arch archive url tarball work bin_src unit_path os="${host%%-*}" arch="${host#*-}" [[ "$os" == "linux" ]] || die "Server install is supported on Linux only (detected: $os). Download a client for $host, or run the agent on a Linux host." need_cmd tar if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then die "Need curl or wget" fi archive="peardata-server-${version}-${host}.tar.gz" url="$(asset_url "$archive")" work="${INSTALL_TMP}/server" mkdir -p "$work" tarball="${work}/${archive}" download "$url" "$tarball" if curl -fsSL --connect-timeout 10 -o "${tarball}.sha256" "${url}.sha256" 2>/dev/null; then if command -v sha256sum >/dev/null 2>&1; then (cd "$work" && sha256sum -c "$(basename "$tarball").sha256") && ok "Checksum OK" || warn "Checksum verify failed (continuing)" elif command -v shasum >/dev/null 2>&1; then (cd "$work" && shasum -a 256 -c "$(basename "$tarball").sha256") && ok "Checksum OK" || warn "Checksum verify failed (continuing)" fi fi mkdir -p "${work}/extract" tar -xzf "$tarball" -C "${work}/extract" bin_src="$(find "${work}/extract" -type f -name 'peardata-server' | head -1 || true)" [[ -n "$bin_src" ]] || die "peardata-server binary not found in archive" chmod +x "$bin_src" log "Installing server to ${SERVER_DIR} (requires root)…" have_sudo || die "Server install needs root/sudo" run_root mkdir -p "${SERVER_DIR}/data" run_root install -m 0755 "$bin_src" "${SERVER_DIR}/peardata-server" if [[ ! -f "${SERVER_DIR}/.env" ]]; then run_root touch "${SERVER_DIR}/.env" run_root chmod 600 "${SERVER_DIR}/.env" fi if ! id peardata >/dev/null 2>&1; then log "Creating system user peardata…" if command -v useradd >/dev/null 2>&1; then run_root useradd --system --home "$SERVER_DIR" --shell /usr/sbin/nologin peardata 2>/dev/null \ || run_root useradd --system --home "$SERVER_DIR" --shell /bin/false peardata elif command -v adduser >/dev/null 2>&1; then run_root adduser --system --home "$SERVER_DIR" --shell /usr/sbin/nologin --no-create-home peardata || true else die "Cannot create peardata user (need useradd or adduser)" fi fi id peardata >/dev/null 2>&1 || die "peardata user was not created" # Host journal access for Logs tab (journalctl as peardata) if getent group systemd-journal >/dev/null 2>&1; then log "Adding peardata to systemd-journal for host log access…" if command -v usermod >/dev/null 2>&1; then run_root usermod -aG systemd-journal peardata || warn "usermod -aG systemd-journal peardata failed" elif command -v gpasswd >/dev/null 2>&1; then run_root gpasswd -a peardata systemd-journal || warn "gpasswd -a peardata systemd-journal failed" else warn "Could not add peardata to systemd-journal (no usermod/gpasswd)" fi else warn "Group systemd-journal not found — host Journal in Logs may be unavailable" fi # Ensure journal is enabled in .env (default on; operators may set PEARDATA_JOURNAL=0) if ! run_root grep -qE '^PEARDATA_JOURNAL=' "${SERVER_DIR}/.env" 2>/dev/null; then log "Enabling PEARDATA_JOURNAL=1 in ${SERVER_DIR}/.env" run_root sh -c "printf '\\n# Host journal for Logs tab (set to 0 to disable)\\nPEARDATA_JOURNAL=1\\n' >> '${SERVER_DIR}/.env'" fi # Docker socket access — container names (e.g. dozzle) + per-container metrics local docker_detected=0 local docker_sock="" local docker_group_name="" docker_sock="$(detect_docker_socket || true)" if [[ -n "$docker_sock" ]]; then docker_detected=1 elif getent group docker >/dev/null 2>&1; then docker_detected=1 docker_sock="/var/run/docker.sock" elif command -v docker >/dev/null 2>&1; then docker_detected=1 docker_sock="/var/run/docker.sock" fi if getent group docker >/dev/null 2>&1; then docker_group_name="docker" fi if [[ "$docker_detected" -eq 1 ]]; then ok "Docker detected${docker_sock:+ (socket ${docker_sock})}" if [[ -n "$docker_group_name" ]]; then log "Adding peardata to ${docker_group_name} for Docker socket / container names…" if command -v usermod >/dev/null 2>&1; then run_root usermod -aG "$docker_group_name" peardata || warn "usermod -aG ${docker_group_name} peardata failed" elif command -v gpasswd >/dev/null 2>&1; then run_root gpasswd -a peardata "$docker_group_name" || warn "gpasswd -a peardata ${docker_group_name} failed" else warn "Could not add peardata to ${docker_group_name} (no usermod/gpasswd)" fi else warn "Docker socket present but no 'docker' group — grant peardata read access to ${docker_sock} manually" fi if ! run_root grep -qE '^PEARDATA_DOCKER=' "${SERVER_DIR}/.env" 2>/dev/null; then log "Enabling PEARDATA_DOCKER=1 in ${SERVER_DIR}/.env (container metrics + names)" run_root sh -c "printf '\\n# Docker collector (names via socket; set to 0 to disable)\\nPEARDATA_DOCKER=1\\nPEARDATA_DOCKER_SOCKET=%s\\n' '${docker_sock}' >> '${SERVER_DIR}/.env'" else # Keep collector flag; refresh socket path if unset if ! run_root grep -qE '^PEARDATA_DOCKER_SOCKET=' "${SERVER_DIR}/.env" 2>/dev/null; then run_root sh -c "printf 'PEARDATA_DOCKER_SOCKET=%s\\n' '${docker_sock}' >> '${SERVER_DIR}/.env'" fi fi else log "Docker not detected — skipping socket access (install Docker later, then re-run installer or: usermod -aG docker peardata && PEARDATA_DOCKER=1)" fi run_root chown -R peardata:peardata "$SERVER_DIR" 2>/dev/null || run_root chown -R peardata "$SERVER_DIR" unit_path="/etc/systemd/system/peardata.service" # systemd SupplementaryGroups replaces the user's supplementary set — list every group we need local supp_groups=() if getent group systemd-journal >/dev/null 2>&1; then supp_groups+=("systemd-journal") fi if [[ "$docker_detected" -eq 1 && -n "$docker_group_name" ]]; then supp_groups+=("$docker_group_name") fi local supp_groups_line="" if [[ ${#supp_groups[@]} -gt 0 ]]; then supp_groups_line="SupplementaryGroups=${supp_groups[*]}" fi local docker_after_line="" local docker_wants_line="" if [[ "$docker_detected" -eq 1 ]]; then docker_after_line="After=docker.service" docker_wants_line="Wants=docker.service" fi log "Writing ${unit_path}…" run_root tee "$unit_path" >/dev/null </dev/null 2>&1; then run_root systemctl daemon-reload if confirm "Enable and start peardata.service now?" "y"; then run_root systemctl enable --now peardata.service ok "Service peardata.service is enabled and started" sleep 2 run_root systemctl --no-pager --full status peardata.service || true log "Logs: journalctl -u peardata -f" else ok "Unit installed. Start later with: sudo systemctl enable --now peardata" fi else warn "systemctl not found — binary installed at ${SERVER_DIR}/peardata-server (start manually)" fi local env_pk="" if [[ -f "${SERVER_DIR}/.env" ]]; then env_pk="$(run_root grep -E '^SERVER_PUBLIC_KEY=' "${SERVER_DIR}/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '[:space:]' || true)" fi local docker_summary="not detected (optional)" if [[ "$docker_detected" -eq 1 ]]; then docker_summary="enabled · peardata ∈ ${docker_group_name:-docker} · ${docker_sock} (human container names)" fi cat </dev/null || true ok "Client app: $app" if confirm "Open PearData now?" "y"; then open "$app" || true fi cat </dev/null | head -1 || true)" install_desktop_entry_linux "$bin" "$icon" ok "Client binary: $bin" ok "Launcher: ${bindir}/peardata-client (ensure ~/.local/bin is on PATH)" cat <