#!/bin/bash # lrops.sh - LiveReview Operations Script # Version: 1.0.0 # Description: One-line installer and management tool for LiveReview # Repository: https://github.com/HexmosTech/LiveReview set -euo pipefail # Exit on error, undefined vars, pipe failures # Ensure the script runs under Bash only if [[ -z "${BASH_VERSION:-}" ]]; then echo "This script must be run with Bash." >&2 echo "Try: bash lrops.sh [options]" >&2 exit 1 fi # ============================================================================= # SCRIPT METADATA AND CONSTANTS # ============================================================================= SCRIPT_VERSION="1.0.0" SCRIPT_NAME="lrops.sh" # Resolve invoking user and home directory robustly (works with sudo) # Priority: SUDO_UID/SUDO_USER -> tilde expansion -> current $HOME INVOKING_USER="${SUDO_USER:-${USER:-$(id -un 2>/dev/null || echo "")}}" if [[ -n "${SUDO_UID:-}" ]]; then INVOKING_HOME="$(getent passwd "${SUDO_UID}" 2>/dev/null | awk -F: '{print $6}')" fi if [[ -z "${INVOKING_HOME:-}" || ! -d "$INVOKING_HOME" ]]; then if [[ -n "${SUDO_USER:-}" ]]; then INVOKING_HOME="$(eval echo ~"${SUDO_USER}")" fi fi if [[ -z "${INVOKING_HOME:-}" || ! -d "$INVOKING_HOME" ]]; then INVOKING_HOME="${HOME}" fi # Default install dir: invoking user's home (never root's HOME when run via sudo) DEFAULT_HOME_DIR="${INVOKING_HOME}" LIVEREVIEW_INSTALL_DIR="${LIVEREVIEW_INSTALL_DIR:-${DEFAULT_HOME_DIR}/livereview}" LIVEREVIEW_SCRIPT_PATH="/usr/local/bin/lrops.sh" GITHUB_REPO="HexmosTech/LiveReview" GITHUB_API_BASE="https://api.github.com/repos/${GITHUB_REPO}" DOCKER_REGISTRY="ghcr.io/hexmostech" DOCKER_IMAGE="livereview" BACKUP_RETENTION_COUNT="10" # Number of pre-update backups to keep (oldest pruned) # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' BOLD='\033[1m' GRAY='\033[0;90m' NC='\033[0m' # No Color # ============================================================================= # LOGGING AND OUTPUT FUNCTIONS # ============================================================================= log_info() { echo -e "${BLUE}ℹ️ INFO:${NC} $*" >&2 } log_success() { echo -e "${GREEN}✅ SUCCESS:${NC} $*" >&2 } log_warning() { echo -e "${YELLOW}⚠️ WARNING:${NC} $*" >&2 } log_error() { echo -e "${RED}❌ ERROR:${NC} $*" >&2 } log_debug() { if [[ "${VERBOSE:-false}" == "true" ]]; then echo -e "${PURPLE}🔍 DEBUG:${NC} $*" >&2 fi } progress() { echo -e "${CYAN}🔄 $*${NC}" >&2 } section_header() { echo >&2 echo -e "${BLUE}$(printf '=%.0s' {1..80})${NC}" >&2 echo -e "${BLUE}📋 $*${NC}" >&2 echo -e "${BLUE}$(printf '=%.0s' {1..80})${NC}" >&2 } # Simple progress countdown with inline updates (one line) progress_sleep() { local seconds=${1:-0} local label=${2:-"Waiting"} local i for (( i=1; i<=seconds; i++ )); do printf "\r%s: %2ds/%2ds" "$label" "$i" "$seconds" >&2 sleep 1 done echo >&2 } # ============================================================================= # PORTABLE SED (GNU/BSD) HELPERS # ============================================================================= # sed -i behaves differently on macOS (BSD sed) vs GNU sed. These helpers # provide a uniform interface: sed_inplace 's/a/b/' path/to/file sed_inplace() { # Usage: sed_inplace 'SED_SCRIPT' FILE local script="$1" local file="$2" case "$(uname -s)" in Darwin) sed -i '' "$script" "$file" ;; *) sed -i "$script" "$file" ;; esac } sudo_sed_inplace() { # Usage: sudo_sed_inplace 'SED_SCRIPT' FILE local script="$1" local file="$2" case "$(uname -s)" in Darwin) sudo sed -i '' "$script" "$file" ;; *) sudo sed -i "$script" "$file" ;; esac } # ============================================================================= # ERROR HANDLING AND CLEANUP # ============================================================================= cleanup() { local exit_code=$? if [[ $exit_code -ne 0 ]]; then log_error "Script failed with exit code $exit_code" log_info "For troubleshooting help, run: $0 --help" fi # Stop sudo keepalive process if running if [[ -n "${SUDO_REFRESH_PID:-}" ]]; then kill "${SUDO_REFRESH_PID}" 2>/dev/null || true fi exit $exit_code } trap cleanup EXIT error_exit() { log_error "$1" exit "${2:-1}" } # ============================================================================= # SUDO SESSION AND DOCKER PRIVILEGES # ============================================================================= # Keep sudo alive during script run to avoid repeated prompts ensure_sudo_session() { # Only if not already root and sudo is available if [[ $EUID -ne 0 ]] && command -v sudo >/dev/null 2>&1; then log_info "Requesting sudo access upfront (to avoid repeated prompts)..." if sudo -v; then # Refresh sudo timestamp in background ( while true; do sleep 60 sudo -n true 2>/dev/null || true done ) & SUDO_REFRESH_PID=$! log_debug "Sudo keepalive process started (PID: $SUDO_REFRESH_PID)" else log_warning "Could not obtain sudo credentials now; you may be prompted later." fi fi } # Start sudo keepalive process for self-update operations start_sudo_keepalive() { # Only if not already root and sudo is available if [[ $EUID -ne 0 ]] && command -v sudo >/dev/null 2>&1; then if sudo -v; then # Refresh sudo timestamp in background ( while true; do sleep 60 sudo -n true 2>/dev/null || true done ) & SUDO_REFRESH_PID=$! log_debug "Sudo keepalive process started (PID: $SUDO_REFRESH_PID)" else log_warning "Could not obtain sudo credentials for self-update" return 1 fi fi } # Stop sudo keepalive process stop_sudo_keepalive() { if [[ -n "${SUDO_REFRESH_PID:-}" ]]; then kill "${SUDO_REFRESH_PID}" 2>/dev/null || true unset SUDO_REFRESH_PID log_debug "Sudo keepalive process stopped" fi } # If Docker requires sudo, transparently wrap docker/docker-compose commands maybe_enable_sudo_for_docker() { # If docker CLI not present, nothing to do here command -v docker >/dev/null 2>&1 || return 0 if docker info >/dev/null 2>&1; then return 0 # No sudo needed fi # Try with sudo non-interactively first if command -v sudo >/dev/null 2>&1 && sudo -n docker info >/dev/null 2>&1; then : else # Fall back to interactive sudo attempt (may prompt once) if command -v sudo >/dev/null 2>&1 && sudo docker info >/dev/null 2>&1; then : else return 0 # Cannot use sudo either; let the regular checks report errors fi fi # At this point docker works with sudo, set wrappers log_info "Docker requires sudo; enabling automatic sudo for Docker commands" USE_SUDO_DOCKER=true # Define shell function for docker (covers 'docker compose' plugin) docker() { command sudo docker "$@"; } } # ============================================================================= # LIVEREVIEW INSTALLATION DETECTION # ============================================================================= # Detect LiveReview installation directory automatically detect_livereview_installation() { local detected_dir="" # Method 1: Check default location local default_dir="${DEFAULT_HOME_DIR}/livereview" if [[ -f "$default_dir/docker-compose.yml" && -f "$default_dir/.env" ]]; then detected_dir="$default_dir" log_debug "Found LiveReview installation at default location: $detected_dir" fi # Method 2: Check environment variable override if [[ -n "${LIVEREVIEW_INSTALL_DIR:-}" && "$LIVEREVIEW_INSTALL_DIR" != "$default_dir" ]]; then if [[ -f "$LIVEREVIEW_INSTALL_DIR/docker-compose.yml" && -f "$LIVEREVIEW_INSTALL_DIR/.env" ]]; then detected_dir="$LIVEREVIEW_INSTALL_DIR" log_debug "Found LiveReview installation at specified location: $detected_dir" fi fi # Method 3: Check other common locations if [[ -z "$detected_dir" ]]; then local common_locations=( "$default_dir" "./livereview" "." ) for location in "${common_locations[@]}"; do if [[ -f "$location/docker-compose.yml" && -f "$location/.env" ]]; then # Verify it's actually a LiveReview installation by checking for specific content if grep -q "livereview-app\|livereview-db" "$location/docker-compose.yml" 2>/dev/null; then detected_dir="$(realpath "$location")" log_debug "Found LiveReview installation at: $detected_dir" break fi fi done fi # Method 4: Try to detect from running Docker containers if [[ -z "$detected_dir" ]] && command -v docker >/dev/null 2>&1; then log_debug "Attempting to detect installation from running containers..." # Look for LiveReview containers and try to find their compose file local container_id container_id=$(docker ps --filter "name=livereview" --format "{{.ID}}" | head -1) if [[ -n "$container_id" ]]; then # Try to get the working directory or volume mounts local inspect_result inspect_result=$(docker inspect "$container_id" 2>/dev/null || echo "") if [[ -n "$inspect_result" ]]; then # Look for volume mounts that might indicate the installation directory local possible_dirs possible_dirs=$(echo "$inspect_result" | grep -oE '"/[^"]*livereview[^"]*"' | tr -d '"' | grep -v '/var/lib/docker' | head -5) for dir in $possible_dirs; do # Try parent directories local parent_dir parent_dir="$(dirname "$dir")" if [[ -f "$parent_dir/docker-compose.yml" && -f "$parent_dir/.env" ]]; then detected_dir="$parent_dir" log_debug "Detected installation from container volume mount: $detected_dir" break fi done fi fi fi # Method 5: Search filesystem (last resort, limited scope) if [[ -z "$detected_dir" ]]; then log_debug "Searching filesystem for LiveReview installation..." local search_paths=("${DEFAULT_HOME_DIR}" ".") for search_path in "${search_paths[@]}"; do if [[ -d "$search_path" ]]; then local found_path found_path=$(find "$search_path" -maxdepth 2 -name "docker-compose.yml" -path "*/livereview/*" 2>/dev/null | head -1) if [[ -n "$found_path" ]]; then local candidate_dir candidate_dir="$(dirname "$found_path")" if [[ -f "$candidate_dir/.env" ]] && grep -q "livereview-app\|livereview-db" "$found_path" 2>/dev/null; then detected_dir="$candidate_dir" log_debug "Found LiveReview installation via filesystem search: $detected_dir" break fi fi fi done fi # Update the global variable if we found an installation if [[ -n "$detected_dir" ]]; then LIVEREVIEW_INSTALL_DIR="$detected_dir" log_debug "LiveReview installation detected at: $LIVEREVIEW_INSTALL_DIR" return 0 else log_debug "No existing LiveReview installation detected, using default: $LIVEREVIEW_INSTALL_DIR" return 1 fi } # ============================================================================= # DOCKER COMPOSE COMPATIBILITY # ============================================================================= # Global variable to store the correct docker compose command DOCKER_COMPOSE_CMD="" # Detect and set the correct docker compose command detect_docker_compose_cmd() { if command -v docker-compose >/dev/null 2>&1; then # Legacy docker-compose is available if [[ "${USE_SUDO_DOCKER:-false}" == "true" ]]; then DOCKER_COMPOSE_CMD="sudo docker-compose" else DOCKER_COMPOSE_CMD="docker-compose" fi log_debug "Using legacy docker-compose command" elif docker compose version >/dev/null 2>&1; then # Modern docker compose plugin is available # 'docker' may already be wrapped to sudo by maybe_enable_sudo_for_docker DOCKER_COMPOSE_CMD="docker compose" log_debug "Using modern docker compose plugin" else log_error "Neither docker-compose nor docker compose is available" return 1 fi return 0 } # Wrapper function to execute docker compose commands docker_compose() { if [[ -z "$DOCKER_COMPOSE_CMD" ]]; then if ! detect_docker_compose_cmd; then return 1 fi fi # If we have an install directory and docker-compose.yml exists there, use it explicitly local compose_file="" if [[ -n "$LIVEREVIEW_INSTALL_DIR" && -f "$LIVEREVIEW_INSTALL_DIR/docker-compose.yml" ]]; then compose_file="-f $LIVEREVIEW_INSTALL_DIR/docker-compose.yml" fi log_debug "Executing: $DOCKER_COMPOSE_CMD $compose_file $*" $DOCKER_COMPOSE_CMD $compose_file "$@" } # ============================================================================= # ARGUMENT PARSING # ============================================================================= # Default values EXPRESS_MODE=false FORCE_INSTALL=false DRY_RUN=false VERBOSE=false DEBUG_MODE=false LIVEREVIEW_VERSION="" SHOW_HELP=false SHOW_VERSION=false # Test flags (for development) TEST_GITHUB_API=false TEST_EXTRACT=false EXTRACT_TO="" LIST_EMBEDDED_DATA=false SHOW_LATEST_VERSION=false LIST_VERSIONS=false GENERATE_CONFIG_ONLY=false INSTALL_TEMPLATES_ONLY=false OUTPUT_DIR="" INSTALL_SELF=false DIAGNOSE=false BACKUP_TARGET_DIR="" parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in --express) EXPRESS_MODE=true shift ;; --force) FORCE_INSTALL=true shift ;; --dry-run) DRY_RUN=true shift ;; --verbose|-v) VERBOSE=true shift ;; --debug) DEBUG_MODE=true VERBOSE=true shift ;; --version) if [[ -n "${2:-}" && ! "$2" =~ ^-- ]]; then LIVEREVIEW_VERSION="$2" shift 2 else SHOW_VERSION=true shift fi ;; --help|-h) SHOW_HELP=true shift ;; # Skip new commands as they're handled in main() setup-demo|setup-production) # These are handled in main() case statement, skip here shift ;; # Test and development flags --test-github-api) TEST_GITHUB_API=true shift ;; --test-extract) TEST_EXTRACT=true if [[ -n "${2:-}" && ! "$2" =~ ^-- ]]; then EXTRACT_TO="$2" shift 2 else shift fi ;; --extract-to) EXTRACT_TO="$2" shift 2 ;; --list-embedded-data) LIST_EMBEDDED_DATA=true shift ;; --show-latest-version) SHOW_LATEST_VERSION=true shift ;; --list-versions) LIST_VERSIONS=true shift ;; --generate-config-only) GENERATE_CONFIG_ONLY=true shift ;; --install-templates-only) INSTALL_TEMPLATES_ONLY=true shift ;; --output-dir) OUTPUT_DIR="$2" shift 2 ;; --install-self) INSTALL_SELF=true shift ;; --diagnose) DIAGNOSE=true shift ;; --backup-dir=*) BACKUP_TARGET_DIR="${1#*=}" shift ;; --backup-dir) BACKUP_TARGET_DIR="$2" shift 2 ;; --show-plan) DRY_RUN=true VERBOSE=true shift ;; --*) log_error "Unknown option: $1" show_help exit 1 ;; *) # Not an option (doesn't start with --), so stop parsing # This allows commands like 'show-mode' to be handled by main() break ;; esac done } # ============================================================================= # HELP AND VERSION DISPLAY # ============================================================================= show_version() { echo "LiveReview Operations Script (lrops.sh) v${SCRIPT_VERSION}" echo "Repository: https://github.com/${GITHUB_REPO}" echo "Docker Registry: ${DOCKER_REGISTRY}/${DOCKER_IMAGE}" } show_help() { cat << 'EOF' LiveReview Operations Script (lrops.sh) USAGE: # Quick installation (recommended) curl -fsSL https://raw.githubusercontent.com/HexmosTech/LiveReview/main/lrops.sh | bash -s -- --express # Two-mode setup commands (new!) lrops.sh setup-demo # Quick demo mode setup (localhost only) lrops.sh setup-production # Production mode setup (with reverse proxy) # Interactive installation curl -fsSL https://raw.githubusercontent.com/HexmosTech/LiveReview/main/lrops.sh | bash # Specific version installation curl -fsSL https://raw.githubusercontent.com/HexmosTech/LiveReview/main/lrops.sh | bash -s -- --version=v1.2.3 --express # Management commands (after installation) lrops.sh status # Show installation status lrops.sh info # Show installation details and file locations lrops.sh start # Start LiveReview services lrops.sh stop # Stop LiveReview services lrops.sh restart # Restart LiveReview services lrops.sh update [version] # Pull newer image (or specific version) and restart lrops.sh backup [--backup-dir ] [name] # Create manual backup (see detailed options below) lrops.sh quick-backup # Create quick timestamped backup lrops.sh list-backups # List all available backups lrops.sh backup-info # Show detailed information about a backup lrops.sh delete-backup # Delete a specific backup lrops.sh restore # Restore a previous backup lrops.sh set-mode # Switch between demo and production modes lrops.sh show-mode # Show current deployment mode and configuration lrops.sh self-update # Update this script to the latest version from GitHub # Backup options: (Use --backup-dir as backup subcommand option - see BACKUP OPTIONS below) lrops.sh uninstall # Safely uninstall (moves directory, keeps backups) lrops.sh logs [service] # Show container logs lrops.sh env validate # Validate .env and suggest fixes lrops.sh help ssl # SSL/TLS setup guidance lrops.sh help backup # Backup strategies lrops.sh help nginx # Nginx reverse proxy setup lrops.sh help caddy # Caddy reverse proxy setup lrops.sh help apache # Apache reverse proxy setup INSTALLATION OPTIONS: --express Use secure defaults, no prompts (demo mode) --force Overwrite existing installation --version=v1.2.3 Install specific version (default: latest) --dry-run Show what would be done without installing --verbose, -v Enable verbose output --debug Enable bash debug tracing (set -x, also enables verbose output) MANAGEMENT OPTIONS: --help, -h Show this help message --version Show script version --diagnose Run diagnostic checks TWO-MODE DEPLOYMENT SYSTEM: Demo Mode (default): Perfect for localhost development and testing - Access: http://localhost:8081/ - Webhooks: Disabled (manual triggers only) - No external access required Production Mode: Ready for external access with reverse proxy - Requires reverse proxy setup - Webhooks enabled for automatic triggers - SSL/TLS recommended TEMPLATE & CONFIGURATION OPTIONS: --list-embedded-data List all available embedded templates --test-extract