#!/usr/bin/env bash ################################################################################ # PicPeak Unified Setup Script # Version: 2.0.0 # Description: Universal installer for PicPeak with Docker and Native options # Supports: Ubuntu, Debian, Fedora, RHEL/CentOS, Raspberry Pi OS ################################################################################ set -euo pipefail IFS=$'\n\t' # Script configuration readonly SCRIPT_VERSION="2.0.0" readonly APP_NAME="PicPeak" readonly REPO_URL="https://github.com/the-luap/picpeak.git" readonly NODE_VERSION="20" readonly MIN_RAM_DOCKER=2048 readonly MIN_RAM_NATIVE=1024 readonly MIN_DISK_GB=2 readonly DEFAULT_PORT=3001 # Installation paths readonly NATIVE_APP_DIR="/opt/picpeak" readonly NATIVE_APP_USER="picpeak" readonly DOCKER_APP_DIR="$HOME/picpeak" # Color codes for output readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly PURPLE='\033[0;35m' readonly CYAN='\033[0;36m' readonly NC='\033[0m' # No Color # Logging readonly LOG_FILE="/tmp/picpeak-setup-$(date +%Y%m%d-%H%M%S).log" exec 1> >(tee -a "$LOG_FILE") exec 2>&1 # Global variables INSTALL_METHOD="" # docker or native OS_TYPE="" OS_VERSION="" PACKAGE_MANAGER="" ADMIN_PASSWORD="" ADMIN_EMAIL="admin@example.com" DOMAIN_NAME="" SMTP_HOST="" SMTP_PORT="" SMTP_USER="" SMTP_PASS="" ENABLE_SSL=false CUSTOM_PORT="" UNATTENDED=false UPDATE_MODE=false UNINSTALL_MODE=false ################################################################################ # Helper Functions ################################################################################ print_banner() { echo -e "${PURPLE}" echo "╔════════════════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ ____ _ ____ _ ____ _ ║" echo "║ | _ \\(_) ___| _ \\ ___ __ _| | __ / ___| ___| |_ _ _ _ __ ║" echo "║ | |_) | |/ __| |_) / _ \\/ _\` | |/ / \\___ \\ / _ \\ __| | | | '_ \\ ║" echo "║ | __/| | (__| __/ __/ (_| | < ___) | __/ |_| |_| | |_) | ║" echo "║ |_| |_|\\___|_| \\___|\\__,_|_|\\_\\ |____/ \\___|\\__|\\__,_| .__/ ║" echo "║ |_| ║" echo "║ ║" echo "║ 🚀 Unified Setup Script v${SCRIPT_VERSION} ║" echo "║ 📸 Secure Photo Sharing for Weddings & Events ║" echo "║ ║" echo "╚════════════════════════════════════════════════════════════════════════╝" echo -e "${NC}\n" } print_header() { echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BLUE} $1${NC}" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" } log_info() { echo -e "${CYAN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_success() { echo -e "${GREEN}✅${NC} $1"; } log_step() { echo -e "${PURPLE}🔄${NC} $1"; } die() { log_error "$1" echo -e "\n${RED}Installation failed. Check the log file: $LOG_FILE${NC}" exit 1 } command_exists() { command -v "$1" >/dev/null 2>&1 } generate_password() { openssl rand -base64 32 | tr -d "=+/" | cut -c1-16 } generate_jwt_secret() { openssl rand -base64 64 | tr -d "\n" } get_available_ram_mb() { if command_exists free; then free -m | awk '/^Mem:/{print $2}' else echo "0" fi } get_available_disk_gb() { df -BG / | awk 'NR==2 {print $4}' | sed 's/G//' } confirm() { local prompt="$1" local default="${2:-n}" local REPLY if [[ "$UNATTENDED" == "true" ]]; then [[ "$default" == "y" ]] && return 0 || return 1 fi if [[ "$default" == "y" ]]; then prompt="$prompt [Y/n]: " else prompt="$prompt [y/N]: " fi read -p "$prompt" REPLY REPLY=${REPLY:-$default} [[ "$REPLY" =~ ^[Yy]$ ]] } detect_os() { if [[ -f /etc/os-release ]]; then . /etc/os-release OS_TYPE="$ID" OS_VERSION="$VERSION_ID" else die "Cannot detect operating system" fi case "$OS_TYPE" in ubuntu|debian|raspbian) PACKAGE_MANAGER="apt" ;; fedora|rhel|centos|rocky|almalinux) PACKAGE_MANAGER="dnf" command_exists dnf || PACKAGE_MANAGER="yum" ;; *) die "Unsupported operating system: $OS_TYPE" ;; esac log_info "Detected OS: $OS_TYPE $OS_VERSION" } ################################################################################ # Installation Method Selection ################################################################################ select_install_method() { if [[ -n "$INSTALL_METHOD" ]]; then return fi if [[ "$UNATTENDED" == "true" ]]; then INSTALL_METHOD="docker" return fi print_header "Select Installation Method" echo "Please choose your preferred installation method:" echo echo -e "${GREEN}1) Docker Installation (Recommended)${NC}" echo " ✅ Easier to install and update" echo " ✅ Isolated environment" echo " ✅ Includes PostgreSQL and Redis" echo " ⚠️ Requires ~4GB RAM" echo echo -e "${YELLOW}2) Native Installation${NC}" echo " ✅ Lower resource usage (~1GB RAM)" echo " ✅ Better for Raspberry Pi" echo " ✅ Direct system control" echo " ⚠️ More complex setup" echo local choice while true; do read -p "Enter your choice (1 or 2): " choice case $choice in 1) INSTALL_METHOD="docker"; break;; 2) INSTALL_METHOD="native"; break;; *) log_error "Invalid choice. Please enter 1 or 2.";; esac done log_success "Selected: ${INSTALL_METHOD^} installation" } ################################################################################ # System Requirements Check ################################################################################ check_system_requirements() { print_header "Checking System Requirements" local required_ram if [[ "$INSTALL_METHOD" == "docker" ]]; then required_ram=$MIN_RAM_DOCKER else required_ram=$MIN_RAM_NATIVE fi # Check RAM local available_ram=$(get_available_ram_mb) if [[ $available_ram -lt $required_ram ]]; then log_warn "System has ${available_ram}MB RAM, recommended: ${required_ram}MB" if ! confirm "Continue with limited RAM?"; then die "Insufficient RAM" fi else log_success "RAM check passed: ${available_ram}MB available" fi # Check disk space local available_disk=$(get_available_disk_gb) if [[ $available_disk -lt $MIN_DISK_GB ]]; then log_warn "System has ${available_disk}GB free space, recommended: ${MIN_DISK_GB}GB" if ! confirm "Continue with limited disk space?"; then die "Insufficient disk space" fi else log_success "Disk space check passed: ${available_disk}GB available" fi # Check architecture local arch=$(uname -m) log_info "System architecture: $arch" # Check required commands if ! command_exists openssl; then log_warn "OpenSSL not found, installing..." install_package openssl fi if ! command_exists curl; then log_warn "curl not found, installing..." install_package curl fi if ! command_exists git; then log_warn "git not found, installing..." install_package git fi } ################################################################################ # Docker Installation ################################################################################ install_docker() { if command_exists docker; then log_success "Docker is already installed" return fi print_header "Installing Docker" log_step "Installing Docker and Docker Compose..." case "$PACKAGE_MANAGER" in apt) curl -fsSL https://get.docker.com | sh ;; dnf|yum) $PACKAGE_MANAGER config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo $PACKAGE_MANAGER install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin ;; esac # Start Docker service systemctl start docker systemctl enable docker # Add current user to docker group if [[ -n "${SUDO_USER:-}" ]]; then usermod -aG docker "$SUDO_USER" fi log_success "Docker installed successfully" } setup_docker_installation() { print_header "Docker Installation Setup" # Install Docker if needed install_docker # Create application directory local app_dir="$DOCKER_APP_DIR" if [[ -n "${SUDO_USER:-}" ]]; then app_dir="/home/$SUDO_USER/picpeak" fi log_step "Creating application directory at $app_dir" mkdir -p "$app_dir"/{storage/events/{active,archived},logs,backup,config} # Clone repository log_step "Downloading PicPeak..." if [[ -d "$app_dir/.git" ]]; then cd "$app_dir" git pull else git clone "$REPO_URL" "$app_dir" fi # Generate secrets local jwt_secret=$(generate_jwt_secret) local db_password=$(generate_password) local redis_password=$(generate_password) [[ -z "$ADMIN_PASSWORD" ]] && ADMIN_PASSWORD=$(generate_password) # Create .env file log_step "Creating configuration..." cat > "$app_dir/.env" < "$app_dir/docker-compose.yml" <<'EOF' version: '3.8' services: postgres: image: postgres:15-alpine container_name: picpeak-postgres environment: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: ${DB_NAME} volumes: - postgres-data:/var/lib/postgresql/data networks: - picpeak-network restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine container_name: picpeak-redis command: redis-server --requirepass ${REDIS_PASSWORD} volumes: - redis-data:/data networks: - picpeak-network restart: unless-stopped healthcheck: test: ["CMD", "redis-cli", "--raw", "incr", "ping"] interval: 10s timeout: 5s retries: 5 backend: build: ./backend container_name: picpeak-backend env_file: .env volumes: - ./storage:/app/storage - ./logs:/app/logs ports: - "${PORT:-3001}:3001" networks: - picpeak-network depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3001/api/health"] interval: 30s timeout: 10s retries: 3 workers: build: ./backend container_name: picpeak-workers command: npm run workers env_file: .env volumes: - ./storage:/app/storage - ./logs:/app/logs networks: - picpeak-network depends_on: - backend restart: unless-stopped volumes: postgres-data: redis-data: networks: picpeak-network: driver: bridge EOF } ################################################################################ # Native Installation ################################################################################ install_nodejs() { if command_exists node && [[ $(node -v | cut -d'v' -f2 | cut -d'.' -f1) -ge $NODE_VERSION ]]; then log_success "Node.js $(node -v) is already installed" return fi log_step "Installing Node.js $NODE_VERSION..." case "$PACKAGE_MANAGER" in apt) curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - apt-get install -y nodejs ;; dnf|yum) curl -fsSL https://rpm.nodesource.com/setup_${NODE_VERSION}.x | bash - $PACKAGE_MANAGER install -y nodejs ;; esac log_success "Node.js installed: $(node -v)" } setup_native_installation() { print_header "Native Installation Setup" # Install Node.js install_nodejs # Install SQLite log_step "Installing SQLite..." install_package sqlite3 # Install build tools log_step "Installing build tools..." case "$PACKAGE_MANAGER" in apt) apt-get install -y build-essential python3 ;; dnf|yum) $PACKAGE_MANAGER groupinstall -y "Development Tools" $PACKAGE_MANAGER install -y python3 ;; esac # Create system user if ! id "$NATIVE_APP_USER" &>/dev/null; then log_step "Creating system user..." useradd -r -s /bin/bash -m -d /home/$NATIVE_APP_USER $NATIVE_APP_USER fi # Create application directory log_step "Creating application directory..." mkdir -p "$NATIVE_APP_DIR"/{app,events/{active,archived},logs,config} # Clone repository log_step "Downloading PicPeak..." if [[ -d "$NATIVE_APP_DIR/app/.git" ]]; then cd "$NATIVE_APP_DIR/app" git pull else git clone "$REPO_URL" "$NATIVE_APP_DIR/app" fi # Install dependencies log_step "Installing dependencies..." # The repository root contains both backend/ and frontend/ # Install backend production dependencies cd "$NATIVE_APP_DIR/app/backend" npm install --production # Generate secrets local jwt_secret=$(generate_jwt_secret) [[ -z "$ADMIN_PASSWORD" ]] && ADMIN_PASSWORD=$(generate_password) # Create .env file log_step "Creating configuration..." cat > "$NATIVE_APP_DIR/app/backend/.env" < /etc/systemd/system/picpeak-backend.service < /etc/systemd/system/picpeak-workers.service < /etc/caddy/Caddyfile </dev/null || true else log_info "Keeping data in $NATIVE_APP_DIR" fi } ################################################################################ # Main Script Flow ################################################################################ parse_arguments() { while [[ $# -gt 0 ]]; do case "$1" in --docker) INSTALL_METHOD="docker" shift ;; --native) INSTALL_METHOD="native" shift ;; --unattended) UNATTENDED=true shift ;; --domain) DOMAIN_NAME="$2" shift 2 ;; --email) ADMIN_EMAIL="$2" shift 2 ;; --admin-password) ADMIN_PASSWORD="$2" shift 2 ;; --smtp-host) SMTP_HOST="$2" shift 2 ;; --smtp-port) SMTP_PORT="$2" shift 2 ;; --smtp-user) SMTP_USER="$2" shift 2 ;; --smtp-pass) SMTP_PASS="$2" shift 2 ;; --enable-ssl) ENABLE_SSL=true shift ;; --port) CUSTOM_PORT="$2" shift 2 ;; --update) UPDATE_MODE=true shift ;; --uninstall) UNINSTALL_MODE=true shift ;; --help) show_help exit 0 ;; *) die "Unknown option: $1" ;; esac done } show_help() { cat <