#!/bin/bash set -e # Constants NOCO_HOME="./nocodb" CURRENT_PATH=$(pwd) REQUIRED_PORTS=(80 443) # Color definitions RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' ORANGE='\033[0;33m' BOLD='\033[1m' NC='\033[0m' # Global variables CONFIG_DOMAIN_NAME="" CONFIG_SSL_ENABLED="" CONFIG_EDITION="" CONFIG_LICENSE_KEY="" CONFIG_REDIS_ENABLED="" CONFIG_MINIO_ENABLED="" CONFIG_MINIO_DOMAIN_NAME="" CONFIG_MINIO_SSL_ENABLED="" CONFIG_WATCHTOWER_ENABLED="" CONFIG_NUM_INSTANCES="" CONFIG_POSTGRES_PASSWORD="" CONFIG_REDIS_PASSWORD="" CONFIG_MINIO_ACCESS_KEY="" CONFIG_MINIO_ACCESS_SECRET="" CONFIG_DOCKER_COMMAND="" declare -a message_arr # Utility functions print_color() { printf "${1}%s${NC}\n" "$2"; } print_info() { print_color "$BLUE" "INFO: $1"; } print_success() { print_color "$GREEN" "SUCCESS: $1"; } print_warning() { print_color "$YELLOW" "WARNING: $1"; } print_error() { print_color "$RED" "ERROR: $1"; } print_box_message() { local message=("$@") local edge="======================================" local padding=" " echo "$edge" for element in "${message[@]}"; do echo "${padding}${element}" done echo "$edge" } print_note() { local note_text="$1" local note_color='\033[0;33m' # Yellow color local bold='\033[1m' local reset='\033[0m' echo -e "${note_color}${bold}NOTE:${reset} ${note_text}" } command_exists() { command -v "$1" >/dev/null 2>&1; } is_valid_domain() { local domain_regex="^([a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9])?\.[a-zA-Z]{2,}$" [[ "$1" =~ $domain_regex ]] } urlencode() { local string="$1" local strlen=${#string} local encoded="" local pos c o for (( pos=0 ; pos/dev/null 2>&1; then ip=$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null) if [ -n "$ip" ]; then echo "$ip" return fi fi # Method 2: Using curl if command -v curl >/dev/null 2>&1; then ip=$(curl -s -4 https://ifconfig.co 2>/dev/null) if [ -n "$ip" ]; then echo "$ip" return fi fi # Method 3: Using wget if command -v wget >/dev/null 2>&1; then ip=$(wget -qO- https://ifconfig.me 2>/dev/null) if [ -n "$ip" ]; then echo "$ip" return fi fi # Method 4: Using host if command -v host >/dev/null 2>&1; then ip=$(host myip.opendns.com resolver1.opendns.com 2>/dev/null | grep "myip.opendns.com has" | awk '{print $4}') if [ -n "$ip" ]; then echo "$ip" return fi fi # If all methods fail, return localhost echo "localhost" } get_nproc() { # Try to get the number of processors using nproc if command -v nproc &> /dev/null; then nproc else # Fallback: Check if /proc/cpuinfo exists and count the number of processors if [[ -f /proc/cpuinfo ]]; then grep -c ^processor /proc/cpuinfo # Fallback for macOS or BSD systems using sysctl elif command -v sysctl &> /dev/null; then sysctl -n hw.ncpu # Default to 1 processor if everything else fails else echo 1 fi fi } prompt() { local prompt_text="$1" local default_value="$2" local response if [ -n "$default_value" ]; then prompt_text+=" (default: $default_value)" fi prompt_text+=": " read -r -p "$prompt_text" response if [ -z "$response" ] && [ -n "$default_value" ]; then echo "$default_value" else echo "$response" fi } prompt_required() { local prompt_text="$1" local response while true; do read -r -p "$prompt_text: " response if [ -n "$response" ]; then echo "$response" return fi print_error "This field is required." done } prompt_number() { local prompt_text="$1" local min="$2" local max="$3" local response while true; do read -r -p "$prompt_text ($min-$max): " response if [[ "$response" =~ ^[0-9]+$ ]] && [ "$response" -ge "$min" ] && [ "$response" -le "$max" ]; then echo "$response" return fi print_error "Please enter a number between $min and $max." done } confirm() { local prompt_text="$1" local default_response="${2:-N}" local response if [ "$default_response" = "Y" ] || [ "$default_response" = "y" ]; then prompt_text+=" [Y/n]: " else prompt_text+=" [y/N]: " fi read -r -p "$prompt_text" response response="${response:-$default_response}" if [ "$response" = "Y" ] || [ "$response" = "y" ]; then return 0 else return 1 fi } generate_contact_email() { local domain="$1" local email if [ -z "$domain" ] || [ "$domain" = "localhost" ] || [[ "$domain" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then email="contact@example.com" else domain="${domain#http://}" domain="${domain#https://}" domain="${domain%%/*}" domain="${domain%%\?*}" if [[ "$domain" =~ [^.]+\.[^.]+$ ]]; then main_domain="${BASH_REMATCH[0]}" else main_domain="$domain" fi email="contact@$main_domain" fi echo "$email" } install_package() { if command_exists yum; then sudo yum install -y "$1" elif command_exists apt; then sudo apt install -y "$1" elif command_exists brew; then brew install "$1" else print_error "Package manager not found. Please install $1 manually." exit 1 fi } add_to_hosts() { local IP="127.0.0.1" local HOSTS_FILE="/etc/hosts" local TEMP_HOSTS_FILE="/tmp/hosts.tmp" if is_valid_domain $CONFIG_MINIO_DOMAIN_NAME; then return 0 elif sudo grep -q "${CONFIG_MINIO_DOMAIN_NAME}" "$HOSTS_FILE"; then return 0 else sudo cp "$HOSTS_FILE" "$TEMP_HOSTS_FILE" echo "$IP ${CONFIG_MINIO_DOMAIN_NAME}" | sudo tee -a "$TEMP_HOSTS_FILE" > /dev/null if sudo mv "$TEMP_HOSTS_FILE" "$HOSTS_FILE"; then print_info "Added ${CONFIG_MINIO_DOMAIN_NAME} to $HOSTS_FILE" print_note "You may need to reboot your system, If the uploaded attachments are not accessible." else print_error "Failed to update $HOSTS_FILE. Please check your permissions." return 1 fi fi } check_for_docker_sudo() { if docker ps >/dev/null 2>&1; then echo "n" else echo "y" fi } read_number() { local prompt="$1" local default="$2" local number while true; do if [ -n "$default" ]; then read -rp "$prompt [$default]: " number number=${number:-$default} else read -rp "$prompt: " number fi if [ -z "$number" ]; then echo "Input cannot be empty. Please enter a number." elif ! [[ $number =~ ^[0-9]+$ ]]; then echo "Invalid input. Please enter a valid number." else echo "$number" return fi done } read_number_range() { local prompt="$1" local min="$2" local max="$3" local default="$4" local number while true; do if [ -n "$default" ]; then number=$(read_number "$prompt ($min-$max)" "$default") else number=$(read_number "$prompt ($min-$max)") fi if [ -z "$number" ]; then continue elif [ "$number" -lt "$min" ] || [ "$number" -gt "$max" ]; then echo "Please enter a number between $min and $max." else echo "$number" return fi done } check_if_docker_is_running() { if ! $CONFIG_DOCKER_COMMAND ps >/dev/null 2>&1; then print_warning "Docker is not running. Most of the commands will not work without Docker." print_info "Use the following command to start Docker:" print_color "$BLUE" " sudo systemctl start docker" fi } # Main functions check_existing_installation() { NOCO_FOUND=false # Check if $NOCO_HOME exists as directory if [ -d "$NOCO_HOME" ]; then NOCO_FOUND=true elif $CONFIG_DOCKER_COMMAND ps --format '{{.Names}}' | grep -q "nocodb"; then NOCO_ID=$($CONFIG_DOCKER_COMMAND ps | grep "nocodb/nocodb" | cut -d ' ' -f 1) CUSTOM_HOME=$($CONFIG_DOCKER_COMMAND inspect --format='{{index .Mounts 0}}' "$NOCO_ID" | cut -d ' ' -f 3) PARENT_DIR=$(dirname "$CUSTOM_HOME") ln -s "$PARENT_DIR" "$NOCO_HOME" basename "$PARENT_DIR" > "$NOCO_HOME/.COMPOSE_PROJECT_NAME" NOCO_FOUND=true else mkdir -p "$NOCO_HOME" fi cd "$NOCO_HOME" || exit 1 # Check if nocodb is already installed if [ "$NOCO_FOUND" = true ]; then echo "NocoDB is already installed. And running." echo "Do you want to reinstall NocoDB? [Y/N] (default: N): " read -r REINSTALL if [ -f "$NOCO_HOME/.COMPOSE_PROJECT_NAME" ]; then COMPOSE_PROJECT_NAME=$(cat "$NOCO_HOME/.COMPOSE_PROJECT_NAME") export COMPOSE_PROJECT_NAME fi if [ "$REINSTALL" != "Y" ] && [ "$REINSTALL" != "y" ]; then management_menu exit 0 else echo "Reinstalling NocoDB..." $CONFIG_DOCKER_COMMAND compose down unset COMPOSE_PROJECT_NAME cd /tmp || exit 1 rm -rf "$NOCO_HOME" cd "$CURRENT_PATH" || exit 1 mkdir -p "$NOCO_HOME" cd "$NOCO_HOME" || exit 1 fi fi } check_system_requirements() { print_info "Performing NocoDB system check and setup" for tool in docker wget lsof openssl; do if ! command_exists "$tool"; then print_warning "$tool is not installed. Setting up for installation..." if [ "$tool" = "docker" ]; then wget -qO- https://get.docker.com/ | sh else install_package "$tool" fi fi done for port in "${REQUIRED_PORTS[@]}"; do if lsof -Pi :"$port" -sTCP:LISTEN -t >/dev/null; then print_warning "Port $port is in use. Please make sure it is free." else print_info "Port $port is free." fi done print_success "System check completed successfully" } get_user_inputs() { CONFIG_DOMAIN_NAME=$(prompt "Enter the IP address or domain name for the NocoDB instance" "$(get_public_ip)") if is_valid_domain "$CONFIG_DOMAIN_NAME"; then if confirm "Do you want to configure SSL for $CONFIG_DOMAIN_NAME?"; then CONFIG_SSL_ENABLED="Y" else CONFIG_SSL_ENABLED="N" fi else CONFIG_SSL_ENABLED="N" fi if confirm "Show Advanced Options?"; then get_advanced_options else set_default_options fi } get_advanced_options() { CONFIG_EDITION=$(prompt "Choose Community or Enterprise Edition [CE/EE]" "CE") if [ "$CONFIG_EDITION" = "EE" ] || [ "$CONFIG_EDITION" = "ee" ]; then CONFIG_LICENSE_KEY=$(prompt_required "Enter the NocoDB license key") fi CONFIG_REDIS_ENABLED=$(confirm "Do you want to enable Redis for caching?" "Y" && echo "Y" || echo "N" "Y") CONFIG_MINIO_ENABLED=$(confirm "Do you want to enable Minio for file storage?" "Y" && echo "Y" || echo "N" "Y") if [ "$CONFIG_MINIO_ENABLED" = "Y" ] || [ "$CONFIG_MINIO_ENABLED" = "y" ]; then CONFIG_MINIO_DOMAIN_NAME=$(prompt "Enter the MinIO domain name" "$(get_public_ip)") if is_valid_domain "$CONFIG_MINIO_DOMAIN_NAME"; then if confirm "Do you want to configure SSL for $CONFIG_MINIO_DOMAIN_NAME?"; then CONFIG_MINIO_SSL_ENABLED="Y" else CONFIG_MINIO_SSL_ENABLED="N" fi else CONFIG_MINIO_SSL_ENABLED="N" fi fi CONFIG_WATCHTOWER_ENABLED=$(confirm "Do you want to enable Watchtower for automatic updates?" "Y" && echo "Y" || echo "N") NUM_CORES=$(get_nproc) CONFIG_NUM_INSTANCES=$(read_number_range "How many instances of NocoDB do you want to run?" 1 "$NUM_CORES" 1) } set_default_options() { CONFIG_SSL_ENABLED="N" CONFIG_EDITION="CE" CONFIG_REDIS_ENABLED="Y" CONFIG_MINIO_ENABLED="Y" CONFIG_MINIO_DOMAIN_NAME=$(get_public_ip) CONFIG_MINIO_SSL_ENABLED="N" CONFIG_WATCHTOWER_ENABLED="Y" CONFIG_NUM_INSTANCES=1 } generate_credentials() { CONFIG_POSTGRES_PASSWORD=$(generate_password) CONFIG_REDIS_PASSWORD=$(generate_password) CONFIG_MINIO_ACCESS_KEY=$(generate_password) CONFIG_MINIO_ACCESS_SECRET=$(generate_password) } create_docker_compose_file() { image="nocodb/nocodb:latest" if [ "${CONFIG_EDITION}" = "EE" ] || [ "${CONFIG_EDITION}" = "ee" ]; then image="nocodb/ee:latest" fi local compose_file="docker-compose.yml" cat > "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" <> "$compose_file" < "$env_file" <> "$env_file" echo "NC_LICENSE_KEY=${CONFIG_LICENSE_KEY}" >> "$env_file" else echo "NC_DB=pg://db:5432?d=nocodb&user=postgres&password=${encoded_password}" >> "$env_file" fi if [ "${CONFIG_REDIS_ENABLED}" = "Y" ]; then cat >> "$env_file" <> "$env_file" <> "$env_file" elif is_valid_domain "$CONFIG_MINIO_DOMAIN_NAME"; then echo "NC_S3_ENDPOINT=http://${CONFIG_MINIO_DOMAIN_NAME}" >> "$env_file" else echo "NC_S3_ENDPOINT=http://${CONFIG_MINIO_DOMAIN_NAME}:9000" >> "$env_file" fi fi } create_update_script() { cat > ./update.sh <