#!/usr/bin/env bash ################################################################################ # PicPeak Unified Setup Script # Version: 2.1.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.1.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_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 FORCE_ADMIN_PASSWORD_RESET=false ################################################################################ # Helper Functions ################################################################################ # Run a command as the application user, even if sudo is not available run_as_user() { local cmd="$*" local current_dir_escaped current_dir_escaped=$(printf '%q' "$(pwd)") if [[ "$(id -u)" -ne 0 ]]; then # Already non-root; preserve working directory bash -lc "cd $current_dir_escaped && $cmd" return $? fi if command_exists sudo; then sudo -H -u "$NATIVE_APP_USER" bash -lc "cd $current_dir_escaped && $cmd" elif command_exists runuser; then runuser -u "$NATIVE_APP_USER" -- bash -lc "cd $current_dir_escaped && $cmd" else su -s /bin/bash - "$NATIVE_APP_USER" -c "cd $current_dir_escaped && $cmd" fi } # Prompt for admin email interactively (unless provided or unattended) prompt_admin_email() { if [[ -n "$ADMIN_EMAIL" ]]; then return fi if [[ "$UNATTENDED" == "true" ]]; then ADMIN_EMAIL="admin@example.com" return fi echo echo "Please enter the admin email address (used for the initial admin account):" read -p "Admin email [admin@example.com]: " ADMIN_EMAIL ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} } 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 } ensure_storage_layout() { local base_dir="$1" local storage_root="$base_dir/storage" local storage_events_dir="$storage_root/events" mkdir -p "$storage_events_dir/active" \ "$storage_events_dir/archived" \ "$storage_root/thumbnails" \ "$storage_root/tmp" local legacy_dir="$base_dir/events" if [[ -d "$legacy_dir" ]]; then log_step "Migrating legacy events directory to storage/events..." mkdir -p "$storage_events_dir" local existing="" if [[ -d "$storage_events_dir" ]]; then existing=$(ls -A "$storage_events_dir" 2>/dev/null || true) fi if [[ ! -d "$storage_events_dir" || -z "$existing" ]]; then rm -rf "$storage_events_dir" mv "$legacy_dir" "$storage_events_dir" else cp -a "$legacy_dir/." "$storage_events_dir/" rm -rf "$legacy_dir" fi fi mkdir -p "$storage_events_dir/active" "$storage_events_dir/archived" } 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() { # Prefer /proc/meminfo (always available on Linux), fallback to free(1) if [[ -r /proc/meminfo ]]; then awk '/^MemTotal:/ { printf "%d\n", $2/1024 }' /proc/meminfo return fi if command_exists free; then free -m | awk '/^Mem:/ {print $2}' return fi echo "0" } 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,data,events} # Clone repository log_step "Downloading PicPeak..." if [[ -d "$app_dir/.git" ]]; then cd "$app_dir" git pull elif [[ -d "$app_dir" ]]; then if [[ -z "$(ls -A "$app_dir" 2>/dev/null)" ]]; then log_warn "Existing directory $app_dir is empty but not a git repository; recreating it..." rm -rf "$app_dir" git clone "$REPO_URL" "$app_dir" else log_warn "Directory $app_dir already exists and is not a git repository." if [[ "$UNATTENDED" == "true" ]]; then local backup_dir="${app_dir}.backup-$(date +%Y%m%d-%H%M%S)" log_warn "Unattended mode: backing up directory to $backup_dir and cloning a fresh copy." mv "$app_dir" "$backup_dir" git clone "$REPO_URL" "$app_dir" else if confirm "Replace existing directory $app_dir with a fresh clone? This will move the current contents to a backup folder." "y"; then local backup_dir="${app_dir}.backup-$(date +%Y%m%d-%H%M%S)" mv "$app_dir" "$backup_dir" log_step "Existing directory moved to $backup_dir" git clone "$REPO_URL" "$app_dir" else die "Installation aborted because $app_dir already exists and is not a PicPeak git repository." fi fi fi else git clone "$REPO_URL" "$app_dir" fi # Determine host user for container mapping (PUID/PGID) local host_uid host_gid if [[ -n "${SUDO_USER:-}" ]]; then host_uid=$(id -u "$SUDO_USER" 2>/dev/null || echo 1000) host_gid=$(id -g "$SUDO_USER" 2>/dev/null || echo 1000) else host_uid=$(id -u 2>/dev/null || echo 1000) host_gid=$(id -g 2>/dev/null || echo 1000) fi # Generate secrets local jwt_secret=$(generate_jwt_secret) local db_password=$(generate_password) local redis_password=$(generate_password) # Create .env file log_step "Creating configuration..." cat > "$app_dir/.env" </dev/null || true # Create docker-compose.yml if it doesn't exist if [[ ! -f "$app_dir/docker-compose.yml" ]]; then log_step "Creating Docker Compose configuration..." create_docker_compose_file "$app_dir" fi # Set up SSL if requested if [[ "$ENABLE_SSL" == "true" ]] && [[ -n "$DOMAIN_NAME" ]]; then setup_ssl_docker "$app_dir" fi # Start services log_step "Starting services..." cd "$app_dir" docker compose up -d # Wait for services to be ready log_step "Waiting for services to initialize..." sleep 10 # Run database migrations log_step "Running database migrations..." docker compose exec -T backend npm run migrate if [[ "$FORCE_ADMIN_PASSWORD_RESET" == "true" ]]; then log_step "Resetting admin credentials..." if docker compose exec -T backend node scripts/reset-admin-password.js --force --credentials-file data/ADMIN_CREDENTIALS.txt; then docker compose cp backend:/app/data/ADMIN_CREDENTIALS.txt "$app_dir/data/ADMIN_CREDENTIALS.txt" 2>/dev/null || true else log_warn "Automatic admin password reset failed; run reset-admin-password.js inside the backend container." fi fi log_success "Docker installation completed!" } create_docker_compose_file() { local app_dir="$1" cat > "$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) if ! $PACKAGE_MANAGER install -y @development-tools; then log_warn "dnf @development-tools group install failed, retrying with legacy groupinstall syntax..." $PACKAGE_MANAGER groupinstall -y "Development Tools" fi $PACKAGE_MANAGER install -y python3 ;; 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,logs,config} ensure_storage_layout "$NATIVE_APP_DIR" chown -R $NATIVE_APP_USER:$NATIVE_APP_USER "$NATIVE_APP_DIR" # Clone repository log_step "Downloading PicPeak..." if [[ -d "$NATIVE_APP_DIR/app/.git" ]]; then cd "$NATIVE_APP_DIR/app" # Ensure correct remote and update even if history was rewritten run_as_user "git config --global --add safe.directory $NATIVE_APP_DIR/app" || true run_as_user "git remote set-url origin $REPO_URL" || true run_as_user "git fetch --all --prune" || true # Prefer checking out remote main and hard resetting to avoid merge prompts if ! run_as_user "git checkout -B main origin/main"; then run_as_user "git checkout main" || true run_as_user "git reset --hard origin/main" fi else run_as_user "git clone $REPO_URL $NATIVE_APP_DIR/app" || { run_as_user "git config --global --add safe.directory $NATIVE_APP_DIR/app" run_as_user "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 # Ensure SQLite data directory exists for native installs mkdir -p "$NATIVE_APP_DIR/app/backend/data" # Build frontend for native serving log_step "Building frontend..." if [[ -d "$NATIVE_APP_DIR/app/frontend" ]]; then cd "$NATIVE_APP_DIR/app/frontend" # Try ci (faster/clean) then fallback to install run_as_user "npm ci --include=dev" || run_as_user "npm install" run_as_user "npm run build" else log_warn "Frontend directory not found; admin UI will not be served by backend" fi # Generate secrets local jwt_secret=$(generate_jwt_secret) # 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 2>&1 && systemctl list-unit-files | grep -q '^picpeak-backend.service'; then native_detected=true fi # Docker detection: docker app dir or user home picpeak dir exists if [[ -d "$DOCKER_APP_DIR" ]] || [[ -n "${SUDO_USER:-}" && -d "/home/${SUDO_USER}/picpeak" ]]; then docker_detected=true fi if [[ "$native_detected" == true ]]; then INSTALL_METHOD="native" update_native_installation elif [[ "$docker_detected" == true ]]; then INSTALL_METHOD="docker" update_docker_installation else die "No existing PicPeak installation found (native dir $NATIVE_APP_DIR/app/backend or docker dir $DOCKER_APP_DIR not present)" fi } update_docker_installation() { local app_dir="$DOCKER_APP_DIR" [[ -n "${SUDO_USER:-}" ]] && app_dir="/home/$SUDO_USER/picpeak" log_step "Updating Docker installation..." cd "$app_dir" # Backup current configuration cp .env .env.backup-$(date +%Y%m%d-%H%M%S) # Pull latest code git pull # Rebuild and restart containers docker compose down docker compose build --no-cache docker compose up -d # Run migrations docker compose exec -T backend npm run migrate log_success "Docker installation updated successfully!" } update_native_installation() { log_step "Updating native installation..." # Stop services systemctl stop picpeak-backend || true if systemctl list-unit-files | grep -q '^picpeak-workers.service'; then systemctl stop picpeak-workers || true fi # Backup current configuration if [[ -f "$NATIVE_APP_DIR/app/backend/.env" ]]; then cp "$NATIVE_APP_DIR/app/backend/.env" "$NATIVE_APP_DIR/app/backend/.env.backup-$(date +%Y%m%d-%H%M%S)" fi # Pull latest code cd "$NATIVE_APP_DIR/app" run_as_user "git config --global --add safe.directory $NATIVE_APP_DIR/app" || true run_as_user "git remote set-url origin $REPO_URL" || true run_as_user "git fetch --all --prune" if ! run_as_user "git checkout -B main origin/main"; then run_as_user "git checkout main" || true run_as_user "git reset --hard origin/main" fi # Update backend dependencies cd "$NATIVE_APP_DIR/app/backend" run_as_user "npm install --production" # Run migrations run_as_user "npm run migrate" # Rebuild frontend (ensure admin UI for native installs) if [[ -d "$NATIVE_APP_DIR/app/frontend" ]]; then log_step "Rebuilding frontend..." cd "$NATIVE_APP_DIR/app/frontend" run_as_user "npm ci --include=dev" || run_as_user "npm install" run_as_user "npm run build" fi # Ensure env has frontend serving flags if ! grep -q '^SERVE_FRONTEND=' "$NATIVE_APP_DIR/app/backend/.env"; then echo "SERVE_FRONTEND=true" >> "$NATIVE_APP_DIR/app/backend/.env" fi if ! grep -q '^FRONTEND_DIR=' "$NATIVE_APP_DIR/app/backend/.env"; then echo "FRONTEND_DIR=$NATIVE_APP_DIR/app/frontend/dist" >> "$NATIVE_APP_DIR/app/backend/.env" fi ensure_storage_layout "$NATIVE_APP_DIR" chown -R $NATIVE_APP_USER:$NATIVE_APP_USER "$NATIVE_APP_DIR/storage" if [[ -f "$NATIVE_APP_DIR/app/backend/.env" ]]; then if grep -q '^STORAGE_PATH=' "$NATIVE_APP_DIR/app/backend/.env"; then sed -i "s|^STORAGE_PATH=.*|STORAGE_PATH=$NATIVE_APP_DIR/storage|" "$NATIVE_APP_DIR/app/backend/.env" else echo "STORAGE_PATH=$NATIVE_APP_DIR/storage" >> "$NATIVE_APP_DIR/app/backend/.env" fi fi # Restart services systemctl restart picpeak-backend log_success "Native installation updated successfully!" } ################################################################################ # Uninstall Functions ################################################################################ uninstall_picpeak() { print_header "Uninstall PicPeak" log_warn "This will remove PicPeak from your system." if ! confirm "Are you sure you want to uninstall PicPeak?" "n"; then log_info "Uninstall cancelled" exit 0 fi # Detect existing installation if [[ -d "$DOCKER_APP_DIR" ]] || [[ -d "/home/${SUDO_USER:-}/picpeak" ]]; then uninstall_docker elif [[ -d "$NATIVE_APP_DIR" ]]; then uninstall_native else die "No PicPeak installation found" fi log_success "PicPeak has been uninstalled" } uninstall_docker() { local app_dir="$DOCKER_APP_DIR" [[ -n "${SUDO_USER:-}" ]] && app_dir="/home/$SUDO_USER/picpeak" log_step "Removing Docker installation..." cd "$app_dir" docker compose down -v if confirm "Remove all data and photos?" "n"; then rm -rf "$app_dir" else log_info "Keeping data in $app_dir" fi } uninstall_native() { log_step "Removing native installation..." # Stop and disable services systemctl stop picpeak-backend picpeak-workers systemctl disable picpeak-backend picpeak-workers rm -f /etc/systemd/system/picpeak-*.service systemctl daemon-reload # Remove Caddy configuration if exists [[ -f /etc/caddy/Caddyfile ]] && rm -f /etc/caddy/Caddyfile if confirm "Remove all data and photos?" "n"; then rm -rf "$NATIVE_APP_DIR" userdel -r $NATIVE_APP_USER 2>/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 ;; --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 ;; --force-admin-password-reset) FORCE_ADMIN_PASSWORD_RESET=true shift ;; --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 <