#!/usr/bin/env bash if [ "$1" == '--debug' ]; then set -x fi set -e # Constants NOCO_HOME="$(pwd)/nocodb" REQUIRED_PORTS=(80 443) state_file="$NOCO_HOME/noco.state" state_dlim="|" # 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="" CONFIG_POSTGRES_SQLITE="" 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"; } die() { : "${1:?}" command -v notify-send >/dev/null && notify-send "upstall" "$1" printf "\033[31;1merr: %b\033[0m\n" "$1" exit "${2:-1}" } trim() { : "${1:?}" _trimstr="${1#"${1%%[![:space:]]*}"}" _trimstr="${_trimstr%"${_trimstr##*[![:space:]]}"}" echo "$_trimstr" } kvstore_get() { # usage kvstore_get [ getval ] line= _key= _value= [ -s "$state_file" ] || return 1 while read -r line; do [ -z "$line" ] && continue _key="${line%%"$state_dlim"*}" _key="$(trim "$_key")" case "$_key" in \#*) continue ;; esac if [ "$1" = "getval" ]; then [ "$2" != "$_key" ] && continue _value="${line##*"$state_dlim"}" _value="$(trim "$_value")" echo "$_value" return 0 else echo "$_key" fi done <"$state_file" unset _key _value [ "$1" = "getval" ] && return 1 } kvstore_rm() { # usage kvstore_rm : "${1:?}" cl= line= file= old_ifs="$IFS" IFS= while read -r line; do cl="$line\n" key="$(trim "${cl%%"$state_dlim"*}")" # catch match if [ "$key" = "$1" ]; then continue fi file="${file}${cl}" done <"$state_file" IFS="$old_ifs" # shellcheck disable=SC2059 printf "$file" >"$state_file" unset cl line file value old_ifs } kvstore_valverify() { # kvstore_valverify case "$1" in *"\n"* | *$state_dlim*) return 1 ;; esac } kvstore_set() { # kvstore_set : "${1:?}" : "${2:?}" key="$(echo "$1" | tr -d "$state_dlim")" key="$(trim "$key")" val="$(trim "$2")" kvstore_get getval "$key" >/dev/null && die "keys must be unique" kvstore_valverify "$val" || die "invalid: $val" echo "${key:?} $state_dlim $val" >>"$state_file" } 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 < strlen; pos++)); do c=${string:$pos:1} case "$c" in [-_.~a-zA-Z0-9]) o="$c" ;; *) printf -v o '%%%02X' "'$c" ;; esac encoded+="$o" done echo "$encoded" } generate_password() { if ! pass="$(kvstore_get getval generated_password)"; then pass="$(tr -dc A-Za-z0-9 /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 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 # Method 3: Using curl if command -v curl >/dev/null 2>&1; then ip="$(curl -s -4 https://ip.me 2>/dev/null)" if echo "$ip" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then echo "$ip" return fi fi # Method 4: Using wget if command -v wget >/dev/null 2>&1; then ip="$(wget -qO- https://ifconfig.me 2>/dev/null)" if echo "$ip" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; 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_oneof() { local response local resp_upper local oneof_text local prompt_text="$1" local default_response="$2" shift 1 prompt_text+=" (default: $default_response) " oneof_text+="[" for one in "$@"; do oneof_text+="$one," done oneof_text="${oneof_text%%,}]" prompt_text+="${oneof_text}: " while true; do read -r -p "$prompt_text" response if [ -z "$response" ]; then echo "$default_response" return fi for one in "$@"; do resp_upper="$(echo "$response" | tr '[:lower:]' '[:upper:]')" one_upper="$(echo "$one" | tr '[:lower:]' '[:upper:]')" if [ "$resp_upper" = "$one_upper" ]; then echo "$one" return fi done print_error "This field should be one of ${oneof_text}." done } 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 secondary_response local response case "$default_response" in "Y") secondary_response="N" ;; "N") secondary_response="Y" ;; esac response="$(prompt_oneof "$prompt_text" "$default_response" "$secondary_response")" if [ "$response" = "Y" ]; then return 0 elif [ "$response" = "N" ]; then return 1 fi } # Function to check if input is IP address is_ip() { local input="$1" [[ "$input" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] } generate_contact_email() { local primary_domain="$1" local secondary_domain="$2" local email local domain_to_use # Try primary domain first if [ -n "$primary_domain" ] && ! is_ip "$primary_domain" && [ "$primary_domain" != "localhost" ]; then domain_to_use="$primary_domain" # Try secondary domain if primary is not valid elif [ -n "$secondary_domain" ] && ! is_ip "$secondary_domain" && [ "$secondary_domain" != "localhost" ]; then domain_to_use="$secondary_domain" # Fallback if neither domain is valid else echo "Warning: No valid domain found for SSL certificate email, using example.com. This may cause self-signed certificate errors in production." email="contact@example.com" echo "$email" return fi # Clean up the chosen domain domain_to_use="${domain_to_use#http://}" domain_to_use="${domain_to_use#https://}" domain_to_use="${domain_to_use%%/*}" domain_to_use="${domain_to_use%%\?*}" email="contact@$domain_to_use" 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 } print_empty_line() { local count=${1:-1} for ((i = 0; i < count; i++)); do echo 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 } persistent_store_isdeleted() { _persistent_store="$1" if [ ! -d "$_persistent_store" ]; then print_warning "Persistent store was deleted without stopping the containers" for container in $($CONFIG_DOCKER_COMMAND ps | grep -Eo 'nocodb-[a-z]+-[0-9]+$'); do if ! $CONFIG_DOCKER_COMMAND stop "$container" >/dev/null 2>&1; then print_error "Failed to stop ${container}" exit 1 fi done return 0 fi return 1 } migrate_0_1() { generated_password=$(grep POSTGRES_PASSWORD "$NOCO_HOME/docker.env" | cut -d= -f2) if [ -n "$generated_password" ]; then kvstore_set generated_password "$generated_password" fi kvstore_set state_version 1 } migrate() { if ! state_version="$(kvstore_get getval state_version)"; then state_version="0" fi case "$state_version" in "0") migrate_0_1 esac } # 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")" if ! persistent_store_isdeleted "$PARENT_DIR"; then ln -s "$PARENT_DIR" "$NOCO_HOME" basename "$PARENT_DIR" >"$NOCO_HOME/.COMPOSE_PROJECT_NAME" NOCO_FOUND=true fi fi mkdir -p "$NOCO_HOME" cd "$NOCO_HOME" || exit 1 migrate # Check if nocodb is already installed if [ "$NOCO_FOUND" = true ]; then echo "NocoDB is already installed. And running." reinstall="$(prompt_oneof "Do you want to reinstall NocoDB" "N" "Y")" if [ -f "$NOCO_HOME/.COMPOSE_PROJECT_NAME" ]; then COMPOSE_PROJECT_NAME=$(cat "$NOCO_HOME/.COMPOSE_PROJECT_NAME") export COMPOSE_PROJECT_NAME fi if [ "$reinstall" == "N" ]; then management_menu exit 0 else echo "Reinstalling NocoDB..." $CONFIG_DOCKER_COMMAND compose down -v unset COMPOSE_PROJECT_NAME 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() { # For fixing test failures due to missing XTerm environment clear || : 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" <>"$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" elif [ "${CONFIG_POSTGRES_SQLITE}" = "P" ]; then 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" <>"$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 <