mirror of
https://github.com/kossakovsky/n8n-install.git
synced 2026-03-07 06:13:05 +00:00
all compose file assembly points now include the override file last when present, giving it highest precedence over other compose files
884 lines
27 KiB
Bash
Executable File
884 lines
27 KiB
Bash
Executable File
#!/bin/bash
|
|
# =============================================================================
|
|
# utils.sh - Shared utilities for n8n-install scripts
|
|
# =============================================================================
|
|
# Common functions and utilities used across all installation scripts.
|
|
#
|
|
# Provides:
|
|
# - Path initialization (init_paths): Sets SCRIPT_DIR, PROJECT_ROOT, ENV_FILE
|
|
# - Logging functions: log_info, log_success, log_warning, log_error
|
|
# - .env manipulation: read_env_var, write_env_var, load_env
|
|
# - Whiptail wrappers: wt_input, wt_yesno, require_whiptail
|
|
# - Validation helpers: require_command, require_file, ensure_file_exists
|
|
# - Profile management: is_profile_active, update_compose_profiles
|
|
# - Doctor output helpers: print_ok, print_warning, print_error
|
|
# - Directory preservation: backup_preserved_dirs, restore_preserved_dirs
|
|
#
|
|
# Usage: source "$(dirname "$0")/utils.sh" && init_paths
|
|
# =============================================================================
|
|
|
|
#=============================================================================
|
|
# CONSTANTS
|
|
#=============================================================================
|
|
DOMAIN_PLACEHOLDER="yourdomain.com"
|
|
|
|
#=============================================================================
|
|
# WHIPTAIL THEME (NEWT_COLORS)
|
|
#=============================================================================
|
|
# Solarized Dark theme with blue/cyan accents
|
|
# Format: element=foreground,background
|
|
# Colors: black, red, green, yellow, blue, magenta, cyan, white
|
|
# Prefix with "bright" for bright variants (e.g., brightblue)
|
|
export NEWT_COLORS='
|
|
root=white,black
|
|
border=blue,black
|
|
window=white,black
|
|
shadow=black,black
|
|
title=brightblue,black
|
|
button=black,blue
|
|
actbutton=black,cyan
|
|
compactbutton=white,black
|
|
checkbox=blue,black
|
|
actcheckbox=black,cyan
|
|
entry=cyan,black
|
|
disentry=gray,black
|
|
label=white,black
|
|
listbox=white,black
|
|
actlistbox=black,blue
|
|
sellistbox=cyan,black
|
|
actsellistbox=black,cyan
|
|
textbox=white,black
|
|
acttextbox=blue,black
|
|
emptyscale=black,black
|
|
fullscale=blue,black
|
|
helpline=blue,black
|
|
roottext=blue,black
|
|
'
|
|
|
|
#=============================================================================
|
|
# PATH INITIALIZATION
|
|
#=============================================================================
|
|
|
|
# Initialize standard paths - call at start of each script
|
|
# WARNING: Must be called directly from script top-level, NOT from within functions.
|
|
# BASH_SOURCE[1] refers to the script that sourced utils.sh.
|
|
# Usage: source utils.sh && init_paths
|
|
init_paths() {
|
|
# BASH_SOURCE[1] = the script that called this function (not utils.sh itself)
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[1]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
ENV_FILE="$PROJECT_ROOT/.env"
|
|
}
|
|
|
|
#=============================================================================
|
|
# LOGGING (Simplified)
|
|
#=============================================================================
|
|
|
|
# Internal logging function
|
|
_log() {
|
|
local level="$1"
|
|
local message="$2"
|
|
echo ""
|
|
echo "[$level] $(date +%H:%M:%S): $message"
|
|
}
|
|
|
|
log_info() {
|
|
_log "INFO" "$1"
|
|
}
|
|
|
|
log_success() {
|
|
_log "OK" "$1"
|
|
}
|
|
|
|
log_warning() {
|
|
_log "WARN" "$1"
|
|
}
|
|
|
|
log_error() {
|
|
_log "ERROR" "$1" >&2
|
|
}
|
|
|
|
# Display a header for major sections
|
|
# Usage: log_header "Section Title"
|
|
log_header() {
|
|
local message="$1"
|
|
local width=60
|
|
local padding=$(( (width - ${#message} - 2) / 2 ))
|
|
local pad_left=$(printf '%*s' "$padding" '' | tr ' ' '=')
|
|
local pad_right=$(printf '%*s' "$((width - ${#message} - 2 - padding))" '' | tr ' ' '=')
|
|
|
|
echo ""
|
|
echo ""
|
|
echo -e "${BRIGHT_GREEN}${pad_left}${NC} ${BOLD}${WHITE}${message}${NC} ${BRIGHT_GREEN}${pad_right}${NC}"
|
|
}
|
|
|
|
# Display a sub-header for sections
|
|
# Usage: log_subheader "Sub Section"
|
|
log_subheader() {
|
|
local message="$1"
|
|
echo ""
|
|
echo -e "${CYAN}--- ${message} ---${NC}"
|
|
}
|
|
|
|
# Display a divider line
|
|
# Usage: log_divider
|
|
log_divider() {
|
|
echo ""
|
|
echo -e "${DIM}${GREEN}$(printf '%.0s-' {1..60})${NC}"
|
|
}
|
|
|
|
# Display text in a box (for important messages)
|
|
# Usage: log_box "Important message"
|
|
log_box() {
|
|
local message="$1"
|
|
local len=${#message}
|
|
local border=$(printf '%*s' "$((len + 4))" '' | tr ' ' '=')
|
|
|
|
echo ""
|
|
echo -e "${BRIGHT_GREEN}+${border}+${NC}"
|
|
echo -e "${BRIGHT_GREEN}|${NC} ${BOLD}${WHITE}${message}${NC} ${BRIGHT_GREEN}|${NC}"
|
|
echo -e "${BRIGHT_GREEN}+${border}+${NC}"
|
|
}
|
|
|
|
#=============================================================================
|
|
# COLOR OUTPUT (for diagnostics and previews)
|
|
#=============================================================================
|
|
# Basic colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
MAGENTA='\033[0;35m'
|
|
WHITE='\033[1;37m'
|
|
GRAY='\033[0;90m'
|
|
NC='\033[0m' # No Color / Reset
|
|
|
|
# Text styles
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
RESET='\033[0m'
|
|
|
|
# Bright colors
|
|
BRIGHT_RED='\033[1;31m'
|
|
BRIGHT_GREEN='\033[1;32m'
|
|
BRIGHT_YELLOW='\033[1;33m'
|
|
BRIGHT_BLUE='\033[1;34m'
|
|
BRIGHT_CYAN='\033[1;36m'
|
|
|
|
# Background colors (for badges/labels)
|
|
BG_RED='\033[41m'
|
|
BG_GREEN='\033[42m'
|
|
BG_YELLOW='\033[43m'
|
|
|
|
print_ok() {
|
|
echo ""
|
|
echo -e " ${GREEN}[OK]${NC} $1"
|
|
}
|
|
|
|
print_error() {
|
|
echo ""
|
|
echo -e " ${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
print_warning() {
|
|
echo ""
|
|
echo -e " ${YELLOW}[WARNING]${NC} $1"
|
|
}
|
|
|
|
print_info() {
|
|
echo ""
|
|
echo -e " ${BLUE}[INFO]${NC} $1"
|
|
}
|
|
|
|
#=============================================================================
|
|
# PROGRESS INDICATORS
|
|
#=============================================================================
|
|
|
|
# Spinner animation frames
|
|
SPINNER_FRAMES=('|' '/' '-' '\')
|
|
SPINNER_PID=""
|
|
|
|
# Start spinner with message
|
|
# Usage: start_spinner "Loading..."
|
|
start_spinner() {
|
|
local message="$1"
|
|
local i=0
|
|
|
|
# Don't start if not in terminal or already running
|
|
[[ ! -t 1 ]] && return
|
|
[[ -n "$SPINNER_PID" ]] && return
|
|
|
|
(
|
|
while true; do
|
|
printf "\r ${GREEN}${SPINNER_FRAMES[$i]}${NC} ${message} "
|
|
i=$(( (i + 1) % 4 ))
|
|
sleep 0.1
|
|
done
|
|
) &
|
|
SPINNER_PID=$!
|
|
disown
|
|
}
|
|
|
|
# Stop spinner and clear line
|
|
# Usage: stop_spinner
|
|
stop_spinner() {
|
|
[[ -z "$SPINNER_PID" ]] && return
|
|
|
|
kill "$SPINNER_PID" 2>/dev/null
|
|
wait "$SPINNER_PID" 2>/dev/null
|
|
SPINNER_PID=""
|
|
|
|
# Clear the spinner line
|
|
printf "\r%*s\r" 80 ""
|
|
}
|
|
|
|
# Show step progress (e.g., Step 3/7)
|
|
# Usage: show_step 3 7 "Installing Docker"
|
|
show_step() {
|
|
local current=$1
|
|
local total=$2
|
|
local description="$3"
|
|
|
|
echo ""
|
|
echo -e "${BRIGHT_GREEN}[${current}/${total}]${NC} ${BOLD}${description}${NC}"
|
|
echo -e "${DIM}$(printf '%.0s.' {1..50})${NC}"
|
|
}
|
|
|
|
# Show a simple progress bar
|
|
# Usage: show_progress 50 100 "Downloading"
|
|
show_progress() {
|
|
local current=$1
|
|
local total=$2
|
|
local label="${3:-Progress}"
|
|
local width=40
|
|
local percent=$(( current * 100 / total ))
|
|
local filled=$(( current * width / total ))
|
|
local empty=$(( width - filled ))
|
|
|
|
local bar_filled=$(printf '%*s' "$filled" '' | tr ' ' '#')
|
|
local bar_empty=$(printf '%*s' "$empty" '' | tr ' ' '-')
|
|
|
|
printf "\r ${label}: ${GREEN}[${bar_filled}${GRAY}${bar_empty}${GREEN}]${NC} ${WHITE}%3d%%${NC}" "$percent"
|
|
}
|
|
|
|
# Complete progress bar with message
|
|
# Usage: complete_progress "Download complete"
|
|
complete_progress() {
|
|
local message="${1:-Done}"
|
|
printf "\r%*s\r" 80 ""
|
|
echo -e " ${GREEN}[OK]${NC} ${message}"
|
|
}
|
|
|
|
#=============================================================================
|
|
# ENVIRONMENT MANAGEMENT
|
|
#=============================================================================
|
|
|
|
# Load .env file safely
|
|
# Usage: load_env [env_file_path]
|
|
load_env() {
|
|
local env_file="${1:-$ENV_FILE}"
|
|
if [[ ! -f "$env_file" ]]; then
|
|
log_error ".env file not found: $env_file"
|
|
return 1
|
|
fi
|
|
set -a
|
|
source "$env_file"
|
|
set +a
|
|
}
|
|
|
|
# Read a variable from .env file
|
|
# Usage: value=$(read_env_var "VAR_NAME" [env_file])
|
|
read_env_var() {
|
|
local var_name="$1"
|
|
local env_file="${2:-$ENV_FILE}"
|
|
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
|
|
grep "^${var_name}=" "$env_file" | cut -d'=' -f2- | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//"
|
|
fi
|
|
}
|
|
|
|
# Write/update a variable in .env file (with automatic .bak cleanup)
|
|
# Usage: write_env_var "VAR_NAME" "value" [env_file]
|
|
write_env_var() {
|
|
local var_name="$1"
|
|
local var_value="$2"
|
|
local env_file="${3:-$ENV_FILE}"
|
|
|
|
if grep -q "^${var_name}=" "$env_file" 2>/dev/null; then
|
|
sed -i.bak "\|^${var_name}=|d" "$env_file"
|
|
rm -f "${env_file}.bak"
|
|
fi
|
|
echo "${var_name}=\"${var_value}\"" >> "$env_file"
|
|
}
|
|
|
|
# Check if a Docker Compose profile is active
|
|
# IMPORTANT: Requires COMPOSE_PROFILES to be set before calling (via load_env or direct assignment)
|
|
# Usage: is_profile_active "n8n" && echo "n8n is active"
|
|
is_profile_active() {
|
|
local profile="$1"
|
|
[[ -n "$COMPOSE_PROFILES" && ",$COMPOSE_PROFILES," == *",$profile,"* ]]
|
|
}
|
|
|
|
# Get n8n workers compose file path if profile is active and file exists
|
|
# Usage: path=$(get_n8n_workers_compose) && COMPOSE_FILES+=("-f" "$path")
|
|
get_n8n_workers_compose() {
|
|
local compose_file="$PROJECT_ROOT/docker-compose.n8n-workers.yml"
|
|
if [ -f "$compose_file" ] && is_profile_active "n8n"; then
|
|
echo "$compose_file"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Get Supabase compose file path if profile is active and file exists
|
|
# Usage: path=$(get_supabase_compose) && COMPOSE_FILES+=("-f" "$path")
|
|
get_supabase_compose() {
|
|
local compose_file="$PROJECT_ROOT/supabase/docker/docker-compose.yml"
|
|
if [ -f "$compose_file" ] && is_profile_active "supabase"; then
|
|
echo "$compose_file"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Get Dify compose file path if profile is active and file exists
|
|
# Usage: path=$(get_dify_compose) && COMPOSE_FILES+=("-f" "$path")
|
|
get_dify_compose() {
|
|
local compose_file="$PROJECT_ROOT/dify/docker/docker-compose.yaml"
|
|
if [ -f "$compose_file" ] && is_profile_active "dify"; then
|
|
echo "$compose_file"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Build array of all active compose files (main + external services)
|
|
# Appends docker-compose.override.yml last if it exists (user overrides, highest precedence)
|
|
# IMPORTANT: Requires COMPOSE_PROFILES to be set before calling (via load_env)
|
|
# Usage: build_compose_files_array; docker compose "${COMPOSE_FILES[@]}" up -d
|
|
# Result is stored in global COMPOSE_FILES array
|
|
build_compose_files_array() {
|
|
COMPOSE_FILES=("-f" "$PROJECT_ROOT/docker-compose.yml")
|
|
|
|
local path
|
|
if path=$(get_n8n_workers_compose); then
|
|
COMPOSE_FILES+=("-f" "$path")
|
|
fi
|
|
if path=$(get_supabase_compose); then
|
|
COMPOSE_FILES+=("-f" "$path")
|
|
fi
|
|
if path=$(get_dify_compose); then
|
|
COMPOSE_FILES+=("-f" "$path")
|
|
fi
|
|
|
|
# Include user overrides last (highest precedence)
|
|
local override="$PROJECT_ROOT/docker-compose.override.yml"
|
|
if [ -f "$override" ]; then
|
|
COMPOSE_FILES+=("-f" "$override")
|
|
fi
|
|
}
|
|
|
|
#=============================================================================
|
|
# UTILITIES
|
|
#=============================================================================
|
|
|
|
# Require a command to be available
|
|
# Usage: require_command "docker" "Install Docker: https://docs.docker.com/engine/install/"
|
|
require_command() {
|
|
local cmd="$1"
|
|
local install_hint="${2:-Please install $cmd}"
|
|
if ! command -v "$cmd" &> /dev/null; then
|
|
log_error "'$cmd' not found. $install_hint"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Cleanup .bak files created by sed -i
|
|
# Usage: cleanup_bak_files [directory]
|
|
cleanup_bak_files() {
|
|
local directory="${1:-$PROJECT_ROOT}"
|
|
find "$directory" -maxdepth 1 -name "*.bak" -type f -delete 2>/dev/null || true
|
|
}
|
|
|
|
# Escape string for JSON output
|
|
# Usage: escaped=$(json_escape "string with \"quotes\"")
|
|
json_escape() {
|
|
local str="$1"
|
|
printf '%s' "$str" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n\r'
|
|
}
|
|
|
|
#=============================================================================
|
|
# FILE UTILITIES
|
|
#=============================================================================
|
|
|
|
# Require a file to exist, exit with error if not found
|
|
# Usage: require_file "/path/to/file" "Custom error message"
|
|
require_file() {
|
|
local file="$1"
|
|
local error_msg="${2:-File not found: $file}"
|
|
if [[ ! -f "$file" ]]; then
|
|
log_error "$error_msg"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Ensure a file exists, create empty file if it doesn't
|
|
# Usage: ensure_file_exists "/path/to/file"
|
|
ensure_file_exists() {
|
|
local file="$1"
|
|
if [[ ! -f "$file" ]]; then
|
|
touch "$file"
|
|
fi
|
|
}
|
|
|
|
#=============================================================================
|
|
# COMPOSE PROFILES MANAGEMENT
|
|
#=============================================================================
|
|
|
|
# Update COMPOSE_PROFILES in .env file
|
|
# Usage: update_compose_profiles "n8n,monitoring,portainer" [env_file]
|
|
update_compose_profiles() {
|
|
local profiles="$1"
|
|
local env_file="${2:-$ENV_FILE}"
|
|
ensure_file_exists "$env_file"
|
|
if grep -q "^COMPOSE_PROFILES=" "$env_file"; then
|
|
sed -i.bak "\|^COMPOSE_PROFILES=|d" "$env_file"
|
|
rm -f "${env_file}.bak"
|
|
fi
|
|
echo "COMPOSE_PROFILES=${profiles}" >> "$env_file"
|
|
}
|
|
|
|
#=============================================================================
|
|
# DEBIAN_FRONTEND MANAGEMENT
|
|
#=============================================================================
|
|
ORIGINAL_DEBIAN_FRONTEND=""
|
|
|
|
# Save current DEBIAN_FRONTEND and set to dialog for whiptail
|
|
# Usage: save_debian_frontend
|
|
save_debian_frontend() {
|
|
ORIGINAL_DEBIAN_FRONTEND="$DEBIAN_FRONTEND"
|
|
export DEBIAN_FRONTEND=dialog
|
|
}
|
|
|
|
# Restore original DEBIAN_FRONTEND value
|
|
# Usage: restore_debian_frontend
|
|
restore_debian_frontend() {
|
|
if [[ -n "$ORIGINAL_DEBIAN_FRONTEND" ]]; then
|
|
export DEBIAN_FRONTEND="$ORIGINAL_DEBIAN_FRONTEND"
|
|
else
|
|
unset DEBIAN_FRONTEND
|
|
fi
|
|
}
|
|
|
|
#=============================================================================
|
|
# SECRET GENERATION
|
|
#=============================================================================
|
|
|
|
# Generate random string with specified characters
|
|
# Usage: gen_random 32 'A-Za-z0-9'
|
|
gen_random() {
|
|
local length="$1"
|
|
local characters="$2"
|
|
head /dev/urandom | tr -dc "$characters" | head -c "$length"
|
|
}
|
|
|
|
# Generate alphanumeric password
|
|
# Usage: gen_password 32
|
|
gen_password() {
|
|
gen_random "$1" 'A-Za-z0-9'
|
|
}
|
|
|
|
# Generate hex string
|
|
# Usage: gen_hex 64 (returns 64 hex characters)
|
|
gen_hex() {
|
|
local length="$1"
|
|
local bytes=$(( (length + 1) / 2 ))
|
|
openssl rand -hex "$bytes" | head -c "$length"
|
|
}
|
|
|
|
# Generate base64 string
|
|
# Usage: gen_base64 64 (returns 64 base64 characters)
|
|
gen_base64() {
|
|
local length="$1"
|
|
local bytes=$(( (length * 3 + 3) / 4 ))
|
|
openssl rand -base64 "$bytes" | head -c "$length"
|
|
}
|
|
|
|
# Generate bcrypt hash using Caddy
|
|
# Usage: hash=$(generate_bcrypt_hash "plaintext_password")
|
|
generate_bcrypt_hash() {
|
|
local plaintext="$1"
|
|
if [[ -n "$plaintext" ]]; then
|
|
caddy hash-password --algorithm bcrypt --plaintext "$plaintext" 2>/dev/null
|
|
fi
|
|
}
|
|
|
|
#=============================================================================
|
|
# VALIDATION
|
|
#=============================================================================
|
|
|
|
# Validate that a value is a positive integer
|
|
# Usage: validate_positive_integer "5" && echo "valid"
|
|
validate_positive_integer() {
|
|
local value="$1"
|
|
[[ "$value" =~ ^0*[1-9][0-9]*$ ]]
|
|
}
|
|
|
|
#=============================================================================
|
|
# WHIPTAIL HELPERS
|
|
#=============================================================================
|
|
|
|
# Ensure whiptail is available
|
|
require_whiptail() {
|
|
if ! command -v whiptail >/dev/null 2>&1; then
|
|
log_error "'whiptail' is not installed. Install with: sudo apt-get install -y whiptail"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Get adaptive terminal size for whiptail dialogs
|
|
# Usage: eval "$(wt_get_size)"
|
|
# Sets: WT_HEIGHT, WT_WIDTH, WT_LIST_HEIGHT
|
|
wt_get_size() {
|
|
local term_lines term_cols
|
|
term_lines=$(tput lines 2>/dev/null || echo 24)
|
|
term_cols=$(tput cols 2>/dev/null || echo 80)
|
|
|
|
# Calculate dimensions with margins
|
|
local height=$((term_lines - 4))
|
|
local width=$((term_cols - 4))
|
|
|
|
# Apply min/max constraints
|
|
[[ $height -lt 10 ]] && height=10
|
|
[[ $height -gt 40 ]] && height=40
|
|
[[ $width -lt 60 ]] && width=60
|
|
[[ $width -gt 120 ]] && width=120
|
|
|
|
# List height for checklists/menus (leave room for title, prompt, buttons)
|
|
local list_height=$((height - 8))
|
|
[[ $list_height -lt 5 ]] && list_height=5
|
|
|
|
echo "WT_HEIGHT=$height WT_WIDTH=$width WT_LIST_HEIGHT=$list_height"
|
|
}
|
|
|
|
# Input box with adaptive sizing
|
|
# Usage: result=$(wt_input "Title" "Prompt" "default")
|
|
# Returns 0 on OK, 1 on Cancel
|
|
wt_input() {
|
|
local title="$1"
|
|
local prompt="$2"
|
|
local default_value="$3"
|
|
eval "$(wt_get_size)"
|
|
local result
|
|
result=$(whiptail --title "$title" --inputbox "$prompt" "$WT_HEIGHT" "$WT_WIDTH" "$default_value" 3>&1 1>&2 2>&3)
|
|
local status=$?
|
|
[[ $status -ne 0 ]] && return 1
|
|
echo "$result"
|
|
return 0
|
|
}
|
|
|
|
# Password box with adaptive sizing
|
|
# Usage: result=$(wt_password "Title" "Prompt")
|
|
# Returns 0 on OK, 1 on Cancel
|
|
wt_password() {
|
|
local title="$1"
|
|
local prompt="$2"
|
|
eval "$(wt_get_size)"
|
|
local result
|
|
result=$(whiptail --title "$title" --passwordbox "$prompt" "$WT_HEIGHT" "$WT_WIDTH" 3>&1 1>&2 2>&3)
|
|
local status=$?
|
|
[[ $status -ne 0 ]] && return 1
|
|
echo "$result"
|
|
return 0
|
|
}
|
|
|
|
# Yes/No box with adaptive sizing
|
|
# Usage: wt_yesno "Title" "Prompt" "default" (default: yes|no)
|
|
# Returns 0 for Yes, 1 for No/Cancel
|
|
wt_yesno() {
|
|
local title="$1"
|
|
local prompt="$2"
|
|
local default_choice="$3"
|
|
eval "$(wt_get_size)"
|
|
local height=$((WT_HEIGHT < 12 ? WT_HEIGHT : 12))
|
|
if [ "$default_choice" = "yes" ]; then
|
|
whiptail --title "$title" --yesno "$prompt" "$height" "$WT_WIDTH"
|
|
else
|
|
whiptail --title "$title" --defaultno --yesno "$prompt" "$height" "$WT_WIDTH"
|
|
fi
|
|
}
|
|
|
|
# Message box with adaptive sizing
|
|
# Usage: wt_msg "Title" "Message"
|
|
wt_msg() {
|
|
local title="$1"
|
|
local message="$2"
|
|
eval "$(wt_get_size)"
|
|
local height=$((WT_HEIGHT < 12 ? WT_HEIGHT : 12))
|
|
whiptail --title "$title" --msgbox "$message" "$height" "$WT_WIDTH"
|
|
}
|
|
|
|
# Checklist (multiple selection) with adaptive sizing
|
|
# Usage: result=$(wt_checklist "Title" "Prompt" "tag1" "desc1" "ON" "tag2" "desc2" "OFF" ...)
|
|
# Returns: space-separated quoted tags, e.g., "tag1" "tag2"
|
|
wt_checklist() {
|
|
local title="$1"
|
|
local prompt="$2"
|
|
shift 2
|
|
eval "$(wt_get_size)"
|
|
whiptail --title "$title" --checklist "$prompt" "$WT_HEIGHT" "$WT_WIDTH" "$WT_LIST_HEIGHT" "$@" 3>&1 1>&2 2>&3
|
|
}
|
|
|
|
# Radiolist (single selection) with adaptive sizing
|
|
# Usage: result=$(wt_radiolist "Title" "Prompt" "default_item" "tag1" "desc1" "ON" ...)
|
|
# Returns: selected tag
|
|
wt_radiolist() {
|
|
local title="$1"
|
|
local prompt="$2"
|
|
local default_item="$3"
|
|
shift 3
|
|
eval "$(wt_get_size)"
|
|
whiptail --title "$title" --default-item "$default_item" --radiolist "$prompt" "$WT_HEIGHT" "$WT_WIDTH" "$WT_LIST_HEIGHT" "$@" 3>&1 1>&2 2>&3
|
|
}
|
|
|
|
# Menu (item selection) with adaptive sizing
|
|
# Usage: result=$(wt_menu "Title" "Prompt" "tag1" "desc1" "tag2" "desc2" ...)
|
|
# Returns: selected tag
|
|
wt_menu() {
|
|
local title="$1"
|
|
local prompt="$2"
|
|
shift 2
|
|
eval "$(wt_get_size)"
|
|
whiptail --title "$title" --menu "$prompt" "$WT_HEIGHT" "$WT_WIDTH" "$WT_LIST_HEIGHT" "$@" 3>&1 1>&2 2>&3
|
|
}
|
|
|
|
# Safe parser for whiptail checklist results (replaces eval)
|
|
# Usage: wt_parse_choices "$CHOICES" result_array
|
|
# Parses quoted output like: "tag1" "tag2" "tag3"
|
|
wt_parse_choices() {
|
|
local choices="$1"
|
|
local -n arr="$2"
|
|
arr=()
|
|
# Remove quotes and split by spaces
|
|
local cleaned="${choices//\"/}"
|
|
read -ra arr <<< "$cleaned"
|
|
}
|
|
|
|
#=============================================================================
|
|
# LEGACY CONTAINER CLEANUP
|
|
#=============================================================================
|
|
|
|
# Remove legacy n8n worker containers from previous naming convention
|
|
# Old format: localai-n8n-worker-N (N = 1-10)
|
|
# New format: n8n-worker-N (managed by docker-compose.n8n-workers.yml)
|
|
# Usage: cleanup_legacy_n8n_workers
|
|
cleanup_legacy_n8n_workers() {
|
|
local removed_count=0
|
|
local container_name
|
|
|
|
log_info "Checking for legacy n8n worker containers..."
|
|
|
|
for i in {1..10}; do
|
|
container_name="localai-n8n-worker-$i"
|
|
|
|
# Check if container exists (running or stopped)
|
|
if docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
|
log_info "Removing legacy container: $container_name"
|
|
docker stop "$container_name" 2>/dev/null || true
|
|
docker rm -f "$container_name" 2>/dev/null || true
|
|
removed_count=$((removed_count + 1))
|
|
fi
|
|
done
|
|
|
|
if [ $removed_count -gt 0 ]; then
|
|
log_success "Removed $removed_count legacy n8n worker container(s)"
|
|
else
|
|
log_info "No legacy n8n worker containers found"
|
|
fi
|
|
}
|
|
|
|
# Clean up legacy postgresus container after rename to databasus
|
|
# This function removes the old "postgresus" container if it exists,
|
|
# allowing the new "databasus" container to take its place.
|
|
# Usage: cleanup_legacy_postgresus
|
|
cleanup_legacy_postgresus() {
|
|
local container_name="postgresus"
|
|
|
|
# Check if container exists (running or stopped)
|
|
if docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
|
log_info "Found legacy postgresus container, migrating to databasus..."
|
|
docker stop "$container_name" 2>/dev/null || true
|
|
docker rm -f "$container_name" 2>/dev/null || true
|
|
log_success "Legacy postgresus container removed. Databasus will use existing data via volume alias."
|
|
fi
|
|
}
|
|
|
|
#=============================================================================
|
|
# USER DETECTION
|
|
#=============================================================================
|
|
|
|
# Get the real user who invoked the script (even when running with sudo)
|
|
# Usage: real_user=$(get_real_user)
|
|
# Returns: username of the real user, or "root" if cannot determine
|
|
get_real_user() {
|
|
# Try SUDO_USER first (set by sudo)
|
|
if [[ -n "${SUDO_USER:-}" && "$SUDO_USER" != "root" ]]; then
|
|
echo "$SUDO_USER"
|
|
return 0
|
|
fi
|
|
|
|
# Try logname (gets login name)
|
|
local logname_user
|
|
logname_user=$(logname 2>/dev/null) || true
|
|
if [[ -n "$logname_user" && "$logname_user" != "root" ]]; then
|
|
echo "$logname_user"
|
|
return 0
|
|
fi
|
|
|
|
# Try who am i (gets TTY user)
|
|
local who_user
|
|
who_user=$(who am i 2>/dev/null | awk '{print $1}') || true
|
|
if [[ -n "$who_user" && "$who_user" != "root" ]]; then
|
|
echo "$who_user"
|
|
return 0
|
|
fi
|
|
|
|
# Check if we're in a user's home directory
|
|
local current_dir="$PWD"
|
|
if [[ "$current_dir" =~ ^/home/([^/]+) ]]; then
|
|
local home_user="${BASH_REMATCH[1]}"
|
|
if id "$home_user" &>/dev/null; then
|
|
echo "$home_user"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Fallback to current user
|
|
whoami
|
|
}
|
|
|
|
# Get the home directory of the real user
|
|
# Usage: real_home=$(get_real_user_home)
|
|
get_real_user_home() {
|
|
local real_user
|
|
real_user=$(get_real_user)
|
|
eval echo "~$real_user"
|
|
}
|
|
|
|
#=============================================================================
|
|
# DIRECTORY PRESERVATION (for git updates)
|
|
#=============================================================================
|
|
# Directories containing user-customizable content that should survive git reset.
|
|
# Used by update.sh to backup before git operations and restore after.
|
|
PRESERVE_DIRS=("python-runner")
|
|
|
|
# Backup preserved directories before git reset
|
|
# Usage: backup_path=$(backup_preserved_dirs) || exit 1
|
|
# Returns: 0 on success (prints backup path to stdout), 1 on failure
|
|
# NOTE: All logs go to stderr to keep stdout clean for the return value
|
|
backup_preserved_dirs() {
|
|
local backup_base=""
|
|
local has_content=0
|
|
|
|
# Check if any directories need backup
|
|
for dir in "${PRESERVE_DIRS[@]}"; do
|
|
if [ -d "$PROJECT_ROOT/$dir" ] && [ -n "$(ls -A "$PROJECT_ROOT/$dir" 2>/dev/null)" ]; then
|
|
has_content=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
# No content to backup
|
|
if [ $has_content -eq 0 ]; then
|
|
echo ""
|
|
return 0
|
|
fi
|
|
|
|
# Create secure temporary directory
|
|
backup_base=$(mktemp -d /tmp/n8n-install-backup.XXXXXXXXXX) || {
|
|
echo "[ERROR] Failed to create backup directory" >&2
|
|
return 1
|
|
}
|
|
chmod 700 "$backup_base"
|
|
|
|
# Backup each directory
|
|
for dir in "${PRESERVE_DIRS[@]}"; do
|
|
# Validate directory name (no path traversal)
|
|
if [[ "$dir" =~ \.\.|^/ ]]; then
|
|
echo "[ERROR] Invalid directory name in PRESERVE_DIRS: $dir" >&2
|
|
rm -rf "$backup_base"
|
|
return 1
|
|
fi
|
|
|
|
if [ -d "$PROJECT_ROOT/$dir" ] && [ -n "$(ls -A "$PROJECT_ROOT/$dir" 2>/dev/null)" ]; then
|
|
echo "[INFO] Backing up $dir/ before git reset..." >&2
|
|
if ! cp -rp "$PROJECT_ROOT/$dir" "$backup_base/$dir"; then
|
|
echo "[ERROR] Failed to backup $dir/. Aborting to prevent data loss." >&2
|
|
rm -rf "$backup_base"
|
|
return 1
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo "$backup_base"
|
|
return 0
|
|
}
|
|
|
|
# Restore preserved directories after git reset
|
|
# Usage: restore_preserved_dirs <backup_base_path>
|
|
# Returns: 0 on success, 1 on failure
|
|
restore_preserved_dirs() {
|
|
local backup_base="$1"
|
|
|
|
# Nothing to restore
|
|
if [ -z "$backup_base" ]; then
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -d "$backup_base" ]; then
|
|
log_warning "Backup directory not found: $backup_base"
|
|
return 0
|
|
fi
|
|
|
|
# Safety checks for PROJECT_ROOT
|
|
if [ -z "$PROJECT_ROOT" ]; then
|
|
log_error "PROJECT_ROOT is not set. Refusing to restore."
|
|
return 1
|
|
fi
|
|
|
|
if [ "$PROJECT_ROOT" = "/" ] || [ "$PROJECT_ROOT" = "/root" ] || [ "$PROJECT_ROOT" = "/home" ]; then
|
|
log_error "PROJECT_ROOT is set to a dangerous path: $PROJECT_ROOT"
|
|
return 1
|
|
fi
|
|
|
|
for dir in "${PRESERVE_DIRS[@]}"; do
|
|
# Validate directory name
|
|
if [[ "$dir" =~ \.\.|^/ ]] || [ -z "$dir" ]; then
|
|
log_error "Invalid directory name: $dir"
|
|
continue
|
|
fi
|
|
|
|
if [ -d "$backup_base/$dir" ]; then
|
|
log_info "Restoring $dir/ after git reset..."
|
|
|
|
# Remove the git-restored version
|
|
if [ -d "$PROJECT_ROOT/$dir" ]; then
|
|
if ! rm -rf "$PROJECT_ROOT/$dir"; then
|
|
log_error "Failed to remove $PROJECT_ROOT/$dir"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Restore from backup
|
|
if ! mv "$backup_base/$dir" "$PROJECT_ROOT/$dir"; then
|
|
log_error "Failed to restore $dir/ from backup"
|
|
return 1
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Cleanup backup directory
|
|
rm -rf "$backup_base"
|
|
return 0
|
|
}
|