#!/usr/bin/env bash set -euo pipefail # ───────────────────────────────────────────────────── # Glastree — Installer interattivo per amministratori # Richiede: bash 4+, curl, git (opzionale), composer, # php 8.3+, node 20+, npm 10+, mysql (opz.) # ───────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; NC='\033[0m' info() { echo -e "${CYAN}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERR]${NC} $*"; } fail() { error "$*"; exit 1; } ask() { local prompt="$1" var_name="$2" default="${3:-}" local val if [ -n "$default" ]; then read -r -p "$(echo -e "${CYAN}?${NC} ${prompt} [${default}]: ")" val val="${val:-$default}" else read -r -p "$(echo -e "${CYAN}?${NC} ${prompt}: ")" val fi printf -v "$var_name" '%s' "$val" } ask_required() { local prompt="$1" var_name="$2" local val while true; do read -r -p "$(echo -e "${CYAN}?${NC} ${prompt}: ")" val [ -n "$val" ] && break warn "Valore obbligatorio." done printf -v "$var_name" '%s' "$val" } confirm() { local prompt="$1" local resp read -r -p "$(echo -e "${CYAN}?${NC} ${prompt} [S/n]: ")" resp [[ "$resp" =~ ^[Ss]?$ ]] } check_cmds() { local missing=() for c in "$@"; do command -v "$c" &>/dev/null || missing+=("$c") done [ ${#missing[@]} -gt 0 ] && fail "Comandi mancanti: ${missing[*]}" } print_banner() { cat << 'EOF' ____ _ _ _ / ___| | __ _ ___| |_ ___ _ __ ___ | |_ ___ | | _| |/ _` / __| __/ _ \ '__/ _ \ | __/ _ \ | |_| | | (_| \__ \ || __/ | | __/ | || __/ \____|_|\__,_|___/\__\___|_| \___| \__\___| Installer interattivo per sysadmin EOF } # ───────────────────────────────────────────────────── # PREFLIGHT # ───────────────────────────────────────────────────── print_banner # Directory dello script SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$SCRIPT_DIR" # Rileva modalità echo "" echo "Seleziona la modalità di installazione:" echo " 1) Fresh Install su Apache (LAMP tradizionale)" echo " 2) Fresh Install via Docker" echo " 3) Restore da Backup (Apache)" ask "Scegli [1/2/3]" INSTALL_MODE "1" case "$INSTALL_MODE" in 1) MODE="apache" ;; 2) MODE="docker" ;; 3) MODE="restore" ;; *) fail "Scelta non valida: $INSTALL_MODE" ;; esac # ───────────────────────────────────────────────────── # PARAMETRI COMUNI # ───────────────────────────────────────────────────── echo "" echo "══════════════════════════════════════════" echo " PARAMETRI GENERALI" echo "══════════════════════════════════════════" ask_required "Nome del sito/app (es. Glastree MyChurch)" APP_NAME ask_required "Nome del primo utente amministratore" ADMIN_NAME ask_required "Email dell'amministratore" ADMIN_EMAIL ask_required "Password per l'amministratore" ADMIN_PASSWORD ask_required "URL pubblico del sito (es. https://glastree.esempio.it)" APP_URL # ───────────────────────────────────────────────────── # APACHE # ───────────────────────────────────────────────────── if [ "$MODE" = "apache" ] || [ "$MODE" = "restore" ]; then echo "" echo "══════════════════════════════════════════" echo " DATABASE" echo "══════════════════════════════════════════" ask "Tipo database" DB_TYPE "mysql" # mysql|sqlite if [ "$DB_TYPE" = "mysql" ]; then ask_required "Host database" DB_HOST ask "Porta database" DB_PORT "3306" ask_required "Nome database (verrà creato se non esiste)" DB_NAME ask_required "Utente database" DB_USER ask_required "Password database" DB_PASS fi fi # ───────────────────────────────────────────────────── # APACHE — FRESH INSTALL # ───────────────────────────────────────────────────── if [ "$MODE" = "apache" ]; then echo "" echo "══════════════════════════════════════════" echo " PRE-FLIGHT CHECKS (Apache)" echo "══════════════════════════════════════════" check_cmds php composer node npm PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') info "PHP versione: $PHP_VER" if php -r 'exit(version_compare(PHP_VERSION, "8.3.0") ? 0 : 1);' 2>/dev/null; then fail "PHP >= 8.3 richiesto (trovato $PHP_VER)" fi for ext in pdo_mysql mbstring xml curl zip gd fileinfo; do php -m | grep -qi "$ext" || warn "Estensione PHP '$ext' non trovata" done echo "" echo "══════════════════════════════════════════" echo " INSTALLAZIONE APACHE" echo "══════════════════════════════════════════" info "Percorso installazione: $SCRIPT_DIR" # 1. Ambiente [ ! -f .env ] && cp .env.example .env "$SCRIPT_DIR"/artisan key:generate --force # 2. Config .env sed -i "s|APP_NAME=.*|APP_NAME=\"$APP_NAME\"|" .env sed -i "s|APP_URL=.*|APP_URL=$APP_URL|" .env sed -i "s|APP_ENV=.*|APP_ENV=production|" .env sed -i "s|APP_DEBUG=.*|APP_DEBUG=false|" .env if [ "$DB_TYPE" = "mysql" ]; then sed -i "s/DB_CONNECTION=.*/DB_CONNECTION=mysql/" .env sed -i "s/DB_HOST=.*/DB_HOST=$DB_HOST/" .env sed -i "s/DB_PORT=.*/DB_PORT=$DB_PORT/" .env sed -i "s/DB_DATABASE=.*/DB_DATABASE=$DB_NAME/" .env sed -i "s/DB_USERNAME=.*/DB_USERNAME=$DB_USER/" .env sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=$DB_PASS/" .env info "Attendo MySQL..." php -r " \$start = time(); while (time() - \$start < 60) { try { new PDO('mysql:host=$DB_HOST;port=$DB_PORT', '$DB_USER', '$DB_PASS'); exit(0); } catch (\PDOException \$e) { sleep(2); } } exit(1); " && ok "MySQL pronto" || warn "Timeout MySQL — continuo comunque" php "$SCRIPT_DIR"/artisan config:clear 2>/dev/null || true else sed -i "s/DB_CONNECTION=.*/DB_CONNECTION=sqlite/" .env touch database/database.sqlite chmod 664 database/database.sqlite fi # 3. Composer info "Installazione dipendenze Composer..." composer install --no-dev --optimize-autoloader --no-scripts # 4. Migration + seed info "Esecuzione migration e seed..." php "$SCRIPT_DIR"/artisan migrate --seed --force # 5. Storage link php "$SCRIPT_DIR"/artisan storage:link --force 2>/dev/null || true # 6. Crea admin info "Creazione utente amministratore..." php "$SCRIPT_DIR"/artisan tinker --execute=" \$u = new \App\Models\User; \$u->name = '$ADMIN_NAME'; \$u->email = '$ADMIN_EMAIL'; \$u->password = bcrypt('$ADMIN_PASSWORD'); \$u->save(); " 2>/dev/null # 7. Asset if command -v npm &>/dev/null; then info "Compilazione asset frontend..." npm ci --ignore-scripts 2>/dev/null && npm run build else warn "npm non trovato — asset non compilati (usa AdminLTE via CDN)" fi # 8. Permessi chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || warn "chown fallito — esegui manualmente: chown -R www-data:www-data storage bootstrap/cache" chmod -R 775 storage bootstrap/cache ok "Installazione Apache completata!" echo "" echo "────────────────────────────────────────────" echo " URL: $APP_URL" echo " Admin: $ADMIN_EMAIL" echo " Cartella: $SCRIPT_DIR" echo "────────────────────────────────────────────" echo "" warn "Configura Apache con il VirtualHost che trovi nella Guida → Installazione." warn "Se hai appena creato il database MySQL, assicurati che l'utente abbia privilegi CREATE." fi # ───────────────────────────────────────────────────── # DOCKER # ───────────────────────────────────────────────────── if [ "$MODE" = "docker" ]; then echo "" echo "══════════════════════════════════════════" echo " PRE-FLIGHT CHECKS (Docker)" echo "══════════════════════════════════════════" check_cmds docker echo "" echo "══════════════════════════════════════════" echo " CONFIGURAZIONE DOCKER" echo "══════════════════════════════════════════" ask "Tipo database per Docker" DOCKER_DB "sqlite" ask "Porta host (es. 8080)" DOCKER_PORT "8080" COMPOSE_FILE="docker-compose.yml" [ "$DOCKER_DB" = "mysql" ] && COMPOSE_FILE="docker-compose.mysql.yml" echo "" echo "══════════════════════════════════════════" echo " INSTALLAZIONE DOCKER" echo "══════════════════════════════════════════" info "Build immagine Docker..." docker compose -f "$COMPOSE_FILE" build # Crea un .env temporaneo per il docker-entrypoint # Il container genera APP_KEY e fa migrate da solo # Ma dobbiamo passare le variabili per admin e app name cat > docker-compose.override.yml << EOF services: glastree: environment: - APP_NAME=$APP_NAME - APP_URL=$APP_URL - APP_ENV=production - APP_DEBUG=false EOF # Se mysql, la configurazione DB è già nel compose # Se sqlite, non serve override per DB info "Avvio container..." docker compose -f "$COMPOSE_FILE" -f docker-compose.override.yml up -d echo "" info "Attendo che il container sia pronto..." CONTAINER_NAME=$(docker compose -f "$COMPOSE_FILE" ps -q glastree 2>/dev/null | head -1) if [ -n "$CONTAINER_NAME" ]; then # Attendi che Apache sia in ascolto for i in $(seq 1 30); do docker exec "$CONTAINER_NAME" curl -sf -o /dev/null http://localhost/ 2>/dev/null && break sleep 2 done # Crea utente admin dentro il container info "Creazione utente amministratore nel container..." docker exec -i "$CONTAINER_NAME" php artisan tinker --execute=" \App\Models\User::where('email', '$ADMIN_EMAIL')->delete(); \$u = new \App\Models\User; \$u->name = '$ADMIN_NAME'; \$u->email = '$ADMIN_EMAIL'; \$u->password = bcrypt('$ADMIN_PASSWORD'); \$u->save(); " 2>/dev/null fi rm -f docker-compose.override.yml ok "Installazione Docker completata!" echo "" echo "────────────────────────────────────────────" echo " URL: http://localhost:${DOCKER_PORT}" echo " Admin: $ADMIN_EMAIL" echo " Container: $(docker compose -f "$COMPOSE_FILE" ps -q glastree 2>/dev/null | head -1)" echo "────────────────────────────────────────────" echo "" warn "Per URL pubblici, imposta APP_URL nelle environment del container." warn "Dati persistenti: volume 'glastree_storage' e 'glastree_db' (sqlite) o 'mysql_data'." fi # ───────────────────────────────────────────────────── # RESTORE DA BACKUP (Apache) # ───────────────────────────────────────────────────── if [ "$MODE" = "restore" ]; then echo "" echo "══════════════════════════════════════════" echo " PRE-FLIGHT CHECKS (Restore)" echo "══════════════════════════════════════════" check_cmds php composer unzip mysql ask_required "Percorso del file ZIP di backup" BACKUP_ZIP [ ! -f "$BACKUP_ZIP" ] && fail "File non trovato: $BACKUP_ZIP" echo "" echo "══════════════════════════════════════════" echo " RESTORE DA BACKUP" echo "══════════════════════════════════════════" # Estrai backup in temp TMP_DIR=$(mktemp -d) info "Estrazione backup in $TMP_DIR ..." unzip -q "$BACKUP_ZIP" -d "$TMP_DIR" # 1. Ambiente [ ! -f .env ] && cp .env.example .env # 2. Ripristina .env dal backup (preservando DB creds) if [ -f "$TMP_DIR/.env" ]; then cp .env .env.backup-pre-restore # Estrai DB_* dal .env corrente for key in DB_CONNECTION DB_HOST DB_PORT DB_DATABASE DB_USERNAME DB_PASSWORD; do current_val=$(grep "^${key}=" .env 2>/dev/null | cut -d= -f2-) [ -n "$current_val" ] && export "CURRENT_${key}=$current_val" done cp "$TMP_DIR/.env" .env # Ripristina le credenziali DB del nuovo server for key in DB_CONNECTION DB_HOST DB_PORT DB_DATABASE DB_USERNAME DB_PASSWORD; do var="CURRENT_${key}" if [ -n "${!var:-}" ]; then sed -i "s|^${key}=.*|${key}=${!var}|" .env fi done ok ".env ripristinato dal backup (credenziali DB preservate)" fi sed -i "s|APP_NAME=.*|APP_NAME=\"$APP_NAME\"|" .env sed -i "s|APP_URL=.*|APP_URL=$APP_URL|" .env sed -i "s|APP_ENV=.*|APP_ENV=production|" .env sed -i "s|APP_DEBUG=.*|APP_DEBUG=false|" .env # 3. rigenera APP_KEY "$SCRIPT_DIR"/artisan key:generate --force # 4. Importa database if [ "$DB_TYPE" = "mysql" ]; then info "Importazione database MySQL..." if [ -f "$TMP_DIR/database.sql" ]; then mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$TMP_DIR/database.sql" ok "Database importato" else warn "database.sql non trovato nel backup" fi else warn "Tipo database non supportato per restore automatico (usa SQLite o importa manualmente)" fi # 5. Ripristina storage if [ -d "$TMP_DIR/storage" ]; then info "Ripristino file storage..." cp -r "$TMP_DIR/storage"/* "$SCRIPT_DIR"/storage/ 2>/dev/null || true ok "Storage ripristinato" fi # 6. Composer composer install --no-dev --optimize-autoloader --no-scripts # 7. Storage link + cache php "$SCRIPT_DIR"/artisan storage:link --force 2>/dev/null || true php "$SCRIPT_DIR"/artisan optimize:clear # 8. Permessi chown -R www-data:www-data storage bootstrap/cache 2>/dev/null || warn "chown fallito" chmod -R 775 storage bootstrap/cache # 9. Crea admin se non esiste php "$SCRIPT_DIR"/artisan tinker --execute=" \$exists = \App\Models\User::where('email', '$ADMIN_EMAIL')->exists(); if (!\$exists) { \$u = new \App\Models\User; \$u->name = '$ADMIN_NAME'; \$u->email = '$ADMIN_EMAIL'; \$u->password = bcrypt('$ADMIN_PASSWORD'); \$u->save(); } " 2>/dev/null # Cleanup rm -rf "$TMP_DIR" ok "Restore completato!" echo "" echo "────────────────────────────────────────────" echo " URL: $APP_URL" echo " Admin: $ADMIN_EMAIL" echo " Cartella: $SCRIPT_DIR" echo "────────────────────────────────────────────" echo "" warn "Verifica che la APP_KEY non abbia invalidato dati criptati (token OAuth, password IMAP)." warn "Se necessario, riconfigura SMTP e Google Drive dalle Impostazioni." fi