From 0d37ec20d4c57bd79db2856915597b81b7fb5958 Mon Sep 17 00:00:00 2001 From: M03ED <50927468+M03ED@users.noreply.github.com> Date: Fri, 1 May 2026 14:31:29 +0330 Subject: [PATCH 1/9] refactor: lib directory and handle duplicate functions once --- lib/common.sh | 270 +++++++++++++++++++++++++++++++++++ lib/env.sh | 96 +++++++++++++ pasarguard.sh | 381 +++----------------------------------------------- pg-node.sh | 266 +++-------------------------------- 4 files changed, 403 insertions(+), 610 deletions(-) create mode 100644 lib/common.sh create mode 100644 lib/env.sh diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..3a525fc --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,270 @@ +#!/usr/bin/env bash + +SHARED_LIB_INSTALL_DIR="/usr/local/lib/pasarguard-scripts/lib" + +colorized_echo() { + local color="$1" + local text="$2" + local style="${3:-0}" + + case "$color" in + red) + printf "\e[${style};91m%s\e[0m\n" "$text" + ;; + green) + printf "\e[${style};92m%s\e[0m\n" "$text" + ;; + yellow) + printf "\e[${style};93m%s\e[0m\n" "$text" + ;; + blue) + printf "\e[${style};94m%s\e[0m\n" "$text" + ;; + magenta) + printf "\e[${style};95m%s\e[0m\n" "$text" + ;; + cyan) + printf "\e[${style};96m%s\e[0m\n" "$text" + ;; + *) + printf "%s\n" "$text" + ;; + esac +} + +die() { + colorized_echo red "$*" + exit 1 +} + +check_running_as_root() { + if [ "$(id -u)" != "0" ]; then + die "This command must be run as root." + fi +} + +detect_os() { + if [ -f /etc/lsb-release ]; then + OS=$(lsb_release -si) + elif [ -f /etc/os-release ]; then + OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') + elif [ -f /etc/redhat-release ]; then + OS=$(awk '{print $1}' /etc/redhat-release) + elif [ -f /etc/arch-release ]; then + OS="Arch Linux" + else + die "Unsupported operating system" + fi +} + +detect_and_update_package_manager() { + colorized_echo blue "Updating package manager" + + if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then + PKG_MANAGER="apt-get" + $PKG_MANAGER update -qq >/dev/null 2>&1 + elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then + PKG_MANAGER="yum" + $PKG_MANAGER update -y -q >/dev/null 2>&1 + $PKG_MANAGER install -y -q epel-release >/dev/null 2>&1 + elif [[ "$OS" == "Fedora"* ]]; then + PKG_MANAGER="dnf" + $PKG_MANAGER update -q -y >/dev/null 2>&1 + elif [[ "$OS" == "Arch Linux" ]] || [[ "$OS" == "Arch"* ]]; then + PKG_MANAGER="pacman" + $PKG_MANAGER -Sy --noconfirm --quiet >/dev/null 2>&1 + elif [[ "$OS" == "openSUSE"* ]]; then + PKG_MANAGER="zypper" + $PKG_MANAGER refresh --quiet >/dev/null 2>&1 + else + die "Unsupported operating system" + fi +} + +install_package() { + local package="$1" + + if [ -z "${PKG_MANAGER:-}" ]; then + detect_and_update_package_manager + fi + + colorized_echo blue "Installing $package" + if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then + $PKG_MANAGER -y -qq install "$package" >/dev/null 2>&1 + elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then + $PKG_MANAGER install -y -q "$package" >/dev/null 2>&1 + elif [[ "$OS" == "Fedora"* ]]; then + $PKG_MANAGER install -y -q "$package" >/dev/null 2>&1 + elif [[ "$OS" == "Arch Linux" ]] || [[ "$OS" == "Arch"* ]]; then + $PKG_MANAGER -S --noconfirm --quiet "$package" >/dev/null 2>&1 + elif [[ "$OS" == "openSUSE"* ]]; then + $PKG_MANAGER --quiet install -y "$package" >/dev/null 2>&1 + else + die "Unsupported operating system" + fi +} + +install_docker() { + colorized_echo blue "Installing Docker" + curl -fsSL https://get.docker.com | sh + colorized_echo green "Docker installed successfully" +} + +detect_compose() { + if docker compose version >/dev/null 2>&1; then + COMPOSE='docker compose' + elif docker-compose version >/dev/null 2>&1; then + COMPOSE='docker-compose' + else + die "docker compose not found" + fi +} + +check_editor() { + if [ -z "${EDITOR:-}" ]; then + if command -v nano >/dev/null 2>&1; then + EDITOR="nano" + elif command -v vi >/dev/null 2>&1; then + EDITOR="vi" + else + detect_os + install_package nano + EDITOR="nano" + fi + fi +} + +identify_the_operating_system_and_architecture() { + if [[ "$(uname)" != "Linux" ]]; then + die "error: This operating system is not supported." + fi + + case "$(uname -m)" in + i386 | i686) + ARCH='32' + ;; + amd64 | x86_64) + ARCH='64' + ;; + armv5tel) + ARCH='arm32-v5' + ;; + armv6l) + ARCH='arm32-v6' + grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' + ;; + armv7 | armv7l) + ARCH='arm32-v7a' + grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' + ;; + armv8 | aarch64) + ARCH='arm64-v8a' + ;; + mips) + ARCH='mips32' + ;; + mipsle) + ARCH='mips32le' + ;; + mips64) + ARCH='mips64' + lscpu | grep -q "Little Endian" && ARCH='mips64le' + ;; + mips64le) + ARCH='mips64le' + ;; + ppc64) + ARCH='ppc64' + ;; + ppc64le) + ARCH='ppc64le' + ;; + riscv64) + ARCH='riscv64' + ;; + s390x) + ARCH='s390x' + ;; + *) + die "error: The architecture is not supported." + ;; + esac +} + +install_yq() { + local base_url="https://github.com/mikefarah/yq/releases/latest/download" + local yq_binary="" + local yq_url="" + + if command -v yq >/dev/null 2>&1; then + colorized_echo green "yq is already installed." + return + fi + + identify_the_operating_system_and_architecture + + case "$ARCH" in + 64 | x86_64) + yq_binary="yq_linux_amd64" + ;; + arm32-v7a | arm32-v6 | arm32-v5 | armv7l) + yq_binary="yq_linux_arm" + ;; + arm64-v8a | aarch64) + yq_binary="yq_linux_arm64" + ;; + 32 | i386 | i686) + yq_binary="yq_linux_386" + ;; + *) + die "Unsupported architecture: $ARCH" + ;; + esac + + yq_url="${base_url}/${yq_binary}" + colorized_echo blue "Downloading yq from ${yq_url}..." + + if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then + colorized_echo yellow "Neither curl nor wget is installed. Attempting to install curl." + install_package curl || die "Failed to install curl. Please install curl or wget manually." + fi + + if command -v curl >/dev/null 2>&1; then + curl -L "$yq_url" -o /usr/local/bin/yq || die "Failed to download yq using curl. Please check your internet connection." + elif command -v wget >/dev/null 2>&1; then + wget -O /usr/local/bin/yq "$yq_url" || die "Failed to download yq using wget. Please check your internet connection." + fi + + chmod +x /usr/local/bin/yq + colorized_echo green "yq installed successfully!" + + if ! echo "$PATH" | grep -q "/usr/local/bin"; then + export PATH="/usr/local/bin:$PATH" + fi +} + +install_shared_libs_from_local() { + local source_dir="$1" + + mkdir -p "$SHARED_LIB_INSTALL_DIR" + install -m 644 "$source_dir/lib/common.sh" "$SHARED_LIB_INSTALL_DIR/common.sh" + if [ -f "$source_dir/lib/env.sh" ]; then + install -m 644 "$source_dir/lib/env.sh" "$SHARED_LIB_INSTALL_DIR/env.sh" + fi +} + +install_shared_libs_from_repo() { + local fetch_repo="$1" + local tmp_dir="" + + tmp_dir=$(mktemp -d) + mkdir -p "$SHARED_LIB_INSTALL_DIR" + + curl -fsSL "https://github.com/${fetch_repo}/raw/main/lib/common.sh" -o "$tmp_dir/common.sh" + install -m 644 "$tmp_dir/common.sh" "$SHARED_LIB_INSTALL_DIR/common.sh" + + curl -fsSL "https://github.com/${fetch_repo}/raw/main/lib/env.sh" -o "$tmp_dir/env.sh" + install -m 644 "$tmp_dir/env.sh" "$SHARED_LIB_INSTALL_DIR/env.sh" + + rm -rf "$tmp_dir" +} diff --git a/lib/env.sh b/lib/env.sh new file mode 100644 index 0000000..b914ee2 --- /dev/null +++ b/lib/env.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +replace_or_append_env_var() { + local key="$1" + local value="$2" + local quote_value="${3:-false}" + local target_file="${4:-$ENV_FILE}" + local formatted_value="$value" + local escaped_value="" + + if [ "$quote_value" = "true" ]; then + local sanitized_value="${value//\"/\\\"}" + formatted_value="\"$sanitized_value\"" + fi + + escaped_value=$(printf '%s' "$formatted_value" | sed -e 's/[&|\\]/\\&/g') + + if grep -q "^$key=" "$target_file"; then + sed -i "s|^$key=.*|$key=$escaped_value|" "$target_file" + else + printf '%s=%s\n' "$key" "$formatted_value" >>"$target_file" + fi +} + +set_or_uncomment_env_var() { + local key="$1" + local value="$2" + local quote_value="${3:-false}" + local target_file="${4:-$ENV_FILE}" + local formatted_value="$value" + local tmp_file="" + + if [ "$quote_value" = "true" ]; then + local sanitized_value="${value//\"/\\\"}" + formatted_value="\"$sanitized_value\"" + fi + + [ -f "$target_file" ] || touch "$target_file" + tmp_file=$(mktemp) + + awk -v env_key="$key" -v env_line="${key} = ${formatted_value}" ' + BEGIN { replaced = 0 } + { + if ($0 ~ "^[[:space:]]*#?[[:space:]]*" env_key "[[:space:]]*=") { + if (replaced == 0) { + print env_line + replaced = 1 + } + next + } + print + } + END { + if (replaced == 0) { + print env_line + } + } + ' "$target_file" >"$tmp_file" + + mv "$tmp_file" "$target_file" +} + +comment_out_env_var() { + local key="$1" + local target_file="${2:-$ENV_FILE}" + local tmp_file="" + + [ -f "$target_file" ] || return 0 + tmp_file=$(mktemp) + + awk -v env_key="$key" ' + BEGIN { done = 0 } + { + if ($0 ~ "^[[:space:]]*#?[[:space:]]*" env_key "[[:space:]]*=") { + if (done == 0) { + line = $0 + sub("^[[:space:]]*#?[[:space:]]*" env_key "[[:space:]]*=[[:space:]]*", "", line) + print "# " env_key " = " line + done = 1 + } + next + } + print + } + ' "$target_file" >"$tmp_file" + + mv "$tmp_file" "$target_file" +} + +delete_env_var() { + local key="$1" + local target_file="${2:-$ENV_FILE}" + + [ -f "$target_file" ] || return 0 + sed -i "/^[[:space:]]*${key}[[:space:]]*=/d" "$target_file" +} diff --git a/pasarguard.sh b/pasarguard.sh index 47a2e66..8143c3a 100755 --- a/pasarguard.sh +++ b/pasarguard.sh @@ -1,6 +1,22 @@ #!/usr/bin/env bash set -e +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +SHARED_LIB_DIR="${SCRIPT_DIR}/lib" +if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then + SHARED_LIB_DIR="/usr/local/lib/pasarguard-scripts/lib" +fi + +if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then + printf 'Missing shared library: %s\n' "$SHARED_LIB_DIR/common.sh" >&2 + exit 1 +fi + +# shellcheck source=lib/common.sh +source "$SHARED_LIB_DIR/common.sh" +# shellcheck source=lib/env.sh +source "$SHARED_LIB_DIR/env.sh" + # Handle @ symbol if used in installation (skip it) if [ "$1" == "@" ]; then shift @@ -17,101 +33,6 @@ COMPOSE_FILE="$APP_DIR/docker-compose.yml" ENV_FILE="$APP_DIR/.env" LAST_XRAY_CORES=10 -replace_or_append_env_var() { - local key="$1" - local value="$2" - local quote_value="${3:-false}" - local target_file="${4:-$ENV_FILE}" - local formatted_value="$value" - - if [ "$quote_value" = "true" ]; then - local sanitized_value="${value//\"/\\\"}" - formatted_value="\"$sanitized_value\"" - fi - - local escaped_value - escaped_value=$(printf '%s' "$formatted_value" | sed -e 's/[&|\\]/\\&/g') - - if grep -q "^$key=" "$target_file"; then - sed -i "s|^$key=.*|$key=$escaped_value|" "$target_file" - else - printf '%s=%s\n' "$key" "$formatted_value" >>"$target_file" - fi -} - -set_or_uncomment_env_var() { - local key="$1" - local value="$2" - local quote_value="${3:-false}" - local target_file="${4:-$ENV_FILE}" - local formatted_value="$value" - local tmp_file="" - - if [ "$quote_value" = "true" ]; then - local sanitized_value="${value//\"/\\\"}" - formatted_value="\"$sanitized_value\"" - fi - - [ -f "$target_file" ] || touch "$target_file" - tmp_file=$(mktemp) - - awk -v env_key="$key" -v env_line="${key} = ${formatted_value}" ' - BEGIN { replaced = 0 } - { - if ($0 ~ "^[[:space:]]*#?[[:space:]]*" env_key "[[:space:]]*=") { - if (replaced == 0) { - print env_line - replaced = 1 - } - next - } - print - } - END { - if (replaced == 0) { - print env_line - } - } - ' "$target_file" >"$tmp_file" - - mv "$tmp_file" "$target_file" -} - -comment_out_env_var() { - local key="$1" - local target_file="${2:-$ENV_FILE}" - local tmp_file="" - - [ -f "$target_file" ] || return 0 - tmp_file=$(mktemp) - - awk -v env_key="$key" ' - BEGIN { done = 0 } - { - if ($0 ~ "^[[:space:]]*#?[[:space:]]*" env_key "[[:space:]]*=") { - if (done == 0) { - line = $0 - sub("^[[:space:]]*#?[[:space:]]*" env_key "[[:space:]]*=[[:space:]]*", "", line) - print "# " env_key " = " line - done = 1 - } - next - } - print - } - ' "$target_file" >"$tmp_file" - - mv "$tmp_file" "$target_file" -} - -delete_env_var() { - local key="$1" - local target_file="${2:-$ENV_FILE}" - - [ -f "$target_file" ] || return 0 - sed -i "/^[[:space:]]*${key}[[:space:]]*=/d" "$target_file" -} - is_valid_proxy_url() { local proxy_url="$1" [[ -z "$proxy_url" ]] && return 1 @@ -137,122 +58,6 @@ get_backup_proxy_url() { return 0 } -colorized_echo() { - local color=$1 - local text=$2 - - case $color in - "red") - printf "\e[91m${text}\e[0m\n" - ;; - "green") - printf "\e[92m${text}\e[0m\n" - ;; - "yellow") - printf "\e[93m${text}\e[0m\n" - ;; - "blue") - printf "\e[94m${text}\e[0m\n" - ;; - "magenta") - printf "\e[95m${text}\e[0m\n" - ;; - "cyan") - printf "\e[96m${text}\e[0m\n" - ;; - *) - echo "${text}" - ;; - esac -} - -check_running_as_root() { - if [ "$(id -u)" != "0" ]; then - colorized_echo red "This command must be run as root." - exit 1 - fi -} - -detect_os() { - # Detect the operating system - if [ -f /etc/lsb-release ]; then - OS=$(lsb_release -si) - elif [ -f /etc/os-release ]; then - OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') - elif [ -f /etc/redhat-release ]; then - OS=$(cat /etc/redhat-release | awk '{print $1}') - elif [ -f /etc/arch-release ]; then - OS="Arch Linux" - else - colorized_echo red "Unsupported operating system" - exit 1 - fi -} - -detect_and_update_package_manager() { - colorized_echo blue "Updating package manager" - if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then - PKG_MANAGER="apt-get" - $PKG_MANAGER update - elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then - PKG_MANAGER="yum" - $PKG_MANAGER update -y - $PKG_MANAGER install -y epel-release - elif [ "$OS" == "Fedora"* ]; then - PKG_MANAGER="dnf" - $PKG_MANAGER update - elif [ "$OS" == "Arch Linux" ]; then - PKG_MANAGER="pacman" - $PKG_MANAGER -Sy - elif [[ "$OS" == "openSUSE"* ]]; then - PKG_MANAGER="zypper" - $PKG_MANAGER refresh - else - colorized_echo red "Unsupported operating system" - exit 1 - fi -} - -install_package() { - if [ -z $PKG_MANAGER ]; then - detect_and_update_package_manager - fi - - PACKAGE=$1 - colorized_echo blue "Installing $PACKAGE" - if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then - $PKG_MANAGER -y install "$PACKAGE" - elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then - $PKG_MANAGER install -y "$PACKAGE" - elif [ "$OS" == "Fedora"* ]; then - $PKG_MANAGER install -y "$PACKAGE" - elif [ "$OS" == "Arch Linux" ]; then - $PKG_MANAGER -S --noconfirm "$PACKAGE" - else - colorized_echo red "Unsupported operating system" - exit 1 - fi -} - -install_docker() { - # Install Docker and Docker Compose using the official installation script - colorized_echo blue "Installing Docker" - curl -fsSL https://get.docker.com | sh - colorized_echo green "Docker installed successfully" -} - -detect_compose() { - # Check if docker compose command exists - if docker compose version >/dev/null 2>&1; then - COMPOSE='docker compose' - elif docker-compose version >/dev/null 2>&1; then - COMPOSE='docker-compose' - else - colorized_echo red "docker compose not found" - exit 1 - fi -} - is_domain() { [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+(xn--[a-z0-9]{2,}|[A-Za-z]{2,})$ ]] && return 0 || return 1 } @@ -1029,6 +834,7 @@ install_pasarguard_script() { FETCH_REPO="PasarGuard/scripts" SCRIPT_URL="https://github.com/$FETCH_REPO/raw/main/pasarguard.sh" colorized_echo blue "Installing pasarguard script" + install_shared_libs_from_repo "$FETCH_REPO" curl -sSL $SCRIPT_URL | install -m 755 /dev/stdin /usr/local/bin/pasarguard colorized_echo green "pasarguard script installed successfully" } @@ -1041,65 +847,6 @@ is_pasarguard_installed() { fi } -identify_the_operating_system_and_architecture() { - if [[ "$(uname)" == 'Linux' ]]; then - case "$(uname -m)" in - 'i386' | 'i686') - ARCH='32' - ;; - 'amd64' | 'x86_64') - ARCH='64' - ;; - 'armv5tel') - ARCH='arm32-v5' - ;; - 'armv6l') - ARCH='arm32-v6' - grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' - ;; - 'armv7' | 'armv7l') - ARCH='arm32-v7a' - grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' - ;; - 'armv8' | 'aarch64') - ARCH='arm64-v8a' - ;; - 'mips') - ARCH='mips32' - ;; - 'mipsle') - ARCH='mips32le' - ;; - 'mips64') - ARCH='mips64' - lscpu | grep -q "Little Endian" && ARCH='mips64le' - ;; - 'mips64le') - ARCH='mips64le' - ;; - 'ppc64') - ARCH='ppc64' - ;; - 'ppc64le') - ARCH='ppc64le' - ;; - 'riscv64') - ARCH='riscv64' - ;; - 's390x') - ARCH='s390x' - ;; - *) - echo "error: The architecture is not supported." - exit 1 - ;; - esac - else - echo "error: This operating system is not supported." - exit 1 - fi -} - send_backup_to_telegram() { if [ -f "$ENV_FILE" ]; then while IFS='=' read -r key value; do @@ -3827,83 +3574,6 @@ install_command() { follow_pasarguard_logs } -install_yq() { - if command -v yq &>/dev/null; then - colorized_echo green "yq is already installed." - return - fi - - identify_the_operating_system_and_architecture - - local base_url="https://github.com/mikefarah/yq/releases/latest/download" - local yq_binary="" - - case "$ARCH" in - '64' | 'x86_64') - yq_binary="yq_linux_amd64" - ;; - 'arm32-v7a' | 'arm32-v6' | 'arm32-v5' | 'armv7l') - yq_binary="yq_linux_arm" - ;; - 'arm64-v8a' | 'aarch64') - yq_binary="yq_linux_arm64" - ;; - '32' | 'i386' | 'i686') - yq_binary="yq_linux_386" - ;; - *) - colorized_echo red "Unsupported architecture: $ARCH" - exit 1 - ;; - esac - - local yq_url="${base_url}/${yq_binary}" - colorized_echo blue "Downloading yq from ${yq_url}..." - - if ! command -v curl &>/dev/null && ! command -v wget &>/dev/null; then - colorized_echo yellow "Neither curl nor wget is installed. Attempting to install curl." - install_package curl || { - colorized_echo red "Failed to install curl. Please install curl or wget manually." - exit 1 - } - fi - - if command -v curl &>/dev/null; then - if curl -L "$yq_url" -o /usr/local/bin/yq; then - chmod +x /usr/local/bin/yq - colorized_echo green "yq installed successfully!" - else - colorized_echo red "Failed to download yq using curl. Please check your internet connection." - exit 1 - fi - elif command -v wget &>/dev/null; then - if wget -O /usr/local/bin/yq "$yq_url"; then - chmod +x /usr/local/bin/yq - colorized_echo green "yq installed successfully!" - else - colorized_echo red "Failed to download yq using wget. Please check your internet connection." - exit 1 - fi - fi - - if ! echo "$PATH" | grep -q "/usr/local/bin"; then - export PATH="/usr/local/bin:$PATH" - fi - - hash -r - - if command -v yq &>/dev/null; then - colorized_echo green "yq is ready to use." - elif [ -x "/usr/local/bin/yq" ]; then - - colorized_echo yellow "yq is installed at /usr/local/bin/yq but not found in PATH." - colorized_echo yellow "You can add /usr/local/bin to your PATH environment variable." - else - colorized_echo red "yq installation failed. Please try again or install manually." - exit 1 - fi -} - down_pasarguard() { $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" down } @@ -4239,9 +3909,10 @@ update_command() { } update_pasarguard_script() { - FETCH_REPO="pasarguard/scripts" + FETCH_REPO="PasarGuard/scripts" SCRIPT_URL="https://github.com/$FETCH_REPO/raw/main/pasarguard.sh" colorized_echo blue "Updating pasarguard script" + install_shared_libs_from_repo "$FETCH_REPO" curl -sSL $SCRIPT_URL | install -m 755 /dev/stdin /usr/local/bin/pasarguard colorized_echo green "pasarguard script updated successfully" } @@ -4250,20 +3921,6 @@ update_pasarguard() { $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" pull } -check_editor() { - if [ -z "$EDITOR" ]; then - if command -v nano >/dev/null 2>&1; then - EDITOR="nano" - elif command -v vi >/dev/null 2>&1; then - EDITOR="vi" - else - detect_os - install_package nano - EDITOR="nano" - fi - fi -} - edit_command() { detect_os check_editor diff --git a/pg-node.sh b/pg-node.sh index f17e04a..7e1d777 100755 --- a/pg-node.sh +++ b/pg-node.sh @@ -1,5 +1,20 @@ #!/usr/bin/env bash set -e + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +SHARED_LIB_DIR="${SCRIPT_DIR}/lib" +if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then + SHARED_LIB_DIR="/usr/local/lib/pasarguard-scripts/lib" +fi + +if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then + printf 'Missing shared library: %s\n' "$SHARED_LIB_DIR/common.sh" >&2 + exit 1 +fi + +# shellcheck source=lib/common.sh +source "$SHARED_LIB_DIR/common.sh" + # Handle global options AUTO_CONFIRM=false APP_NAME="" @@ -71,40 +86,6 @@ SCRIPT_URL="https://github.com/$FETCH_REPO/raw/main/pg-node.sh" NODE_SERVICE_REPO="PasarGuard/node-serviced" NODE_SERVICE_RELEASE_API="https://api.github.com/repos/${NODE_SERVICE_REPO}/releases/latest" NODE_SERVICE_BINARY_NAME="node-serviced" -colorized_echo() { - local color=$1 - local text=$2 - local style=${3:-0} # Default style is normal - case $color in - "red") - printf "\e[${style};91m${text}\e[0m\n" - ;; - "green") - printf "\e[${style};92m${text}\e[0m\n" - ;; - "yellow") - printf "\e[${style};93m${text}\e[0m\n" - ;; - "blue") - printf "\e[${style};94m${text}\e[0m\n" - ;; - "magenta") - printf "\e[${style};95m${text}\e[0m\n" - ;; - "cyan") - printf "\e[${style};96m${text}\e[0m\n" - ;; - *) - echo "${text}" - ;; - esac -} -check_running_as_root() { - if [ "$(id -u)" != "0" ]; then - colorized_echo red "This command must be run as root." - exit 1 - fi -} set_service_paths() { SERVICE_NAME="${APP_NAME}-service" SERVICE_BINARY_PATH="/usr/local/bin/${SERVICE_NAME}" @@ -184,83 +165,6 @@ configure_firewall_for_port() { local hint="If a firewall is enabled (e.g., UFW or firewalld), allow ${port}/${proto}." colorized_echo yellow "$hint" } -detect_os() { - # Detect the operating system - if [ -f /etc/lsb-release ]; then - OS=$(lsb_release -si) - elif [ -f /etc/os-release ]; then - OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') - elif [ -f /etc/redhat-release ]; then - OS=$(cat /etc/redhat-release | awk '{print $1}') - elif [ -f /etc/arch-release ]; then - OS="Arch" - else - colorized_echo red "Unsupported operating system" - exit 1 - fi -} -detect_and_update_package_manager() { - colorized_echo blue "Updating package manager" - if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then - PKG_MANAGER="apt-get" - $PKG_MANAGER update -qq >/dev/null 2>&1 - elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then - PKG_MANAGER="yum" - $PKG_MANAGER update -y -q >/dev/null 2>&1 - $PKG_MANAGER install -y -q epel-release >/dev/null 2>&1 - elif [[ "$OS" == "Fedora"* ]]; then - PKG_MANAGER="dnf" - $PKG_MANAGER update -q -y >/dev/null 2>&1 - elif [[ "$OS" == "Arch"* ]]; then - PKG_MANAGER="pacman" - $PKG_MANAGER -Sy --noconfirm --quiet >/dev/null 2>&1 - elif [[ "$OS" == "openSUSE"* ]]; then - PKG_MANAGER="zypper" - $PKG_MANAGER refresh --quiet >/dev/null 2>&1 - else - colorized_echo red "Unsupported operating system" - exit 1 - fi -} -detect_compose() { - # Check if docker compose command exists - if docker compose >/dev/null 2>&1; then - COMPOSE='docker compose' - elif docker-compose >/dev/null 2>&1; then - COMPOSE='docker-compose' - else - colorized_echo red "docker compose not found" - exit 1 - fi -} -install_package() { - if [ -z "$PKG_MANAGER" ]; then - detect_and_update_package_manager - fi - PACKAGE=$1 - colorized_echo blue "Installing $PACKAGE" - if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then - $PKG_MANAGER -y -qq install "$PACKAGE" >/dev/null 2>&1 - elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then - $PKG_MANAGER install -y -q "$PACKAGE" >/dev/null 2>&1 - elif [[ "$OS" == "Fedora"* ]]; then - $PKG_MANAGER install -y -q "$PACKAGE" >/dev/null 2>&1 - elif [[ "$OS" == "Arch"* ]]; then - $PKG_MANAGER -S --noconfirm --quiet "$PACKAGE" >/dev/null 2>&1 - elif [[ "$OS" == "openSUSE"* ]]; then - PKG_MANAGER="zypper" - $PKG_MANAGER --quiet install -y "$PACKAGE" >/dev/null 2>&1 - else - colorized_echo red "Unsupported operating system" - exit 1 - fi -} -install_docker() { - # Install Docker and Docker Compose using the official installation script - colorized_echo blue "Installing Docker" - curl -fsSL https://get.docker.com | sh - colorized_echo green "Docker installed successfully" -} install_node_script() { colorized_echo blue "Installing node script" TARGET_PATH="/usr/local/bin/$APP_NAME" @@ -279,6 +183,8 @@ install_node_script() { if grep -q "^APP_NAME=" "$TEMP_FILE"; then sed -i "s|^APP_NAME=.*|APP_NAME=\"$APP_NAME\"|" "$TEMP_FILE" fi + + install_shared_libs_from_repo "$FETCH_REPO" # Remove old file if it exists if [ -f "$TARGET_PATH" ]; then @@ -817,6 +723,7 @@ follow_node_logs() { } update_node_script() { colorized_echo blue "Updating node script" + install_shared_libs_from_repo "$FETCH_REPO" curl -sSL $SCRIPT_URL | install -m 755 /dev/stdin /usr/local/bin/$APP_NAME colorized_echo green "node script updated successfully" } @@ -1453,64 +1360,6 @@ update_command() { colorized_echo blue "node updated successfully" } -identify_the_operating_system_and_architecture() { - if [[ "$(uname)" == 'Linux' ]]; then - case "$(uname -m)" in - 'i386' | 'i686') - ARCH='32' - ;; - 'amd64' | 'x86_64') - ARCH='64' - ;; - 'armv5tel') - ARCH='arm32-v5' - ;; - 'armv6l') - ARCH='arm32-v6' - grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' - ;; - 'armv7' | 'armv7l') - ARCH='arm32-v7a' - grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' - ;; - 'armv8' | 'aarch64') - ARCH='arm64-v8a' - ;; - 'mips') - ARCH='mips32' - ;; - 'mipsle') - ARCH='mips32le' - ;; - 'mips64') - ARCH='mips64' - lscpu | grep -q "Little Endian" && ARCH='mips64le' - ;; - 'mips64le') - ARCH='mips64le' - ;; - 'ppc64') - ARCH='ppc64' - ;; - 'ppc64le') - ARCH='ppc64le' - ;; - 'riscv64') - ARCH='riscv64' - ;; - 's390x') - ARCH='s390x' - ;; - *) - echo "error: The architecture is not supported." - exit 1 - ;; - esac - else - echo "error: This operating system is not supported." - exit 1 - fi -} # Function to update the Xray core get_xray_core() { local requested_version="${1:-}" @@ -1650,72 +1499,6 @@ get_current_xray_core_version() { fi echo "Not installed" } -install_yq() { - if command -v yq &>/dev/null; then - colorized_echo green "yq is already installed." - return - fi - identify_the_operating_system_and_architecture - local base_url="https://github.com/mikefarah/yq/releases/latest/download" - local yq_binary="" - case "$ARCH" in - '64' | 'x86_64') - yq_binary="yq_linux_amd64" - ;; - 'arm32-v7a' | 'arm32-v6' | 'arm32-v5' | 'armv7l') - yq_binary="yq_linux_arm" - ;; - 'arm64-v8a' | 'aarch64') - yq_binary="yq_linux_arm64" - ;; - '32' | 'i386' | 'i686') - yq_binary="yq_linux_386" - ;; - *) - colorized_echo red "Unsupported architecture: $ARCH" - exit 1 - ;; - esac - local yq_url="${base_url}/${yq_binary}" - colorized_echo blue "Downloading yq from ${yq_url}..." - if ! command -v curl &>/dev/null && ! command -v wget &>/dev/null; then - colorized_echo yellow "Neither curl nor wget is installed. Attempting to install curl." - install_package curl || { - colorized_echo red "Failed to install curl. Please install curl or wget manually." - exit 1 - } - fi - if command -v curl &>/dev/null; then - if curl -L "$yq_url" -o /usr/local/bin/yq; then - chmod +x /usr/local/bin/yq - colorized_echo green "yq installed successfully!" - else - colorized_echo red "Failed to download yq using curl. Please check your internet connection." - exit 1 - fi - elif command -v wget &>/dev/null; then - if wget -O /usr/local/bin/yq "$yq_url"; then - chmod +x /usr/local/bin/yq - colorized_echo green "yq installed successfully!" - else - colorized_echo red "Failed to download yq using wget. Please check your internet connection." - exit 1 - fi - fi - if ! echo "$PATH" | grep -q "/usr/local/bin"; then - export PATH="/usr/local/bin:$PATH" - fi - hash -r - if command -v yq &>/dev/null; then - colorized_echo green "yq is ready to use." - elif [ -x "/usr/local/bin/yq" ]; then - colorized_echo yellow "yq is installed at /usr/local/bin/yq but not found in PATH." - colorized_echo yellow "You can add /usr/local/bin to your PATH environment variable." - else - colorized_echo red "yq installation failed. Please try again or install manually." - exit 1 - fi -} update_core_command() { check_running_as_root local core_version_arg="" @@ -1768,19 +1551,6 @@ update_core_command() { restart_command -n --no-restart-service colorized_echo blue "Installation of XRAY-CORE version $selected_version completed." } -check_editor() { - if [ -z "$EDITOR" ]; then - if command -v nano >/dev/null 2>&1; then - EDITOR="nano" - elif command -v vi >/dev/null 2>&1; then - EDITOR="vi" - else - detect_os - install_package nano - EDITOR="nano" - fi - fi -} edit_command() { detect_os check_editor From d22afb514967112b0f665e083bf3b2fc940012cd Mon Sep 17 00:00:00 2001 From: M03ED <50927468+M03ED@users.noreply.github.com> Date: Fri, 1 May 2026 14:36:40 +0330 Subject: [PATCH 2/9] refactor: github, system and docker shared lib --- lib/common.sh | 234 -------------------------------------------------- lib/docker.sh | 33 +++++++ lib/github.sh | 53 ++++++++++++ lib/system.sh | 191 ++++++++++++++++++++++++++++++++++++++++ pasarguard.sh | 34 ++++---- pg-node.sh | 31 ++++--- 6 files changed, 314 insertions(+), 262 deletions(-) create mode 100644 lib/docker.sh create mode 100644 lib/github.sh create mode 100644 lib/system.sh diff --git a/lib/common.sh b/lib/common.sh index 3a525fc..a4c8d09 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -SHARED_LIB_INSTALL_DIR="/usr/local/lib/pasarguard-scripts/lib" - colorized_echo() { local color="$1" local text="$2" @@ -36,235 +34,3 @@ die() { colorized_echo red "$*" exit 1 } - -check_running_as_root() { - if [ "$(id -u)" != "0" ]; then - die "This command must be run as root." - fi -} - -detect_os() { - if [ -f /etc/lsb-release ]; then - OS=$(lsb_release -si) - elif [ -f /etc/os-release ]; then - OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') - elif [ -f /etc/redhat-release ]; then - OS=$(awk '{print $1}' /etc/redhat-release) - elif [ -f /etc/arch-release ]; then - OS="Arch Linux" - else - die "Unsupported operating system" - fi -} - -detect_and_update_package_manager() { - colorized_echo blue "Updating package manager" - - if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then - PKG_MANAGER="apt-get" - $PKG_MANAGER update -qq >/dev/null 2>&1 - elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then - PKG_MANAGER="yum" - $PKG_MANAGER update -y -q >/dev/null 2>&1 - $PKG_MANAGER install -y -q epel-release >/dev/null 2>&1 - elif [[ "$OS" == "Fedora"* ]]; then - PKG_MANAGER="dnf" - $PKG_MANAGER update -q -y >/dev/null 2>&1 - elif [[ "$OS" == "Arch Linux" ]] || [[ "$OS" == "Arch"* ]]; then - PKG_MANAGER="pacman" - $PKG_MANAGER -Sy --noconfirm --quiet >/dev/null 2>&1 - elif [[ "$OS" == "openSUSE"* ]]; then - PKG_MANAGER="zypper" - $PKG_MANAGER refresh --quiet >/dev/null 2>&1 - else - die "Unsupported operating system" - fi -} - -install_package() { - local package="$1" - - if [ -z "${PKG_MANAGER:-}" ]; then - detect_and_update_package_manager - fi - - colorized_echo blue "Installing $package" - if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then - $PKG_MANAGER -y -qq install "$package" >/dev/null 2>&1 - elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then - $PKG_MANAGER install -y -q "$package" >/dev/null 2>&1 - elif [[ "$OS" == "Fedora"* ]]; then - $PKG_MANAGER install -y -q "$package" >/dev/null 2>&1 - elif [[ "$OS" == "Arch Linux" ]] || [[ "$OS" == "Arch"* ]]; then - $PKG_MANAGER -S --noconfirm --quiet "$package" >/dev/null 2>&1 - elif [[ "$OS" == "openSUSE"* ]]; then - $PKG_MANAGER --quiet install -y "$package" >/dev/null 2>&1 - else - die "Unsupported operating system" - fi -} - -install_docker() { - colorized_echo blue "Installing Docker" - curl -fsSL https://get.docker.com | sh - colorized_echo green "Docker installed successfully" -} - -detect_compose() { - if docker compose version >/dev/null 2>&1; then - COMPOSE='docker compose' - elif docker-compose version >/dev/null 2>&1; then - COMPOSE='docker-compose' - else - die "docker compose not found" - fi -} - -check_editor() { - if [ -z "${EDITOR:-}" ]; then - if command -v nano >/dev/null 2>&1; then - EDITOR="nano" - elif command -v vi >/dev/null 2>&1; then - EDITOR="vi" - else - detect_os - install_package nano - EDITOR="nano" - fi - fi -} - -identify_the_operating_system_and_architecture() { - if [[ "$(uname)" != "Linux" ]]; then - die "error: This operating system is not supported." - fi - - case "$(uname -m)" in - i386 | i686) - ARCH='32' - ;; - amd64 | x86_64) - ARCH='64' - ;; - armv5tel) - ARCH='arm32-v5' - ;; - armv6l) - ARCH='arm32-v6' - grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' - ;; - armv7 | armv7l) - ARCH='arm32-v7a' - grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' - ;; - armv8 | aarch64) - ARCH='arm64-v8a' - ;; - mips) - ARCH='mips32' - ;; - mipsle) - ARCH='mips32le' - ;; - mips64) - ARCH='mips64' - lscpu | grep -q "Little Endian" && ARCH='mips64le' - ;; - mips64le) - ARCH='mips64le' - ;; - ppc64) - ARCH='ppc64' - ;; - ppc64le) - ARCH='ppc64le' - ;; - riscv64) - ARCH='riscv64' - ;; - s390x) - ARCH='s390x' - ;; - *) - die "error: The architecture is not supported." - ;; - esac -} - -install_yq() { - local base_url="https://github.com/mikefarah/yq/releases/latest/download" - local yq_binary="" - local yq_url="" - - if command -v yq >/dev/null 2>&1; then - colorized_echo green "yq is already installed." - return - fi - - identify_the_operating_system_and_architecture - - case "$ARCH" in - 64 | x86_64) - yq_binary="yq_linux_amd64" - ;; - arm32-v7a | arm32-v6 | arm32-v5 | armv7l) - yq_binary="yq_linux_arm" - ;; - arm64-v8a | aarch64) - yq_binary="yq_linux_arm64" - ;; - 32 | i386 | i686) - yq_binary="yq_linux_386" - ;; - *) - die "Unsupported architecture: $ARCH" - ;; - esac - - yq_url="${base_url}/${yq_binary}" - colorized_echo blue "Downloading yq from ${yq_url}..." - - if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then - colorized_echo yellow "Neither curl nor wget is installed. Attempting to install curl." - install_package curl || die "Failed to install curl. Please install curl or wget manually." - fi - - if command -v curl >/dev/null 2>&1; then - curl -L "$yq_url" -o /usr/local/bin/yq || die "Failed to download yq using curl. Please check your internet connection." - elif command -v wget >/dev/null 2>&1; then - wget -O /usr/local/bin/yq "$yq_url" || die "Failed to download yq using wget. Please check your internet connection." - fi - - chmod +x /usr/local/bin/yq - colorized_echo green "yq installed successfully!" - - if ! echo "$PATH" | grep -q "/usr/local/bin"; then - export PATH="/usr/local/bin:$PATH" - fi -} - -install_shared_libs_from_local() { - local source_dir="$1" - - mkdir -p "$SHARED_LIB_INSTALL_DIR" - install -m 644 "$source_dir/lib/common.sh" "$SHARED_LIB_INSTALL_DIR/common.sh" - if [ -f "$source_dir/lib/env.sh" ]; then - install -m 644 "$source_dir/lib/env.sh" "$SHARED_LIB_INSTALL_DIR/env.sh" - fi -} - -install_shared_libs_from_repo() { - local fetch_repo="$1" - local tmp_dir="" - - tmp_dir=$(mktemp -d) - mkdir -p "$SHARED_LIB_INSTALL_DIR" - - curl -fsSL "https://github.com/${fetch_repo}/raw/main/lib/common.sh" -o "$tmp_dir/common.sh" - install -m 644 "$tmp_dir/common.sh" "$SHARED_LIB_INSTALL_DIR/common.sh" - - curl -fsSL "https://github.com/${fetch_repo}/raw/main/lib/env.sh" -o "$tmp_dir/env.sh" - install -m 644 "$tmp_dir/env.sh" "$SHARED_LIB_INSTALL_DIR/env.sh" - - rm -rf "$tmp_dir" -} diff --git a/lib/docker.sh b/lib/docker.sh new file mode 100644 index 0000000..0f3e77e --- /dev/null +++ b/lib/docker.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +install_docker() { + colorized_echo blue "Installing Docker" + curl -fsSL https://get.docker.com | sh + colorized_echo green "Docker installed successfully" +} + +detect_compose() { + if docker compose version >/dev/null 2>&1; then + COMPOSE='docker compose' + elif docker-compose version >/dev/null 2>&1; then + COMPOSE='docker-compose' + else + die "docker compose not found" + fi +} + +compose_up() { + $COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" up -d --remove-orphans +} + +compose_down() { + $COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" down +} + +compose_logs() { + $COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" logs +} + +compose_logs_follow() { + $COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" logs -f +} diff --git a/lib/github.sh b/lib/github.sh new file mode 100644 index 0000000..8ad6d96 --- /dev/null +++ b/lib/github.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +SHARED_LIB_INSTALL_DIR="/usr/local/lib/pasarguard-scripts/lib" + +github_raw_url() { + local repo="$1" + local path="$2" + + printf 'https://github.com/%s/raw/main/%s\n' "$repo" "$path" +} + +github_download_file() { + local url="$1" + local target_path="$2" + + curl -fsSL "$url" -o "$target_path" +} + +github_install_script_from_repo() { + local repo="$1" + local script_name="$2" + local install_name="$3" + + curl -fsSL "$(github_raw_url "$repo" "$script_name")" | install -m 755 /dev/stdin "/usr/local/bin/$install_name" +} + +install_shared_libs_from_local() { + local source_dir="$1" + local lib_name="" + + mkdir -p "$SHARED_LIB_INSTALL_DIR" + for lib_name in common.sh system.sh docker.sh github.sh env.sh; do + if [ -f "$source_dir/lib/$lib_name" ]; then + install -m 644 "$source_dir/lib/$lib_name" "$SHARED_LIB_INSTALL_DIR/$lib_name" + fi + done +} + +install_shared_libs_from_repo() { + local fetch_repo="$1" + local tmp_dir="" + local lib_name="" + + tmp_dir=$(mktemp -d) + mkdir -p "$SHARED_LIB_INSTALL_DIR" + + for lib_name in common.sh system.sh docker.sh github.sh env.sh; do + github_download_file "$(github_raw_url "$fetch_repo" "lib/$lib_name")" "$tmp_dir/$lib_name" + install -m 644 "$tmp_dir/$lib_name" "$SHARED_LIB_INSTALL_DIR/$lib_name" + done + + rm -rf "$tmp_dir" +} diff --git a/lib/system.sh b/lib/system.sh new file mode 100644 index 0000000..81ef122 --- /dev/null +++ b/lib/system.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash + +check_running_as_root() { + if [ "$(id -u)" != "0" ]; then + die "This command must be run as root." + fi +} + +detect_os() { + if [ -f /etc/lsb-release ]; then + OS=$(lsb_release -si) + elif [ -f /etc/os-release ]; then + OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') + elif [ -f /etc/redhat-release ]; then + OS=$(awk '{print $1}' /etc/redhat-release) + elif [ -f /etc/arch-release ]; then + OS="Arch Linux" + else + die "Unsupported operating system" + fi +} + +detect_and_update_package_manager() { + colorized_echo blue "Updating package manager" + + if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then + PKG_MANAGER="apt-get" + $PKG_MANAGER update -qq >/dev/null 2>&1 + elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then + PKG_MANAGER="yum" + $PKG_MANAGER update -y -q >/dev/null 2>&1 + $PKG_MANAGER install -y -q epel-release >/dev/null 2>&1 + elif [[ "$OS" == "Fedora"* ]]; then + PKG_MANAGER="dnf" + $PKG_MANAGER update -q -y >/dev/null 2>&1 + elif [[ "$OS" == "Arch Linux" ]] || [[ "$OS" == "Arch"* ]]; then + PKG_MANAGER="pacman" + $PKG_MANAGER -Sy --noconfirm --quiet >/dev/null 2>&1 + elif [[ "$OS" == "openSUSE"* ]]; then + PKG_MANAGER="zypper" + $PKG_MANAGER refresh --quiet >/dev/null 2>&1 + else + die "Unsupported operating system" + fi +} + +install_package() { + local package="$1" + + if [ -z "${PKG_MANAGER:-}" ]; then + detect_and_update_package_manager + fi + + colorized_echo blue "Installing $package" + if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then + $PKG_MANAGER -y -qq install "$package" >/dev/null 2>&1 + elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then + $PKG_MANAGER install -y -q "$package" >/dev/null 2>&1 + elif [[ "$OS" == "Fedora"* ]]; then + $PKG_MANAGER install -y -q "$package" >/dev/null 2>&1 + elif [[ "$OS" == "Arch Linux" ]] || [[ "$OS" == "Arch"* ]]; then + $PKG_MANAGER -S --noconfirm --quiet "$package" >/dev/null 2>&1 + elif [[ "$OS" == "openSUSE"* ]]; then + $PKG_MANAGER --quiet install -y "$package" >/dev/null 2>&1 + else + die "Unsupported operating system" + fi +} + +check_editor() { + if [ -z "${EDITOR:-}" ]; then + if command -v nano >/dev/null 2>&1; then + EDITOR="nano" + elif command -v vi >/dev/null 2>&1; then + EDITOR="vi" + else + detect_os + install_package nano + EDITOR="nano" + fi + fi +} + +identify_the_operating_system_and_architecture() { + if [[ "$(uname)" != "Linux" ]]; then + die "error: This operating system is not supported." + fi + + case "$(uname -m)" in + i386 | i686) + ARCH='32' + ;; + amd64 | x86_64) + ARCH='64' + ;; + armv5tel) + ARCH='arm32-v5' + ;; + armv6l) + ARCH='arm32-v6' + grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' + ;; + armv7 | armv7l) + ARCH='arm32-v7a' + grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' + ;; + armv8 | aarch64) + ARCH='arm64-v8a' + ;; + mips) + ARCH='mips32' + ;; + mipsle) + ARCH='mips32le' + ;; + mips64) + ARCH='mips64' + lscpu | grep -q "Little Endian" && ARCH='mips64le' + ;; + mips64le) + ARCH='mips64le' + ;; + ppc64) + ARCH='ppc64' + ;; + ppc64le) + ARCH='ppc64le' + ;; + riscv64) + ARCH='riscv64' + ;; + s390x) + ARCH='s390x' + ;; + *) + die "error: The architecture is not supported." + ;; + esac +} + +install_yq() { + local base_url="https://github.com/mikefarah/yq/releases/latest/download" + local yq_binary="" + local yq_url="" + + if command -v yq >/dev/null 2>&1; then + colorized_echo green "yq is already installed." + return + fi + + identify_the_operating_system_and_architecture + + case "$ARCH" in + 64 | x86_64) + yq_binary="yq_linux_amd64" + ;; + arm32-v7a | arm32-v6 | arm32-v5 | armv7l) + yq_binary="yq_linux_arm" + ;; + arm64-v8a | aarch64) + yq_binary="yq_linux_arm64" + ;; + 32 | i386 | i686) + yq_binary="yq_linux_386" + ;; + *) + die "Unsupported architecture: $ARCH" + ;; + esac + + yq_url="${base_url}/${yq_binary}" + colorized_echo blue "Downloading yq from ${yq_url}..." + + if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then + colorized_echo yellow "Neither curl nor wget is installed. Attempting to install curl." + install_package curl || die "Failed to install curl. Please install curl or wget manually." + fi + + if command -v curl >/dev/null 2>&1; then + curl -L "$yq_url" -o /usr/local/bin/yq || die "Failed to download yq using curl. Please check your internet connection." + elif command -v wget >/dev/null 2>&1; then + wget -O /usr/local/bin/yq "$yq_url" || die "Failed to download yq using wget. Please check your internet connection." + fi + + chmod +x /usr/local/bin/yq + colorized_echo green "yq installed successfully!" + + if ! echo "$PATH" | grep -q "/usr/local/bin"; then + export PATH="/usr/local/bin:$PATH" + fi +} diff --git a/pasarguard.sh b/pasarguard.sh index 8143c3a..433d165 100755 --- a/pasarguard.sh +++ b/pasarguard.sh @@ -7,13 +7,21 @@ if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then SHARED_LIB_DIR="/usr/local/lib/pasarguard-scripts/lib" fi -if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then - printf 'Missing shared library: %s\n' "$SHARED_LIB_DIR/common.sh" >&2 - exit 1 -fi +for shared_lib in common.sh system.sh docker.sh github.sh env.sh; do + if [ ! -f "$SHARED_LIB_DIR/$shared_lib" ]; then + printf 'Missing shared library: %s\n' "$SHARED_LIB_DIR/$shared_lib" >&2 + exit 1 + fi +done # shellcheck source=lib/common.sh source "$SHARED_LIB_DIR/common.sh" +# shellcheck source=lib/system.sh +source "$SHARED_LIB_DIR/system.sh" +# shellcheck source=lib/docker.sh +source "$SHARED_LIB_DIR/docker.sh" +# shellcheck source=lib/github.sh +source "$SHARED_LIB_DIR/github.sh" # shellcheck source=lib/env.sh source "$SHARED_LIB_DIR/env.sh" @@ -832,10 +840,9 @@ verify_and_start_container() { install_pasarguard_script() { FETCH_REPO="PasarGuard/scripts" - SCRIPT_URL="https://github.com/$FETCH_REPO/raw/main/pasarguard.sh" colorized_echo blue "Installing pasarguard script" install_shared_libs_from_repo "$FETCH_REPO" - curl -sSL $SCRIPT_URL | install -m 755 /dev/stdin /usr/local/bin/pasarguard + github_install_script_from_repo "$FETCH_REPO" "pasarguard.sh" "pasarguard" colorized_echo green "pasarguard script installed successfully" } @@ -3181,11 +3188,7 @@ install_pasarguard() { } up_pasarguard() { - $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" up -d --remove-orphans -} - -follow_pasarguard_logs() { - $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" logs -f + compose_up } status_command() { @@ -3575,15 +3578,15 @@ install_command() { } down_pasarguard() { - $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" down + compose_down } show_pasarguard_logs() { - $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" logs + compose_logs } follow_pasarguard_logs() { - $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" logs -f + compose_logs_follow } pasarguard_cli() { @@ -3910,10 +3913,9 @@ update_command() { update_pasarguard_script() { FETCH_REPO="PasarGuard/scripts" - SCRIPT_URL="https://github.com/$FETCH_REPO/raw/main/pasarguard.sh" colorized_echo blue "Updating pasarguard script" install_shared_libs_from_repo "$FETCH_REPO" - curl -sSL $SCRIPT_URL | install -m 755 /dev/stdin /usr/local/bin/pasarguard + github_install_script_from_repo "$FETCH_REPO" "pasarguard.sh" "pasarguard" colorized_echo green "pasarguard script updated successfully" } diff --git a/pg-node.sh b/pg-node.sh index 7e1d777..c68bab7 100755 --- a/pg-node.sh +++ b/pg-node.sh @@ -7,13 +7,21 @@ if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then SHARED_LIB_DIR="/usr/local/lib/pasarguard-scripts/lib" fi -if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then - printf 'Missing shared library: %s\n' "$SHARED_LIB_DIR/common.sh" >&2 - exit 1 -fi +for shared_lib in common.sh system.sh docker.sh github.sh; do + if [ ! -f "$SHARED_LIB_DIR/$shared_lib" ]; then + printf 'Missing shared library: %s\n' "$SHARED_LIB_DIR/$shared_lib" >&2 + exit 1 + fi +done # shellcheck source=lib/common.sh source "$SHARED_LIB_DIR/common.sh" +# shellcheck source=lib/system.sh +source "$SHARED_LIB_DIR/system.sh" +# shellcheck source=lib/docker.sh +source "$SHARED_LIB_DIR/docker.sh" +# shellcheck source=lib/github.sh +source "$SHARED_LIB_DIR/github.sh" # Handle global options AUTO_CONFIRM=false @@ -82,7 +90,6 @@ SSL_CERT_FILE="$DATA_DIR/certs/ssl_cert.pem" SSL_KEY_FILE="$DATA_DIR/certs/ssl_key.pem" LAST_XRAY_CORES=5 FETCH_REPO="PasarGuard/scripts" -SCRIPT_URL="https://github.com/$FETCH_REPO/raw/main/pg-node.sh" NODE_SERVICE_REPO="PasarGuard/node-serviced" NODE_SERVICE_RELEASE_API="https://api.github.com/repos/${NODE_SERVICE_REPO}/releases/latest" NODE_SERVICE_BINARY_NAME="node-serviced" @@ -172,8 +179,8 @@ install_node_script() { # Download script to temp file first colorized_echo cyan " Downloading script from GitHub..." - if ! curl -sSL "$SCRIPT_URL" -o "$TEMP_FILE"; then - colorized_echo red "✗ Failed to download script from $SCRIPT_URL" + if ! github_download_file "$(github_raw_url "$FETCH_REPO" "pg-node.sh")" "$TEMP_FILE"; then + colorized_echo red "✗ Failed to download script from $(github_raw_url "$FETCH_REPO" "pg-node.sh")" rm -f "$TEMP_FILE" exit 1 fi @@ -710,21 +717,21 @@ uninstall_node_data_files() { fi } up_node() { - $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" up -d --remove-orphans + compose_up } down_node() { - $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" down + compose_down } show_node_logs() { - $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" logs + compose_logs } follow_node_logs() { - $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" logs -f + compose_logs_follow } update_node_script() { colorized_echo blue "Updating node script" install_shared_libs_from_repo "$FETCH_REPO" - curl -sSL $SCRIPT_URL | install -m 755 /dev/stdin /usr/local/bin/$APP_NAME + github_install_script_from_repo "$FETCH_REPO" "pg-node.sh" "$APP_NAME" colorized_echo green "node script updated successfully" } update_node() { From 1b9f30ca623a1c737ca375e05afa3a4993d6b956 Mon Sep 17 00:00:00 2001 From: M03ED <50927468+M03ED@users.noreply.github.com> Date: Fri, 1 May 2026 14:48:32 +0330 Subject: [PATCH 3/9] feat: handle temp file usage --- README.md | 20 ++++++++-------- lib/common.sh | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ lib/env.sh | 8 +++++-- lib/github.sh | 2 +- pg-node.sh | 4 ++-- 5 files changed, 84 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index a7586d0..cc25e38 100644 --- a/README.md +++ b/README.md @@ -24,37 +24,37 @@ - **Install pasarguard with SQLite**: ```bash - curl -fsSLo /tmp/pg.sh https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh && sudo bash /tmp/pg.sh install + sudo bash -c "$(curl -fsSL https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh)" @ install ``` - **Install pasarguard with MySQL**: ```bash - curl -fsSLo /tmp/pg.sh https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh && sudo bash /tmp/pg.sh install --database mysql + sudo bash -c "$(curl -fsSL https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh)" @ install --database mysql ``` - **Install pasarguard with PostgreSQL**: ```bash - curl -fsSLo /tmp/pg.sh https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh && sudo bash /tmp/pg.sh install --database postgresql + sudo bash -c "$(curl -fsSL https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh)" @ install --database postgresql ``` - **Install pasarguard with TimescaleDB(v1+ only) and pre-release version**: ```bash - curl -fsSLo /tmp/pg.sh https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh && sudo bash /tmp/pg.sh install --database timescaledb --pre-release + sudo bash -c "$(curl -fsSL https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh)" @ install --database timescaledb --pre-release ``` - **Install pasarguard with MariaDB and Dev branch**: ```bash - curl -fsSLo /tmp/pg.sh https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh && sudo bash /tmp/pg.sh install --database mariadb --dev + sudo bash -c "$(curl -fsSL https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh)" @ install --database mariadb --dev ``` - **Install pasarguard with MariaDB and Manual version**: ```bash - curl -fsSLo /tmp/pg.sh https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh && sudo bash /tmp/pg.sh install --database mariadb --version v0.5.2 + sudo bash -c "$(curl -fsSL https://github.com/PasarGuard/scripts/raw/main/pasarguard.sh)" @ install --database mariadb --version v0.5.2 ``` ## Installing Node @@ -63,22 +63,22 @@ - **Install Node** ```bash - curl -fsSLo /tmp/pg-node.sh https://github.com/PasarGuard/scripts/raw/main/pg-node.sh && sudo bash /tmp/pg-node.sh install + sudo bash -c "$(curl -fsSL https://github.com/PasarGuard/scripts/raw/main/pg-node.sh)" @ install ``` - **Install Node Manual version:** ```bash - curl -fsSLo /tmp/pg-node.sh https://github.com/PasarGuard/scripts/raw/main/pg-node.sh && sudo bash /tmp/pg-node.sh install --version 0.1.0 + sudo bash -c "$(curl -fsSL https://github.com/PasarGuard/scripts/raw/main/pg-node.sh)" @ install --version 0.1.0 ``` - **Install Node pre-release version:** ```bash - curl -fsSLo /tmp/pg-node.sh https://github.com/PasarGuard/scripts/raw/main/pg-node.sh && sudo bash /tmp/pg-node.sh install --pre-release + sudo bash -c "$(curl -fsSL https://github.com/PasarGuard/scripts/raw/main/pg-node.sh)" @ install --pre-release ``` - **Install Node with custom name:** ```bash - curl -fsSLo /tmp/pg-node.sh https://github.com/PasarGuard/scripts/raw/main/pg-node.sh && sudo bash /tmp/pg-node.sh install --name Node2 + sudo bash -c "$(curl -fsSL https://github.com/PasarGuard/scripts/raw/main/pg-node.sh)" @ install --name Node2 ``` > 📌 **Tip:** diff --git a/lib/common.sh b/lib/common.sh index a4c8d09..6f6d1b7 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -34,3 +34,68 @@ die() { colorized_echo red "$*" exit 1 } + +temp_root_dir() { + local root="" + + if [ -n "${APP_TMP_DIR:-}" ]; then + root="$APP_TMP_DIR" + elif [ -n "${DATA_DIR:-}" ]; then + root="$DATA_DIR/tmp" + elif [ -n "${APP_NAME:-}" ]; then + root="/var/lib/$APP_NAME/tmp" + else + root="/var/lib/pasarguard-scripts/tmp" + fi + + mkdir -p "$root" + printf '%s\n' "$root" +} + +create_temp_dir() { + local prefix="${1:-tmpdir}" + local root="" + local candidate="" + local attempt=0 + + root=$(temp_root_dir) + while [ "$attempt" -lt 20 ]; do + candidate="${root}/${prefix}-$$-${RANDOM}-${attempt}" + if mkdir "$candidate" 2>/dev/null; then + printf '%s\n' "$candidate" + return 0 + fi + attempt=$((attempt + 1)) + done + + die "Failed to create temporary directory in $root" +} + +create_temp_file() { + local prefix="${1:-tmpfile}" + local suffix="${2:-}" + local root="" + + root=$(temp_root_dir) + create_temp_file_in_dir "$root" "$prefix" "$suffix" +} + +create_temp_file_in_dir() { + local dir="$1" + local prefix="${2:-tmpfile}" + local suffix="${3:-}" + local candidate="" + local attempt=0 + + mkdir -p "$dir" + while [ "$attempt" -lt 20 ]; do + candidate="${dir}/${prefix}-$$-${RANDOM}-${attempt}${suffix}" + if (set -C; : >"$candidate") 2>/dev/null; then + printf '%s\n' "$candidate" + return 0 + fi + attempt=$((attempt + 1)) + done + + die "Failed to create temporary file in $dir" +} diff --git a/lib/env.sh b/lib/env.sh index b914ee2..d49a6e2 100644 --- a/lib/env.sh +++ b/lib/env.sh @@ -29,6 +29,7 @@ set_or_uncomment_env_var() { local target_file="${4:-$ENV_FILE}" local formatted_value="$value" local tmp_file="" + local target_dir="" if [ "$quote_value" = "true" ]; then local sanitized_value="${value//\"/\\\"}" @@ -36,7 +37,8 @@ set_or_uncomment_env_var() { fi [ -f "$target_file" ] || touch "$target_file" - tmp_file=$(mktemp) + target_dir=$(dirname "$target_file") + tmp_file=$(create_temp_file_in_dir "$target_dir" "env-edit" ".tmp") awk -v env_key="$key" -v env_line="${key} = ${formatted_value}" ' BEGIN { replaced = 0 } @@ -64,9 +66,11 @@ comment_out_env_var() { local key="$1" local target_file="${2:-$ENV_FILE}" local tmp_file="" + local target_dir="" [ -f "$target_file" ] || return 0 - tmp_file=$(mktemp) + target_dir=$(dirname "$target_file") + tmp_file=$(create_temp_file_in_dir "$target_dir" "env-comment" ".tmp") awk -v env_key="$key" ' BEGIN { done = 0 } diff --git a/lib/github.sh b/lib/github.sh index 8ad6d96..7124462 100644 --- a/lib/github.sh +++ b/lib/github.sh @@ -41,7 +41,7 @@ install_shared_libs_from_repo() { local tmp_dir="" local lib_name="" - tmp_dir=$(mktemp -d) + tmp_dir=$(create_temp_dir "shared-libs") mkdir -p "$SHARED_LIB_INSTALL_DIR" for lib_name in common.sh system.sh docker.sh github.sh env.sh; do diff --git a/pg-node.sh b/pg-node.sh index c68bab7..9bb83cd 100755 --- a/pg-node.sh +++ b/pg-node.sh @@ -175,7 +175,7 @@ configure_firewall_for_port() { install_node_script() { colorized_echo blue "Installing node script" TARGET_PATH="/usr/local/bin/$APP_NAME" - TEMP_FILE=$(mktemp) + TEMP_FILE=$(create_temp_file "pg-node-script" ".sh") # Download script to temp file first colorized_echo cyan " Downloading script from GitHub..." @@ -236,7 +236,7 @@ install_node_service_script() { colorized_echo red "node-serviced asset not found for platform $platform (expected $asset_name)" exit 1 fi - tmp_dir=$(mktemp -d) + tmp_dir=$(create_temp_dir "node-serviced") archive_path="${tmp_dir}/${asset_name}" colorized_echo cyan " Downloading ${asset_name}..." if ! curl -sSL "$asset_url" -o "$archive_path"; then From 273b4d42dc6b48f17a97a6be27f05e096aaf7099 Mon Sep 17 00:00:00 2001 From: M03ED <50927468+M03ED@users.noreply.github.com> Date: Fri, 1 May 2026 15:05:58 +0330 Subject: [PATCH 4/9] feat: move compose files --- node.yml => docker-compose/node.yml | 0 pasarguard-mariadb.yml => docker-compose/pasarguard-mariadb.yml | 0 pasarguard-mysql.yml => docker-compose/pasarguard-mysql.yml | 0 .../pasarguard-postgresql.yml | 0 .../pasarguard-timescaledb.yml | 0 pasarguard.sh | 2 +- pg-node.sh | 2 +- 7 files changed, 2 insertions(+), 2 deletions(-) rename node.yml => docker-compose/node.yml (100%) rename pasarguard-mariadb.yml => docker-compose/pasarguard-mariadb.yml (100%) rename pasarguard-mysql.yml => docker-compose/pasarguard-mysql.yml (100%) rename pasarguard-postgresql.yml => docker-compose/pasarguard-postgresql.yml (100%) rename pasarguard-timescaledb.yml => docker-compose/pasarguard-timescaledb.yml (100%) diff --git a/node.yml b/docker-compose/node.yml similarity index 100% rename from node.yml rename to docker-compose/node.yml diff --git a/pasarguard-mariadb.yml b/docker-compose/pasarguard-mariadb.yml similarity index 100% rename from pasarguard-mariadb.yml rename to docker-compose/pasarguard-mariadb.yml diff --git a/pasarguard-mysql.yml b/docker-compose/pasarguard-mysql.yml similarity index 100% rename from pasarguard-mysql.yml rename to docker-compose/pasarguard-mysql.yml diff --git a/pasarguard-postgresql.yml b/docker-compose/pasarguard-postgresql.yml similarity index 100% rename from pasarguard-postgresql.yml rename to docker-compose/pasarguard-postgresql.yml diff --git a/pasarguard-timescaledb.yml b/docker-compose/pasarguard-timescaledb.yml similarity index 100% rename from pasarguard-timescaledb.yml rename to docker-compose/pasarguard-timescaledb.yml diff --git a/pasarguard.sh b/pasarguard.sh index 433d165..c5ab912 100755 --- a/pasarguard.sh +++ b/pasarguard.sh @@ -3085,7 +3085,7 @@ install_pasarguard() { local database_type=$3 FILES_URL_PREFIX="https://raw.githubusercontent.com/pasarguard/panel" - COMPOSE_FILES_URL_PREFIX="https://raw.githubusercontent.com/pasarguard/scripts/main" + COMPOSE_FILES_URL_PREFIX="https://raw.githubusercontent.com/pasarguard/scripts/main/docker-compose" mkdir -p "$DATA_DIR" mkdir -p "$APP_DIR" diff --git a/pg-node.sh b/pg-node.sh index 9bb83cd..ee55b94 100755 --- a/pg-node.sh +++ b/pg-node.sh @@ -523,7 +523,7 @@ read_and_save_file() { install_node() { local node_version=$1 FILES_URL_PREFIX="https://raw.githubusercontent.com/PasarGuard/node/main" - COMPOSE_FILES_URL_PREFIX="https://raw.githubusercontent.com/PasarGuard/scripts/main" + COMPOSE_FILES_URL_PREFIX="https://raw.githubusercontent.com/PasarGuard/scripts/main/docker-compose" colorized_echo blue "Creating directories..." colorized_echo cyan " Command: mkdir -p $DATA_DIR $DATA_DIR/certs $APP_DIR" mkdir -p "$DATA_DIR" From fdd8513dd6f071690eb8de6ed1636e713eb7a1e8 Mon Sep 17 00:00:00 2001 From: M03ED <50927468+M03ED@users.noreply.github.com> Date: Fri, 1 May 2026 15:29:21 +0330 Subject: [PATCH 5/9] feat: separate backup and restore bash code --- lib/github.sh | 6 +- lib/pasarguard-backup.sh | 1314 +++++++++++++++++++++ lib/pasarguard-restore.sh | 887 +++++++++++++++ pasarguard.sh | 2256 +------------------------------------ pg-node.sh | 4 +- 5 files changed, 2239 insertions(+), 2228 deletions(-) create mode 100644 lib/pasarguard-backup.sh create mode 100644 lib/pasarguard-restore.sh diff --git a/lib/github.sh b/lib/github.sh index 7124462..c6c5c67 100644 --- a/lib/github.sh +++ b/lib/github.sh @@ -26,10 +26,11 @@ github_install_script_from_repo() { install_shared_libs_from_local() { local source_dir="$1" + shift local lib_name="" mkdir -p "$SHARED_LIB_INSTALL_DIR" - for lib_name in common.sh system.sh docker.sh github.sh env.sh; do + for lib_name in "$@"; do if [ -f "$source_dir/lib/$lib_name" ]; then install -m 644 "$source_dir/lib/$lib_name" "$SHARED_LIB_INSTALL_DIR/$lib_name" fi @@ -38,13 +39,14 @@ install_shared_libs_from_local() { install_shared_libs_from_repo() { local fetch_repo="$1" + shift local tmp_dir="" local lib_name="" tmp_dir=$(create_temp_dir "shared-libs") mkdir -p "$SHARED_LIB_INSTALL_DIR" - for lib_name in common.sh system.sh docker.sh github.sh env.sh; do + for lib_name in "$@"; do github_download_file "$(github_raw_url "$fetch_repo" "lib/$lib_name")" "$tmp_dir/$lib_name" install -m 644 "$tmp_dir/$lib_name" "$SHARED_LIB_INSTALL_DIR/$lib_name" done diff --git a/lib/pasarguard-backup.sh b/lib/pasarguard-backup.sh new file mode 100644 index 0000000..568b9ba --- /dev/null +++ b/lib/pasarguard-backup.sh @@ -0,0 +1,1314 @@ +#!/usr/bin/env bash + +send_backup_to_telegram() { + if [ -f "$ENV_FILE" ]; then + while IFS='=' read -r key value; do + if [[ -z "$key" || "$key" =~ ^# ]]; then + continue + fi + key=$(echo "$key" | xargs) + value=$(echo "$value" | xargs) + value=$(echo "$value" | sed -E 's/^["'"'"'](.*)["'"'"']$/\1/') + if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + export "$key"="$value" + else + colorized_echo yellow "Skipping invalid line in .env: $key=$value" + fi + done <"$ENV_FILE" + else + colorized_echo red "Environment file (.env) not found." + exit 1 + fi + + if [ "$BACKUP_SERVICE_ENABLED" != "true" ]; then + colorized_echo yellow "Backup service is not enabled. Skipping Telegram upload." + return + fi + + # Validate Telegram configuration + if [ -z "$BACKUP_TELEGRAM_BOT_KEY" ]; then + colorized_echo red "Error: BACKUP_TELEGRAM_BOT_KEY is not set in .env file" + return 1 + fi + + if [ -z "$BACKUP_TELEGRAM_CHAT_ID" ]; then + colorized_echo red "Error: BACKUP_TELEGRAM_CHAT_ID is not set in .env file" + return 1 + fi + + local proxy_url="" + local curl_proxy_args=() + if proxy_url=$(get_backup_proxy_url); then + curl_proxy_args=(--proxy "$proxy_url") + fi + + local server_ip="$(curl "${curl_proxy_args[@]}" -4 -s --max-time 5 ifconfig.me 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')" + if [ -z "$server_ip" ]; then + server_ip=$(hostname -I 2>/dev/null | awk '{print $1}') + fi + if [ -z "$server_ip" ]; then + server_ip="Unknown IP" + fi + local backup_dir="$APP_DIR/backup" + local latest_backup=$(ls -t "$backup_dir" 2>/dev/null | head -n 1) + + if [ -z "$latest_backup" ]; then + colorized_echo red "No backups found to send." + return 1 + fi + + local backup_paths=() + local cleanup_dir="" + + local telegram_split_bytes=$((49 * 1000 * 1000)) + + if [[ "$latest_backup" =~ \.part[0-9]{2}\.zip$ ]]; then + local base="${latest_backup%%.part*}" + while IFS= read -r file; do + [ -n "$file" ] && backup_paths+=("$file") + done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base}.part*.zip" | sort) + if [ ${#backup_paths[@]} -eq 0 ]; then + colorized_echo red "Incomplete backup parts for $base" + return 1 + fi + elif [[ "$latest_backup" =~ \.z[0-9]{2}$ ]]; then + local base="${latest_backup%.z??}" + while IFS= read -r file; do + [ -n "$file" ] && backup_paths+=("$file") + done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base}.z[0-9][0-9]" | sort) + if [ -f "$backup_dir/${base}.zip" ]; then + backup_paths+=("$backup_dir/${base}.zip") + else + colorized_echo red "Missing final .zip file for split archive $base" + return 1 + fi + elif [[ "$latest_backup" =~ \.zip$ ]]; then + local base="${latest_backup%.zip}" + local split_files=() + while IFS= read -r file; do + [ -n "$file" ] && split_files+=("$file") + done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base}.z[0-9][0-9]" | sort) + if [ ${#split_files[@]} -gt 0 ]; then + backup_paths=("${split_files[@]}") + fi + backup_paths+=("$backup_dir/$latest_backup") + elif [[ "$latest_backup" =~ \.tar\.gz$ ]]; then + cleanup_dir="/tmp/pasarguard_backup_split" + rm -rf "$cleanup_dir" + mkdir -p "$cleanup_dir" + local legacy_backup="$backup_dir/$latest_backup" + local backup_size=$(du -m "$legacy_backup" | cut -f1) + if [ "$backup_size" -gt 49 ]; then + colorized_echo yellow "Legacy backup is larger than 49MB. Splitting before upload..." + split -b "$telegram_split_bytes" "$legacy_backup" "$cleanup_dir/${latest_backup}_part_" + else + cp "$legacy_backup" "$cleanup_dir/$latest_backup" + fi + while IFS= read -r file; do + [ -n "$file" ] && backup_paths+=("$file") + done < <(find "$cleanup_dir" -maxdepth 1 -type f -print | sort) + if [ ${#backup_paths[@]} -eq 0 ]; then + colorized_echo red "Failed to prepare legacy backup for upload." + rm -rf "$cleanup_dir" + return 1 + fi + else + colorized_echo red "Unsupported backup format: $latest_backup" + return 1 + fi + + local backup_time=$(date "+%Y-%m-%d %H:%M:%S %Z") + + for part in "${backup_paths[@]}"; do + local part_name=$(basename "$part") + local custom_filename="$part_name" + + local escaped_server_ip=$(printf '%s' "$server_ip" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g') + local escaped_filename=$(printf '%s' "$custom_filename" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g') + local escaped_time=$(printf '%s' "$backup_time" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g') + local caption="📦 *Backup Information*\n🌐 *Server IP*: \`$escaped_server_ip\`\n📁 *Backup File*: \`$escaped_filename\`\n⏰ *Backup Time*: \`$escaped_time\`" + + local response=$(curl "${curl_proxy_args[@]}" -s -w "\n%{http_code}" -F chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ + -F document=@"$part;filename=$custom_filename" \ + -F caption="$(printf '%b' "$caption")" \ + -F parse_mode="MarkdownV2" \ + "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument" 2>&1) + + local http_code=$(echo "$response" | tail -n1) + local response_body=$(echo "$response" | sed '$d') + + if [ "$http_code" == "200" ]; then + # Check if response contains "ok":true + if echo "$response_body" | grep -q '"ok":true'; then + colorized_echo green "Backup part $custom_filename successfully sent to Telegram." + else + # Extract error message from Telegram response + local error_msg=$(echo "$response_body" | grep -o '"description":"[^"]*"' | cut -d'"' -f4 || echo "Unknown error") + colorized_echo red "Failed to send backup part $custom_filename to Telegram: $error_msg" + echo "Telegram API status: $http_code" >&2 + echo "Telegram API Response: $response_body" >&2 + fi + else + local error_msg=$(echo "$response_body" | grep -o '"description":"[^"]*"' | cut -d'"' -f4 || echo "HTTP $http_code") + colorized_echo red "Failed to send backup part $custom_filename to Telegram: $error_msg" + echo "Telegram API Response: $response_body" >&2 + fi + done + + if [ ${#uploaded_files[@]} -gt 0 ]; then + local files_list="" + for file in "${uploaded_files[@]}"; do + files_list+="- $file"$'\n' + done + files_list="${files_list%$'\n'}" + + local info_message=$'📦 Backup Upload Summary\n' + info_message+=$'──────────────────────\n' + info_message+="🌐 Server IP: $server_ip"$'\n' + info_message+="⏰ Time: $backup_time"$'\n' + info_message+=$'\n✅ Files Uploaded:\n' + info_message+="$files_list"$'\n' + info_message+=$'\n📂 Extraction Guide:\n' + info_message+=$'🪟 Windows: Install and use 7-Zip. Place the .zip and every .zXX part together, then start extraction from the .zip file.\n' + info_message+=$'🐧 Linux: Run unzip (e.g., unzip backup_xxx.zip) with all .zXX parts in the same directory.\n' + info_message+=$'🍎 macOS: Use Archive Utility or run unzip backup_xxx.zip from Terminal with the .zXX parts beside the .zip file.\n' + info_message+=$'⚠️ Always download the .zip and every .zXX part before extracting.' + + curl "${curl_proxy_args[@]}" -s -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \ + -d chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ + -d text="$info_message" >/dev/null 2>&1 || true + fi + + if [ -n "$cleanup_dir" ]; then + rm -rf "$cleanup_dir" + fi +} + +send_backup_error_to_telegram() { + local error_messages=$1 + local log_file=$2 + local proxy_url="" + local curl_proxy_args=() + if proxy_url=$(get_backup_proxy_url); then + curl_proxy_args=(--proxy "$proxy_url") + fi + local server_ip="$(curl "${curl_proxy_args[@]}" -4 -s --max-time 5 ifconfig.me 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')" + if [ -z "$server_ip" ]; then + server_ip=$(hostname -I 2>/dev/null | awk '{print $1}') + fi + if [ -z "$server_ip" ]; then + server_ip="Unknown IP" + fi + local error_time=$(date "+%Y-%m-%d %H:%M:%S %Z") + local message="⚠️ Backup Error Notification +🌐 Server IP: $server_ip +❌ Errors: $error_messages +⏰ Time: $error_time" + + local max_length=1000 + if [ ${#message} -gt $max_length ]; then + message="${message:0:$((max_length - 25))}... +[Message truncated]" + fi + + curl "${curl_proxy_args[@]}" -s -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \ + -d chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ + -d text="$message" >/dev/null 2>&1 && + colorized_echo green "Backup error notification sent to Telegram." || + colorized_echo red "Failed to send error notification to Telegram." + + if [ -f "$log_file" ]; then + + response=$(curl "${curl_proxy_args[@]}" -s -w "%{http_code}" -o /tmp/tg_response.json \ + -F chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ + -F document=@"$log_file;filename=backup_error.log" \ + -F caption="📜 Backup Error Log - $error_time" \ + "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument") + + http_code="${response:(-3)}" + if [ "$http_code" -eq 200 ]; then + colorized_echo green "Backup error log sent to Telegram." + else + colorized_echo red "Failed to send backup error log to Telegram. HTTP code: $http_code" + cat /tmp/tg_response.json + fi + else + colorized_echo red "Log file not found: $log_file" + fi +} + +backup_service() { + local telegram_bot_key="" + local telegram_chat_id="" + local cron_schedule="" + local interval_hours="" + local backup_proxy_enabled="false" + local backup_proxy_url="" + + colorized_echo blue "=====================================" + colorized_echo blue " Welcome to Backup Service " + colorized_echo blue "=====================================" + + if grep -q "BACKUP_SERVICE_ENABLED=true" "$ENV_FILE"; then + while true; do + telegram_bot_key=$(awk -F'=' '/^BACKUP_TELEGRAM_BOT_KEY=/ {print $2}' "$ENV_FILE") + telegram_chat_id=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_ID=/ {print $2}' "$ENV_FILE") + cron_schedule=$(awk -F'=' '/^BACKUP_CRON_SCHEDULE=/ {print $2}' "$ENV_FILE" | tr -d '"') + backup_proxy_enabled=$(awk -F'=' '/^BACKUP_PROXY_ENABLED=/ {print $2}' "$ENV_FILE") + backup_proxy_url=$(awk -F'=' '/^BACKUP_PROXY_URL=/ {print substr($0, index($0,"=")+1); exit}' "$ENV_FILE") + backup_proxy_url=$(echo "$backup_proxy_url" | sed -e 's/^"//' -e 's/"$//') + [ -z "$backup_proxy_enabled" ] && backup_proxy_enabled="false" + + if [[ "$cron_schedule" == "0 0 * * *" ]]; then + interval_hours=24 + else + interval_hours=$(echo "$cron_schedule" | grep -oP '(?<=\*/)[0-9]+') + fi + + colorized_echo green "=====================================" + colorized_echo green "Current Backup Configuration:" + colorized_echo cyan "Telegram Bot API Key: $telegram_bot_key" + colorized_echo cyan "Telegram Chat ID: $telegram_chat_id" + colorized_echo cyan "Backup Interval: Every $interval_hours hour(s)" + if [[ "$backup_proxy_enabled" == "true" && -n "$backup_proxy_url" ]]; then + colorized_echo cyan "Proxy: Enabled ($backup_proxy_url)" + else + colorized_echo cyan "Proxy: Disabled" + fi + colorized_echo green "=====================================" + echo "Choose an option:" + echo "1. Check Backup Service" + echo "2. Edit Backup Service" + echo "3. Reconfigure Backup Service" + echo "4. Remove Backup Service" + echo "5. Request Instant Backup" + echo "6. Exit" + read -p "Enter your choice (1-6): " user_choice + + case $user_choice in + 1) + view_backup_service + echo "" + ;; + 2) + edit_backup_service + echo "" + ;; + 3) + colorized_echo yellow "Starting reconfiguration..." + remove_backup_service + break + ;; + 4) + colorized_echo yellow "Removing Backup Service..." + remove_backup_service + return + ;; + 5) + colorized_echo yellow "Starting instant backup..." + backup_command + colorized_echo green "Instant backup completed." + echo "" + ;; + 6) + colorized_echo yellow "Exiting..." + return + ;; + *) + colorized_echo red "Invalid choice. Please try again." + echo "" + ;; + esac + done + else + colorized_echo yellow "No backup service is currently configured." + fi + + while true; do + printf "Enter your Telegram bot API key: " + read telegram_bot_key + if [[ -n "$telegram_bot_key" ]]; then + break + else + colorized_echo red "API key cannot be empty. Please try again." + fi + done + + while true; do + printf "Enter your Telegram chat ID: " + read telegram_chat_id + if [[ -n "$telegram_chat_id" ]]; then + break + else + colorized_echo red "Chat ID cannot be empty. Please try again." + fi + done + + while true; do + printf "Set up the backup interval in hours (1-24):\n" + read interval_hours + + if ! [[ "$interval_hours" =~ ^[0-9]+$ ]]; then + colorized_echo red "Invalid input. Please enter a valid number." + continue + fi + + if [[ "$interval_hours" -eq 24 ]]; then + cron_schedule="0 0 * * *" + colorized_echo green "Setting backup to run daily at midnight." + break + fi + + if [[ "$interval_hours" -ge 1 && "$interval_hours" -le 23 ]]; then + cron_schedule="0 */$interval_hours * * *" + colorized_echo green "Setting backup to run every $interval_hours hour(s)." + break + else + colorized_echo red "Invalid input. Please enter a number between 1-24." + fi + done + + while true; do + read -p "Do you need to use an HTTP/SOCKS proxy for Telegram backups? (y/N): " proxy_choice + case "$proxy_choice" in + [Yy]*) + backup_proxy_enabled="true" + break + ;; + [Nn]*|"") + backup_proxy_enabled="false" + break + ;; + *) + colorized_echo red "Invalid choice. Please enter y or n." + ;; + esac + done + + if [ "$backup_proxy_enabled" = "true" ]; then + while true; do + read -p "Enter proxy URL (e.g. http://127.0.0.1:8080 or socks5://127.0.0.1:1080): " backup_proxy_url + backup_proxy_url=$(echo "$backup_proxy_url" | xargs) + if [ -z "$backup_proxy_url" ]; then + colorized_echo red "Proxy URL cannot be empty." + continue + fi + if is_valid_proxy_url "$backup_proxy_url"; then + break + else + colorized_echo red "Invalid proxy URL. Supported prefixes: http://, https://, socks5://, socks5h://, socks4://." + fi + done + else + backup_proxy_url="" + fi + + sed -i '/^BACKUP_SERVICE_ENABLED/d' "$ENV_FILE" + sed -i '/^BACKUP_TELEGRAM_BOT_KEY/d' "$ENV_FILE" + sed -i '/^BACKUP_TELEGRAM_CHAT_ID/d' "$ENV_FILE" + sed -i '/^BACKUP_CRON_SCHEDULE/d' "$ENV_FILE" + sed -i '/^BACKUP_PROXY_ENABLED/d' "$ENV_FILE" + sed -i '/^BACKUP_PROXY_URL/d' "$ENV_FILE" + + { + echo "" + echo "# Backup service configuration" + echo "BACKUP_SERVICE_ENABLED=true" + echo "BACKUP_TELEGRAM_BOT_KEY=$telegram_bot_key" + echo "BACKUP_TELEGRAM_CHAT_ID=$telegram_chat_id" + echo "BACKUP_CRON_SCHEDULE=\"$cron_schedule\"" + echo "BACKUP_PROXY_ENABLED=$backup_proxy_enabled" + echo "BACKUP_PROXY_URL=\"$backup_proxy_url\"" + } >>"$ENV_FILE" + + colorized_echo green "Backup service configuration saved in $ENV_FILE." + + # Use full path to the script for cron job + local script_path="/usr/local/bin/$APP_NAME" + if [ ! -f "$script_path" ]; then + script_path=$(which "$APP_NAME" 2>/dev/null || echo "/usr/local/bin/$APP_NAME") + fi + # Set PATH for cron to ensure docker and other tools are found + local backup_command="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin bash $script_path backup" + add_cron_job "$cron_schedule" "$backup_command" + + colorized_echo green "Backup service successfully configured." + + # Run initial backup + colorized_echo blue "Running initial backup..." + backup_command + if [ $? -eq 0 ]; then + colorized_echo green "Initial backup completed successfully." + else + colorized_echo yellow "Initial backup completed with warnings. Check logs if needed." + fi + if [[ "$interval_hours" -eq 24 ]]; then + colorized_echo cyan "Backups will be sent to Telegram daily (every 24 hours at midnight)." + else + colorized_echo cyan "Backups will be sent to Telegram every $interval_hours hour(s)." + fi + colorized_echo green "=====================================" +} + +add_cron_job() { + local schedule="$1" + local command="$2" + local temp_cron=$(mktemp) + + crontab -l 2>/dev/null >"$temp_cron" || true + grep -v "$command" "$temp_cron" >"${temp_cron}.tmp" && mv "${temp_cron}.tmp" "$temp_cron" + echo "$schedule $command # pasarguard-backup-service" >>"$temp_cron" + + if crontab "$temp_cron"; then + colorized_echo green "Cron job successfully added." + else + colorized_echo red "Failed to add cron job. Please check manually." + fi + rm -f "$temp_cron" +} + +view_backup_service() { + if ! grep -q "BACKUP_SERVICE_ENABLED=true" "$ENV_FILE"; then + colorized_echo red "Backup service is not configured." + return 1 + fi + + local telegram_bot_key=$(awk -F'=' '/^BACKUP_TELEGRAM_BOT_KEY=/ {print $2}' "$ENV_FILE") + local telegram_chat_id=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_ID=/ {print $2}' "$ENV_FILE") + local cron_schedule=$(awk -F'=' '/^BACKUP_CRON_SCHEDULE=/ {print $2}' "$ENV_FILE" | tr -d '"') + local backup_proxy_enabled=$(awk -F'=' '/^BACKUP_PROXY_ENABLED=/ {print $2}' "$ENV_FILE") + local backup_proxy_url=$(awk -F'=' '/^BACKUP_PROXY_URL=/ {print substr($0, index($0,"=")+1); exit}' "$ENV_FILE") + backup_proxy_url=$(echo "$backup_proxy_url" | sed -e 's/^"//' -e 's/"$//') + [ -z "$backup_proxy_enabled" ] && backup_proxy_enabled="false" + local interval_hours="" + + if [[ "$cron_schedule" == "0 0 * * *" ]]; then + interval_hours=24 + else + interval_hours=$(echo "$cron_schedule" | grep -oP '(?<=\*/)[0-9]+') + fi + + colorized_echo blue "=====================================" + colorized_echo blue " Backup Service Details " + colorized_echo blue "=====================================" + colorized_echo green "Status: Enabled" + colorized_echo cyan "Telegram Bot API Key: $telegram_bot_key" + colorized_echo cyan "Telegram Chat ID: $telegram_chat_id" + colorized_echo cyan "Cron Schedule: $cron_schedule" + if [[ "$interval_hours" -eq 24 ]]; then + colorized_echo cyan "Backup Interval: Daily at midnight (every 24 hours)" + else + colorized_echo cyan "Backup Interval: Every $interval_hours hour(s)" + fi + if [[ "$backup_proxy_enabled" == "true" && -n "$backup_proxy_url" ]]; then + colorized_echo cyan "Proxy: Enabled ($backup_proxy_url)" + else + colorized_echo cyan "Proxy: Disabled" + fi + colorized_echo blue "=====================================" + echo "" + read -p "Press Enter to continue..." +} + +edit_backup_service() { + if ! grep -q "BACKUP_SERVICE_ENABLED=true" "$ENV_FILE"; then + colorized_echo red "Backup service is not configured." + return 1 + fi + + local telegram_bot_key=$(awk -F'=' '/^BACKUP_TELEGRAM_BOT_KEY=/ {print $2}' "$ENV_FILE") + local telegram_chat_id=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_ID=/ {print $2}' "$ENV_FILE") + local cron_schedule=$(awk -F'=' '/^BACKUP_CRON_SCHEDULE=/ {print $2}' "$ENV_FILE" | tr -d '"') + local backup_proxy_enabled=$(awk -F'=' '/^BACKUP_PROXY_ENABLED=/ {print $2}' "$ENV_FILE") + local backup_proxy_url=$(awk -F'=' '/^BACKUP_PROXY_URL=/ {print substr($0, index($0,"=")+1); exit}' "$ENV_FILE") + backup_proxy_url=$(echo "$backup_proxy_url" | sed -e 's/^"//' -e 's/"$//') + [ -z "$backup_proxy_enabled" ] && backup_proxy_enabled="false" + local interval_hours="" + + if [[ "$cron_schedule" == "0 0 * * *" ]]; then + interval_hours=24 + else + interval_hours=$(echo "$cron_schedule" | grep -oP '(?<=\*/)[0-9]+') + fi + + colorized_echo blue "=====================================" + colorized_echo blue " Edit Backup Service " + colorized_echo blue "=====================================" + echo "Current configuration:" + local proxy_display="Disabled" + if [[ "$backup_proxy_enabled" == "true" && -n "$backup_proxy_url" ]]; then + proxy_display="Enabled ($backup_proxy_url)" + fi + colorized_echo cyan "1. Telegram Bot API Key: $telegram_bot_key" + colorized_echo cyan "2. Telegram Chat ID: $telegram_chat_id" + colorized_echo cyan "3. Backup Interval: Every $interval_hours hour(s)" + colorized_echo cyan "4. Proxy: $proxy_display" + colorized_echo yellow "5. Cancel" + echo "" + read -p "Which setting would you like to edit? (1-5): " edit_choice + + case $edit_choice in + 1) + while true; do + printf "Enter new Telegram bot API key [current: $telegram_bot_key]: " + read new_bot_key + if [[ -n "$new_bot_key" ]]; then + sed -i "s|^BACKUP_TELEGRAM_BOT_KEY=.*|BACKUP_TELEGRAM_BOT_KEY=$new_bot_key|" "$ENV_FILE" + colorized_echo green "Telegram Bot API Key updated successfully." + break + else + colorized_echo red "API key cannot be empty. Please try again." + fi + done + ;; + 2) + while true; do + printf "Enter new Telegram chat ID [current: $telegram_chat_id]: " + read new_chat_id + if [[ -n "$new_chat_id" ]]; then + sed -i "s|^BACKUP_TELEGRAM_CHAT_ID=.*|BACKUP_TELEGRAM_CHAT_ID=$new_chat_id|" "$ENV_FILE" + colorized_echo green "Telegram Chat ID updated successfully." + break + else + colorized_echo red "Chat ID cannot be empty. Please try again." + fi + done + ;; + 3) + while true; do + printf "Set new backup interval in hours (1-24) [current: $interval_hours]:\n" + read new_interval_hours + + if ! [[ "$new_interval_hours" =~ ^[0-9]+$ ]]; then + colorized_echo red "Invalid input. Please enter a valid number." + continue + fi + + local new_cron_schedule="" + if [[ "$new_interval_hours" -eq 24 ]]; then + new_cron_schedule="0 0 * * *" + colorized_echo green "Setting backup to run daily at midnight." + elif [[ "$new_interval_hours" -ge 1 && "$new_interval_hours" -le 23 ]]; then + new_cron_schedule="0 */$new_interval_hours * * *" + colorized_echo green "Setting backup to run every $new_interval_hours hour(s)." + else + colorized_echo red "Invalid input. Please enter a number between 1-24." + continue + fi + + sed -i "s|^BACKUP_CRON_SCHEDULE=.*|BACKUP_CRON_SCHEDULE=\"$new_cron_schedule\"|" "$ENV_FILE" + + # Use full path to the script for cron job + local script_path="/usr/local/bin/$APP_NAME" + if [ ! -f "$script_path" ]; then + script_path=$(which "$APP_NAME" 2>/dev/null || echo "/usr/local/bin/$APP_NAME") + fi + # Set PATH for cron to ensure docker and other tools are found + local backup_command="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin bash $script_path backup" + local temp_cron=$(mktemp) + crontab -l 2>/dev/null >"$temp_cron" || true + grep -v "# pasarguard-backup-service" "$temp_cron" >"${temp_cron}.tmp" && mv "${temp_cron}.tmp" "$temp_cron" + echo "$new_cron_schedule $backup_command # pasarguard-backup-service" >>"$temp_cron" + + if crontab "$temp_cron"; then + colorized_echo green "Backup interval and cron schedule updated successfully." + else + colorized_echo red "Failed to update cron job. Please check manually." + fi + rm -f "$temp_cron" + break + done + ;; + 4) + local new_proxy_enabled="$backup_proxy_enabled" + local new_proxy_url="$backup_proxy_url" + while true; do + read -p "Enable proxy for Telegram backups? (y/N) [current: $proxy_display]: " proxy_choice + case "$proxy_choice" in + [Yy]*) + new_proxy_enabled="true" + break + ;; + [Nn]*|"") + new_proxy_enabled="false" + break + ;; + *) + colorized_echo red "Invalid choice. Please enter y or n." + ;; + esac + done + + if [ "$new_proxy_enabled" = "true" ]; then + while true; do + read -p "Enter proxy URL (e.g. http://127.0.0.1:8080 or socks5://127.0.0.1:1080) [current: $backup_proxy_url]: " input_proxy_url + if [ -z "$input_proxy_url" ]; then + if [ -n "$backup_proxy_url" ]; then + input_proxy_url="$backup_proxy_url" + else + colorized_echo red "Proxy URL cannot be empty." + continue + fi + fi + input_proxy_url=$(echo "$input_proxy_url" | xargs) + if is_valid_proxy_url "$input_proxy_url"; then + new_proxy_url="$input_proxy_url" + break + else + colorized_echo red "Invalid proxy URL. Supported prefixes: http://, https://, socks5://, socks5h://, socks4://." + fi + done + else + new_proxy_url="" + fi + + replace_or_append_env_var "BACKUP_PROXY_ENABLED" "$new_proxy_enabled" + replace_or_append_env_var "BACKUP_PROXY_URL" "$new_proxy_url" true + colorized_echo green "Backup proxy configuration updated successfully." + ;; + 5) + colorized_echo yellow "Edit cancelled." + return + ;; + *) + colorized_echo red "Invalid choice." + return + ;; + esac + + colorized_echo green "Backup service configuration updated successfully." +} + +remove_backup_service() { + colorized_echo red "in process..." + + sed -i '/^# Backup service configuration/d' "$ENV_FILE" + sed -i '/BACKUP_SERVICE_ENABLED/d' "$ENV_FILE" + sed -i '/BACKUP_TELEGRAM_BOT_KEY/d' "$ENV_FILE" + sed -i '/BACKUP_TELEGRAM_CHAT_ID/d' "$ENV_FILE" + sed -i '/BACKUP_CRON_SCHEDULE/d' "$ENV_FILE" + sed -i '/BACKUP_PROXY_ENABLED/d' "$ENV_FILE" + sed -i '/BACKUP_PROXY_URL/d' "$ENV_FILE" + + local temp_cron=$(mktemp) + crontab -l 2>/dev/null >"$temp_cron" + + sed -i '/# pasarguard-backup-service/d' "$temp_cron" + + if crontab "$temp_cron"; then + colorized_echo green "Backup service task removed from crontab." + else + colorized_echo red "Failed to update crontab. Please check manually." + fi + + rm -f "$temp_cron" + + colorized_echo green "Backup service has been removed." +} + + +backup_command() { + colorized_echo blue "Starting backup process..." + + # Check if pasarguard is installed + if ! is_pasarguard_installed; then + colorized_echo red "pasarguard is not installed!" + return 1 + fi + + local backup_dir="$APP_DIR/backup" + local temp_dir="/tmp/pasarguard_backup" + local timestamp=$(date +"%Y%m%d%H%M%S") + local backup_file="$backup_dir/backup_$timestamp.zip" + local error_messages=() + local log_file="/var/log/pasarguard_backup_error.log" + local final_backup_paths=() + local split_size_arg="47m" # keep Telegram chunks under 50MB + >"$log_file" + echo "Backup Log - $(date)" >>"$log_file" + + colorized_echo blue "Reading environment configuration..." + + if ! command -v rsync >/dev/null 2>&1; then + detect_os + install_package rsync + fi + + if ! command -v zip >/dev/null 2>&1; then + detect_os + install_package zip + fi + + # Remove old backups before creating new one (keep only latest) + rm -f "$backup_dir"/backup_*.tar.gz + rm -f "$backup_dir"/backup_*.zip + rm -f "$backup_dir"/backup_*.z[0-9][0-9] 2>/dev/null || true + mkdir -p "$backup_dir" + + # Clean up temp directory completely before starting + rm -rf "$temp_dir" + mkdir -p "$temp_dir" + + if [ -f "$ENV_FILE" ]; then + while IFS='=' read -r key value; do + if [[ -z "$key" || "$key" =~ ^# ]]; then + continue + fi + key=$(echo "$key" | xargs) + value=$(echo "$value" | xargs) + # Remove surrounding quotes from value if present + value=$(echo "$value" | sed -E 's/^["'\''](.*)["'\'']$/\1/') + if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + export "$key"="$value" + else + echo "Skipping invalid line in .env: $key=$value" >>"$log_file" + fi + done <"$ENV_FILE" + else + error_messages+=("Environment file (.env) not found.") + echo "Environment file (.env) not found." >>"$log_file" + send_backup_error_to_telegram "${error_messages[*]}" "$log_file" + exit 1 + fi + + local db_type="" + local sqlite_file="" + local db_host="" + local db_port="" + local db_user="" + local db_password="" + local db_name="" + local container_name="" + + # SQLALCHEMY_DATABASE_URL should already be loaded from .env above + # Just log what we have + echo "SQLALCHEMY_DATABASE_URL from environment: ${SQLALCHEMY_DATABASE_URL:-not set}" >>"$log_file" + + if [ -z "$SQLALCHEMY_DATABASE_URL" ]; then + colorized_echo red "Error: SQLALCHEMY_DATABASE_URL not found in .env file or not set" + echo "Please check $ENV_FILE for SQLALCHEMY_DATABASE_URL" >>"$log_file" + error_messages+=("SQLALCHEMY_DATABASE_URL not found in .env file") + colorized_echo yellow "Please check the log file for details: $log_file" + return 1 + fi + + if [ -n "$SQLALCHEMY_DATABASE_URL" ]; then + echo "Parsing SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL%%@*}" >>"$log_file" + + # Extract database type from scheme + if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^sqlite ]]; then + db_type="sqlite" + # Extract SQLite file path + # SQLite URLs: sqlite:///relative/path or sqlite:////absolute/path + local sqlite_url_part="${SQLALCHEMY_DATABASE_URL#*://}" + sqlite_url_part="${sqlite_url_part%%\?*}" + sqlite_url_part="${sqlite_url_part%%#*}" + + # SQLite URL format: + # sqlite:////absolute/path (4 slashes = absolute path /path) + # After removing 'sqlite://', //absolute/path remains, convert to /absolute/path + if [[ "$sqlite_url_part" =~ ^//(.*)$ ]]; then + # Absolute path: sqlite:////absolute/path -> /absolute/path + sqlite_file="/${BASH_REMATCH[1]}" + elif [[ "$sqlite_url_part" =~ ^/(.*)$ ]]; then + # Could be absolute (sqlite:///path) or relative depending on context + # In practice, treat as absolute since SQLAlchemy uses 4 slashes for absolute + sqlite_file="/${BASH_REMATCH[1]}" + else + # Relative path (no leading slash) + sqlite_file="$sqlite_url_part" + fi + elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^(mysql|mariadb|postgresql)[^:]*:// ]]; then + # Extract scheme to determine type + if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^mariadb[^:]*:// ]]; then + db_type="mariadb" + elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^mysql[^:]*:// ]]; then + db_type="mysql" + elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^postgresql[^:]*:// ]]; then + # Check if it's timescaledb by checking for specific patterns or container + if grep -q "image: timescale/timescaledb" "$COMPOSE_FILE" 2>/dev/null; then + db_type="timescaledb" + else + db_type="postgresql" + fi + fi + + # Parse connection string: scheme://[user[:password]@]host[:port]/database[?query] + # Remove scheme prefix + local url_part="${SQLALCHEMY_DATABASE_URL#*://}" + # Remove query parameters if present + url_part="${url_part%%\?*}" + url_part="${url_part%%#*}" + + # Extract auth part (user:password@) + if [[ "$url_part" =~ ^([^@]+)@(.+)$ ]]; then + local auth_part="${BASH_REMATCH[1]}" + url_part="${BASH_REMATCH[2]}" + + # Extract username and password + if [[ "$auth_part" =~ ^([^:]+):(.+)$ ]]; then + db_user="${BASH_REMATCH[1]}" + db_password="${BASH_REMATCH[2]}" + else + db_user="$auth_part" + fi + fi + + # Extract host, port, and database + if [[ "$url_part" =~ ^([^:/]+)(:([0-9]+))?/(.+)$ ]]; then + db_host="${BASH_REMATCH[1]}" + db_port="${BASH_REMATCH[3]:-}" + db_name="${BASH_REMATCH[4]}" + + # Remove query parameters from database name if any + db_name="${db_name%%\?*}" + db_name="${db_name%%#*}" + + # Set default ports if not specified + if [ -z "$db_port" ]; then + if [[ "$db_type" =~ ^(mysql|mariadb)$ ]]; then + db_port="3306" + elif [[ "$db_type" =~ ^(postgresql|timescaledb)$ ]]; then + db_port="5432" + fi + fi + fi + + # For local databases, try to find container name from docker-compose + if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then + container_name=$(find_container "$db_type") + echo "Container name/ID for $db_type: $container_name" >>"$log_file" + fi + fi + fi + + if [ -n "$db_type" ]; then + echo "Database detected: $db_type" >>"$log_file" + echo "Database host: ${db_host:-localhost}" >>"$log_file" + colorized_echo blue "Database detected: $db_type" + colorized_echo blue "Backing up database..." + case $db_type in + mariadb) + if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then + if [ -z "$container_name" ]; then + colorized_echo red "Error: MariaDB container not found. Is the container running?" + echo "MariaDB container not found. Container name: ${container_name:-empty}" >>"$log_file" + error_messages+=("MariaDB container not found or not running.") + else + local verified_container=$(check_container "$container_name" "$db_type") + if [ -z "$verified_container" ]; then + colorized_echo red "Error: MariaDB container not found or not running." + echo "Container not found or not running: $container_name" >>"$log_file" + error_messages+=("MariaDB container not found or not running.") + else + container_name="$verified_container" + # Local Docker container + # Try root user with MYSQL_ROOT_PASSWORD first for all databases backup + if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then + colorized_echo blue "Backing up all MariaDB databases from container: $container_name (using root user)" + if docker exec "$container_name" mariadb-dump -u root -p"$MYSQL_ROOT_PASSWORD" --all-databases --ignore-database=mysql --ignore-database=performance_schema --ignore-database=information_schema --ignore-database=sys --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo green "MariaDB backup completed successfully (all databases)" + else + # Fallback to SQL URL credentials for specific database + colorized_echo yellow "Root backup failed, falling back to app user for specific database" + local backup_user="${db_user:-${DB_USER:-}}" + local backup_password="${db_password:-${DB_PASSWORD:-}}" + + if [ -z "$backup_password" ] || [ -z "$db_name" ]; then + colorized_echo red "Error: Cannot fallback - missing database name or password in SQLALCHEMY_DATABASE_URL" + error_messages+=("MariaDB backup failed - root backup failed and fallback credentials incomplete.") + else + colorized_echo blue "Backing up MariaDB database '$db_name' from container: $container_name (using app user)" + if ! docker exec "$container_name" mariadb-dump -u "$backup_user" -p"$backup_password" "$db_name" --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo red "MariaDB dump failed. Check log file for details." + error_messages+=("MariaDB dump failed.") + else + colorized_echo green "MariaDB backup completed successfully" + fi + fi + fi + else + # No MYSQL_ROOT_PASSWORD, use SQL URL credentials for specific database + local backup_user="${db_user:-${DB_USER:-}}" + local backup_password="${db_password:-${DB_PASSWORD:-}}" + + if [ -z "$backup_password" ]; then + colorized_echo red "Error: Database password not found. Check MYSQL_ROOT_PASSWORD or SQLALCHEMY_DATABASE_URL in .env" + error_messages+=("MariaDB password not found.") + elif [ -z "$db_name" ]; then + colorized_echo red "Error: Database name not found in SQLALCHEMY_DATABASE_URL" + error_messages+=("MariaDB database name not found.") + else + colorized_echo blue "Backing up MariaDB database '$db_name' from container: $container_name (using app user)" + if ! docker exec "$container_name" mariadb-dump -u "$backup_user" -p"$backup_password" "$db_name" --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo red "MariaDB dump failed. Check log file for details." + error_messages+=("MariaDB dump failed.") + else + colorized_echo green "MariaDB backup completed successfully" + fi + fi + fi + fi + fi + else + # Remote database - would need mariadb-client installed + colorized_echo red "Remote MariaDB backup not yet supported. Please use local database or install mariadb-client." + error_messages+=("Remote MariaDB backup not yet supported. Please use local database or install mariadb-client.") + fi + ;; + mysql) + if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then + if [ -z "$container_name" ]; then + colorized_echo red "Error: MySQL container not found. Is the container running?" + echo "MySQL container not found. Container name: ${container_name:-empty}" >>"$log_file" + error_messages+=("MySQL container not found or not running.") + else + local verified_container=$(check_container "$container_name" "$db_type") + if [ -z "$verified_container" ]; then + colorized_echo red "Error: MySQL/MariaDB container not found or not running." + echo "Container not found or not running: $container_name" >>"$log_file" + error_messages+=("MySQL/MariaDB container not found or not running.") + else + container_name="$verified_container" + # Check if this is actually a MariaDB container (try mariadb-dump first) + local is_mariadb=false + if docker exec "$container_name" mariadb-dump --version >/dev/null 2>&1; then + is_mariadb=true + fi + + # Local Docker container + # Try root user with MYSQL_ROOT_PASSWORD first for all databases backup + if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then + # Choose command based on whether it's MariaDB or MySQL + local mysql_cmd="mysql" + local dump_cmd="mysqldump" + local db_type_name="MySQL" + if [ "$is_mariadb" = true ]; then + mysql_cmd="mariadb" + dump_cmd="mariadb-dump" + db_type_name="MariaDB" + fi + + colorized_echo blue "Backing up all $db_type_name databases from container: $container_name (using root user)" + databases=$(docker exec "$container_name" "$mysql_cmd" -u root -p"$MYSQL_ROOT_PASSWORD" -e "SHOW DATABASES;" 2>>"$log_file" | grep -Ev "^(Database|mysql|performance_schema|information_schema|sys)$" || true) + if [ -z "$databases" ]; then + colorized_echo yellow "No user databases found, falling back to specific database backup" + # Fallback to SQL URL credentials + local backup_user="${db_user:-${DB_USER:-}}" + local backup_password="${db_password:-${DB_PASSWORD:-}}" + + if [ -z "$backup_password" ] || [ -z "$db_name" ]; then + colorized_echo red "Error: Cannot fallback - missing database name or password in SQLALCHEMY_DATABASE_URL" + error_messages+=("MySQL backup failed - no databases found and fallback credentials incomplete.") + else + colorized_echo blue "Backing up $db_type_name database '$db_name' from container: $container_name (using app user)" + if ! docker exec "$container_name" "$dump_cmd" -u "$backup_user" -p"$backup_password" "$db_name" --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo red "$db_type_name dump failed. Check log file for details." + error_messages+=("$db_type_name dump failed.") + else + colorized_echo green "$db_type_name backup completed successfully" + fi + fi + elif ! docker exec "$container_name" "$dump_cmd" -u root -p"$MYSQL_ROOT_PASSWORD" --databases $databases --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + # Root backup failed, fallback to SQL URL credentials + colorized_echo yellow "Root backup failed, falling back to app user for specific database" + local backup_user="${db_user:-${DB_USER:-}}" + local backup_password="${db_password:-${DB_PASSWORD:-}}" + + if [ -z "$backup_password" ] || [ -z "$db_name" ]; then + colorized_echo red "Error: Cannot fallback - missing database name or password in SQLALCHEMY_DATABASE_URL" + error_messages+=("MySQL backup failed - root backup failed and fallback credentials incomplete.") + else + colorized_echo blue "Backing up $db_type_name database '$db_name' from container: $container_name (using app user)" + if ! docker exec "$container_name" "$dump_cmd" -u "$backup_user" -p"$backup_password" "$db_name" --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo red "$db_type_name dump failed. Check log file for details." + error_messages+=("$db_type_name dump failed.") + else + colorized_echo green "$db_type_name backup completed successfully" + fi + fi + else + colorized_echo green "$db_type_name backup completed successfully (all databases)" + fi + else + # No MYSQL_ROOT_PASSWORD, use SQL URL credentials for specific database + local backup_user="${db_user:-${DB_USER:-}}" + local backup_password="${db_password:-${DB_PASSWORD:-}}" + local dump_cmd="mysqldump" + local db_type_name="MySQL" + if [ "$is_mariadb" = true ]; then + dump_cmd="mariadb-dump" + db_type_name="MariaDB" + fi + + if [ -z "$backup_password" ]; then + colorized_echo red "Error: Database password not found. Check MYSQL_ROOT_PASSWORD or SQLALCHEMY_DATABASE_URL in .env" + error_messages+=("MySQL password not found.") + elif [ -z "$db_name" ]; then + colorized_echo red "Error: Database name not found in SQLALCHEMY_DATABASE_URL" + error_messages+=("MySQL database name not found.") + else + colorized_echo blue "Backing up $db_type_name database '$db_name' from container: $container_name (using app user)" + if ! docker exec "$container_name" "$dump_cmd" -u "$backup_user" -p"$backup_password" "$db_name" --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo red "$db_type_name dump failed. Check log file for details." + error_messages+=("$db_type_name dump failed.") + else + colorized_echo green "$db_type_name backup completed successfully" + fi + fi + fi + fi + fi + else + # Remote database - would need mysql-client installed + colorized_echo red "Remote MySQL backup not yet supported. Please use local database or install mysql-client." + error_messages+=("Remote MySQL backup not yet supported. Please use local database or install mysql-client.") + fi + ;; + postgresql) + if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then + if [ -z "$container_name" ]; then + colorized_echo red "Error: PostgreSQL container not found. Is the container running?" + echo "PostgreSQL container not found. Container name: ${container_name:-empty}" >>"$log_file" + error_messages+=("PostgreSQL container not found or not running.") + else + local verified_container=$(check_container "$container_name" "$db_type") + if [ -z "$verified_container" ]; then + colorized_echo red "Error: PostgreSQL container not found or not running." + echo "Container not found or not running: $container_name" >>"$log_file" + error_messages+=("PostgreSQL container not found or not running.") + else + container_name="$verified_container" + # Local Docker container + # Try postgres superuser with DB_PASSWORD first for pg_dumpall (all databases) + if [ -n "${DB_PASSWORD:-}" ]; then + colorized_echo blue "Backing up all PostgreSQL databases from container: $container_name (using postgres superuser)" + export PGPASSWORD="$DB_PASSWORD" + if docker exec "$container_name" pg_dumpall -U postgres >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo green "PostgreSQL backup completed successfully (all databases)" + unset PGPASSWORD + else + # Fallback to pg_dump with SQL URL credentials + unset PGPASSWORD + colorized_echo yellow "pg_dumpall failed, falling back to pg_dump for specific database" + local backup_user="${db_user:-${DB_USER:-postgres}}" + local backup_password="${db_password:-${DB_PASSWORD:-}}" + + if [ -z "$backup_password" ] || [ -z "$db_name" ]; then + colorized_echo red "Error: Cannot fallback - missing database name or password in SQLALCHEMY_DATABASE_URL" + error_messages+=("PostgreSQL backup failed - pg_dumpall failed and fallback credentials incomplete.") + else + colorized_echo blue "Backing up PostgreSQL database '$db_name' from container: $container_name (using app user)" + export PGPASSWORD="$backup_password" + if ! docker exec "$container_name" pg_dump -U "$backup_user" -d "$db_name" --clean --if-exists >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo red "PostgreSQL dump failed. Check log file for details." + error_messages+=("PostgreSQL dump failed.") + else + colorized_echo green "PostgreSQL backup completed successfully" + fi + unset PGPASSWORD + fi + fi + else + # No DB_PASSWORD, use SQL URL credentials for pg_dump + local backup_user="${db_user:-${DB_USER:-postgres}}" + local backup_password="${db_password:-${DB_PASSWORD:-}}" + + if [ -z "$backup_password" ]; then + colorized_echo red "Error: Database password not found. Check DB_PASSWORD or SQLALCHEMY_DATABASE_URL in .env" + error_messages+=("PostgreSQL password not found.") + elif [ -z "$db_name" ]; then + colorized_echo red "Error: Database name not found in SQLALCHEMY_DATABASE_URL" + error_messages+=("PostgreSQL database name not found.") + else + colorized_echo blue "Backing up PostgreSQL database '$db_name' from container: $container_name (using app user)" + export PGPASSWORD="$backup_password" + if ! docker exec "$container_name" pg_dump -U "$backup_user" -d "$db_name" --clean --if-exists >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo red "PostgreSQL dump failed. Check log file for details." + error_messages+=("PostgreSQL dump failed.") + else + colorized_echo green "PostgreSQL backup completed successfully" + fi + unset PGPASSWORD + fi + fi + fi + fi + else + # Remote database - would need postgresql-client installed + colorized_echo red "Remote PostgreSQL backup not yet supported. Please use local database or install postgresql-client." + error_messages+=("Remote PostgreSQL backup not yet supported. Please use local database or install postgresql-client.") + fi + ;; + timescaledb) + if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then + if [ -z "$container_name" ]; then + colorized_echo red "Error: TimescaleDB container not found. Is the container running?" + echo "Container name detection failed. Checked for: timescaledb, postgresql" >>"$log_file" + error_messages+=("TimescaleDB container not found or not running.") + else + # Get actual container name/ID - ps -q returns container ID, which is what we need + # But first verify the container exists + local actual_container="" + if docker inspect "$container_name" >/dev/null 2>&1; then + actual_container="$container_name" + else + # Try to find container by service name using docker compose + actual_container=$($COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" ps -q timescaledb 2>/dev/null) + if [ -z "$actual_container" ]; then + actual_container=$($COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" ps -q postgresql 2>/dev/null) + fi + if [ -z "$actual_container" ]; then + # Try with full container name pattern + local full_container_name="${APP_NAME}-timescaledb-1" + if docker inspect "$full_container_name" >/dev/null 2>&1; then + actual_container="$full_container_name" + else + full_container_name="${APP_NAME}-postgresql-1" + if docker inspect "$full_container_name" >/dev/null 2>&1; then + actual_container="$full_container_name" + fi + fi + fi + fi + + if [ -z "$actual_container" ]; then + colorized_echo red "Error: TimescaleDB container not found. Is the container running?" + echo "Container not found. Tried: $container_name and various patterns" >>"$log_file" + error_messages+=("TimescaleDB container not found or not running.") + else + container_name="$actual_container" + # Local Docker container + # Use SQL URL credentials directly for pg_dump (more reliable than pg_dumpall) + local backup_user="${db_user:-${DB_USER:-postgres}}" + local backup_password="${db_password:-${DB_PASSWORD:-}}" + + if [ -z "$backup_password" ]; then + colorized_echo red "Error: Database password not found. Check DB_PASSWORD or SQLALCHEMY_DATABASE_URL in .env" + error_messages+=("TimescaleDB password not found.") + elif [ -z "$db_name" ]; then + colorized_echo red "Error: Database name not found in SQLALCHEMY_DATABASE_URL" + error_messages+=("TimescaleDB database name not found.") + else + colorized_echo blue "Backing up TimescaleDB database '$db_name' from container: $container_name (using user: $backup_user)" + export PGPASSWORD="$backup_password" + if ! docker exec "$container_name" pg_dump -U "$backup_user" -d "$db_name" --clean --if-exists >"$temp_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo red "TimescaleDB dump failed. Check log file for details: $log_file" + error_messages+=("TimescaleDB dump failed for database '$db_name'.") + else + colorized_echo green "TimescaleDB backup completed successfully" + fi + unset PGPASSWORD + fi + fi + fi + else + # Remote database - would need postgresql-client installed + colorized_echo red "Remote TimescaleDB backup not yet supported. Please use local database or install postgresql-client." + error_messages+=("Remote TimescaleDB backup not yet supported. Please use local database or install postgresql-client.") + fi + ;; + sqlite) + if [ -f "$sqlite_file" ]; then + if ! cp "$sqlite_file" "$temp_dir/db_backup.sqlite" 2>>"$log_file"; then + error_messages+=("Failed to copy SQLite database.") + fi + else + error_messages+=("SQLite database file not found at $sqlite_file.") + fi + ;; + esac + else + colorized_echo yellow "Warning: No database type detected. Skipping database backup." + echo "Warning: No database type detected." >>"$log_file" + echo "SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL:-not set}" >>"$log_file" + fi + + colorized_echo blue "Copying configuration files..." + if ! cp "$APP_DIR/.env" "$temp_dir/" 2>>"$log_file"; then + error_messages+=("Failed to copy .env file.") + echo "Failed to copy .env file" >>"$log_file" + fi + if ! cp "$APP_DIR/docker-compose.yml" "$temp_dir/" 2>>"$log_file"; then + error_messages+=("Failed to copy docker-compose.yml file.") + echo "Failed to copy docker-compose.yml file" >>"$log_file" + fi + + colorized_echo blue "Copying data directory..." + # Ensure destination directory exists and is empty (already cleaned above, but be explicit) + if [ -d "$DATA_DIR" ]; then + if ! rsync -av --exclude 'xray-core' --exclude 'mysql' "$DATA_DIR/" "$temp_dir/pasarguard_data/" >>"$log_file" 2>&1; then + error_messages+=("Failed to copy data directory.") + echo "Failed to copy data directory" >>"$log_file" + fi + else + colorized_echo yellow "Data directory $DATA_DIR does not exist. Skipping data directory backup." + echo "Data directory $DATA_DIR does not exist. Skipping." >>"$log_file" + # Create empty directory structure so tar doesn't fail + mkdir -p "$temp_dir/pasarguard_data" + fi + + # Remove Unix socket files so zip doesn't fail with ENXIO ("No such device or address") + if [ -d "$temp_dir" ]; then + local socket_files + socket_files=$(find "$temp_dir" -type s -print 2>/dev/null || true) + if [ -n "$socket_files" ]; then + colorized_echo yellow "Removing Unix socket files before archiving (zip cannot archive sockets)." + printf "%s\n" "$socket_files" >>"$log_file" + find "$temp_dir" -type s -delete >>"$log_file" 2>&1 || true + fi + fi + + colorized_echo blue "Creating backup archive..." + # Verify temp_dir exists and has content before creating archive + if [ ! -d "$temp_dir" ] || [ -z "$(ls -A "$temp_dir" 2>/dev/null)" ]; then + error_messages+=("Temporary directory is empty or missing. Cannot create archive.") + echo "Temporary directory is empty or missing: $temp_dir" >>"$log_file" + elif ! (cd "$temp_dir" && zip -rq -s "$split_size_arg" "$backup_file" .) 2>>"$log_file"; then + error_messages+=("Failed to create backup archive.") + echo "Failed to create backup archive." >>"$log_file" + else + local backup_size=$(du -h "$backup_file" | cut -f1) + colorized_echo green "Backup archive created: $backup_file (Size: $backup_size)" + fi + + if [ -f "$backup_file" ]; then + while IFS= read -r file; do + final_backup_paths+=("$file") + done < <(find "$backup_dir" -maxdepth 1 -type f -name "backup_${timestamp}.z[0-9][0-9]" | sort) + final_backup_paths+=("$backup_file") + fi + + # Clean up temp directory after archive is created + rm -rf "$temp_dir" + + if [ ${#error_messages[@]} -gt 0 ]; then + colorized_echo red "Backup completed with errors:" + for error in "${error_messages[@]}"; do + colorized_echo red " - $error" + done + colorized_echo yellow "Check log file: $log_file" + if [ -f "$ENV_FILE" ]; then + send_backup_error_to_telegram "${error_messages[*]}" "$log_file" + fi + return 1 + fi + + if [ ${#final_backup_paths[@]} -eq 0 ]; then + colorized_echo red "Backup file was not created. Check log file: $log_file" + return 1 + fi + + if [ ${#final_backup_paths[@]} -eq 1 ]; then + colorized_echo green "Backup completed successfully: ${final_backup_paths[0]}" + else + colorized_echo green "Backup completed successfully in ${#final_backup_paths[@]} parts:" + for part in "${final_backup_paths[@]}"; do + colorized_echo green " - $(basename "$part")" + done + fi + if [ -f "$ENV_FILE" ]; then + send_backup_to_telegram "$backup_file" + fi +} + diff --git a/lib/pasarguard-restore.sh b/lib/pasarguard-restore.sh new file mode 100644 index 0000000..b62fd0e --- /dev/null +++ b/lib/pasarguard-restore.sh @@ -0,0 +1,887 @@ +#!/usr/bin/env bash + +restore_command() { + colorized_echo blue "Starting restore process..." + + # Check if pasarguard is installed + if ! is_pasarguard_installed; then + colorized_echo red "pasarguard's not installed!" + exit 1 + fi + + detect_compose + + if ! is_pasarguard_up; then + colorized_echo red "pasarguard is not up. Please start pasarguard first." + exit 1 + fi + + local current_db_user="" + local current_db_password="" + local current_db_name="" + local current_sqlalchemy_url="" + local current_mysql_root_password="" + + if [ -f "$ENV_FILE" ]; then + set +e + while IFS='=' read -r key value || [ -n "$key" ]; do + if [[ -z "$key" || "$key" =~ ^# ]]; then + continue + fi + key=$(echo "$key" | xargs 2>/dev/null || echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + value=$(echo "$value" | xargs 2>/dev/null || echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + value=$(echo "$value" | sed -E 's/^["'"'"'](.*)["'"'"']$/\1/' 2>/dev/null || echo "$value") + case "$key" in + MYSQL_ROOT_PASSWORD) + current_mysql_root_password="$value" + ;; + DB_USER) + current_db_user="$value" + ;; + DB_PASSWORD) + current_db_password="$value" + ;; + DB_NAME) + current_db_name="$value" + ;; + SQLALCHEMY_DATABASE_URL) + current_sqlalchemy_url="$value" + ;; + esac + done <"$ENV_FILE" + set -e + fi + + local backup_dir="$APP_DIR/backup" + local temp_restore_dir="/tmp/pasarguard_restore" + local log_file="/var/log/pasarguard_restore_error.log" + >"$log_file" + echo "Restore Log - $(date)" >>"$log_file" + + # Clean up temp directory + rm -rf "$temp_restore_dir" + mkdir -p "$temp_restore_dir" + + # Check if backup directory exists + if [ ! -d "$backup_dir" ]; then + colorized_echo red "Backup directory not found: $backup_dir" + exit 1 + fi + + # List available backup files (find all backup-related files in backup directory) + local backup_candidates=() + while IFS= read -r -d '' file; do + backup_candidates+=("$file") + done < <(find "$backup_dir" -maxdepth 1 \( -name "*backup*.gz" -o -name "*backup*.tar.gz" -o -name "*.tar.gz" -o -name "*backup*.zip" -o -name "*.zip" \) -type f -print0 2>/dev/null) + + if [ ${#backup_candidates[@]} -eq 0 ]; then + # Fallback: try to find any archive files + while IFS= read -r -d '' file; do + backup_candidates+=("$file") + done < <(find "$backup_dir" -maxdepth 1 \( -name "*.gz" -o -name "*.zip" \) -type f -print0 2>/dev/null) + fi + + local backup_files=() + for file in "${backup_candidates[@]}"; do + local filename=$(basename "$file") + if [[ "$filename" =~ \.part[0-9]{2}\.zip$ ]] && [[ ! "$filename" =~ \.part01\.zip$ ]]; then + continue + fi + if [[ "$filename" =~ \.z[0-9]{2}$ ]]; then + continue + fi + backup_files+=("$file") + done + + if [ ${#backup_files[@]} -eq 0 ]; then + colorized_echo red "No backup files found in $backup_dir" + colorized_echo yellow "Looking for files with extensions: .gz, .zip, .tar.gz or containing 'backup'" + exit 1 + fi + + colorized_echo blue "Available backup files:" + local i=1 + for file in "${backup_files[@]}"; do + if [ -f "$file" ]; then + local filename=$(basename "$file") + if [[ "$filename" =~ \.part[0-9]{2}\.zip$ ]]; then + local base_name="${filename%%.part*}" + local part_count=$(find "$backup_dir" -maxdepth 1 -type f -name "${base_name}.part*.zip" | wc -l | awk '{print $1}') + [ -z "$part_count" ] && part_count=0 + local total_size_bytes=0 + while IFS= read -r part_file; do + local part_size=$(stat -c%s "$part_file" 2>/dev/null || stat -f%z "$part_file" 2>/dev/null) + if [ -z "$part_size" ]; then + part_size=$(wc -c <"$part_file") + fi + total_size_bytes=$((total_size_bytes + part_size)) + done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base_name}.part*.zip") + local human_size="" + if command -v numfmt >/dev/null 2>&1; then + human_size=$(numfmt --to=iec --suffix=B "$total_size_bytes" 2>/dev/null || awk -v size="$total_size_bytes" 'BEGIN { printf "%.2f MB", size/1048576 }') + else + human_size=$(awk -v size="$total_size_bytes" 'BEGIN { printf "%.2f MB", size/1048576 }') + fi + local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S") + echo "$i. $filename (Parts: ${part_count:-1}, Total Size: $human_size, Date: $file_date)" + elif [[ "$filename" =~ \.zip$ ]]; then + local base_name="${filename%.zip}" + local zip_part_files=() + while IFS= read -r part_file; do + zip_part_files+=("$part_file") + done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base_name}.z[0-9][0-9]" | sort) + if [ ${#zip_part_files[@]} -gt 0 ]; then + local total_size_bytes=0 + for part_file in "${zip_part_files[@]}"; do + local part_size=$(stat -c%s "$part_file" 2>/dev/null || stat -f%z "$part_file" 2>/dev/null) + if [ -z "$part_size" ]; then + part_size=$(wc -c <"$part_file") + fi + total_size_bytes=$((total_size_bytes + part_size)) + done + local main_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null) + if [ -z "$main_size" ]; then + main_size=$(wc -c <"$file") + fi + total_size_bytes=$((total_size_bytes + main_size)) + local part_display="" + if command -v numfmt >/dev/null 2>&1; then + part_display=$(numfmt --to=iec --suffix=B "$total_size_bytes" 2>/dev/null || awk -v size="$total_size_bytes" 'BEGIN { printf "%.2f MB", size/1048576 }') + else + part_display=$(awk -v size="$total_size_bytes" 'BEGIN { printf "%.2f MB", size/1048576 }') + fi + local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S") + local part_count=$(( ${#zip_part_files[@]} + 1 )) + echo "$i. $filename (Zip splits: $part_count parts, Total Size: $part_display, Date: $file_date)" + else + local file_size=$(du -h "$file" | cut -f1) + local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S") + echo "$i. $filename (Size: $file_size, Date: $file_date)" + fi + else + local file_size=$(du -h "$file" | cut -f1) + local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S") + echo "$i. $filename (Size: $file_size, Date: $file_date)" + fi + ((i++)) + fi + done + + local file_count=$((i-1)) + if [ "$file_count" -eq 0 ]; then + colorized_echo red "No valid backup files found." + exit 1 + fi + + # Select backup file + while true; do + printf "Select backup file to restore from (1-%d): " "$file_count" + read -r selection + if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le "$file_count" ]; then + break + else + colorized_echo red "Invalid selection. Please enter a number between 1 and $file_count." + fi + done + + local selected_file="${backup_files[$((selection-1))]}" + local selected_filename=$(basename "$selected_file") + + colorized_echo blue "Selected backup: $selected_filename" + + colorized_echo blue "Preparing archive for extraction..." + local archive_to_extract="$selected_file" + local archive_format="tar" + + if [[ "$selected_filename" =~ \.part[0-9]{2}\.zip$ ]]; then + archive_format="zip" + local base_name="${selected_filename%%.part*}" + colorized_echo yellow "Detected split zip backup. Checking available parts..." + if [ ! -f "$backup_dir/${base_name}.part01.zip" ]; then + colorized_echo red "Missing ${base_name}.part01.zip. Cannot restore split backup." + rm -rf "$temp_restore_dir" + exit 1 + fi + local concatenated_file="$temp_restore_dir/${base_name}_combined.zip" + >"$concatenated_file" + local part_count=0 + while IFS= read -r part_file; do + cat "$part_file" >>"$concatenated_file" + part_count=$((part_count + 1)) + done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base_name}.part*.zip" | sort) + if [ "$part_count" -eq 0 ]; then + colorized_echo red "No parts found for $base_name" + rm -rf "$temp_restore_dir" + exit 1 + fi + archive_to_extract="$concatenated_file" + colorized_echo green "✓ Combined $part_count part(s)" + elif [[ "$selected_filename" =~ \.zip$ ]]; then + archive_format="zip" + local base_name="${selected_filename%.zip}" + local zip_split_parts=() + while IFS= read -r part_file; do + [ -n "$part_file" ] && zip_split_parts+=("$part_file") + done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base_name}.z[0-9][0-9]" | sort) + + if [ ${#zip_split_parts[@]} -gt 0 ]; then + colorized_echo yellow "Detected split zip backup (.zXX + .zip). Rebuilding archive..." + local expected_part=1 + for part_file in "${zip_split_parts[@]}"; do + local expected_name + expected_name=$(printf "%s.z%02d" "$base_name" "$expected_part") + if [ "$(basename "$part_file")" != "$expected_name" ]; then + colorized_echo red "Missing split part $expected_name. Cannot restore split backup." + rm -rf "$temp_restore_dir" + exit 1 + fi + expected_part=$((expected_part + 1)) + done + + local concatenated_file="$temp_restore_dir/${base_name}_combined.zip" + if command -v zip >/dev/null 2>&1 && zip -s 0 "$selected_file" --out "$concatenated_file" >>"$log_file" 2>&1; then + archive_to_extract="$concatenated_file" + colorized_echo green "✓ Rebuilt split zip archive with zip utility" + else + if command -v zip >/dev/null 2>&1; then + colorized_echo yellow "zip rebuild failed. Falling back to direct concatenation..." + else + colorized_echo yellow "zip utility not found. Falling back to direct concatenation..." + fi + >"$concatenated_file" + local part_count=0 + for part_file in "${zip_split_parts[@]}"; do + if ! cat "$part_file" >>"$concatenated_file"; then + colorized_echo red "Failed to read split part: $(basename "$part_file")" + rm -rf "$temp_restore_dir" + exit 1 + fi + part_count=$((part_count + 1)) + done + if ! cat "$selected_file" >>"$concatenated_file"; then + colorized_echo red "Failed to read main zip file: $selected_filename" + rm -rf "$temp_restore_dir" + exit 1 + fi + archive_to_extract="$concatenated_file" + colorized_echo green "✓ Combined $((part_count + 1)) split part(s)" + fi + fi + else + archive_format="tar" + fi + + colorized_echo blue "Extracting backup..." + if [ "$archive_format" = "zip" ]; then + if ! command -v unzip >/dev/null 2>&1; then + detect_os + install_package unzip + fi + if ! unzip -tq "$archive_to_extract" >/dev/null 2>>"$log_file"; then + colorized_echo red "ERROR: The backup file is not a valid zip archive." + echo "File is not a valid zip archive: $archive_to_extract" >>"$log_file" + rm -rf "$temp_restore_dir" + exit 1 + fi + if ! unzip -oq "$archive_to_extract" -d "$temp_restore_dir" 2>>"$log_file"; then + colorized_echo red "Failed to extract backup file." + echo "Failed to extract $archive_to_extract" >>"$log_file" + rm -rf "$temp_restore_dir" + exit 1 + fi + else + if ! gzip -t "$archive_to_extract" 2>/dev/null; then + colorized_echo red "ERROR: The backup file is not a valid gzip archive." + echo "File is not a valid gzip archive: $archive_to_extract" >>"$log_file" + rm -rf "$temp_restore_dir" + exit 1 + fi + if ! tar -xzf "$archive_to_extract" -C "$temp_restore_dir" 2>>"$log_file"; then + colorized_echo red "Failed to extract backup file." + echo "Failed to extract $archive_to_extract" >>"$log_file" + rm -rf "$temp_restore_dir" + exit 1 + fi + fi + colorized_echo green "✓ Archive extracted successfully" + + # Load environment variables from extracted .env + colorized_echo blue "Loading configuration from backup..." + local extracted_env="$temp_restore_dir/.env" + if [ ! -f "$extracted_env" ]; then + colorized_echo red "Environment file not found in backup." + rm -rf "$temp_restore_dir" + exit 1 + fi + + local db_type="" + local sqlite_file="" + local db_host="" + local db_port="" + local db_user="" + local db_password="" + local db_name="" + local container_name="" + + # Load variables from extracted .env + # Check if file is readable + if [ ! -r "$extracted_env" ]; then + colorized_echo red "ERROR: .env file is not readable" + rm -rf "$temp_restore_dir" + exit 1 + fi + + # Check for binary content or null bytes (warning only, not fatal) + if grep -q $'\x00' "$extracted_env" 2>/dev/null; then + colorized_echo yellow "WARNING: .env file contains null bytes, cleaning..." + fi + + local env_vars_loaded=0 + + # Check if file has null bytes - if not, use it directly + local env_file_to_use="$extracted_env" + if grep -q $'\x00' "$extracted_env" 2>/dev/null; then + # File has null bytes, create cleaned version + local cleaned_env="/tmp/pasarguard_env_cleaned_$$" + set +e + tr -d '\000' < "$extracted_env" > "$cleaned_env" 2>/dev/null + local tr_result=$? + set -e + if [ $tr_result -eq 0 ] && [ -s "$cleaned_env" ]; then + env_file_to_use="$cleaned_env" + else + rm -f "$cleaned_env" + fi + fi + + # Use the EXACT same pattern as backup_command function + # This ensures compatibility and works in the current shell (no subshell) + colorized_echo blue "Loading environment variables..." + if [ -f "$env_file_to_use" ]; then + # Temporarily disable exit on error for the loop to handle failures gracefully + set +e + while IFS='=' read -r key value || [ -n "$key" ]; do + if [[ -z "$key" || "$key" =~ ^# ]]; then + continue + fi + # Trim whitespace from key and value + key=$(echo "$key" | xargs 2>/dev/null || echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + value=$(echo "$value" | xargs 2>/dev/null || echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + # Remove surrounding quotes from value if present + value=$(echo "$value" | sed -E 's/^["'\''](.*)["'\'']$/\1/' 2>/dev/null || echo "$value") + if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then + export "$key"="$value" 2>/dev/null || true + env_vars_loaded=$((env_vars_loaded + 1)) + else + echo "Skipping invalid line in .env: $key=$value" >&2 + fi + done <"$env_file_to_use" + set -e # Re-enable exit on error + else + colorized_echo red "Environment file (.env) not found in backup." + rm -rf "$temp_restore_dir" + exit 1 + fi + + # Clean up temporary cleaned file if we created one + if [ -n "${cleaned_env:-}" ] && [ -f "$cleaned_env" ]; then + rm -f "$cleaned_env" + fi + + colorized_echo green "✓ Loaded $env_vars_loaded environment variables" + + if [ -z "$SQLALCHEMY_DATABASE_URL" ]; then + colorized_echo red "SQLALCHEMY_DATABASE_URL not found in backup .env file" + colorized_echo yellow "Available environment variables:" + grep -v '^#' "$extracted_env" | grep '=' | cut -d'=' -f1 | head -10 + rm -rf "$temp_restore_dir" + exit 1 + fi + + colorized_echo green "✓ Found SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL:0:50}..." + + # Parse database configuration (similar to backup function) + colorized_echo blue "Detecting database type..." + if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^sqlite ]]; then + db_type="sqlite" + colorized_echo green "✓ Detected SQLite database" + local sqlite_url_part="${SQLALCHEMY_DATABASE_URL#*://}" + sqlite_url_part="${sqlite_url_part%%\?*}" + sqlite_url_part="${sqlite_url_part%%#*}" + + if [[ "$sqlite_url_part" =~ ^//(.*)$ ]]; then + sqlite_file="/${BASH_REMATCH[1]}" + elif [[ "$sqlite_url_part" =~ ^/(.*)$ ]]; then + sqlite_file="/${BASH_REMATCH[1]}" + else + sqlite_file="$sqlite_url_part" + fi + colorized_echo blue "Database file: $sqlite_file" + elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^(mysql|mariadb|postgresql)[^:]*:// ]]; then + if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^mariadb[^:]*:// ]]; then + db_type="mariadb" + colorized_echo green "✓ Detected MariaDB database" + elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^mysql[^:]*:// ]]; then + db_type="mysql" + colorized_echo green "✓ Detected MySQL database" + elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^postgresql[^:]*:// ]]; then + # Check if it's timescaledb - use set +e to prevent failure on file not found + set +e + if grep -q "image: timescale/timescaledb" "$temp_restore_dir/docker-compose.yml" 2>/dev/null; then + db_type="timescaledb" + colorized_echo green "✓ Detected TimescaleDB database" + else + db_type="postgresql" + colorized_echo green "✓ Detected PostgreSQL database" + fi + set -e + fi + + local url_part="${SQLALCHEMY_DATABASE_URL#*://}" + url_part="${url_part%%\?*}" + url_part="${url_part%%#*}" + + if [[ "$url_part" =~ ^([^@]+)@(.+)$ ]]; then + local auth_part="${BASH_REMATCH[1]}" + url_part="${BASH_REMATCH[2]}" + + if [[ "$auth_part" =~ ^([^:]+):(.+)$ ]]; then + db_user="${BASH_REMATCH[1]}" + db_password="${BASH_REMATCH[2]}" + else + db_user="$auth_part" + fi + fi + + if [[ "$url_part" =~ ^([^:/]+)(:([0-9]+))?/(.+)$ ]]; then + db_host="${BASH_REMATCH[1]}" + db_port="${BASH_REMATCH[3]:-}" + db_name="${BASH_REMATCH[4]}" + db_name="${db_name%%\?*}" + db_name="${db_name%%#*}" + + if [ -z "$db_port" ]; then + if [[ "$db_type" =~ ^(mysql|mariadb)$ ]]; then + db_port="3306" + elif [[ "$db_type" =~ ^(postgresql|timescaledb)$ ]]; then + db_port="5432" + fi + fi + fi + + # Find container name for local databases + if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then + set +e + container_name=$(find_container "$db_type") + set -e + fi + fi + + if [ -z "$db_type" ]; then + colorized_echo red "Could not determine database type from backup." + colorized_echo yellow "SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL:-not set}" + rm -rf "$temp_restore_dir" + exit 1 + fi + + colorized_echo green "✓ Database configuration detected: $db_type" + + # Confirm restore + colorized_echo red "⚠️ DANGER: This will PERMANENTLY overwrite your current $db_type database!" + colorized_echo yellow "WARNING: This will overwrite your current $db_type database!" + colorized_echo blue "Database type: $db_type" + if [ -n "$db_name" ]; then + colorized_echo blue "Database name: $db_name" + fi + if [ -n "$container_name" ]; then + colorized_echo blue "Container: $container_name" + fi + + while true; do + printf "Do you want to proceed with the restore? (yes/no): " + read -r confirm + if [[ "$confirm" =~ ^[Yy](es)?$ ]]; then + break + elif [[ "$confirm" =~ ^[Nn](o)?$ ]]; then + colorized_echo yellow "Restore cancelled." + rm -rf "$temp_restore_dir" + exit 0 + else + colorized_echo red "Please answer yes or no." + fi + done + + # Stop pasarguard services before restore for clean state + colorized_echo blue "Stopping pasarguard services for clean restore..." + if [[ "$db_type" == "sqlite" ]]; then + # For SQLite, stop all services since we need to restore files + down_pasarguard + else + # For containerized databases, stop only application services + # Keep database containers running for restore via docker exec + stop_pasarguard_app_services + fi + + # Perform restore + colorized_echo red "⚠️ DANGER: Starting database restore - this will overwrite existing data!" + colorized_echo blue "Starting database restore..." + + case $db_type in + sqlite) + if [ ! -f "$temp_restore_dir/db_backup.sqlite" ]; then + colorized_echo red "SQLite backup file not found in backup archive." + rm -rf "$temp_restore_dir" + exit 1 + fi + + if [ -f "$sqlite_file" ]; then + cp "$sqlite_file" "${sqlite_file}.backup.$(date +%Y%m%d%H%M%S)" 2>>"$log_file" + fi + + if cp "$temp_restore_dir/db_backup.sqlite" "$sqlite_file" 2>>"$log_file"; then + colorized_echo green "SQLite database restored successfully." + else + colorized_echo red "Failed to restore SQLite database." + echo "SQLite restore failed" >>"$log_file" + rm -rf "$temp_restore_dir" + exit 1 + fi + ;; + + mariadb|mysql) + if [ ! -f "$temp_restore_dir/db_backup.sql" ]; then + colorized_echo red "Database backup file not found in backup archive." + rm -rf "$temp_restore_dir" + exit 1 + fi + + if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then + if [ -z "$container_name" ]; then + colorized_echo red "Error: MySQL/MariaDB container not found. Is the container running?" + echo "MySQL/MariaDB container not found. Container name: ${container_name:-empty}" >>"$log_file" + rm -rf "$temp_restore_dir" + exit 1 + else + local verified_container=$(verify_and_start_container "$container_name" "$db_type") + if [ -z "$verified_container" ]; then + colorized_echo red "Failed to start database container. Please start it manually." + rm -rf "$temp_restore_dir" + exit 1 + fi + container_name="$verified_container" + + # Check if this is actually a MariaDB container + local is_mariadb=false + local mysql_cmd="mysql" + local db_type_name="MySQL" + if docker exec "$container_name" mariadb --version >/dev/null 2>&1; then + is_mariadb=true + mysql_cmd="mariadb" + db_type_name="MariaDB" + fi + + colorized_echo blue "Restoring $db_type_name database from container: $container_name" + + local restore_success=false + local backup_restore_user="${db_user:-${DB_USER:-}}" + local backup_restore_password="${db_password:-${DB_PASSWORD:-}}" + local app_db_target="${db_name:-${current_db_name:-}}" + + # Try root password from backup .env first + if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then + colorized_echo blue "Trying root user from backup .env..." + if docker exec -i "$container_name" "$mysql_cmd" -u root -p"$MYSQL_ROOT_PASSWORD" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then + restore_success=true + colorized_echo green "$db_type_name database restored successfully." + else + colorized_echo yellow "Root restore failed with backup .env credentials, trying fallback..." + echo "$db_type_name restore failed with backup MYSQL_ROOT_PASSWORD" >>"$log_file" + fi + fi + + # If root password changed after backup, try current installation value + if [ "$restore_success" = false ] && [ -n "$current_mysql_root_password" ] && [ "$current_mysql_root_password" != "${MYSQL_ROOT_PASSWORD:-}" ]; then + colorized_echo blue "Trying root user from current installation .env..." + if docker exec -i "$container_name" "$mysql_cmd" -u root -p"$current_mysql_root_password" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then + restore_success=true + colorized_echo green "$db_type_name database restored successfully." + else + colorized_echo yellow "Root restore failed with current .env credentials, trying app user fallback..." + echo "$db_type_name restore failed with current MYSQL_ROOT_PASSWORD" >>"$log_file" + fi + fi + + # Try app user from backup SQL URL/.env + if [ "$restore_success" = false ] && [ -n "$backup_restore_user" ] && [ -n "$backup_restore_password" ]; then + colorized_echo blue "Trying app user '$backup_restore_user' from backup credentials..." + if [ -n "$app_db_target" ]; then + if docker exec -i "$container_name" "$mysql_cmd" -u "$backup_restore_user" -p"$backup_restore_password" "$app_db_target" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then + restore_success=true + colorized_echo green "$db_type_name database restored successfully." + fi + fi + if [ "$restore_success" = false ] && docker exec -i "$container_name" "$mysql_cmd" -u "$backup_restore_user" -p"$backup_restore_password" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then + restore_success=true + colorized_echo green "$db_type_name database restored successfully." + elif [ "$restore_success" = false ]; then + colorized_echo yellow "App user restore failed with backup credentials, trying current installation credentials..." + echo "$db_type_name restore failed with backup app credentials" >>"$log_file" + fi + fi + + # Final fallback: current installation app credentials + if [ "$restore_success" = false ] && [ -n "$current_db_user" ] && [ -n "$current_db_password" ] && { [ "$current_db_user" != "$backup_restore_user" ] || [ "$current_db_password" != "$backup_restore_password" ]; }; then + colorized_echo blue "Trying app user '$current_db_user' from current installation .env..." + if [ -n "$app_db_target" ]; then + if docker exec -i "$container_name" "$mysql_cmd" -u "$current_db_user" -p"$current_db_password" "$app_db_target" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then + restore_success=true + colorized_echo green "$db_type_name database restored successfully." + fi + fi + if [ "$restore_success" = false ] && docker exec -i "$container_name" "$mysql_cmd" -u "$current_db_user" -p"$current_db_password" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then + restore_success=true + colorized_echo green "$db_type_name database restored successfully." + elif [ "$restore_success" = false ]; then + echo "$db_type_name restore failed with current app credentials" >>"$log_file" + fi + fi + + if [ "$restore_success" = false ]; then + colorized_echo red "Failed to restore $db_type_name database with all available credentials." + colorized_echo yellow "Check log file for details: $log_file" + rm -rf "$temp_restore_dir" + exit 1 + fi + fi + else + colorized_echo red "Remote $db_type restore not supported yet." + rm -rf "$temp_restore_dir" + exit 1 + fi + ;; + + postgresql|timescaledb) + if [ ! -f "$temp_restore_dir/db_backup.sql" ]; then + colorized_echo red "Database backup file not found in backup archive." + rm -rf "$temp_restore_dir" + exit 1 + fi + + # Verify backup file is not empty and is readable + if [ ! -s "$temp_restore_dir/db_backup.sql" ]; then + colorized_echo red "Database backup file is empty or unreadable." + rm -rf "$temp_restore_dir" + exit 1 + fi + + local backup_size=$(du -h "$temp_restore_dir/db_backup.sql" | cut -f1) + colorized_echo blue "Backup file size: $backup_size" + + if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]] && [ -n "$container_name" ]; then + local verified_container=$(verify_and_start_container "$container_name" "$db_type") + if [ -z "$verified_container" ]; then + colorized_echo red "Failed to start database container. Please start it manually." + rm -rf "$temp_restore_dir" + exit 1 + fi + container_name="$verified_container" + + colorized_echo blue "Restoring $db_type database from container: $container_name" + + # Prepare restore credentials + local restore_user="${db_user:-${DB_USER:-postgres}}" + local restore_password="${db_password:-${DB_PASSWORD:-}}" + + if [ -z "$restore_password" ]; then + colorized_echo red "No database password found for restore." + rm -rf "$temp_restore_dir" + exit 1 + fi + + export PGPASSWORD="$restore_password" + local restore_success=false + + if [ "$db_type" = "timescaledb" ]; then + # TimescaleDB requires special restore procedure to handle version mismatches. + # A plain psql restore fails when the backup was taken with a different + # TimescaleDB version because DROP EXTENSION / CREATE EXTENSION cycles + # break when the shared library is already loaded with the new version. + # The fix: drop & recreate the database, then use the official + # timescaledb_pre_restore() / timescaledb_post_restore() wrapper. + # See: https://docs.timescale.com/self-hosted/latest/backup-and-restore/ + colorized_echo blue "Using TimescaleDB-safe restore procedure..." + + # Use target installation's identity when available, falling back to backup values. + # This ensures cross-server restores work correctly when the local DB user/name + # differs from the backup source. + local target_db_name="${current_db_name:-$db_name}" + local target_db_owner="${current_db_user:-$restore_user}" + + # Drop and recreate the target database for a clean slate + colorized_echo blue "Dropping and recreating database '$target_db_name'..." + docker exec "$container_name" psql -U postgres -d postgres \ + -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$target_db_name' AND pid <> pg_backend_pid();" \ + >>"$log_file" 2>&1 + docker exec "$container_name" psql -U postgres -d postgres \ + -c "DROP DATABASE IF EXISTS \"$target_db_name\";" >>"$log_file" 2>&1 + docker exec "$container_name" psql -U postgres -d postgres \ + -c "CREATE DATABASE \"$target_db_name\" OWNER \"$target_db_owner\";" >>"$log_file" 2>&1 + + # Create the timescaledb extension in the fresh database + docker exec "$container_name" psql -U postgres -d "$target_db_name" \ + -c "CREATE EXTENSION IF NOT EXISTS timescaledb;" >>"$log_file" 2>&1 + + # Call pre_restore to put TimescaleDB into restore mode + colorized_echo blue "Calling timescaledb_pre_restore()..." + docker exec "$container_name" psql -U postgres -d "$target_db_name" \ + -c "SELECT timescaledb_pre_restore();" >>"$log_file" 2>&1 + + # Filter out extension DROP/CREATE statements from the dump. + # pg_dump --clean --if-exists generates DROP EXTENSION / CREATE EXTENSION + # lines that would undo the pre_restore() setup above. + colorized_echo blue "Preparing dump (filtering extension statements)..." + grep -v -E '^\s*(DROP|CREATE)\s+EXTENSION\s+(IF\s+(EXISTS|NOT\s+EXISTS)\s+)?timescaledb\b' \ + "$temp_restore_dir/db_backup.sql" > "$temp_restore_dir/db_backup_filtered.sql" 2>>"$log_file" + + # Restore the filtered dump with ON_ERROR_STOP so psql exits non-zero on SQL errors + colorized_echo blue "Restoring database dump..." + if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U "$restore_user" -d "$target_db_name" < "$temp_restore_dir/db_backup_filtered.sql" 2>>"$log_file"; then + restore_success=true + else + # Fallback: try with postgres superuser + colorized_echo yellow "Trying with postgres superuser..." + if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U postgres -d "$target_db_name" < "$temp_restore_dir/db_backup_filtered.sql" 2>>"$log_file"; then + restore_success=true + fi + fi + + # Clean up filtered dump + rm -f "$temp_restore_dir/db_backup_filtered.sql" + + # Call post_restore regardless of outcome to leave DB in a usable state + colorized_echo blue "Calling timescaledb_post_restore()..." + docker exec "$container_name" psql -U postgres -d "$target_db_name" \ + -c "SELECT timescaledb_post_restore();" >>"$log_file" 2>&1 + + if [ "$restore_success" = true ]; then + colorized_echo green "TimescaleDB database restored successfully." + fi + else + # Plain PostgreSQL restore with ON_ERROR_STOP so psql exits non-zero on SQL errors + colorized_echo blue "Attempting restore using app user '$restore_user' to database '$db_name'..." + if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U "$restore_user" -d "$db_name" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo green "$db_type database restored successfully." + restore_success=true + else + # If that fails, try using postgres superuser + colorized_echo yellow "Trying with postgres superuser..." + if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U postgres -d "$db_name" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo green "$db_type database restored successfully." + restore_success=true + else + # Try restoring to postgres database (for pg_dumpall backups) + if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U postgres -d postgres < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo green "$db_type database restored successfully." + restore_success=true + fi + fi + fi + fi + + unset PGPASSWORD + + if [ "$restore_success" = false ]; then + colorized_echo red "Failed to restore $db_type database." + colorized_echo yellow "Check log file for details: $log_file" + rm -rf "$temp_restore_dir" + exit 1 + fi + else + colorized_echo red "Remote $db_type restore not supported yet." + rm -rf "$temp_restore_dir" + exit 1 + fi + ;; + *) + colorized_echo red "Unsupported database type: $db_type" + rm -rf "$temp_restore_dir" + exit 1 + ;; + esac + + # Restore data directory if included in backup + colorized_echo blue "Restoring data directory..." + local extracted_data_dir="$temp_restore_dir/pasarguard_data" + if [ -d "$extracted_data_dir" ]; then + if ! command -v rsync >/dev/null 2>&1; then + detect_os + install_package rsync + fi + mkdir -p "$DATA_DIR" + if ! rsync -a "$extracted_data_dir/" "$DATA_DIR/" 2>>"$log_file"; then + colorized_echo red "Failed to restore data directory." + echo "Failed to restore data directory from $extracted_data_dir to $DATA_DIR" >>"$log_file" + rm -rf "$temp_restore_dir" + exit 1 + fi + colorized_echo green "Data directory restored to $DATA_DIR." + else + colorized_echo yellow "No pasarguard_data directory found in backup. Skipping data restore." + fi + + # Restore configuration files if needed + colorized_echo blue "Restoring configuration files..." + if [ -f "$temp_restore_dir/.env" ]; then + cp "$temp_restore_dir/.env" "$APP_DIR/.env.backup.$(date +%Y%m%d%H%M%S)" 2>>"$log_file" + cp "$temp_restore_dir/.env" "$APP_DIR/.env" 2>>"$log_file" + colorized_echo green "Environment file restored." + local preserve_db_credentials=false + if [[ "$db_type" != "sqlite" ]]; then + if [ -n "$current_db_user" ] && [ -n "${DB_USER:-}" ] && [ "$current_db_user" != "$DB_USER" ]; then + preserve_db_credentials=true + elif [ -n "$current_db_name" ] && [ -n "${DB_NAME:-}" ] && [ "$current_db_name" != "$DB_NAME" ]; then + preserve_db_credentials=true + elif [ -n "$current_db_password" ] && [ -n "${DB_PASSWORD:-}" ] && [ "$current_db_password" != "$DB_PASSWORD" ]; then + preserve_db_credentials=true + fi + fi + if [ "$preserve_db_credentials" = true ]; then + colorized_echo yellow "Database credentials in backup differ from current installation; preserving current database credentials." + if [ -n "$current_db_user" ]; then + replace_or_append_env_var "DB_USER" "$current_db_user" false "$ENV_FILE" + fi + if [ -n "$current_db_name" ]; then + replace_or_append_env_var "DB_NAME" "$current_db_name" false "$ENV_FILE" + fi + if [ -n "$current_db_password" ]; then + replace_or_append_env_var "DB_PASSWORD" "$current_db_password" false "$ENV_FILE" + fi + if [ -n "$current_sqlalchemy_url" ]; then + replace_or_append_env_var "SQLALCHEMY_DATABASE_URL" "$current_sqlalchemy_url" true "$ENV_FILE" + fi + fi + fi + + if [ -f "$temp_restore_dir/docker-compose.yml" ]; then + cp "$temp_restore_dir/docker-compose.yml" "$APP_DIR/docker-compose.yml.backup.$(date +%Y%m%d%H%M%S)" 2>>"$log_file" + cp "$temp_restore_dir/docker-compose.yml" "$APP_DIR/docker-compose.yml" 2>>"$log_file" + colorized_echo green "Docker Compose file restored." + fi + + # Clean up + rm -rf "$temp_restore_dir" + + # Restart pasarguard services + colorized_echo blue "Restarting pasarguard services..." + if [[ "$db_type" == "sqlite" ]]; then + # For SQLite, restart all services + up_pasarguard + else + # For containerized databases, restart only application services + start_pasarguard_app_services + fi + + colorized_echo green "Restore completed successfully!" + colorized_echo green "PasarGuard services have been restarted." +} + diff --git a/pasarguard.sh b/pasarguard.sh index c5ab912..d3f1dd6 100755 --- a/pasarguard.sh +++ b/pasarguard.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env bash set -e SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" @@ -7,7 +7,7 @@ if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then SHARED_LIB_DIR="/usr/local/lib/pasarguard-scripts/lib" fi -for shared_lib in common.sh system.sh docker.sh github.sh env.sh; do +for shared_lib in common.sh system.sh docker.sh github.sh env.sh pasarguard-backup.sh pasarguard-restore.sh; do if [ ! -f "$SHARED_LIB_DIR/$shared_lib" ]; then printf 'Missing shared library: %s\n' "$SHARED_LIB_DIR/$shared_lib" >&2 exit 1 @@ -24,6 +24,10 @@ source "$SHARED_LIB_DIR/docker.sh" source "$SHARED_LIB_DIR/github.sh" # shellcheck source=lib/env.sh source "$SHARED_LIB_DIR/env.sh" +# shellcheck source=lib/pasarguard-backup.sh +source "$SHARED_LIB_DIR/pasarguard-backup.sh" +# shellcheck source=lib/pasarguard-restore.sh +source "$SHARED_LIB_DIR/pasarguard-restore.sh" # Handle @ symbol if used in installation (skip it) if [ "$1" == "@" ]; then @@ -841,7 +845,7 @@ verify_and_start_container() { install_pasarguard_script() { FETCH_REPO="PasarGuard/scripts" colorized_echo blue "Installing pasarguard script" - install_shared_libs_from_repo "$FETCH_REPO" + install_shared_libs_from_repo "$FETCH_REPO" common.sh system.sh docker.sh github.sh env.sh pasarguard-backup.sh pasarguard-restore.sh github_install_script_from_repo "$FETCH_REPO" "pasarguard.sh" "pasarguard" colorized_echo green "pasarguard script installed successfully" } @@ -854,2202 +858,6 @@ is_pasarguard_installed() { fi } -send_backup_to_telegram() { - if [ -f "$ENV_FILE" ]; then - while IFS='=' read -r key value; do - if [[ -z "$key" || "$key" =~ ^# ]]; then - continue - fi - key=$(echo "$key" | xargs) - value=$(echo "$value" | xargs) - value=$(echo "$value" | sed -E 's/^["'"'"'](.*)["'"'"']$/\1/') - if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then - export "$key"="$value" - else - colorized_echo yellow "Skipping invalid line in .env: $key=$value" - fi - done <"$ENV_FILE" - else - colorized_echo red "Environment file (.env) not found." - exit 1 - fi - - if [ "$BACKUP_SERVICE_ENABLED" != "true" ]; then - colorized_echo yellow "Backup service is not enabled. Skipping Telegram upload." - return - fi - - # Validate Telegram configuration - if [ -z "$BACKUP_TELEGRAM_BOT_KEY" ]; then - colorized_echo red "Error: BACKUP_TELEGRAM_BOT_KEY is not set in .env file" - return 1 - fi - - if [ -z "$BACKUP_TELEGRAM_CHAT_ID" ]; then - colorized_echo red "Error: BACKUP_TELEGRAM_CHAT_ID is not set in .env file" - return 1 - fi - - local proxy_url="" - local curl_proxy_args=() - if proxy_url=$(get_backup_proxy_url); then - curl_proxy_args=(--proxy "$proxy_url") - fi - - local server_ip="$(curl "${curl_proxy_args[@]}" -4 -s --max-time 5 ifconfig.me 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')" - if [ -z "$server_ip" ]; then - server_ip=$(hostname -I 2>/dev/null | awk '{print $1}') - fi - if [ -z "$server_ip" ]; then - server_ip="Unknown IP" - fi - local backup_dir="$APP_DIR/backup" - local latest_backup=$(ls -t "$backup_dir" 2>/dev/null | head -n 1) - - if [ -z "$latest_backup" ]; then - colorized_echo red "No backups found to send." - return 1 - fi - - local backup_paths=() - local cleanup_dir="" - - local telegram_split_bytes=$((49 * 1000 * 1000)) - - if [[ "$latest_backup" =~ \.part[0-9]{2}\.zip$ ]]; then - local base="${latest_backup%%.part*}" - while IFS= read -r file; do - [ -n "$file" ] && backup_paths+=("$file") - done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base}.part*.zip" | sort) - if [ ${#backup_paths[@]} -eq 0 ]; then - colorized_echo red "Incomplete backup parts for $base" - return 1 - fi - elif [[ "$latest_backup" =~ \.z[0-9]{2}$ ]]; then - local base="${latest_backup%.z??}" - while IFS= read -r file; do - [ -n "$file" ] && backup_paths+=("$file") - done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base}.z[0-9][0-9]" | sort) - if [ -f "$backup_dir/${base}.zip" ]; then - backup_paths+=("$backup_dir/${base}.zip") - else - colorized_echo red "Missing final .zip file for split archive $base" - return 1 - fi - elif [[ "$latest_backup" =~ \.zip$ ]]; then - local base="${latest_backup%.zip}" - local split_files=() - while IFS= read -r file; do - [ -n "$file" ] && split_files+=("$file") - done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base}.z[0-9][0-9]" | sort) - if [ ${#split_files[@]} -gt 0 ]; then - backup_paths=("${split_files[@]}") - fi - backup_paths+=("$backup_dir/$latest_backup") - elif [[ "$latest_backup" =~ \.tar\.gz$ ]]; then - cleanup_dir="/tmp/pasarguard_backup_split" - rm -rf "$cleanup_dir" - mkdir -p "$cleanup_dir" - local legacy_backup="$backup_dir/$latest_backup" - local backup_size=$(du -m "$legacy_backup" | cut -f1) - if [ "$backup_size" -gt 49 ]; then - colorized_echo yellow "Legacy backup is larger than 49MB. Splitting before upload..." - split -b "$telegram_split_bytes" "$legacy_backup" "$cleanup_dir/${latest_backup}_part_" - else - cp "$legacy_backup" "$cleanup_dir/$latest_backup" - fi - while IFS= read -r file; do - [ -n "$file" ] && backup_paths+=("$file") - done < <(find "$cleanup_dir" -maxdepth 1 -type f -print | sort) - if [ ${#backup_paths[@]} -eq 0 ]; then - colorized_echo red "Failed to prepare legacy backup for upload." - rm -rf "$cleanup_dir" - return 1 - fi - else - colorized_echo red "Unsupported backup format: $latest_backup" - return 1 - fi - - local backup_time=$(date "+%Y-%m-%d %H:%M:%S %Z") - - for part in "${backup_paths[@]}"; do - local part_name=$(basename "$part") - local custom_filename="$part_name" - - local escaped_server_ip=$(printf '%s' "$server_ip" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g') - local escaped_filename=$(printf '%s' "$custom_filename" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g') - local escaped_time=$(printf '%s' "$backup_time" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g') - local caption="📦 *Backup Information*\n🌐 *Server IP*: \`$escaped_server_ip\`\n📁 *Backup File*: \`$escaped_filename\`\n⏰ *Backup Time*: \`$escaped_time\`" - - local response=$(curl "${curl_proxy_args[@]}" -s -w "\n%{http_code}" -F chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ - -F document=@"$part;filename=$custom_filename" \ - -F caption="$(printf '%b' "$caption")" \ - -F parse_mode="MarkdownV2" \ - "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument" 2>&1) - - local http_code=$(echo "$response" | tail -n1) - local response_body=$(echo "$response" | sed '$d') - - if [ "$http_code" == "200" ]; then - # Check if response contains "ok":true - if echo "$response_body" | grep -q '"ok":true'; then - colorized_echo green "Backup part $custom_filename successfully sent to Telegram." - else - # Extract error message from Telegram response - local error_msg=$(echo "$response_body" | grep -o '"description":"[^"]*"' | cut -d'"' -f4 || echo "Unknown error") - colorized_echo red "Failed to send backup part $custom_filename to Telegram: $error_msg" - echo "Telegram API status: $http_code" >&2 - echo "Telegram API Response: $response_body" >&2 - fi - else - local error_msg=$(echo "$response_body" | grep -o '"description":"[^"]*"' | cut -d'"' -f4 || echo "HTTP $http_code") - colorized_echo red "Failed to send backup part $custom_filename to Telegram: $error_msg" - echo "Telegram API Response: $response_body" >&2 - fi - done - - if [ ${#uploaded_files[@]} -gt 0 ]; then - local files_list="" - for file in "${uploaded_files[@]}"; do - files_list+="- $file"$'\n' - done - files_list="${files_list%$'\n'}" - - local info_message=$'📦 Backup Upload Summary\n' - info_message+=$'──────────────────────\n' - info_message+="🌐 Server IP: $server_ip"$'\n' - info_message+="⏰ Time: $backup_time"$'\n' - info_message+=$'\n✅ Files Uploaded:\n' - info_message+="$files_list"$'\n' - info_message+=$'\n📂 Extraction Guide:\n' - info_message+=$'🪟 Windows: Install and use 7-Zip. Place the .zip and every .zXX part together, then start extraction from the .zip file.\n' - info_message+=$'🐧 Linux: Run unzip (e.g., unzip backup_xxx.zip) with all .zXX parts in the same directory.\n' - info_message+=$'🍎 macOS: Use Archive Utility or run unzip backup_xxx.zip from Terminal with the .zXX parts beside the .zip file.\n' - info_message+=$'⚠️ Always download the .zip and every .zXX part before extracting.' - - curl "${curl_proxy_args[@]}" -s -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \ - -d chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ - -d text="$info_message" >/dev/null 2>&1 || true - fi - - if [ -n "$cleanup_dir" ]; then - rm -rf "$cleanup_dir" - fi -} - -send_backup_error_to_telegram() { - local error_messages=$1 - local log_file=$2 - local proxy_url="" - local curl_proxy_args=() - if proxy_url=$(get_backup_proxy_url); then - curl_proxy_args=(--proxy "$proxy_url") - fi - local server_ip="$(curl "${curl_proxy_args[@]}" -4 -s --max-time 5 ifconfig.me 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')" - if [ -z "$server_ip" ]; then - server_ip=$(hostname -I 2>/dev/null | awk '{print $1}') - fi - if [ -z "$server_ip" ]; then - server_ip="Unknown IP" - fi - local error_time=$(date "+%Y-%m-%d %H:%M:%S %Z") - local message="⚠️ Backup Error Notification -🌐 Server IP: $server_ip -❌ Errors: $error_messages -⏰ Time: $error_time" - - local max_length=1000 - if [ ${#message} -gt $max_length ]; then - message="${message:0:$((max_length - 25))}... -[Message truncated]" - fi - - curl "${curl_proxy_args[@]}" -s -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \ - -d chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ - -d text="$message" >/dev/null 2>&1 && - colorized_echo green "Backup error notification sent to Telegram." || - colorized_echo red "Failed to send error notification to Telegram." - - if [ -f "$log_file" ]; then - - response=$(curl "${curl_proxy_args[@]}" -s -w "%{http_code}" -o /tmp/tg_response.json \ - -F chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ - -F document=@"$log_file;filename=backup_error.log" \ - -F caption="📜 Backup Error Log - $error_time" \ - "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument") - - http_code="${response:(-3)}" - if [ "$http_code" -eq 200 ]; then - colorized_echo green "Backup error log sent to Telegram." - else - colorized_echo red "Failed to send backup error log to Telegram. HTTP code: $http_code" - cat /tmp/tg_response.json - fi - else - colorized_echo red "Log file not found: $log_file" - fi -} - -backup_service() { - local telegram_bot_key="" - local telegram_chat_id="" - local cron_schedule="" - local interval_hours="" - local backup_proxy_enabled="false" - local backup_proxy_url="" - - colorized_echo blue "=====================================" - colorized_echo blue " Welcome to Backup Service " - colorized_echo blue "=====================================" - - if grep -q "BACKUP_SERVICE_ENABLED=true" "$ENV_FILE"; then - while true; do - telegram_bot_key=$(awk -F'=' '/^BACKUP_TELEGRAM_BOT_KEY=/ {print $2}' "$ENV_FILE") - telegram_chat_id=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_ID=/ {print $2}' "$ENV_FILE") - cron_schedule=$(awk -F'=' '/^BACKUP_CRON_SCHEDULE=/ {print $2}' "$ENV_FILE" | tr -d '"') - backup_proxy_enabled=$(awk -F'=' '/^BACKUP_PROXY_ENABLED=/ {print $2}' "$ENV_FILE") - backup_proxy_url=$(awk -F'=' '/^BACKUP_PROXY_URL=/ {print substr($0, index($0,"=")+1); exit}' "$ENV_FILE") - backup_proxy_url=$(echo "$backup_proxy_url" | sed -e 's/^"//' -e 's/"$//') - [ -z "$backup_proxy_enabled" ] && backup_proxy_enabled="false" - - if [[ "$cron_schedule" == "0 0 * * *" ]]; then - interval_hours=24 - else - interval_hours=$(echo "$cron_schedule" | grep -oP '(?<=\*/)[0-9]+') - fi - - colorized_echo green "=====================================" - colorized_echo green "Current Backup Configuration:" - colorized_echo cyan "Telegram Bot API Key: $telegram_bot_key" - colorized_echo cyan "Telegram Chat ID: $telegram_chat_id" - colorized_echo cyan "Backup Interval: Every $interval_hours hour(s)" - if [[ "$backup_proxy_enabled" == "true" && -n "$backup_proxy_url" ]]; then - colorized_echo cyan "Proxy: Enabled ($backup_proxy_url)" - else - colorized_echo cyan "Proxy: Disabled" - fi - colorized_echo green "=====================================" - echo "Choose an option:" - echo "1. Check Backup Service" - echo "2. Edit Backup Service" - echo "3. Reconfigure Backup Service" - echo "4. Remove Backup Service" - echo "5. Request Instant Backup" - echo "6. Exit" - read -p "Enter your choice (1-6): " user_choice - - case $user_choice in - 1) - view_backup_service - echo "" - ;; - 2) - edit_backup_service - echo "" - ;; - 3) - colorized_echo yellow "Starting reconfiguration..." - remove_backup_service - break - ;; - 4) - colorized_echo yellow "Removing Backup Service..." - remove_backup_service - return - ;; - 5) - colorized_echo yellow "Starting instant backup..." - backup_command - colorized_echo green "Instant backup completed." - echo "" - ;; - 6) - colorized_echo yellow "Exiting..." - return - ;; - *) - colorized_echo red "Invalid choice. Please try again." - echo "" - ;; - esac - done - else - colorized_echo yellow "No backup service is currently configured." - fi - - while true; do - printf "Enter your Telegram bot API key: " - read telegram_bot_key - if [[ -n "$telegram_bot_key" ]]; then - break - else - colorized_echo red "API key cannot be empty. Please try again." - fi - done - - while true; do - printf "Enter your Telegram chat ID: " - read telegram_chat_id - if [[ -n "$telegram_chat_id" ]]; then - break - else - colorized_echo red "Chat ID cannot be empty. Please try again." - fi - done - - while true; do - printf "Set up the backup interval in hours (1-24):\n" - read interval_hours - - if ! [[ "$interval_hours" =~ ^[0-9]+$ ]]; then - colorized_echo red "Invalid input. Please enter a valid number." - continue - fi - - if [[ "$interval_hours" -eq 24 ]]; then - cron_schedule="0 0 * * *" - colorized_echo green "Setting backup to run daily at midnight." - break - fi - - if [[ "$interval_hours" -ge 1 && "$interval_hours" -le 23 ]]; then - cron_schedule="0 */$interval_hours * * *" - colorized_echo green "Setting backup to run every $interval_hours hour(s)." - break - else - colorized_echo red "Invalid input. Please enter a number between 1-24." - fi - done - - while true; do - read -p "Do you need to use an HTTP/SOCKS proxy for Telegram backups? (y/N): " proxy_choice - case "$proxy_choice" in - [Yy]*) - backup_proxy_enabled="true" - break - ;; - [Nn]*|"") - backup_proxy_enabled="false" - break - ;; - *) - colorized_echo red "Invalid choice. Please enter y or n." - ;; - esac - done - - if [ "$backup_proxy_enabled" = "true" ]; then - while true; do - read -p "Enter proxy URL (e.g. http://127.0.0.1:8080 or socks5://127.0.0.1:1080): " backup_proxy_url - backup_proxy_url=$(echo "$backup_proxy_url" | xargs) - if [ -z "$backup_proxy_url" ]; then - colorized_echo red "Proxy URL cannot be empty." - continue - fi - if is_valid_proxy_url "$backup_proxy_url"; then - break - else - colorized_echo red "Invalid proxy URL. Supported prefixes: http://, https://, socks5://, socks5h://, socks4://." - fi - done - else - backup_proxy_url="" - fi - - sed -i '/^BACKUP_SERVICE_ENABLED/d' "$ENV_FILE" - sed -i '/^BACKUP_TELEGRAM_BOT_KEY/d' "$ENV_FILE" - sed -i '/^BACKUP_TELEGRAM_CHAT_ID/d' "$ENV_FILE" - sed -i '/^BACKUP_CRON_SCHEDULE/d' "$ENV_FILE" - sed -i '/^BACKUP_PROXY_ENABLED/d' "$ENV_FILE" - sed -i '/^BACKUP_PROXY_URL/d' "$ENV_FILE" - - { - echo "" - echo "# Backup service configuration" - echo "BACKUP_SERVICE_ENABLED=true" - echo "BACKUP_TELEGRAM_BOT_KEY=$telegram_bot_key" - echo "BACKUP_TELEGRAM_CHAT_ID=$telegram_chat_id" - echo "BACKUP_CRON_SCHEDULE=\"$cron_schedule\"" - echo "BACKUP_PROXY_ENABLED=$backup_proxy_enabled" - echo "BACKUP_PROXY_URL=\"$backup_proxy_url\"" - } >>"$ENV_FILE" - - colorized_echo green "Backup service configuration saved in $ENV_FILE." - - # Use full path to the script for cron job - local script_path="/usr/local/bin/$APP_NAME" - if [ ! -f "$script_path" ]; then - script_path=$(which "$APP_NAME" 2>/dev/null || echo "/usr/local/bin/$APP_NAME") - fi - # Set PATH for cron to ensure docker and other tools are found - local backup_command="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin bash $script_path backup" - add_cron_job "$cron_schedule" "$backup_command" - - colorized_echo green "Backup service successfully configured." - - # Run initial backup - colorized_echo blue "Running initial backup..." - backup_command - if [ $? -eq 0 ]; then - colorized_echo green "Initial backup completed successfully." - else - colorized_echo yellow "Initial backup completed with warnings. Check logs if needed." - fi - if [[ "$interval_hours" -eq 24 ]]; then - colorized_echo cyan "Backups will be sent to Telegram daily (every 24 hours at midnight)." - else - colorized_echo cyan "Backups will be sent to Telegram every $interval_hours hour(s)." - fi - colorized_echo green "=====================================" -} - -add_cron_job() { - local schedule="$1" - local command="$2" - local temp_cron=$(mktemp) - - crontab -l 2>/dev/null >"$temp_cron" || true - grep -v "$command" "$temp_cron" >"${temp_cron}.tmp" && mv "${temp_cron}.tmp" "$temp_cron" - echo "$schedule $command # pasarguard-backup-service" >>"$temp_cron" - - if crontab "$temp_cron"; then - colorized_echo green "Cron job successfully added." - else - colorized_echo red "Failed to add cron job. Please check manually." - fi - rm -f "$temp_cron" -} - -view_backup_service() { - if ! grep -q "BACKUP_SERVICE_ENABLED=true" "$ENV_FILE"; then - colorized_echo red "Backup service is not configured." - return 1 - fi - - local telegram_bot_key=$(awk -F'=' '/^BACKUP_TELEGRAM_BOT_KEY=/ {print $2}' "$ENV_FILE") - local telegram_chat_id=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_ID=/ {print $2}' "$ENV_FILE") - local cron_schedule=$(awk -F'=' '/^BACKUP_CRON_SCHEDULE=/ {print $2}' "$ENV_FILE" | tr -d '"') - local backup_proxy_enabled=$(awk -F'=' '/^BACKUP_PROXY_ENABLED=/ {print $2}' "$ENV_FILE") - local backup_proxy_url=$(awk -F'=' '/^BACKUP_PROXY_URL=/ {print substr($0, index($0,"=")+1); exit}' "$ENV_FILE") - backup_proxy_url=$(echo "$backup_proxy_url" | sed -e 's/^"//' -e 's/"$//') - [ -z "$backup_proxy_enabled" ] && backup_proxy_enabled="false" - local interval_hours="" - - if [[ "$cron_schedule" == "0 0 * * *" ]]; then - interval_hours=24 - else - interval_hours=$(echo "$cron_schedule" | grep -oP '(?<=\*/)[0-9]+') - fi - - colorized_echo blue "=====================================" - colorized_echo blue " Backup Service Details " - colorized_echo blue "=====================================" - colorized_echo green "Status: Enabled" - colorized_echo cyan "Telegram Bot API Key: $telegram_bot_key" - colorized_echo cyan "Telegram Chat ID: $telegram_chat_id" - colorized_echo cyan "Cron Schedule: $cron_schedule" - if [[ "$interval_hours" -eq 24 ]]; then - colorized_echo cyan "Backup Interval: Daily at midnight (every 24 hours)" - else - colorized_echo cyan "Backup Interval: Every $interval_hours hour(s)" - fi - if [[ "$backup_proxy_enabled" == "true" && -n "$backup_proxy_url" ]]; then - colorized_echo cyan "Proxy: Enabled ($backup_proxy_url)" - else - colorized_echo cyan "Proxy: Disabled" - fi - colorized_echo blue "=====================================" - echo "" - read -p "Press Enter to continue..." -} - -edit_backup_service() { - if ! grep -q "BACKUP_SERVICE_ENABLED=true" "$ENV_FILE"; then - colorized_echo red "Backup service is not configured." - return 1 - fi - - local telegram_bot_key=$(awk -F'=' '/^BACKUP_TELEGRAM_BOT_KEY=/ {print $2}' "$ENV_FILE") - local telegram_chat_id=$(awk -F'=' '/^BACKUP_TELEGRAM_CHAT_ID=/ {print $2}' "$ENV_FILE") - local cron_schedule=$(awk -F'=' '/^BACKUP_CRON_SCHEDULE=/ {print $2}' "$ENV_FILE" | tr -d '"') - local backup_proxy_enabled=$(awk -F'=' '/^BACKUP_PROXY_ENABLED=/ {print $2}' "$ENV_FILE") - local backup_proxy_url=$(awk -F'=' '/^BACKUP_PROXY_URL=/ {print substr($0, index($0,"=")+1); exit}' "$ENV_FILE") - backup_proxy_url=$(echo "$backup_proxy_url" | sed -e 's/^"//' -e 's/"$//') - [ -z "$backup_proxy_enabled" ] && backup_proxy_enabled="false" - local interval_hours="" - - if [[ "$cron_schedule" == "0 0 * * *" ]]; then - interval_hours=24 - else - interval_hours=$(echo "$cron_schedule" | grep -oP '(?<=\*/)[0-9]+') - fi - - colorized_echo blue "=====================================" - colorized_echo blue " Edit Backup Service " - colorized_echo blue "=====================================" - echo "Current configuration:" - local proxy_display="Disabled" - if [[ "$backup_proxy_enabled" == "true" && -n "$backup_proxy_url" ]]; then - proxy_display="Enabled ($backup_proxy_url)" - fi - colorized_echo cyan "1. Telegram Bot API Key: $telegram_bot_key" - colorized_echo cyan "2. Telegram Chat ID: $telegram_chat_id" - colorized_echo cyan "3. Backup Interval: Every $interval_hours hour(s)" - colorized_echo cyan "4. Proxy: $proxy_display" - colorized_echo yellow "5. Cancel" - echo "" - read -p "Which setting would you like to edit? (1-5): " edit_choice - - case $edit_choice in - 1) - while true; do - printf "Enter new Telegram bot API key [current: $telegram_bot_key]: " - read new_bot_key - if [[ -n "$new_bot_key" ]]; then - sed -i "s|^BACKUP_TELEGRAM_BOT_KEY=.*|BACKUP_TELEGRAM_BOT_KEY=$new_bot_key|" "$ENV_FILE" - colorized_echo green "Telegram Bot API Key updated successfully." - break - else - colorized_echo red "API key cannot be empty. Please try again." - fi - done - ;; - 2) - while true; do - printf "Enter new Telegram chat ID [current: $telegram_chat_id]: " - read new_chat_id - if [[ -n "$new_chat_id" ]]; then - sed -i "s|^BACKUP_TELEGRAM_CHAT_ID=.*|BACKUP_TELEGRAM_CHAT_ID=$new_chat_id|" "$ENV_FILE" - colorized_echo green "Telegram Chat ID updated successfully." - break - else - colorized_echo red "Chat ID cannot be empty. Please try again." - fi - done - ;; - 3) - while true; do - printf "Set new backup interval in hours (1-24) [current: $interval_hours]:\n" - read new_interval_hours - - if ! [[ "$new_interval_hours" =~ ^[0-9]+$ ]]; then - colorized_echo red "Invalid input. Please enter a valid number." - continue - fi - - local new_cron_schedule="" - if [[ "$new_interval_hours" -eq 24 ]]; then - new_cron_schedule="0 0 * * *" - colorized_echo green "Setting backup to run daily at midnight." - elif [[ "$new_interval_hours" -ge 1 && "$new_interval_hours" -le 23 ]]; then - new_cron_schedule="0 */$new_interval_hours * * *" - colorized_echo green "Setting backup to run every $new_interval_hours hour(s)." - else - colorized_echo red "Invalid input. Please enter a number between 1-24." - continue - fi - - sed -i "s|^BACKUP_CRON_SCHEDULE=.*|BACKUP_CRON_SCHEDULE=\"$new_cron_schedule\"|" "$ENV_FILE" - - # Use full path to the script for cron job - local script_path="/usr/local/bin/$APP_NAME" - if [ ! -f "$script_path" ]; then - script_path=$(which "$APP_NAME" 2>/dev/null || echo "/usr/local/bin/$APP_NAME") - fi - # Set PATH for cron to ensure docker and other tools are found - local backup_command="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin bash $script_path backup" - local temp_cron=$(mktemp) - crontab -l 2>/dev/null >"$temp_cron" || true - grep -v "# pasarguard-backup-service" "$temp_cron" >"${temp_cron}.tmp" && mv "${temp_cron}.tmp" "$temp_cron" - echo "$new_cron_schedule $backup_command # pasarguard-backup-service" >>"$temp_cron" - - if crontab "$temp_cron"; then - colorized_echo green "Backup interval and cron schedule updated successfully." - else - colorized_echo red "Failed to update cron job. Please check manually." - fi - rm -f "$temp_cron" - break - done - ;; - 4) - local new_proxy_enabled="$backup_proxy_enabled" - local new_proxy_url="$backup_proxy_url" - while true; do - read -p "Enable proxy for Telegram backups? (y/N) [current: $proxy_display]: " proxy_choice - case "$proxy_choice" in - [Yy]*) - new_proxy_enabled="true" - break - ;; - [Nn]*|"") - new_proxy_enabled="false" - break - ;; - *) - colorized_echo red "Invalid choice. Please enter y or n." - ;; - esac - done - - if [ "$new_proxy_enabled" = "true" ]; then - while true; do - read -p "Enter proxy URL (e.g. http://127.0.0.1:8080 or socks5://127.0.0.1:1080) [current: $backup_proxy_url]: " input_proxy_url - if [ -z "$input_proxy_url" ]; then - if [ -n "$backup_proxy_url" ]; then - input_proxy_url="$backup_proxy_url" - else - colorized_echo red "Proxy URL cannot be empty." - continue - fi - fi - input_proxy_url=$(echo "$input_proxy_url" | xargs) - if is_valid_proxy_url "$input_proxy_url"; then - new_proxy_url="$input_proxy_url" - break - else - colorized_echo red "Invalid proxy URL. Supported prefixes: http://, https://, socks5://, socks5h://, socks4://." - fi - done - else - new_proxy_url="" - fi - - replace_or_append_env_var "BACKUP_PROXY_ENABLED" "$new_proxy_enabled" - replace_or_append_env_var "BACKUP_PROXY_URL" "$new_proxy_url" true - colorized_echo green "Backup proxy configuration updated successfully." - ;; - 5) - colorized_echo yellow "Edit cancelled." - return - ;; - *) - colorized_echo red "Invalid choice." - return - ;; - esac - - colorized_echo green "Backup service configuration updated successfully." -} - -remove_backup_service() { - colorized_echo red "in process..." - - sed -i '/^# Backup service configuration/d' "$ENV_FILE" - sed -i '/BACKUP_SERVICE_ENABLED/d' "$ENV_FILE" - sed -i '/BACKUP_TELEGRAM_BOT_KEY/d' "$ENV_FILE" - sed -i '/BACKUP_TELEGRAM_CHAT_ID/d' "$ENV_FILE" - sed -i '/BACKUP_CRON_SCHEDULE/d' "$ENV_FILE" - sed -i '/BACKUP_PROXY_ENABLED/d' "$ENV_FILE" - sed -i '/BACKUP_PROXY_URL/d' "$ENV_FILE" - - local temp_cron=$(mktemp) - crontab -l 2>/dev/null >"$temp_cron" - - sed -i '/# pasarguard-backup-service/d' "$temp_cron" - - if crontab "$temp_cron"; then - colorized_echo green "Backup service task removed from crontab." - else - colorized_echo red "Failed to update crontab. Please check manually." - fi - - rm -f "$temp_cron" - - colorized_echo green "Backup service has been removed." -} - -restore_command() { - colorized_echo blue "Starting restore process..." - - # Check if pasarguard is installed - if ! is_pasarguard_installed; then - colorized_echo red "pasarguard's not installed!" - exit 1 - fi - - detect_compose - - if ! is_pasarguard_up; then - colorized_echo red "pasarguard is not up. Please start pasarguard first." - exit 1 - fi - - local current_db_user="" - local current_db_password="" - local current_db_name="" - local current_sqlalchemy_url="" - local current_mysql_root_password="" - - if [ -f "$ENV_FILE" ]; then - set +e - while IFS='=' read -r key value || [ -n "$key" ]; do - if [[ -z "$key" || "$key" =~ ^# ]]; then - continue - fi - key=$(echo "$key" | xargs 2>/dev/null || echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - value=$(echo "$value" | xargs 2>/dev/null || echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - value=$(echo "$value" | sed -E 's/^["'"'"'](.*)["'"'"']$/\1/' 2>/dev/null || echo "$value") - case "$key" in - MYSQL_ROOT_PASSWORD) - current_mysql_root_password="$value" - ;; - DB_USER) - current_db_user="$value" - ;; - DB_PASSWORD) - current_db_password="$value" - ;; - DB_NAME) - current_db_name="$value" - ;; - SQLALCHEMY_DATABASE_URL) - current_sqlalchemy_url="$value" - ;; - esac - done <"$ENV_FILE" - set -e - fi - - local backup_dir="$APP_DIR/backup" - local temp_restore_dir="/tmp/pasarguard_restore" - local log_file="/var/log/pasarguard_restore_error.log" - >"$log_file" - echo "Restore Log - $(date)" >>"$log_file" - - # Clean up temp directory - rm -rf "$temp_restore_dir" - mkdir -p "$temp_restore_dir" - - # Check if backup directory exists - if [ ! -d "$backup_dir" ]; then - colorized_echo red "Backup directory not found: $backup_dir" - exit 1 - fi - - # List available backup files (find all backup-related files in backup directory) - local backup_candidates=() - while IFS= read -r -d '' file; do - backup_candidates+=("$file") - done < <(find "$backup_dir" -maxdepth 1 \( -name "*backup*.gz" -o -name "*backup*.tar.gz" -o -name "*.tar.gz" -o -name "*backup*.zip" -o -name "*.zip" \) -type f -print0 2>/dev/null) - - if [ ${#backup_candidates[@]} -eq 0 ]; then - # Fallback: try to find any archive files - while IFS= read -r -d '' file; do - backup_candidates+=("$file") - done < <(find "$backup_dir" -maxdepth 1 \( -name "*.gz" -o -name "*.zip" \) -type f -print0 2>/dev/null) - fi - - local backup_files=() - for file in "${backup_candidates[@]}"; do - local filename=$(basename "$file") - if [[ "$filename" =~ \.part[0-9]{2}\.zip$ ]] && [[ ! "$filename" =~ \.part01\.zip$ ]]; then - continue - fi - if [[ "$filename" =~ \.z[0-9]{2}$ ]]; then - continue - fi - backup_files+=("$file") - done - - if [ ${#backup_files[@]} -eq 0 ]; then - colorized_echo red "No backup files found in $backup_dir" - colorized_echo yellow "Looking for files with extensions: .gz, .zip, .tar.gz or containing 'backup'" - exit 1 - fi - - colorized_echo blue "Available backup files:" - local i=1 - for file in "${backup_files[@]}"; do - if [ -f "$file" ]; then - local filename=$(basename "$file") - if [[ "$filename" =~ \.part[0-9]{2}\.zip$ ]]; then - local base_name="${filename%%.part*}" - local part_count=$(find "$backup_dir" -maxdepth 1 -type f -name "${base_name}.part*.zip" | wc -l | awk '{print $1}') - [ -z "$part_count" ] && part_count=0 - local total_size_bytes=0 - while IFS= read -r part_file; do - local part_size=$(stat -c%s "$part_file" 2>/dev/null || stat -f%z "$part_file" 2>/dev/null) - if [ -z "$part_size" ]; then - part_size=$(wc -c <"$part_file") - fi - total_size_bytes=$((total_size_bytes + part_size)) - done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base_name}.part*.zip") - local human_size="" - if command -v numfmt >/dev/null 2>&1; then - human_size=$(numfmt --to=iec --suffix=B "$total_size_bytes" 2>/dev/null || awk -v size="$total_size_bytes" 'BEGIN { printf "%.2f MB", size/1048576 }') - else - human_size=$(awk -v size="$total_size_bytes" 'BEGIN { printf "%.2f MB", size/1048576 }') - fi - local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S") - echo "$i. $filename (Parts: ${part_count:-1}, Total Size: $human_size, Date: $file_date)" - elif [[ "$filename" =~ \.zip$ ]]; then - local base_name="${filename%.zip}" - local zip_part_files=() - while IFS= read -r part_file; do - zip_part_files+=("$part_file") - done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base_name}.z[0-9][0-9]" | sort) - if [ ${#zip_part_files[@]} -gt 0 ]; then - local total_size_bytes=0 - for part_file in "${zip_part_files[@]}"; do - local part_size=$(stat -c%s "$part_file" 2>/dev/null || stat -f%z "$part_file" 2>/dev/null) - if [ -z "$part_size" ]; then - part_size=$(wc -c <"$part_file") - fi - total_size_bytes=$((total_size_bytes + part_size)) - done - local main_size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null) - if [ -z "$main_size" ]; then - main_size=$(wc -c <"$file") - fi - total_size_bytes=$((total_size_bytes + main_size)) - local part_display="" - if command -v numfmt >/dev/null 2>&1; then - part_display=$(numfmt --to=iec --suffix=B "$total_size_bytes" 2>/dev/null || awk -v size="$total_size_bytes" 'BEGIN { printf "%.2f MB", size/1048576 }') - else - part_display=$(awk -v size="$total_size_bytes" 'BEGIN { printf "%.2f MB", size/1048576 }') - fi - local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S") - local part_count=$(( ${#zip_part_files[@]} + 1 )) - echo "$i. $filename (Zip splits: $part_count parts, Total Size: $part_display, Date: $file_date)" - else - local file_size=$(du -h "$file" | cut -f1) - local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S") - echo "$i. $filename (Size: $file_size, Date: $file_date)" - fi - else - local file_size=$(du -h "$file" | cut -f1) - local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S") - echo "$i. $filename (Size: $file_size, Date: $file_date)" - fi - ((i++)) - fi - done - - local file_count=$((i-1)) - if [ "$file_count" -eq 0 ]; then - colorized_echo red "No valid backup files found." - exit 1 - fi - - # Select backup file - while true; do - printf "Select backup file to restore from (1-%d): " "$file_count" - read -r selection - if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le "$file_count" ]; then - break - else - colorized_echo red "Invalid selection. Please enter a number between 1 and $file_count." - fi - done - - local selected_file="${backup_files[$((selection-1))]}" - local selected_filename=$(basename "$selected_file") - - colorized_echo blue "Selected backup: $selected_filename" - - colorized_echo blue "Preparing archive for extraction..." - local archive_to_extract="$selected_file" - local archive_format="tar" - - if [[ "$selected_filename" =~ \.part[0-9]{2}\.zip$ ]]; then - archive_format="zip" - local base_name="${selected_filename%%.part*}" - colorized_echo yellow "Detected split zip backup. Checking available parts..." - if [ ! -f "$backup_dir/${base_name}.part01.zip" ]; then - colorized_echo red "Missing ${base_name}.part01.zip. Cannot restore split backup." - rm -rf "$temp_restore_dir" - exit 1 - fi - local concatenated_file="$temp_restore_dir/${base_name}_combined.zip" - >"$concatenated_file" - local part_count=0 - while IFS= read -r part_file; do - cat "$part_file" >>"$concatenated_file" - part_count=$((part_count + 1)) - done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base_name}.part*.zip" | sort) - if [ "$part_count" -eq 0 ]; then - colorized_echo red "No parts found for $base_name" - rm -rf "$temp_restore_dir" - exit 1 - fi - archive_to_extract="$concatenated_file" - colorized_echo green "✓ Combined $part_count part(s)" - elif [[ "$selected_filename" =~ \.zip$ ]]; then - archive_format="zip" - local base_name="${selected_filename%.zip}" - local zip_split_parts=() - while IFS= read -r part_file; do - [ -n "$part_file" ] && zip_split_parts+=("$part_file") - done < <(find "$backup_dir" -maxdepth 1 -type f -name "${base_name}.z[0-9][0-9]" | sort) - - if [ ${#zip_split_parts[@]} -gt 0 ]; then - colorized_echo yellow "Detected split zip backup (.zXX + .zip). Rebuilding archive..." - local expected_part=1 - for part_file in "${zip_split_parts[@]}"; do - local expected_name - expected_name=$(printf "%s.z%02d" "$base_name" "$expected_part") - if [ "$(basename "$part_file")" != "$expected_name" ]; then - colorized_echo red "Missing split part $expected_name. Cannot restore split backup." - rm -rf "$temp_restore_dir" - exit 1 - fi - expected_part=$((expected_part + 1)) - done - - local concatenated_file="$temp_restore_dir/${base_name}_combined.zip" - if command -v zip >/dev/null 2>&1 && zip -s 0 "$selected_file" --out "$concatenated_file" >>"$log_file" 2>&1; then - archive_to_extract="$concatenated_file" - colorized_echo green "✓ Rebuilt split zip archive with zip utility" - else - if command -v zip >/dev/null 2>&1; then - colorized_echo yellow "zip rebuild failed. Falling back to direct concatenation..." - else - colorized_echo yellow "zip utility not found. Falling back to direct concatenation..." - fi - >"$concatenated_file" - local part_count=0 - for part_file in "${zip_split_parts[@]}"; do - if ! cat "$part_file" >>"$concatenated_file"; then - colorized_echo red "Failed to read split part: $(basename "$part_file")" - rm -rf "$temp_restore_dir" - exit 1 - fi - part_count=$((part_count + 1)) - done - if ! cat "$selected_file" >>"$concatenated_file"; then - colorized_echo red "Failed to read main zip file: $selected_filename" - rm -rf "$temp_restore_dir" - exit 1 - fi - archive_to_extract="$concatenated_file" - colorized_echo green "✓ Combined $((part_count + 1)) split part(s)" - fi - fi - else - archive_format="tar" - fi - - colorized_echo blue "Extracting backup..." - if [ "$archive_format" = "zip" ]; then - if ! command -v unzip >/dev/null 2>&1; then - detect_os - install_package unzip - fi - if ! unzip -tq "$archive_to_extract" >/dev/null 2>>"$log_file"; then - colorized_echo red "ERROR: The backup file is not a valid zip archive." - echo "File is not a valid zip archive: $archive_to_extract" >>"$log_file" - rm -rf "$temp_restore_dir" - exit 1 - fi - if ! unzip -oq "$archive_to_extract" -d "$temp_restore_dir" 2>>"$log_file"; then - colorized_echo red "Failed to extract backup file." - echo "Failed to extract $archive_to_extract" >>"$log_file" - rm -rf "$temp_restore_dir" - exit 1 - fi - else - if ! gzip -t "$archive_to_extract" 2>/dev/null; then - colorized_echo red "ERROR: The backup file is not a valid gzip archive." - echo "File is not a valid gzip archive: $archive_to_extract" >>"$log_file" - rm -rf "$temp_restore_dir" - exit 1 - fi - if ! tar -xzf "$archive_to_extract" -C "$temp_restore_dir" 2>>"$log_file"; then - colorized_echo red "Failed to extract backup file." - echo "Failed to extract $archive_to_extract" >>"$log_file" - rm -rf "$temp_restore_dir" - exit 1 - fi - fi - colorized_echo green "✓ Archive extracted successfully" - - # Load environment variables from extracted .env - colorized_echo blue "Loading configuration from backup..." - local extracted_env="$temp_restore_dir/.env" - if [ ! -f "$extracted_env" ]; then - colorized_echo red "Environment file not found in backup." - rm -rf "$temp_restore_dir" - exit 1 - fi - - local db_type="" - local sqlite_file="" - local db_host="" - local db_port="" - local db_user="" - local db_password="" - local db_name="" - local container_name="" - - # Load variables from extracted .env - # Check if file is readable - if [ ! -r "$extracted_env" ]; then - colorized_echo red "ERROR: .env file is not readable" - rm -rf "$temp_restore_dir" - exit 1 - fi - - # Check for binary content or null bytes (warning only, not fatal) - if grep -q $'\x00' "$extracted_env" 2>/dev/null; then - colorized_echo yellow "WARNING: .env file contains null bytes, cleaning..." - fi - - local env_vars_loaded=0 - - # Check if file has null bytes - if not, use it directly - local env_file_to_use="$extracted_env" - if grep -q $'\x00' "$extracted_env" 2>/dev/null; then - # File has null bytes, create cleaned version - local cleaned_env="/tmp/pasarguard_env_cleaned_$$" - set +e - tr -d '\000' < "$extracted_env" > "$cleaned_env" 2>/dev/null - local tr_result=$? - set -e - if [ $tr_result -eq 0 ] && [ -s "$cleaned_env" ]; then - env_file_to_use="$cleaned_env" - else - rm -f "$cleaned_env" - fi - fi - - # Use the EXACT same pattern as backup_command function - # This ensures compatibility and works in the current shell (no subshell) - colorized_echo blue "Loading environment variables..." - if [ -f "$env_file_to_use" ]; then - # Temporarily disable exit on error for the loop to handle failures gracefully - set +e - while IFS='=' read -r key value || [ -n "$key" ]; do - if [[ -z "$key" || "$key" =~ ^# ]]; then - continue - fi - # Trim whitespace from key and value - key=$(echo "$key" | xargs 2>/dev/null || echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - value=$(echo "$value" | xargs 2>/dev/null || echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - # Remove surrounding quotes from value if present - value=$(echo "$value" | sed -E 's/^["'\''](.*)["'\'']$/\1/' 2>/dev/null || echo "$value") - if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then - export "$key"="$value" 2>/dev/null || true - env_vars_loaded=$((env_vars_loaded + 1)) - else - echo "Skipping invalid line in .env: $key=$value" >&2 - fi - done <"$env_file_to_use" - set -e # Re-enable exit on error - else - colorized_echo red "Environment file (.env) not found in backup." - rm -rf "$temp_restore_dir" - exit 1 - fi - - # Clean up temporary cleaned file if we created one - if [ -n "${cleaned_env:-}" ] && [ -f "$cleaned_env" ]; then - rm -f "$cleaned_env" - fi - - colorized_echo green "✓ Loaded $env_vars_loaded environment variables" - - if [ -z "$SQLALCHEMY_DATABASE_URL" ]; then - colorized_echo red "SQLALCHEMY_DATABASE_URL not found in backup .env file" - colorized_echo yellow "Available environment variables:" - grep -v '^#' "$extracted_env" | grep '=' | cut -d'=' -f1 | head -10 - rm -rf "$temp_restore_dir" - exit 1 - fi - - colorized_echo green "✓ Found SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL:0:50}..." - - # Parse database configuration (similar to backup function) - colorized_echo blue "Detecting database type..." - if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^sqlite ]]; then - db_type="sqlite" - colorized_echo green "✓ Detected SQLite database" - local sqlite_url_part="${SQLALCHEMY_DATABASE_URL#*://}" - sqlite_url_part="${sqlite_url_part%%\?*}" - sqlite_url_part="${sqlite_url_part%%#*}" - - if [[ "$sqlite_url_part" =~ ^//(.*)$ ]]; then - sqlite_file="/${BASH_REMATCH[1]}" - elif [[ "$sqlite_url_part" =~ ^/(.*)$ ]]; then - sqlite_file="/${BASH_REMATCH[1]}" - else - sqlite_file="$sqlite_url_part" - fi - colorized_echo blue "Database file: $sqlite_file" - elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^(mysql|mariadb|postgresql)[^:]*:// ]]; then - if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^mariadb[^:]*:// ]]; then - db_type="mariadb" - colorized_echo green "✓ Detected MariaDB database" - elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^mysql[^:]*:// ]]; then - db_type="mysql" - colorized_echo green "✓ Detected MySQL database" - elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^postgresql[^:]*:// ]]; then - # Check if it's timescaledb - use set +e to prevent failure on file not found - set +e - if grep -q "image: timescale/timescaledb" "$temp_restore_dir/docker-compose.yml" 2>/dev/null; then - db_type="timescaledb" - colorized_echo green "✓ Detected TimescaleDB database" - else - db_type="postgresql" - colorized_echo green "✓ Detected PostgreSQL database" - fi - set -e - fi - - local url_part="${SQLALCHEMY_DATABASE_URL#*://}" - url_part="${url_part%%\?*}" - url_part="${url_part%%#*}" - - if [[ "$url_part" =~ ^([^@]+)@(.+)$ ]]; then - local auth_part="${BASH_REMATCH[1]}" - url_part="${BASH_REMATCH[2]}" - - if [[ "$auth_part" =~ ^([^:]+):(.+)$ ]]; then - db_user="${BASH_REMATCH[1]}" - db_password="${BASH_REMATCH[2]}" - else - db_user="$auth_part" - fi - fi - - if [[ "$url_part" =~ ^([^:/]+)(:([0-9]+))?/(.+)$ ]]; then - db_host="${BASH_REMATCH[1]}" - db_port="${BASH_REMATCH[3]:-}" - db_name="${BASH_REMATCH[4]}" - db_name="${db_name%%\?*}" - db_name="${db_name%%#*}" - - if [ -z "$db_port" ]; then - if [[ "$db_type" =~ ^(mysql|mariadb)$ ]]; then - db_port="3306" - elif [[ "$db_type" =~ ^(postgresql|timescaledb)$ ]]; then - db_port="5432" - fi - fi - fi - - # Find container name for local databases - if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then - set +e - container_name=$(find_container "$db_type") - set -e - fi - fi - - if [ -z "$db_type" ]; then - colorized_echo red "Could not determine database type from backup." - colorized_echo yellow "SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL:-not set}" - rm -rf "$temp_restore_dir" - exit 1 - fi - - colorized_echo green "✓ Database configuration detected: $db_type" - - # Confirm restore - colorized_echo red "⚠️ DANGER: This will PERMANENTLY overwrite your current $db_type database!" - colorized_echo yellow "WARNING: This will overwrite your current $db_type database!" - colorized_echo blue "Database type: $db_type" - if [ -n "$db_name" ]; then - colorized_echo blue "Database name: $db_name" - fi - if [ -n "$container_name" ]; then - colorized_echo blue "Container: $container_name" - fi - - while true; do - printf "Do you want to proceed with the restore? (yes/no): " - read -r confirm - if [[ "$confirm" =~ ^[Yy](es)?$ ]]; then - break - elif [[ "$confirm" =~ ^[Nn](o)?$ ]]; then - colorized_echo yellow "Restore cancelled." - rm -rf "$temp_restore_dir" - exit 0 - else - colorized_echo red "Please answer yes or no." - fi - done - - # Stop pasarguard services before restore for clean state - colorized_echo blue "Stopping pasarguard services for clean restore..." - if [[ "$db_type" == "sqlite" ]]; then - # For SQLite, stop all services since we need to restore files - down_pasarguard - else - # For containerized databases, stop only application services - # Keep database containers running for restore via docker exec - stop_pasarguard_app_services - fi - - # Perform restore - colorized_echo red "⚠️ DANGER: Starting database restore - this will overwrite existing data!" - colorized_echo blue "Starting database restore..." - - case $db_type in - sqlite) - if [ ! -f "$temp_restore_dir/db_backup.sqlite" ]; then - colorized_echo red "SQLite backup file not found in backup archive." - rm -rf "$temp_restore_dir" - exit 1 - fi - - if [ -f "$sqlite_file" ]; then - cp "$sqlite_file" "${sqlite_file}.backup.$(date +%Y%m%d%H%M%S)" 2>>"$log_file" - fi - - if cp "$temp_restore_dir/db_backup.sqlite" "$sqlite_file" 2>>"$log_file"; then - colorized_echo green "SQLite database restored successfully." - else - colorized_echo red "Failed to restore SQLite database." - echo "SQLite restore failed" >>"$log_file" - rm -rf "$temp_restore_dir" - exit 1 - fi - ;; - - mariadb|mysql) - if [ ! -f "$temp_restore_dir/db_backup.sql" ]; then - colorized_echo red "Database backup file not found in backup archive." - rm -rf "$temp_restore_dir" - exit 1 - fi - - if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then - if [ -z "$container_name" ]; then - colorized_echo red "Error: MySQL/MariaDB container not found. Is the container running?" - echo "MySQL/MariaDB container not found. Container name: ${container_name:-empty}" >>"$log_file" - rm -rf "$temp_restore_dir" - exit 1 - else - local verified_container=$(verify_and_start_container "$container_name" "$db_type") - if [ -z "$verified_container" ]; then - colorized_echo red "Failed to start database container. Please start it manually." - rm -rf "$temp_restore_dir" - exit 1 - fi - container_name="$verified_container" - - # Check if this is actually a MariaDB container - local is_mariadb=false - local mysql_cmd="mysql" - local db_type_name="MySQL" - if docker exec "$container_name" mariadb --version >/dev/null 2>&1; then - is_mariadb=true - mysql_cmd="mariadb" - db_type_name="MariaDB" - fi - - colorized_echo blue "Restoring $db_type_name database from container: $container_name" - - local restore_success=false - local backup_restore_user="${db_user:-${DB_USER:-}}" - local backup_restore_password="${db_password:-${DB_PASSWORD:-}}" - local app_db_target="${db_name:-${current_db_name:-}}" - - # Try root password from backup .env first - if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then - colorized_echo blue "Trying root user from backup .env..." - if docker exec -i "$container_name" "$mysql_cmd" -u root -p"$MYSQL_ROOT_PASSWORD" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then - restore_success=true - colorized_echo green "$db_type_name database restored successfully." - else - colorized_echo yellow "Root restore failed with backup .env credentials, trying fallback..." - echo "$db_type_name restore failed with backup MYSQL_ROOT_PASSWORD" >>"$log_file" - fi - fi - - # If root password changed after backup, try current installation value - if [ "$restore_success" = false ] && [ -n "$current_mysql_root_password" ] && [ "$current_mysql_root_password" != "${MYSQL_ROOT_PASSWORD:-}" ]; then - colorized_echo blue "Trying root user from current installation .env..." - if docker exec -i "$container_name" "$mysql_cmd" -u root -p"$current_mysql_root_password" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then - restore_success=true - colorized_echo green "$db_type_name database restored successfully." - else - colorized_echo yellow "Root restore failed with current .env credentials, trying app user fallback..." - echo "$db_type_name restore failed with current MYSQL_ROOT_PASSWORD" >>"$log_file" - fi - fi - - # Try app user from backup SQL URL/.env - if [ "$restore_success" = false ] && [ -n "$backup_restore_user" ] && [ -n "$backup_restore_password" ]; then - colorized_echo blue "Trying app user '$backup_restore_user' from backup credentials..." - if [ -n "$app_db_target" ]; then - if docker exec -i "$container_name" "$mysql_cmd" -u "$backup_restore_user" -p"$backup_restore_password" "$app_db_target" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then - restore_success=true - colorized_echo green "$db_type_name database restored successfully." - fi - fi - if [ "$restore_success" = false ] && docker exec -i "$container_name" "$mysql_cmd" -u "$backup_restore_user" -p"$backup_restore_password" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then - restore_success=true - colorized_echo green "$db_type_name database restored successfully." - elif [ "$restore_success" = false ]; then - colorized_echo yellow "App user restore failed with backup credentials, trying current installation credentials..." - echo "$db_type_name restore failed with backup app credentials" >>"$log_file" - fi - fi - - # Final fallback: current installation app credentials - if [ "$restore_success" = false ] && [ -n "$current_db_user" ] && [ -n "$current_db_password" ] && { [ "$current_db_user" != "$backup_restore_user" ] || [ "$current_db_password" != "$backup_restore_password" ]; }; then - colorized_echo blue "Trying app user '$current_db_user' from current installation .env..." - if [ -n "$app_db_target" ]; then - if docker exec -i "$container_name" "$mysql_cmd" -u "$current_db_user" -p"$current_db_password" "$app_db_target" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then - restore_success=true - colorized_echo green "$db_type_name database restored successfully." - fi - fi - if [ "$restore_success" = false ] && docker exec -i "$container_name" "$mysql_cmd" -u "$current_db_user" -p"$current_db_password" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then - restore_success=true - colorized_echo green "$db_type_name database restored successfully." - elif [ "$restore_success" = false ]; then - echo "$db_type_name restore failed with current app credentials" >>"$log_file" - fi - fi - - if [ "$restore_success" = false ]; then - colorized_echo red "Failed to restore $db_type_name database with all available credentials." - colorized_echo yellow "Check log file for details: $log_file" - rm -rf "$temp_restore_dir" - exit 1 - fi - fi - else - colorized_echo red "Remote $db_type restore not supported yet." - rm -rf "$temp_restore_dir" - exit 1 - fi - ;; - - postgresql|timescaledb) - if [ ! -f "$temp_restore_dir/db_backup.sql" ]; then - colorized_echo red "Database backup file not found in backup archive." - rm -rf "$temp_restore_dir" - exit 1 - fi - - # Verify backup file is not empty and is readable - if [ ! -s "$temp_restore_dir/db_backup.sql" ]; then - colorized_echo red "Database backup file is empty or unreadable." - rm -rf "$temp_restore_dir" - exit 1 - fi - - local backup_size=$(du -h "$temp_restore_dir/db_backup.sql" | cut -f1) - colorized_echo blue "Backup file size: $backup_size" - - if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]] && [ -n "$container_name" ]; then - local verified_container=$(verify_and_start_container "$container_name" "$db_type") - if [ -z "$verified_container" ]; then - colorized_echo red "Failed to start database container. Please start it manually." - rm -rf "$temp_restore_dir" - exit 1 - fi - container_name="$verified_container" - - colorized_echo blue "Restoring $db_type database from container: $container_name" - - # Prepare restore credentials - local restore_user="${db_user:-${DB_USER:-postgres}}" - local restore_password="${db_password:-${DB_PASSWORD:-}}" - - if [ -z "$restore_password" ]; then - colorized_echo red "No database password found for restore." - rm -rf "$temp_restore_dir" - exit 1 - fi - - export PGPASSWORD="$restore_password" - local restore_success=false - - if [ "$db_type" = "timescaledb" ]; then - # TimescaleDB requires special restore procedure to handle version mismatches. - # A plain psql restore fails when the backup was taken with a different - # TimescaleDB version because DROP EXTENSION / CREATE EXTENSION cycles - # break when the shared library is already loaded with the new version. - # The fix: drop & recreate the database, then use the official - # timescaledb_pre_restore() / timescaledb_post_restore() wrapper. - # See: https://docs.timescale.com/self-hosted/latest/backup-and-restore/ - colorized_echo blue "Using TimescaleDB-safe restore procedure..." - - # Use target installation's identity when available, falling back to backup values. - # This ensures cross-server restores work correctly when the local DB user/name - # differs from the backup source. - local target_db_name="${current_db_name:-$db_name}" - local target_db_owner="${current_db_user:-$restore_user}" - - # Drop and recreate the target database for a clean slate - colorized_echo blue "Dropping and recreating database '$target_db_name'..." - docker exec "$container_name" psql -U postgres -d postgres \ - -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$target_db_name' AND pid <> pg_backend_pid();" \ - >>"$log_file" 2>&1 - docker exec "$container_name" psql -U postgres -d postgres \ - -c "DROP DATABASE IF EXISTS \"$target_db_name\";" >>"$log_file" 2>&1 - docker exec "$container_name" psql -U postgres -d postgres \ - -c "CREATE DATABASE \"$target_db_name\" OWNER \"$target_db_owner\";" >>"$log_file" 2>&1 - - # Create the timescaledb extension in the fresh database - docker exec "$container_name" psql -U postgres -d "$target_db_name" \ - -c "CREATE EXTENSION IF NOT EXISTS timescaledb;" >>"$log_file" 2>&1 - - # Call pre_restore to put TimescaleDB into restore mode - colorized_echo blue "Calling timescaledb_pre_restore()..." - docker exec "$container_name" psql -U postgres -d "$target_db_name" \ - -c "SELECT timescaledb_pre_restore();" >>"$log_file" 2>&1 - - # Filter out extension DROP/CREATE statements from the dump. - # pg_dump --clean --if-exists generates DROP EXTENSION / CREATE EXTENSION - # lines that would undo the pre_restore() setup above. - colorized_echo blue "Preparing dump (filtering extension statements)..." - grep -v -E '^\s*(DROP|CREATE)\s+EXTENSION\s+(IF\s+(EXISTS|NOT\s+EXISTS)\s+)?timescaledb\b' \ - "$temp_restore_dir/db_backup.sql" > "$temp_restore_dir/db_backup_filtered.sql" 2>>"$log_file" - - # Restore the filtered dump with ON_ERROR_STOP so psql exits non-zero on SQL errors - colorized_echo blue "Restoring database dump..." - if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U "$restore_user" -d "$target_db_name" < "$temp_restore_dir/db_backup_filtered.sql" 2>>"$log_file"; then - restore_success=true - else - # Fallback: try with postgres superuser - colorized_echo yellow "Trying with postgres superuser..." - if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U postgres -d "$target_db_name" < "$temp_restore_dir/db_backup_filtered.sql" 2>>"$log_file"; then - restore_success=true - fi - fi - - # Clean up filtered dump - rm -f "$temp_restore_dir/db_backup_filtered.sql" - - # Call post_restore regardless of outcome to leave DB in a usable state - colorized_echo blue "Calling timescaledb_post_restore()..." - docker exec "$container_name" psql -U postgres -d "$target_db_name" \ - -c "SELECT timescaledb_post_restore();" >>"$log_file" 2>&1 - - if [ "$restore_success" = true ]; then - colorized_echo green "TimescaleDB database restored successfully." - fi - else - # Plain PostgreSQL restore with ON_ERROR_STOP so psql exits non-zero on SQL errors - colorized_echo blue "Attempting restore using app user '$restore_user' to database '$db_name'..." - if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U "$restore_user" -d "$db_name" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo green "$db_type database restored successfully." - restore_success=true - else - # If that fails, try using postgres superuser - colorized_echo yellow "Trying with postgres superuser..." - if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U postgres -d "$db_name" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo green "$db_type database restored successfully." - restore_success=true - else - # Try restoring to postgres database (for pg_dumpall backups) - if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U postgres -d postgres < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo green "$db_type database restored successfully." - restore_success=true - fi - fi - fi - fi - - unset PGPASSWORD - - if [ "$restore_success" = false ]; then - colorized_echo red "Failed to restore $db_type database." - colorized_echo yellow "Check log file for details: $log_file" - rm -rf "$temp_restore_dir" - exit 1 - fi - else - colorized_echo red "Remote $db_type restore not supported yet." - rm -rf "$temp_restore_dir" - exit 1 - fi - ;; - *) - colorized_echo red "Unsupported database type: $db_type" - rm -rf "$temp_restore_dir" - exit 1 - ;; - esac - - # Restore data directory if included in backup - colorized_echo blue "Restoring data directory..." - local extracted_data_dir="$temp_restore_dir/pasarguard_data" - if [ -d "$extracted_data_dir" ]; then - if ! command -v rsync >/dev/null 2>&1; then - detect_os - install_package rsync - fi - mkdir -p "$DATA_DIR" - if ! rsync -a "$extracted_data_dir/" "$DATA_DIR/" 2>>"$log_file"; then - colorized_echo red "Failed to restore data directory." - echo "Failed to restore data directory from $extracted_data_dir to $DATA_DIR" >>"$log_file" - rm -rf "$temp_restore_dir" - exit 1 - fi - colorized_echo green "Data directory restored to $DATA_DIR." - else - colorized_echo yellow "No pasarguard_data directory found in backup. Skipping data restore." - fi - - # Restore configuration files if needed - colorized_echo blue "Restoring configuration files..." - if [ -f "$temp_restore_dir/.env" ]; then - cp "$temp_restore_dir/.env" "$APP_DIR/.env.backup.$(date +%Y%m%d%H%M%S)" 2>>"$log_file" - cp "$temp_restore_dir/.env" "$APP_DIR/.env" 2>>"$log_file" - colorized_echo green "Environment file restored." - local preserve_db_credentials=false - if [[ "$db_type" != "sqlite" ]]; then - if [ -n "$current_db_user" ] && [ -n "${DB_USER:-}" ] && [ "$current_db_user" != "$DB_USER" ]; then - preserve_db_credentials=true - elif [ -n "$current_db_name" ] && [ -n "${DB_NAME:-}" ] && [ "$current_db_name" != "$DB_NAME" ]; then - preserve_db_credentials=true - elif [ -n "$current_db_password" ] && [ -n "${DB_PASSWORD:-}" ] && [ "$current_db_password" != "$DB_PASSWORD" ]; then - preserve_db_credentials=true - fi - fi - if [ "$preserve_db_credentials" = true ]; then - colorized_echo yellow "Database credentials in backup differ from current installation; preserving current database credentials." - if [ -n "$current_db_user" ]; then - replace_or_append_env_var "DB_USER" "$current_db_user" false "$ENV_FILE" - fi - if [ -n "$current_db_name" ]; then - replace_or_append_env_var "DB_NAME" "$current_db_name" false "$ENV_FILE" - fi - if [ -n "$current_db_password" ]; then - replace_or_append_env_var "DB_PASSWORD" "$current_db_password" false "$ENV_FILE" - fi - if [ -n "$current_sqlalchemy_url" ]; then - replace_or_append_env_var "SQLALCHEMY_DATABASE_URL" "$current_sqlalchemy_url" true "$ENV_FILE" - fi - fi - fi - - if [ -f "$temp_restore_dir/docker-compose.yml" ]; then - cp "$temp_restore_dir/docker-compose.yml" "$APP_DIR/docker-compose.yml.backup.$(date +%Y%m%d%H%M%S)" 2>>"$log_file" - cp "$temp_restore_dir/docker-compose.yml" "$APP_DIR/docker-compose.yml" 2>>"$log_file" - colorized_echo green "Docker Compose file restored." - fi - - # Clean up - rm -rf "$temp_restore_dir" - - # Restart pasarguard services - colorized_echo blue "Restarting pasarguard services..." - if [[ "$db_type" == "sqlite" ]]; then - # For SQLite, restart all services - up_pasarguard - else - # For containerized databases, restart only application services - start_pasarguard_app_services - fi - - colorized_echo green "Restore completed successfully!" - colorized_echo green "PasarGuard services have been restarted." -} - -backup_command() { - colorized_echo blue "Starting backup process..." - - # Check if pasarguard is installed - if ! is_pasarguard_installed; then - colorized_echo red "pasarguard is not installed!" - return 1 - fi - - local backup_dir="$APP_DIR/backup" - local temp_dir="/tmp/pasarguard_backup" - local timestamp=$(date +"%Y%m%d%H%M%S") - local backup_file="$backup_dir/backup_$timestamp.zip" - local error_messages=() - local log_file="/var/log/pasarguard_backup_error.log" - local final_backup_paths=() - local split_size_arg="47m" # keep Telegram chunks under 50MB - >"$log_file" - echo "Backup Log - $(date)" >>"$log_file" - - colorized_echo blue "Reading environment configuration..." - - if ! command -v rsync >/dev/null 2>&1; then - detect_os - install_package rsync - fi - - if ! command -v zip >/dev/null 2>&1; then - detect_os - install_package zip - fi - - # Remove old backups before creating new one (keep only latest) - rm -f "$backup_dir"/backup_*.tar.gz - rm -f "$backup_dir"/backup_*.zip - rm -f "$backup_dir"/backup_*.z[0-9][0-9] 2>/dev/null || true - mkdir -p "$backup_dir" - - # Clean up temp directory completely before starting - rm -rf "$temp_dir" - mkdir -p "$temp_dir" - - if [ -f "$ENV_FILE" ]; then - while IFS='=' read -r key value; do - if [[ -z "$key" || "$key" =~ ^# ]]; then - continue - fi - key=$(echo "$key" | xargs) - value=$(echo "$value" | xargs) - # Remove surrounding quotes from value if present - value=$(echo "$value" | sed -E 's/^["'\''](.*)["'\'']$/\1/') - if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then - export "$key"="$value" - else - echo "Skipping invalid line in .env: $key=$value" >>"$log_file" - fi - done <"$ENV_FILE" - else - error_messages+=("Environment file (.env) not found.") - echo "Environment file (.env) not found." >>"$log_file" - send_backup_error_to_telegram "${error_messages[*]}" "$log_file" - exit 1 - fi - - local db_type="" - local sqlite_file="" - local db_host="" - local db_port="" - local db_user="" - local db_password="" - local db_name="" - local container_name="" - - # SQLALCHEMY_DATABASE_URL should already be loaded from .env above - # Just log what we have - echo "SQLALCHEMY_DATABASE_URL from environment: ${SQLALCHEMY_DATABASE_URL:-not set}" >>"$log_file" - - if [ -z "$SQLALCHEMY_DATABASE_URL" ]; then - colorized_echo red "Error: SQLALCHEMY_DATABASE_URL not found in .env file or not set" - echo "Please check $ENV_FILE for SQLALCHEMY_DATABASE_URL" >>"$log_file" - error_messages+=("SQLALCHEMY_DATABASE_URL not found in .env file") - colorized_echo yellow "Please check the log file for details: $log_file" - return 1 - fi - - if [ -n "$SQLALCHEMY_DATABASE_URL" ]; then - echo "Parsing SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL%%@*}" >>"$log_file" - - # Extract database type from scheme - if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^sqlite ]]; then - db_type="sqlite" - # Extract SQLite file path - # SQLite URLs: sqlite:///relative/path or sqlite:////absolute/path - local sqlite_url_part="${SQLALCHEMY_DATABASE_URL#*://}" - sqlite_url_part="${sqlite_url_part%%\?*}" - sqlite_url_part="${sqlite_url_part%%#*}" - - # SQLite URL format: - # sqlite:////absolute/path (4 slashes = absolute path /path) - # After removing 'sqlite://', //absolute/path remains, convert to /absolute/path - if [[ "$sqlite_url_part" =~ ^//(.*)$ ]]; then - # Absolute path: sqlite:////absolute/path -> /absolute/path - sqlite_file="/${BASH_REMATCH[1]}" - elif [[ "$sqlite_url_part" =~ ^/(.*)$ ]]; then - # Could be absolute (sqlite:///path) or relative depending on context - # In practice, treat as absolute since SQLAlchemy uses 4 slashes for absolute - sqlite_file="/${BASH_REMATCH[1]}" - else - # Relative path (no leading slash) - sqlite_file="$sqlite_url_part" - fi - elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^(mysql|mariadb|postgresql)[^:]*:// ]]; then - # Extract scheme to determine type - if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^mariadb[^:]*:// ]]; then - db_type="mariadb" - elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^mysql[^:]*:// ]]; then - db_type="mysql" - elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^postgresql[^:]*:// ]]; then - # Check if it's timescaledb by checking for specific patterns or container - if grep -q "image: timescale/timescaledb" "$COMPOSE_FILE" 2>/dev/null; then - db_type="timescaledb" - else - db_type="postgresql" - fi - fi - - # Parse connection string: scheme://[user[:password]@]host[:port]/database[?query] - # Remove scheme prefix - local url_part="${SQLALCHEMY_DATABASE_URL#*://}" - # Remove query parameters if present - url_part="${url_part%%\?*}" - url_part="${url_part%%#*}" - - # Extract auth part (user:password@) - if [[ "$url_part" =~ ^([^@]+)@(.+)$ ]]; then - local auth_part="${BASH_REMATCH[1]}" - url_part="${BASH_REMATCH[2]}" - - # Extract username and password - if [[ "$auth_part" =~ ^([^:]+):(.+)$ ]]; then - db_user="${BASH_REMATCH[1]}" - db_password="${BASH_REMATCH[2]}" - else - db_user="$auth_part" - fi - fi - - # Extract host, port, and database - if [[ "$url_part" =~ ^([^:/]+)(:([0-9]+))?/(.+)$ ]]; then - db_host="${BASH_REMATCH[1]}" - db_port="${BASH_REMATCH[3]:-}" - db_name="${BASH_REMATCH[4]}" - - # Remove query parameters from database name if any - db_name="${db_name%%\?*}" - db_name="${db_name%%#*}" - - # Set default ports if not specified - if [ -z "$db_port" ]; then - if [[ "$db_type" =~ ^(mysql|mariadb)$ ]]; then - db_port="3306" - elif [[ "$db_type" =~ ^(postgresql|timescaledb)$ ]]; then - db_port="5432" - fi - fi - fi - - # For local databases, try to find container name from docker-compose - if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then - container_name=$(find_container "$db_type") - echo "Container name/ID for $db_type: $container_name" >>"$log_file" - fi - fi - fi - - if [ -n "$db_type" ]; then - echo "Database detected: $db_type" >>"$log_file" - echo "Database host: ${db_host:-localhost}" >>"$log_file" - colorized_echo blue "Database detected: $db_type" - colorized_echo blue "Backing up database..." - case $db_type in - mariadb) - if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then - if [ -z "$container_name" ]; then - colorized_echo red "Error: MariaDB container not found. Is the container running?" - echo "MariaDB container not found. Container name: ${container_name:-empty}" >>"$log_file" - error_messages+=("MariaDB container not found or not running.") - else - local verified_container=$(check_container "$container_name" "$db_type") - if [ -z "$verified_container" ]; then - colorized_echo red "Error: MariaDB container not found or not running." - echo "Container not found or not running: $container_name" >>"$log_file" - error_messages+=("MariaDB container not found or not running.") - else - container_name="$verified_container" - # Local Docker container - # Try root user with MYSQL_ROOT_PASSWORD first for all databases backup - if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then - colorized_echo blue "Backing up all MariaDB databases from container: $container_name (using root user)" - if docker exec "$container_name" mariadb-dump -u root -p"$MYSQL_ROOT_PASSWORD" --all-databases --ignore-database=mysql --ignore-database=performance_schema --ignore-database=information_schema --ignore-database=sys --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo green "MariaDB backup completed successfully (all databases)" - else - # Fallback to SQL URL credentials for specific database - colorized_echo yellow "Root backup failed, falling back to app user for specific database" - local backup_user="${db_user:-${DB_USER:-}}" - local backup_password="${db_password:-${DB_PASSWORD:-}}" - - if [ -z "$backup_password" ] || [ -z "$db_name" ]; then - colorized_echo red "Error: Cannot fallback - missing database name or password in SQLALCHEMY_DATABASE_URL" - error_messages+=("MariaDB backup failed - root backup failed and fallback credentials incomplete.") - else - colorized_echo blue "Backing up MariaDB database '$db_name' from container: $container_name (using app user)" - if ! docker exec "$container_name" mariadb-dump -u "$backup_user" -p"$backup_password" "$db_name" --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo red "MariaDB dump failed. Check log file for details." - error_messages+=("MariaDB dump failed.") - else - colorized_echo green "MariaDB backup completed successfully" - fi - fi - fi - else - # No MYSQL_ROOT_PASSWORD, use SQL URL credentials for specific database - local backup_user="${db_user:-${DB_USER:-}}" - local backup_password="${db_password:-${DB_PASSWORD:-}}" - - if [ -z "$backup_password" ]; then - colorized_echo red "Error: Database password not found. Check MYSQL_ROOT_PASSWORD or SQLALCHEMY_DATABASE_URL in .env" - error_messages+=("MariaDB password not found.") - elif [ -z "$db_name" ]; then - colorized_echo red "Error: Database name not found in SQLALCHEMY_DATABASE_URL" - error_messages+=("MariaDB database name not found.") - else - colorized_echo blue "Backing up MariaDB database '$db_name' from container: $container_name (using app user)" - if ! docker exec "$container_name" mariadb-dump -u "$backup_user" -p"$backup_password" "$db_name" --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo red "MariaDB dump failed. Check log file for details." - error_messages+=("MariaDB dump failed.") - else - colorized_echo green "MariaDB backup completed successfully" - fi - fi - fi - fi - fi - else - # Remote database - would need mariadb-client installed - colorized_echo red "Remote MariaDB backup not yet supported. Please use local database or install mariadb-client." - error_messages+=("Remote MariaDB backup not yet supported. Please use local database or install mariadb-client.") - fi - ;; - mysql) - if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then - if [ -z "$container_name" ]; then - colorized_echo red "Error: MySQL container not found. Is the container running?" - echo "MySQL container not found. Container name: ${container_name:-empty}" >>"$log_file" - error_messages+=("MySQL container not found or not running.") - else - local verified_container=$(check_container "$container_name" "$db_type") - if [ -z "$verified_container" ]; then - colorized_echo red "Error: MySQL/MariaDB container not found or not running." - echo "Container not found or not running: $container_name" >>"$log_file" - error_messages+=("MySQL/MariaDB container not found or not running.") - else - container_name="$verified_container" - # Check if this is actually a MariaDB container (try mariadb-dump first) - local is_mariadb=false - if docker exec "$container_name" mariadb-dump --version >/dev/null 2>&1; then - is_mariadb=true - fi - - # Local Docker container - # Try root user with MYSQL_ROOT_PASSWORD first for all databases backup - if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then - # Choose command based on whether it's MariaDB or MySQL - local mysql_cmd="mysql" - local dump_cmd="mysqldump" - local db_type_name="MySQL" - if [ "$is_mariadb" = true ]; then - mysql_cmd="mariadb" - dump_cmd="mariadb-dump" - db_type_name="MariaDB" - fi - - colorized_echo blue "Backing up all $db_type_name databases from container: $container_name (using root user)" - databases=$(docker exec "$container_name" "$mysql_cmd" -u root -p"$MYSQL_ROOT_PASSWORD" -e "SHOW DATABASES;" 2>>"$log_file" | grep -Ev "^(Database|mysql|performance_schema|information_schema|sys)$" || true) - if [ -z "$databases" ]; then - colorized_echo yellow "No user databases found, falling back to specific database backup" - # Fallback to SQL URL credentials - local backup_user="${db_user:-${DB_USER:-}}" - local backup_password="${db_password:-${DB_PASSWORD:-}}" - - if [ -z "$backup_password" ] || [ -z "$db_name" ]; then - colorized_echo red "Error: Cannot fallback - missing database name or password in SQLALCHEMY_DATABASE_URL" - error_messages+=("MySQL backup failed - no databases found and fallback credentials incomplete.") - else - colorized_echo blue "Backing up $db_type_name database '$db_name' from container: $container_name (using app user)" - if ! docker exec "$container_name" "$dump_cmd" -u "$backup_user" -p"$backup_password" "$db_name" --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo red "$db_type_name dump failed. Check log file for details." - error_messages+=("$db_type_name dump failed.") - else - colorized_echo green "$db_type_name backup completed successfully" - fi - fi - elif ! docker exec "$container_name" "$dump_cmd" -u root -p"$MYSQL_ROOT_PASSWORD" --databases $databases --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - # Root backup failed, fallback to SQL URL credentials - colorized_echo yellow "Root backup failed, falling back to app user for specific database" - local backup_user="${db_user:-${DB_USER:-}}" - local backup_password="${db_password:-${DB_PASSWORD:-}}" - - if [ -z "$backup_password" ] || [ -z "$db_name" ]; then - colorized_echo red "Error: Cannot fallback - missing database name or password in SQLALCHEMY_DATABASE_URL" - error_messages+=("MySQL backup failed - root backup failed and fallback credentials incomplete.") - else - colorized_echo blue "Backing up $db_type_name database '$db_name' from container: $container_name (using app user)" - if ! docker exec "$container_name" "$dump_cmd" -u "$backup_user" -p"$backup_password" "$db_name" --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo red "$db_type_name dump failed. Check log file for details." - error_messages+=("$db_type_name dump failed.") - else - colorized_echo green "$db_type_name backup completed successfully" - fi - fi - else - colorized_echo green "$db_type_name backup completed successfully (all databases)" - fi - else - # No MYSQL_ROOT_PASSWORD, use SQL URL credentials for specific database - local backup_user="${db_user:-${DB_USER:-}}" - local backup_password="${db_password:-${DB_PASSWORD:-}}" - local dump_cmd="mysqldump" - local db_type_name="MySQL" - if [ "$is_mariadb" = true ]; then - dump_cmd="mariadb-dump" - db_type_name="MariaDB" - fi - - if [ -z "$backup_password" ]; then - colorized_echo red "Error: Database password not found. Check MYSQL_ROOT_PASSWORD or SQLALCHEMY_DATABASE_URL in .env" - error_messages+=("MySQL password not found.") - elif [ -z "$db_name" ]; then - colorized_echo red "Error: Database name not found in SQLALCHEMY_DATABASE_URL" - error_messages+=("MySQL database name not found.") - else - colorized_echo blue "Backing up $db_type_name database '$db_name' from container: $container_name (using app user)" - if ! docker exec "$container_name" "$dump_cmd" -u "$backup_user" -p"$backup_password" "$db_name" --events --triggers >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo red "$db_type_name dump failed. Check log file for details." - error_messages+=("$db_type_name dump failed.") - else - colorized_echo green "$db_type_name backup completed successfully" - fi - fi - fi - fi - fi - else - # Remote database - would need mysql-client installed - colorized_echo red "Remote MySQL backup not yet supported. Please use local database or install mysql-client." - error_messages+=("Remote MySQL backup not yet supported. Please use local database or install mysql-client.") - fi - ;; - postgresql) - if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then - if [ -z "$container_name" ]; then - colorized_echo red "Error: PostgreSQL container not found. Is the container running?" - echo "PostgreSQL container not found. Container name: ${container_name:-empty}" >>"$log_file" - error_messages+=("PostgreSQL container not found or not running.") - else - local verified_container=$(check_container "$container_name" "$db_type") - if [ -z "$verified_container" ]; then - colorized_echo red "Error: PostgreSQL container not found or not running." - echo "Container not found or not running: $container_name" >>"$log_file" - error_messages+=("PostgreSQL container not found or not running.") - else - container_name="$verified_container" - # Local Docker container - # Try postgres superuser with DB_PASSWORD first for pg_dumpall (all databases) - if [ -n "${DB_PASSWORD:-}" ]; then - colorized_echo blue "Backing up all PostgreSQL databases from container: $container_name (using postgres superuser)" - export PGPASSWORD="$DB_PASSWORD" - if docker exec "$container_name" pg_dumpall -U postgres >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo green "PostgreSQL backup completed successfully (all databases)" - unset PGPASSWORD - else - # Fallback to pg_dump with SQL URL credentials - unset PGPASSWORD - colorized_echo yellow "pg_dumpall failed, falling back to pg_dump for specific database" - local backup_user="${db_user:-${DB_USER:-postgres}}" - local backup_password="${db_password:-${DB_PASSWORD:-}}" - - if [ -z "$backup_password" ] || [ -z "$db_name" ]; then - colorized_echo red "Error: Cannot fallback - missing database name or password in SQLALCHEMY_DATABASE_URL" - error_messages+=("PostgreSQL backup failed - pg_dumpall failed and fallback credentials incomplete.") - else - colorized_echo blue "Backing up PostgreSQL database '$db_name' from container: $container_name (using app user)" - export PGPASSWORD="$backup_password" - if ! docker exec "$container_name" pg_dump -U "$backup_user" -d "$db_name" --clean --if-exists >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo red "PostgreSQL dump failed. Check log file for details." - error_messages+=("PostgreSQL dump failed.") - else - colorized_echo green "PostgreSQL backup completed successfully" - fi - unset PGPASSWORD - fi - fi - else - # No DB_PASSWORD, use SQL URL credentials for pg_dump - local backup_user="${db_user:-${DB_USER:-postgres}}" - local backup_password="${db_password:-${DB_PASSWORD:-}}" - - if [ -z "$backup_password" ]; then - colorized_echo red "Error: Database password not found. Check DB_PASSWORD or SQLALCHEMY_DATABASE_URL in .env" - error_messages+=("PostgreSQL password not found.") - elif [ -z "$db_name" ]; then - colorized_echo red "Error: Database name not found in SQLALCHEMY_DATABASE_URL" - error_messages+=("PostgreSQL database name not found.") - else - colorized_echo blue "Backing up PostgreSQL database '$db_name' from container: $container_name (using app user)" - export PGPASSWORD="$backup_password" - if ! docker exec "$container_name" pg_dump -U "$backup_user" -d "$db_name" --clean --if-exists >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo red "PostgreSQL dump failed. Check log file for details." - error_messages+=("PostgreSQL dump failed.") - else - colorized_echo green "PostgreSQL backup completed successfully" - fi - unset PGPASSWORD - fi - fi - fi - fi - else - # Remote database - would need postgresql-client installed - colorized_echo red "Remote PostgreSQL backup not yet supported. Please use local database or install postgresql-client." - error_messages+=("Remote PostgreSQL backup not yet supported. Please use local database or install postgresql-client.") - fi - ;; - timescaledb) - if [[ "$db_host" == "127.0.0.1" || "$db_host" == "localhost" || "$db_host" == "::1" ]]; then - if [ -z "$container_name" ]; then - colorized_echo red "Error: TimescaleDB container not found. Is the container running?" - echo "Container name detection failed. Checked for: timescaledb, postgresql" >>"$log_file" - error_messages+=("TimescaleDB container not found or not running.") - else - # Get actual container name/ID - ps -q returns container ID, which is what we need - # But first verify the container exists - local actual_container="" - if docker inspect "$container_name" >/dev/null 2>&1; then - actual_container="$container_name" - else - # Try to find container by service name using docker compose - actual_container=$($COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" ps -q timescaledb 2>/dev/null) - if [ -z "$actual_container" ]; then - actual_container=$($COMPOSE -f "$COMPOSE_FILE" -p "$APP_NAME" ps -q postgresql 2>/dev/null) - fi - if [ -z "$actual_container" ]; then - # Try with full container name pattern - local full_container_name="${APP_NAME}-timescaledb-1" - if docker inspect "$full_container_name" >/dev/null 2>&1; then - actual_container="$full_container_name" - else - full_container_name="${APP_NAME}-postgresql-1" - if docker inspect "$full_container_name" >/dev/null 2>&1; then - actual_container="$full_container_name" - fi - fi - fi - fi - - if [ -z "$actual_container" ]; then - colorized_echo red "Error: TimescaleDB container not found. Is the container running?" - echo "Container not found. Tried: $container_name and various patterns" >>"$log_file" - error_messages+=("TimescaleDB container not found or not running.") - else - container_name="$actual_container" - # Local Docker container - # Use SQL URL credentials directly for pg_dump (more reliable than pg_dumpall) - local backup_user="${db_user:-${DB_USER:-postgres}}" - local backup_password="${db_password:-${DB_PASSWORD:-}}" - - if [ -z "$backup_password" ]; then - colorized_echo red "Error: Database password not found. Check DB_PASSWORD or SQLALCHEMY_DATABASE_URL in .env" - error_messages+=("TimescaleDB password not found.") - elif [ -z "$db_name" ]; then - colorized_echo red "Error: Database name not found in SQLALCHEMY_DATABASE_URL" - error_messages+=("TimescaleDB database name not found.") - else - colorized_echo blue "Backing up TimescaleDB database '$db_name' from container: $container_name (using user: $backup_user)" - export PGPASSWORD="$backup_password" - if ! docker exec "$container_name" pg_dump -U "$backup_user" -d "$db_name" --clean --if-exists >"$temp_dir/db_backup.sql" 2>>"$log_file"; then - colorized_echo red "TimescaleDB dump failed. Check log file for details: $log_file" - error_messages+=("TimescaleDB dump failed for database '$db_name'.") - else - colorized_echo green "TimescaleDB backup completed successfully" - fi - unset PGPASSWORD - fi - fi - fi - else - # Remote database - would need postgresql-client installed - colorized_echo red "Remote TimescaleDB backup not yet supported. Please use local database or install postgresql-client." - error_messages+=("Remote TimescaleDB backup not yet supported. Please use local database or install postgresql-client.") - fi - ;; - sqlite) - if [ -f "$sqlite_file" ]; then - if ! cp "$sqlite_file" "$temp_dir/db_backup.sqlite" 2>>"$log_file"; then - error_messages+=("Failed to copy SQLite database.") - fi - else - error_messages+=("SQLite database file not found at $sqlite_file.") - fi - ;; - esac - else - colorized_echo yellow "Warning: No database type detected. Skipping database backup." - echo "Warning: No database type detected." >>"$log_file" - echo "SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL:-not set}" >>"$log_file" - fi - - colorized_echo blue "Copying configuration files..." - if ! cp "$APP_DIR/.env" "$temp_dir/" 2>>"$log_file"; then - error_messages+=("Failed to copy .env file.") - echo "Failed to copy .env file" >>"$log_file" - fi - if ! cp "$APP_DIR/docker-compose.yml" "$temp_dir/" 2>>"$log_file"; then - error_messages+=("Failed to copy docker-compose.yml file.") - echo "Failed to copy docker-compose.yml file" >>"$log_file" - fi - - colorized_echo blue "Copying data directory..." - # Ensure destination directory exists and is empty (already cleaned above, but be explicit) - if [ -d "$DATA_DIR" ]; then - if ! rsync -av --exclude 'xray-core' --exclude 'mysql' "$DATA_DIR/" "$temp_dir/pasarguard_data/" >>"$log_file" 2>&1; then - error_messages+=("Failed to copy data directory.") - echo "Failed to copy data directory" >>"$log_file" - fi - else - colorized_echo yellow "Data directory $DATA_DIR does not exist. Skipping data directory backup." - echo "Data directory $DATA_DIR does not exist. Skipping." >>"$log_file" - # Create empty directory structure so tar doesn't fail - mkdir -p "$temp_dir/pasarguard_data" - fi - - # Remove Unix socket files so zip doesn't fail with ENXIO ("No such device or address") - if [ -d "$temp_dir" ]; then - local socket_files - socket_files=$(find "$temp_dir" -type s -print 2>/dev/null || true) - if [ -n "$socket_files" ]; then - colorized_echo yellow "Removing Unix socket files before archiving (zip cannot archive sockets)." - printf "%s\n" "$socket_files" >>"$log_file" - find "$temp_dir" -type s -delete >>"$log_file" 2>&1 || true - fi - fi - - colorized_echo blue "Creating backup archive..." - # Verify temp_dir exists and has content before creating archive - if [ ! -d "$temp_dir" ] || [ -z "$(ls -A "$temp_dir" 2>/dev/null)" ]; then - error_messages+=("Temporary directory is empty or missing. Cannot create archive.") - echo "Temporary directory is empty or missing: $temp_dir" >>"$log_file" - elif ! (cd "$temp_dir" && zip -rq -s "$split_size_arg" "$backup_file" .) 2>>"$log_file"; then - error_messages+=("Failed to create backup archive.") - echo "Failed to create backup archive." >>"$log_file" - else - local backup_size=$(du -h "$backup_file" | cut -f1) - colorized_echo green "Backup archive created: $backup_file (Size: $backup_size)" - fi - - if [ -f "$backup_file" ]; then - while IFS= read -r file; do - final_backup_paths+=("$file") - done < <(find "$backup_dir" -maxdepth 1 -type f -name "backup_${timestamp}.z[0-9][0-9]" | sort) - final_backup_paths+=("$backup_file") - fi - - # Clean up temp directory after archive is created - rm -rf "$temp_dir" - - if [ ${#error_messages[@]} -gt 0 ]; then - colorized_echo red "Backup completed with errors:" - for error in "${error_messages[@]}"; do - colorized_echo red " - $error" - done - colorized_echo yellow "Check log file: $log_file" - if [ -f "$ENV_FILE" ]; then - send_backup_error_to_telegram "${error_messages[*]}" "$log_file" - fi - return 1 - fi - - if [ ${#final_backup_paths[@]} -eq 0 ]; then - colorized_echo red "Backup file was not created. Check log file: $log_file" - return 1 - fi - - if [ ${#final_backup_paths[@]} -eq 1 ]; then - colorized_echo green "Backup completed successfully: ${final_backup_paths[0]}" - else - colorized_echo green "Backup completed successfully in ${#final_backup_paths[@]} parts:" - for part in "${final_backup_paths[@]}"; do - colorized_echo green " - $(basename "$part")" - done - fi - if [ -f "$ENV_FILE" ]; then - send_backup_to_telegram "$backup_file" - fi -} - set_pasarguard_panel_image() { local target_image="$1" local service_name="" @@ -3299,7 +1107,7 @@ check_existing_database_volumes() { return 0 fi - colorized_echo yellow "⚠️ WARNING: Found existing volumes/directories that may conflict with the installation:" + colorized_echo yellow "⚠️ WARNING: Found existing volumes/directories that may conflict with the installation:" for path in "${existing_paths[@]}"; do local dir_size=$(du -sh "$path" 2>/dev/null | cut -f1 || echo "unknown size") @@ -3322,7 +1130,7 @@ check_existing_database_volumes() { done echo - colorized_echo red "⚠️ DANGER: These volumes may contain data from a previous pasarguard installation." + colorized_echo red "⚠️ DANGER: These volumes may contain data from a previous pasarguard installation." colorized_echo yellow "If you proceed without deleting them, there may be conflicts or data corruption." echo colorized_echo cyan "Do you want to delete these volumes? (default: no)" @@ -3334,9 +1142,9 @@ check_existing_database_volumes() { for path in "${existing_paths[@]}"; do if rm -rf "$path" 2>/dev/null; then - colorized_echo green "✓ Deleted directory: $path" + colorized_echo green "✓ Deleted directory: $path" else - colorized_echo red "✗ Failed to delete directory: $path (may be in use or permission denied)" + colorized_echo red "✗ Failed to delete directory: $path (may be in use or permission denied)" fi done @@ -3345,9 +1153,9 @@ check_existing_database_volumes() { local actual_vol=$(docker volume ls --format '{{.Name}}' 2>/dev/null | grep -E "^${prefixed_vol}$|^${vol_name}$" | head -n1) if [ -n "$actual_vol" ]; then if docker volume rm "$actual_vol" >/dev/null 2>&1; then - colorized_echo green "✓ Deleted Docker volume: $actual_vol" + colorized_echo green "✓ Deleted Docker volume: $actual_vol" else - colorized_echo red "✗ Failed to delete Docker volume: $actual_vol (may be in use)" + colorized_echo red "✗ Failed to delete Docker volume: $actual_vol (may be in use)" fi fi done @@ -3914,7 +1722,7 @@ update_command() { update_pasarguard_script() { FETCH_REPO="PasarGuard/scripts" colorized_echo blue "Updating pasarguard script" - install_shared_libs_from_repo "$FETCH_REPO" + install_shared_libs_from_repo "$FETCH_REPO" common.sh system.sh docker.sh github.sh env.sh pasarguard-backup.sh pasarguard-restore.sh github_install_script_from_repo "$FETCH_REPO" "pasarguard.sh" "pasarguard" colorized_echo green "pasarguard script updated successfully" } @@ -4010,24 +1818,24 @@ usage() { echo colorized_echo cyan "Commands:" - colorized_echo yellow " up $(tput sgr0)– Start services" - colorized_echo yellow " down $(tput sgr0)– Stop services" - colorized_echo yellow " restart $(tput sgr0)– Restart services" - colorized_echo yellow " status $(tput sgr0)– Show status" - colorized_echo yellow " logs $(tput sgr0)– Show logs" - colorized_echo yellow " cli $(tput sgr0)– pasarguard CLI" - colorized_echo yellow " tui $(tput sgr0)– pasarguard TUI" - colorized_echo yellow " install $(tput sgr0)– Install pasarguard" - colorized_echo yellow " update $(tput sgr0)– Update to latest version" - colorized_echo yellow " uninstall $(tput sgr0)– Uninstall pasarguard" - colorized_echo yellow " install-script $(tput sgr0)– Install pasarguard script" - colorized_echo yellow " install-node $(tput sgr0)– Install PasarGuard node" - colorized_echo yellow " backup $(tput sgr0)– Manual backup launch" - colorized_echo yellow " backup-service $(tput sgr0)– pasarguard Backup service to backup to TG, and a new job in crontab" - colorized_echo yellow " restore $(tput sgr0)– Restore database from backup file" - colorized_echo yellow " edit $(tput sgr0)– Edit docker-compose.yml (via nano or vi editor)" - colorized_echo yellow " edit-env $(tput sgr0)– Edit environment file (via nano or vi editor)" - colorized_echo yellow " help $(tput sgr0)– Show this help message" + colorized_echo yellow " up $(tput sgr0)– Start services" + colorized_echo yellow " down $(tput sgr0)– Stop services" + colorized_echo yellow " restart $(tput sgr0)– Restart services" + colorized_echo yellow " status $(tput sgr0)– Show status" + colorized_echo yellow " logs $(tput sgr0)– Show logs" + colorized_echo yellow " cli $(tput sgr0)– pasarguard CLI" + colorized_echo yellow " tui $(tput sgr0)– pasarguard TUI" + colorized_echo yellow " install $(tput sgr0)– Install pasarguard" + colorized_echo yellow " update $(tput sgr0)– Update to latest version" + colorized_echo yellow " uninstall $(tput sgr0)– Uninstall pasarguard" + colorized_echo yellow " install-script $(tput sgr0)– Install pasarguard script" + colorized_echo yellow " install-node $(tput sgr0)– Install PasarGuard node" + colorized_echo yellow " backup $(tput sgr0)– Manual backup launch" + colorized_echo yellow " backup-service $(tput sgr0)– pasarguard Backup service to backup to TG, and a new job in crontab" + colorized_echo yellow " restore $(tput sgr0)– Restore database from backup file" + colorized_echo yellow " edit $(tput sgr0)– Edit docker-compose.yml (via nano or vi editor)" + colorized_echo yellow " edit-env $(tput sgr0)– Edit environment file (via nano or vi editor)" + colorized_echo yellow " help $(tput sgr0)– Show this help message" echo colorized_echo cyan "Directories:" diff --git a/pg-node.sh b/pg-node.sh index ee55b94..32a0237 100755 --- a/pg-node.sh +++ b/pg-node.sh @@ -191,7 +191,7 @@ install_node_script() { sed -i "s|^APP_NAME=.*|APP_NAME=\"$APP_NAME\"|" "$TEMP_FILE" fi - install_shared_libs_from_repo "$FETCH_REPO" + install_shared_libs_from_repo "$FETCH_REPO" common.sh system.sh docker.sh github.sh # Remove old file if it exists if [ -f "$TARGET_PATH" ]; then @@ -730,7 +730,7 @@ follow_node_logs() { } update_node_script() { colorized_echo blue "Updating node script" - install_shared_libs_from_repo "$FETCH_REPO" + install_shared_libs_from_repo "$FETCH_REPO" common.sh system.sh docker.sh github.sh github_install_script_from_repo "$FETCH_REPO" "pg-node.sh" "$APP_NAME" colorized_echo green "node script updated successfully" } From edde1ab9e9c23ca8b33fae0fdb7fdd1ba3abc6e7 Mon Sep 17 00:00:00 2001 From: M03ED <50927468+M03ED@users.noreply.github.com> Date: Fri, 1 May 2026 23:29:38 +0330 Subject: [PATCH 6/9] fix --- lib/docker.sh | 4 +++- lib/github.sh | 25 ++++++++++++++++++++++++- lib/system.sh | 8 ++++++++ pasarguard.sh | 43 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/lib/docker.sh b/lib/docker.sh index 0f3e77e..e43cde2 100644 --- a/lib/docker.sh +++ b/lib/docker.sh @@ -2,7 +2,9 @@ install_docker() { colorized_echo blue "Installing Docker" - curl -fsSL https://get.docker.com | sh + if ! bash -o pipefail -c 'curl -fsSL https://get.docker.com | sh'; then + die "Failed to install Docker" + fi colorized_echo green "Docker installed successfully" } diff --git a/lib/github.sh b/lib/github.sh index c6c5c67..17c1e16 100644 --- a/lib/github.sh +++ b/lib/github.sh @@ -20,8 +20,31 @@ github_install_script_from_repo() { local repo="$1" local script_name="$2" local install_name="$3" + local tmp_file="" - curl -fsSL "$(github_raw_url "$repo" "$script_name")" | install -m 755 /dev/stdin "/usr/local/bin/$install_name" + tmp_file=$(mktemp) || return 1 + trap 'rm -f "$tmp_file"' RETURN + + if ! curl -fSL "$(github_raw_url "$repo" "$script_name")" -o "$tmp_file"; then + trap - RETURN + rm -f "$tmp_file" + return 1 + fi + + if ! chmod 755 "$tmp_file"; then + trap - RETURN + rm -f "$tmp_file" + return 1 + fi + + if ! install -m 755 "$tmp_file" "/usr/local/bin/$install_name"; then + trap - RETURN + rm -f "$tmp_file" + return 1 + fi + + trap - RETURN + rm -f "$tmp_file" } install_shared_libs_from_local() { diff --git a/lib/system.sh b/lib/system.sh index 81ef122..6b2b206 100644 --- a/lib/system.sh +++ b/lib/system.sh @@ -21,6 +21,10 @@ detect_os() { } detect_and_update_package_manager() { + if [ -z "${OS:-}" ]; then + detect_os + fi + colorized_echo blue "Updating package manager" if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then @@ -47,6 +51,10 @@ detect_and_update_package_manager() { install_package() { local package="$1" + if [ -z "${OS:-}" ]; then + detect_os + fi + if [ -z "${PKG_MANAGER:-}" ]; then detect_and_update_package_manager fi diff --git a/pasarguard.sh b/pasarguard.sh index d3f1dd6..f8f1415 100755 --- a/pasarguard.sh +++ b/pasarguard.sh @@ -3,11 +3,52 @@ set -e SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SHARED_LIB_DIR="${SCRIPT_DIR}/lib" +REQUIRED_SHARED_LIBS="common.sh system.sh docker.sh github.sh env.sh pasarguard-backup.sh pasarguard-restore.sh" if [ ! -f "$SHARED_LIB_DIR/common.sh" ]; then SHARED_LIB_DIR="/usr/local/lib/pasarguard-scripts/lib" fi -for shared_lib in common.sh system.sh docker.sh github.sh env.sh pasarguard-backup.sh pasarguard-restore.sh; do +bootstrap_pasarguard_shared_libs() { + local fetch_repo="PasarGuard/scripts" + local bootstrap_dir="/usr/local/lib/pasarguard-scripts/lib" + local tmp_dir="" + local shared_lib="" + + tmp_dir=$(mktemp -d) || return 1 + mkdir -p "$bootstrap_dir" || { + rm -rf "$tmp_dir" + return 1 + } + + for shared_lib in $REQUIRED_SHARED_LIBS; do + if ! curl -fsSL "https://github.com/${fetch_repo}/raw/main/lib/${shared_lib}" -o "$tmp_dir/$shared_lib"; then + rm -rf "$tmp_dir" + return 1 + fi + if ! install -m 644 "$tmp_dir/$shared_lib" "$bootstrap_dir/$shared_lib"; then + rm -rf "$tmp_dir" + return 1 + fi + done + + rm -rf "$tmp_dir" + SHARED_LIB_DIR="$bootstrap_dir" + return 0 +} + +missing_shared_lib=false +for shared_lib in $REQUIRED_SHARED_LIBS; do + if [ ! -f "$SHARED_LIB_DIR/$shared_lib" ]; then + missing_shared_lib=true + break + fi +done + +if [ "$missing_shared_lib" = true ]; then + bootstrap_pasarguard_shared_libs +fi + +for shared_lib in $REQUIRED_SHARED_LIBS; do if [ ! -f "$SHARED_LIB_DIR/$shared_lib" ]; then printf 'Missing shared library: %s\n' "$SHARED_LIB_DIR/$shared_lib" >&2 exit 1 From a61d3dcdf1335041eeeb64333f33b88b6886d37f Mon Sep 17 00:00:00 2001 From: M03ED <50927468+M03ED@users.noreply.github.com> Date: Fri, 1 May 2026 23:46:13 +0330 Subject: [PATCH 7/9] fix --- lib/pasarguard-backup.sh | 15 ++++++++++----- lib/pasarguard-restore.sh | 29 ++++++++++++++++++----------- lib/system.sh | 35 +++++++++++++++++++++++++++++++---- pasarguard.sh | 2 +- 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/lib/pasarguard-backup.sh b/lib/pasarguard-backup.sh index 568b9ba..30d5213 100644 --- a/lib/pasarguard-backup.sh +++ b/lib/pasarguard-backup.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env bash send_backup_to_telegram() { if [ -f "$ENV_FILE" ]; then @@ -58,6 +58,7 @@ send_backup_to_telegram() { fi local backup_paths=() + local uploaded_files=() local cleanup_dir="" local telegram_split_bytes=$((49 * 1000 * 1000)) @@ -140,6 +141,7 @@ send_backup_to_telegram() { if [ "$http_code" == "200" ]; then # Check if response contains "ok":true if echo "$response_body" | grep -q '"ok":true'; then + uploaded_files+=("$custom_filename") colorized_echo green "Backup part $custom_filename successfully sent to Telegram." else # Extract error message from Telegram response @@ -691,7 +693,7 @@ remove_backup_service() { sed -i '/BACKUP_PROXY_URL/d' "$ENV_FILE" local temp_cron=$(mktemp) - crontab -l 2>/dev/null >"$temp_cron" + crontab -l 2>/dev/null >"$temp_cron" || true sed -i '/# pasarguard-backup-service/d' "$temp_cron" @@ -779,10 +781,13 @@ backup_command() { local db_password="" local db_name="" local container_name="" + local safe_sqlalchemy_url="" + + safe_sqlalchemy_url=$(printf '%s' "${SQLALCHEMY_DATABASE_URL:-not set}" | sed -E 's#^([^:]+://)([^@/]+)@#\1REDACTED@#') # SQLALCHEMY_DATABASE_URL should already be loaded from .env above # Just log what we have - echo "SQLALCHEMY_DATABASE_URL from environment: ${SQLALCHEMY_DATABASE_URL:-not set}" >>"$log_file" + echo "SQLALCHEMY_DATABASE_URL from environment: ${safe_sqlalchemy_url}" >>"$log_file" if [ -z "$SQLALCHEMY_DATABASE_URL" ]; then colorized_echo red "Error: SQLALCHEMY_DATABASE_URL not found in .env file or not set" @@ -793,7 +798,7 @@ backup_command() { fi if [ -n "$SQLALCHEMY_DATABASE_URL" ]; then - echo "Parsing SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL%%@*}" >>"$log_file" + echo "Parsing SQLALCHEMY_DATABASE_URL: ${safe_sqlalchemy_url}" >>"$log_file" # Extract database type from scheme if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^sqlite ]]; then @@ -1221,7 +1226,7 @@ backup_command() { else colorized_echo yellow "Warning: No database type detected. Skipping database backup." echo "Warning: No database type detected." >>"$log_file" - echo "SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL:-not set}" >>"$log_file" + echo "SQLALCHEMY_DATABASE_URL: ${safe_sqlalchemy_url}" >>"$log_file" fi colorized_echo blue "Copying configuration files..." diff --git a/lib/pasarguard-restore.sh b/lib/pasarguard-restore.sh index b62fd0e..e2931bf 100644 --- a/lib/pasarguard-restore.sh +++ b/lib/pasarguard-restore.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env bash restore_command() { colorized_echo blue "Starting restore process..." @@ -720,20 +720,23 @@ restore_command() { # Drop and recreate the target database for a clean slate colorized_echo blue "Dropping and recreating database '$target_db_name'..." docker exec "$container_name" psql -U postgres -d postgres \ - -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$target_db_name' AND pid <> pg_backend_pid();" \ + -v db_name="$target_db_name" \ + -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = :'db_name' AND pid <> pg_backend_pid();" \ >>"$log_file" 2>&1 docker exec "$container_name" psql -U postgres -d postgres \ - -c "DROP DATABASE IF EXISTS \"$target_db_name\";" >>"$log_file" 2>&1 + -v db_name="$target_db_name" \ + -c "DROP DATABASE IF EXISTS :\"db_name\";" >>"$log_file" 2>&1 docker exec "$container_name" psql -U postgres -d postgres \ - -c "CREATE DATABASE \"$target_db_name\" OWNER \"$target_db_owner\";" >>"$log_file" 2>&1 + -v db_name="$target_db_name" -v db_owner="$target_db_owner" \ + -c "CREATE DATABASE :\"db_name\" OWNER :\"db_owner\";" >>"$log_file" 2>&1 # Create the timescaledb extension in the fresh database - docker exec "$container_name" psql -U postgres -d "$target_db_name" \ + docker exec "$container_name" psql -U postgres --dbname="$target_db_name" \ -c "CREATE EXTENSION IF NOT EXISTS timescaledb;" >>"$log_file" 2>&1 # Call pre_restore to put TimescaleDB into restore mode colorized_echo blue "Calling timescaledb_pre_restore()..." - docker exec "$container_name" psql -U postgres -d "$target_db_name" \ + docker exec "$container_name" psql -U postgres --dbname="$target_db_name" \ -c "SELECT timescaledb_pre_restore();" >>"$log_file" 2>&1 # Filter out extension DROP/CREATE statements from the dump. @@ -745,12 +748,12 @@ restore_command() { # Restore the filtered dump with ON_ERROR_STOP so psql exits non-zero on SQL errors colorized_echo blue "Restoring database dump..." - if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U "$restore_user" -d "$target_db_name" < "$temp_restore_dir/db_backup_filtered.sql" 2>>"$log_file"; then + if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U "$restore_user" --dbname="$target_db_name" < "$temp_restore_dir/db_backup_filtered.sql" 2>>"$log_file"; then restore_success=true else # Fallback: try with postgres superuser colorized_echo yellow "Trying with postgres superuser..." - if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U postgres -d "$target_db_name" < "$temp_restore_dir/db_backup_filtered.sql" 2>>"$log_file"; then + if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U postgres --dbname="$target_db_name" < "$temp_restore_dir/db_backup_filtered.sql" 2>>"$log_file"; then restore_success=true fi fi @@ -760,7 +763,7 @@ restore_command() { # Call post_restore regardless of outcome to leave DB in a usable state colorized_echo blue "Calling timescaledb_post_restore()..." - docker exec "$container_name" psql -U postgres -d "$target_db_name" \ + docker exec "$container_name" psql -U postgres --dbname="$target_db_name" \ -c "SELECT timescaledb_post_restore();" >>"$log_file" 2>&1 if [ "$restore_success" = true ]; then @@ -832,7 +835,9 @@ restore_command() { # Restore configuration files if needed colorized_echo blue "Restoring configuration files..." if [ -f "$temp_restore_dir/.env" ]; then - cp "$temp_restore_dir/.env" "$APP_DIR/.env.backup.$(date +%Y%m%d%H%M%S)" 2>>"$log_file" + if [ -f "$APP_DIR/.env" ]; then + cp "$APP_DIR/.env" "$APP_DIR/.env.backup.$(date +%Y%m%d%H%M%S)" 2>>"$log_file" + fi cp "$temp_restore_dir/.env" "$APP_DIR/.env" 2>>"$log_file" colorized_echo green "Environment file restored." local preserve_db_credentials=false @@ -863,7 +868,9 @@ restore_command() { fi if [ -f "$temp_restore_dir/docker-compose.yml" ]; then - cp "$temp_restore_dir/docker-compose.yml" "$APP_DIR/docker-compose.yml.backup.$(date +%Y%m%d%H%M%S)" 2>>"$log_file" + if [ -f "$APP_DIR/docker-compose.yml" ]; then + cp "$APP_DIR/docker-compose.yml" "$APP_DIR/docker-compose.yml.backup.$(date +%Y%m%d%H%M%S)" 2>>"$log_file" + fi cp "$temp_restore_dir/docker-compose.yml" "$APP_DIR/docker-compose.yml" 2>>"$log_file" colorized_echo green "Docker Compose file restored." fi diff --git a/lib/system.sh b/lib/system.sh index 6b2b206..125a3c0 100644 --- a/lib/system.sh +++ b/lib/system.sh @@ -7,7 +7,7 @@ check_running_as_root() { } detect_os() { - if [ -f /etc/lsb-release ]; then + if [ -f /etc/lsb-release ] && command -v lsb_release >/dev/null 2>&1; then OS=$(lsb_release -si) elif [ -f /etc/os-release ]; then OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') @@ -150,6 +150,11 @@ install_yq() { local base_url="https://github.com/mikefarah/yq/releases/latest/download" local yq_binary="" local yq_url="" + local checksum_url="${base_url}/checksums" + local binary_tmp="" + local checksum_tmp="" + local expected_checksum="" + local actual_checksum="" if command -v yq >/dev/null 2>&1; then colorized_echo green "yq is already installed." @@ -184,16 +189,38 @@ install_yq() { install_package curl || die "Failed to install curl. Please install curl or wget manually." fi + binary_tmp=$(create_temp_file "yq" ".bin") + checksum_tmp=$(create_temp_file "yq" ".checksums") + if command -v curl >/dev/null 2>&1; then - curl -L "$yq_url" -o /usr/local/bin/yq || die "Failed to download yq using curl. Please check your internet connection." + curl -fsSL "$yq_url" -o "$binary_tmp" || die "Failed to download yq using curl. Please check your internet connection." + curl -fsSL "$checksum_url" -o "$checksum_tmp" || die "Failed to download yq checksums using curl." elif command -v wget >/dev/null 2>&1; then - wget -O /usr/local/bin/yq "$yq_url" || die "Failed to download yq using wget. Please check your internet connection." + wget -q -O "$binary_tmp" "$yq_url" || die "Failed to download yq using wget. Please check your internet connection." + wget -q -O "$checksum_tmp" "$checksum_url" || die "Failed to download yq checksums using wget." fi - chmod +x /usr/local/bin/yq + expected_checksum=$(awk -v name="$yq_binary" '$2 == name { print $1; exit }' "$checksum_tmp") + [ -n "$expected_checksum" ] || die "Failed to resolve published checksum for $yq_binary." + + if command -v sha256sum >/dev/null 2>&1; then + actual_checksum=$(sha256sum "$binary_tmp" | awk '{print $1}') + elif command -v shasum >/dev/null 2>&1; then + actual_checksum=$(shasum -a 256 "$binary_tmp" | awk '{print $1}') + elif command -v openssl >/dev/null 2>&1; then + actual_checksum=$(openssl dgst -sha256 "$binary_tmp" | awk '{print $NF}') + else + die "No SHA-256 tool available to verify yq download." + fi + + [ "$actual_checksum" = "$expected_checksum" ] || die "Downloaded yq checksum mismatch." + + install -m 755 "$binary_tmp" /usr/local/bin/yq colorized_echo green "yq installed successfully!" if ! echo "$PATH" | grep -q "/usr/local/bin"; then export PATH="/usr/local/bin:$PATH" fi + + rm -f "$binary_tmp" "$checksum_tmp" } diff --git a/pasarguard.sh b/pasarguard.sh index f8f1415..e845ad4 100755 --- a/pasarguard.sh +++ b/pasarguard.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env bash set -e SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" From a021ba5282fa038d7e31036c1a15e0fc675ae324 Mon Sep 17 00:00:00 2001 From: M03ED <50927468+M03ED@users.noreply.github.com> Date: Wed, 6 May 2026 12:12:20 +0330 Subject: [PATCH 8/9] fix: emoji --- lib/pasarguard-backup.sh | 33 +++++++++++++-------------- lib/pasarguard-restore.sh | 29 ++++++++++++----------- pasarguard.sh | 48 +++++++++++++++++++-------------------- 3 files changed, 54 insertions(+), 56 deletions(-) diff --git a/lib/pasarguard-backup.sh b/lib/pasarguard-backup.sh index 30d5213..560df99 100644 --- a/lib/pasarguard-backup.sh +++ b/lib/pasarguard-backup.sh @@ -127,7 +127,7 @@ send_backup_to_telegram() { local escaped_server_ip=$(printf '%s' "$server_ip" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g') local escaped_filename=$(printf '%s' "$custom_filename" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g') local escaped_time=$(printf '%s' "$backup_time" | sed 's/[_*\[\]()~`>#+\-=|{}!.]/\\&/g') - local caption="📦 *Backup Information*\n🌐 *Server IP*: \`$escaped_server_ip\`\n📁 *Backup File*: \`$escaped_filename\`\n⏰ *Backup Time*: \`$escaped_time\`" + local caption="📦 *Backup Information*\n🌐 *Server IP*: \`$escaped_server_ip\`\n📁 *Backup File*: \`$escaped_filename\`\n⏰ *Backup Time*: \`$escaped_time\`" local response=$(curl "${curl_proxy_args[@]}" -s -w "\n%{http_code}" -F chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ -F document=@"$part;filename=$custom_filename" \ @@ -164,17 +164,17 @@ send_backup_to_telegram() { done files_list="${files_list%$'\n'}" - local info_message=$'📦 Backup Upload Summary\n' - info_message+=$'──────────────────────\n' - info_message+="🌐 Server IP: $server_ip"$'\n' - info_message+="⏰ Time: $backup_time"$'\n' - info_message+=$'\n✅ Files Uploaded:\n' + local info_message=$'📦 Backup Upload Summary\n' + info_message+=$'──────────────────────\n' + info_message+="🌐 Server IP: $server_ip"$'\n' + info_message+="⏰ Time: $backup_time"$'\n' + info_message+=$'\n✅ Files Uploaded:\n' info_message+="$files_list"$'\n' - info_message+=$'\n📂 Extraction Guide:\n' - info_message+=$'🪟 Windows: Install and use 7-Zip. Place the .zip and every .zXX part together, then start extraction from the .zip file.\n' - info_message+=$'🐧 Linux: Run unzip (e.g., unzip backup_xxx.zip) with all .zXX parts in the same directory.\n' - info_message+=$'🍎 macOS: Use Archive Utility or run unzip backup_xxx.zip from Terminal with the .zXX parts beside the .zip file.\n' - info_message+=$'⚠️ Always download the .zip and every .zXX part before extracting.' + info_message+=$'\n📂 Extraction Guide:\n' + info_message+=$'🪟 Windows: Install and use 7-Zip. Place the .zip and every .zXX part together, then start extraction from the .zip file.\n' + info_message+=$'🐧 Linux: Run unzip (e.g., unzip backup_xxx.zip) with all .zXX parts in the same directory.\n' + info_message+=$'🍎 macOS: Use Archive Utility or run unzip backup_xxx.zip from Terminal with the .zXX parts beside the .zip file.\n' + info_message+=$'⚠️ Always download the .zip and every .zXX part before extracting.' curl "${curl_proxy_args[@]}" -s -X POST "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendMessage" \ -d chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ @@ -202,10 +202,10 @@ send_backup_error_to_telegram() { server_ip="Unknown IP" fi local error_time=$(date "+%Y-%m-%d %H:%M:%S %Z") - local message="⚠️ Backup Error Notification -🌐 Server IP: $server_ip -❌ Errors: $error_messages -⏰ Time: $error_time" + local message="⚠️ Backup Error Notification +🌐 Server IP: $server_ip +❌ Errors: $error_messages +⏰ Time: $error_time" local max_length=1000 if [ ${#message} -gt $max_length ]; then @@ -224,7 +224,7 @@ send_backup_error_to_telegram() { response=$(curl "${curl_proxy_args[@]}" -s -w "%{http_code}" -o /tmp/tg_response.json \ -F chat_id="$BACKUP_TELEGRAM_CHAT_ID" \ -F document=@"$log_file;filename=backup_error.log" \ - -F caption="📜 Backup Error Log - $error_time" \ + -F caption="📜 Backup Error Log - $error_time" \ "https://api.telegram.org/bot$BACKUP_TELEGRAM_BOT_KEY/sendDocument") http_code="${response:(-3)}" @@ -1316,4 +1316,3 @@ backup_command() { send_backup_to_telegram "$backup_file" fi } - diff --git a/lib/pasarguard-restore.sh b/lib/pasarguard-restore.sh index e2931bf..9566975 100644 --- a/lib/pasarguard-restore.sh +++ b/lib/pasarguard-restore.sh @@ -215,7 +215,7 @@ restore_command() { exit 1 fi archive_to_extract="$concatenated_file" - colorized_echo green "✓ Combined $part_count part(s)" + colorized_echo green "✓ Combined $part_count part(s)" elif [[ "$selected_filename" =~ \.zip$ ]]; then archive_format="zip" local base_name="${selected_filename%.zip}" @@ -241,7 +241,7 @@ restore_command() { local concatenated_file="$temp_restore_dir/${base_name}_combined.zip" if command -v zip >/dev/null 2>&1 && zip -s 0 "$selected_file" --out "$concatenated_file" >>"$log_file" 2>&1; then archive_to_extract="$concatenated_file" - colorized_echo green "✓ Rebuilt split zip archive with zip utility" + colorized_echo green "✓ Rebuilt split zip archive with zip utility" else if command -v zip >/dev/null 2>&1; then colorized_echo yellow "zip rebuild failed. Falling back to direct concatenation..." @@ -264,7 +264,7 @@ restore_command() { exit 1 fi archive_to_extract="$concatenated_file" - colorized_echo green "✓ Combined $((part_count + 1)) split part(s)" + colorized_echo green "✓ Combined $((part_count + 1)) split part(s)" fi fi else @@ -303,7 +303,7 @@ restore_command() { exit 1 fi fi - colorized_echo green "✓ Archive extracted successfully" + colorized_echo green "✓ Archive extracted successfully" # Load environment variables from extracted .env colorized_echo blue "Loading configuration from backup..." @@ -388,7 +388,7 @@ restore_command() { rm -f "$cleaned_env" fi - colorized_echo green "✓ Loaded $env_vars_loaded environment variables" + colorized_echo green "✓ Loaded $env_vars_loaded environment variables" if [ -z "$SQLALCHEMY_DATABASE_URL" ]; then colorized_echo red "SQLALCHEMY_DATABASE_URL not found in backup .env file" @@ -398,13 +398,13 @@ restore_command() { exit 1 fi - colorized_echo green "✓ Found SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL:0:50}..." + colorized_echo green "✓ Found SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL:0:50}..." # Parse database configuration (similar to backup function) colorized_echo blue "Detecting database type..." if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^sqlite ]]; then db_type="sqlite" - colorized_echo green "✓ Detected SQLite database" + colorized_echo green "✓ Detected SQLite database" local sqlite_url_part="${SQLALCHEMY_DATABASE_URL#*://}" sqlite_url_part="${sqlite_url_part%%\?*}" sqlite_url_part="${sqlite_url_part%%#*}" @@ -420,19 +420,19 @@ restore_command() { elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^(mysql|mariadb|postgresql)[^:]*:// ]]; then if [[ "$SQLALCHEMY_DATABASE_URL" =~ ^mariadb[^:]*:// ]]; then db_type="mariadb" - colorized_echo green "✓ Detected MariaDB database" + colorized_echo green "✓ Detected MariaDB database" elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^mysql[^:]*:// ]]; then db_type="mysql" - colorized_echo green "✓ Detected MySQL database" + colorized_echo green "✓ Detected MySQL database" elif [[ "$SQLALCHEMY_DATABASE_URL" =~ ^postgresql[^:]*:// ]]; then # Check if it's timescaledb - use set +e to prevent failure on file not found set +e if grep -q "image: timescale/timescaledb" "$temp_restore_dir/docker-compose.yml" 2>/dev/null; then db_type="timescaledb" - colorized_echo green "✓ Detected TimescaleDB database" + colorized_echo green "✓ Detected TimescaleDB database" else db_type="postgresql" - colorized_echo green "✓ Detected PostgreSQL database" + colorized_echo green "✓ Detected PostgreSQL database" fi set -e fi @@ -484,10 +484,10 @@ restore_command() { exit 1 fi - colorized_echo green "✓ Database configuration detected: $db_type" + colorized_echo green "✓ Database configuration detected: $db_type" # Confirm restore - colorized_echo red "⚠️ DANGER: This will PERMANENTLY overwrite your current $db_type database!" + colorized_echo red "⚠️ DANGER: This will PERMANENTLY overwrite your current $db_type database!" colorized_echo yellow "WARNING: This will overwrite your current $db_type database!" colorized_echo blue "Database type: $db_type" if [ -n "$db_name" ]; then @@ -523,7 +523,7 @@ restore_command() { fi # Perform restore - colorized_echo red "⚠️ DANGER: Starting database restore - this will overwrite existing data!" + colorized_echo red "⚠️ DANGER: Starting database restore - this will overwrite existing data!" colorized_echo blue "Starting database restore..." case $db_type in @@ -891,4 +891,3 @@ restore_command() { colorized_echo green "Restore completed successfully!" colorized_echo green "PasarGuard services have been restarted." } - diff --git a/pasarguard.sh b/pasarguard.sh index e845ad4..0da005e 100755 --- a/pasarguard.sh +++ b/pasarguard.sh @@ -1148,7 +1148,7 @@ check_existing_database_volumes() { return 0 fi - colorized_echo yellow "⚠️ WARNING: Found existing volumes/directories that may conflict with the installation:" + colorized_echo yellow "⚠️ WARNING: Found existing volumes/directories that may conflict with the installation:" for path in "${existing_paths[@]}"; do local dir_size=$(du -sh "$path" 2>/dev/null | cut -f1 || echo "unknown size") @@ -1171,7 +1171,7 @@ check_existing_database_volumes() { done echo - colorized_echo red "⚠️ DANGER: These volumes may contain data from a previous pasarguard installation." + colorized_echo red "⚠️ DANGER: These volumes may contain data from a previous pasarguard installation." colorized_echo yellow "If you proceed without deleting them, there may be conflicts or data corruption." echo colorized_echo cyan "Do you want to delete these volumes? (default: no)" @@ -1183,9 +1183,9 @@ check_existing_database_volumes() { for path in "${existing_paths[@]}"; do if rm -rf "$path" 2>/dev/null; then - colorized_echo green "✓ Deleted directory: $path" + colorized_echo green "✓ Deleted directory: $path" else - colorized_echo red "✗ Failed to delete directory: $path (may be in use or permission denied)" + colorized_echo red "✗ Failed to delete directory: $path (may be in use or permission denied)" fi done @@ -1194,9 +1194,9 @@ check_existing_database_volumes() { local actual_vol=$(docker volume ls --format '{{.Name}}' 2>/dev/null | grep -E "^${prefixed_vol}$|^${vol_name}$" | head -n1) if [ -n "$actual_vol" ]; then if docker volume rm "$actual_vol" >/dev/null 2>&1; then - colorized_echo green "✓ Deleted Docker volume: $actual_vol" + colorized_echo green "✓ Deleted Docker volume: $actual_vol" else - colorized_echo red "✗ Failed to delete Docker volume: $actual_vol (may be in use)" + colorized_echo red "✗ Failed to delete Docker volume: $actual_vol (may be in use)" fi fi done @@ -1859,24 +1859,24 @@ usage() { echo colorized_echo cyan "Commands:" - colorized_echo yellow " up $(tput sgr0)– Start services" - colorized_echo yellow " down $(tput sgr0)– Stop services" - colorized_echo yellow " restart $(tput sgr0)– Restart services" - colorized_echo yellow " status $(tput sgr0)– Show status" - colorized_echo yellow " logs $(tput sgr0)– Show logs" - colorized_echo yellow " cli $(tput sgr0)– pasarguard CLI" - colorized_echo yellow " tui $(tput sgr0)– pasarguard TUI" - colorized_echo yellow " install $(tput sgr0)– Install pasarguard" - colorized_echo yellow " update $(tput sgr0)– Update to latest version" - colorized_echo yellow " uninstall $(tput sgr0)– Uninstall pasarguard" - colorized_echo yellow " install-script $(tput sgr0)– Install pasarguard script" - colorized_echo yellow " install-node $(tput sgr0)– Install PasarGuard node" - colorized_echo yellow " backup $(tput sgr0)– Manual backup launch" - colorized_echo yellow " backup-service $(tput sgr0)– pasarguard Backup service to backup to TG, and a new job in crontab" - colorized_echo yellow " restore $(tput sgr0)– Restore database from backup file" - colorized_echo yellow " edit $(tput sgr0)– Edit docker-compose.yml (via nano or vi editor)" - colorized_echo yellow " edit-env $(tput sgr0)– Edit environment file (via nano or vi editor)" - colorized_echo yellow " help $(tput sgr0)– Show this help message" + colorized_echo yellow " up $(tput sgr0)– Start services" + colorized_echo yellow " down $(tput sgr0)– Stop services" + colorized_echo yellow " restart $(tput sgr0)– Restart services" + colorized_echo yellow " status $(tput sgr0)– Show status" + colorized_echo yellow " logs $(tput sgr0)– Show logs" + colorized_echo yellow " cli $(tput sgr0)– pasarguard CLI" + colorized_echo yellow " tui $(tput sgr0)– pasarguard TUI" + colorized_echo yellow " install $(tput sgr0)– Install pasarguard" + colorized_echo yellow " update $(tput sgr0)– Update to latest version" + colorized_echo yellow " uninstall $(tput sgr0)– Uninstall pasarguard" + colorized_echo yellow " install-script $(tput sgr0)– Install pasarguard script" + colorized_echo yellow " install-node $(tput sgr0)– Install PasarGuard node" + colorized_echo yellow " backup $(tput sgr0)– Manual backup launch" + colorized_echo yellow " backup-service $(tput sgr0)– pasarguard Backup service to backup to TG, and a new job in crontab" + colorized_echo yellow " restore $(tput sgr0)– Restore database from backup file" + colorized_echo yellow " edit $(tput sgr0)– Edit docker-compose.yml (via nano or vi editor)" + colorized_echo yellow " edit-env $(tput sgr0)– Edit environment file (via nano or vi editor)" + colorized_echo yellow " help $(tput sgr0)– Show this help message" echo colorized_echo cyan "Directories:" From cd4e86087b34dc9638bbfe6c15ff43ea039f1223 Mon Sep 17 00:00:00 2001 From: M03ED <50927468+M03ED@users.noreply.github.com> Date: Wed, 6 May 2026 13:10:11 +0330 Subject: [PATCH 9/9] fix --- lib/pasarguard-backup.sh | 130 +++++++++++++++++++++++++++++++------- lib/pasarguard-restore.sh | 29 +++++++-- 2 files changed, 129 insertions(+), 30 deletions(-) diff --git a/lib/pasarguard-backup.sh b/lib/pasarguard-backup.sh index 560df99..b3471c6 100644 --- a/lib/pasarguard-backup.sh +++ b/lib/pasarguard-backup.sh @@ -1,5 +1,29 @@ #!/usr/bin/env bash +mask_telegram_bot_key() { + local secret="$1" + local length=${#secret} + + if [ -z "$secret" ]; then + printf '%s\n' "" + return 0 + fi + + if [ "$length" -le 6 ]; then + printf '****%s\n' "$secret" + return 0 + fi + + printf '****%s\n' "${secret: -6}" +} + +filter_backup_cron_entries() { + local source_file="$1" + local target_file="$2" + + grep -F -v "# pasarguard-backup-service" "$source_file" >"$target_file" || true +} + send_backup_to_telegram() { if [ -f "$ENV_FILE" ]; then while IFS='=' read -r key value; do @@ -260,6 +284,7 @@ backup_service() { backup_proxy_url=$(awk -F'=' '/^BACKUP_PROXY_URL=/ {print substr($0, index($0,"=")+1); exit}' "$ENV_FILE") backup_proxy_url=$(echo "$backup_proxy_url" | sed -e 's/^"//' -e 's/"$//') [ -z "$backup_proxy_enabled" ] && backup_proxy_enabled="false" + local masked_telegram_bot_key="" if [[ "$cron_schedule" == "0 0 * * *" ]]; then interval_hours=24 @@ -267,9 +292,11 @@ backup_service() { interval_hours=$(echo "$cron_schedule" | grep -oP '(?<=\*/)[0-9]+') fi + masked_telegram_bot_key=$(mask_telegram_bot_key "$telegram_bot_key") + colorized_echo green "=====================================" colorized_echo green "Current Backup Configuration:" - colorized_echo cyan "Telegram Bot API Key: $telegram_bot_key" + colorized_echo cyan "Telegram Bot API Key: $masked_telegram_bot_key" colorized_echo cyan "Telegram Chat ID: $telegram_chat_id" colorized_echo cyan "Backup Interval: Every $interval_hours hour(s)" if [[ "$backup_proxy_enabled" == "true" && -n "$backup_proxy_url" ]]; then @@ -456,9 +483,11 @@ add_cron_job() { local schedule="$1" local command="$2" local temp_cron=$(mktemp) + local filtered_cron="${temp_cron}.tmp" crontab -l 2>/dev/null >"$temp_cron" || true - grep -v "$command" "$temp_cron" >"${temp_cron}.tmp" && mv "${temp_cron}.tmp" "$temp_cron" + filter_backup_cron_entries "$temp_cron" "$filtered_cron" + mv "$filtered_cron" "$temp_cron" echo "$schedule $command # pasarguard-backup-service" >>"$temp_cron" if crontab "$temp_cron"; then @@ -483,6 +512,7 @@ view_backup_service() { backup_proxy_url=$(echo "$backup_proxy_url" | sed -e 's/^"//' -e 's/"$//') [ -z "$backup_proxy_enabled" ] && backup_proxy_enabled="false" local interval_hours="" + local masked_telegram_bot_key="" if [[ "$cron_schedule" == "0 0 * * *" ]]; then interval_hours=24 @@ -490,11 +520,13 @@ view_backup_service() { interval_hours=$(echo "$cron_schedule" | grep -oP '(?<=\*/)[0-9]+') fi + masked_telegram_bot_key=$(mask_telegram_bot_key "$telegram_bot_key") + colorized_echo blue "=====================================" colorized_echo blue " Backup Service Details " colorized_echo blue "=====================================" colorized_echo green "Status: Enabled" - colorized_echo cyan "Telegram Bot API Key: $telegram_bot_key" + colorized_echo cyan "Telegram Bot API Key: $masked_telegram_bot_key" colorized_echo cyan "Telegram Chat ID: $telegram_chat_id" colorized_echo cyan "Cron Schedule: $cron_schedule" if [[ "$interval_hours" -eq 24 ]]; then @@ -526,6 +558,7 @@ edit_backup_service() { backup_proxy_url=$(echo "$backup_proxy_url" | sed -e 's/^"//' -e 's/"$//') [ -z "$backup_proxy_enabled" ] && backup_proxy_enabled="false" local interval_hours="" + local masked_telegram_bot_key="" if [[ "$cron_schedule" == "0 0 * * *" ]]; then interval_hours=24 @@ -533,6 +566,8 @@ edit_backup_service() { interval_hours=$(echo "$cron_schedule" | grep -oP '(?<=\*/)[0-9]+') fi + masked_telegram_bot_key=$(mask_telegram_bot_key "$telegram_bot_key") + colorized_echo blue "=====================================" colorized_echo blue " Edit Backup Service " colorized_echo blue "=====================================" @@ -541,7 +576,7 @@ edit_backup_service() { if [[ "$backup_proxy_enabled" == "true" && -n "$backup_proxy_url" ]]; then proxy_display="Enabled ($backup_proxy_url)" fi - colorized_echo cyan "1. Telegram Bot API Key: $telegram_bot_key" + colorized_echo cyan "1. Telegram Bot API Key: $masked_telegram_bot_key" colorized_echo cyan "2. Telegram Chat ID: $telegram_chat_id" colorized_echo cyan "3. Backup Interval: Every $interval_hours hour(s)" colorized_echo cyan "4. Proxy: $proxy_display" @@ -552,7 +587,7 @@ edit_backup_service() { case $edit_choice in 1) while true; do - printf "Enter new Telegram bot API key [current: $telegram_bot_key]: " + printf "Enter new Telegram bot API key [current: %s]: " "$masked_telegram_bot_key" read new_bot_key if [[ -n "$new_bot_key" ]]; then sed -i "s|^BACKUP_TELEGRAM_BOT_KEY=.*|BACKUP_TELEGRAM_BOT_KEY=$new_bot_key|" "$ENV_FILE" @@ -608,8 +643,10 @@ edit_backup_service() { # Set PATH for cron to ensure docker and other tools are found local backup_command="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin bash $script_path backup" local temp_cron=$(mktemp) + local filtered_cron="${temp_cron}.tmp" crontab -l 2>/dev/null >"$temp_cron" || true - grep -v "# pasarguard-backup-service" "$temp_cron" >"${temp_cron}.tmp" && mv "${temp_cron}.tmp" "$temp_cron" + filter_backup_cron_entries "$temp_cron" "$filtered_cron" + mv "$filtered_cron" "$temp_cron" echo "$new_cron_schedule $backup_command # pasarguard-backup-service" >>"$temp_cron" if crontab "$temp_cron"; then @@ -693,9 +730,11 @@ remove_backup_service() { sed -i '/BACKUP_PROXY_URL/d' "$ENV_FILE" local temp_cron=$(mktemp) + local filtered_cron="${temp_cron}.tmp" crontab -l 2>/dev/null >"$temp_cron" || true - sed -i '/# pasarguard-backup-service/d' "$temp_cron" + filter_backup_cron_entries "$temp_cron" "$filtered_cron" + mv "$filtered_cron" "$temp_cron" if crontab "$temp_cron"; then colorized_echo green "Backup service task removed from crontab." @@ -719,13 +758,63 @@ backup_command() { fi local backup_dir="$APP_DIR/backup" - local temp_dir="/tmp/pasarguard_backup" local timestamp=$(date +"%Y%m%d%H%M%S") local backup_file="$backup_dir/backup_$timestamp.zip" local error_messages=() - local log_file="/var/log/pasarguard_backup_error.log" local final_backup_paths=() local split_size_arg="47m" # keep Telegram chunks under 50MB + local temp_dir="" + local log_file="" + local lock_file="${TMPDIR:-/tmp}/${APP_NAME}-backup.lock" + local lock_dir="${TMPDIR:-/tmp}/${APP_NAME}-backup.lock.d" + local lock_fd=9 + local keep_log_file=false + + mkdir -p "$backup_dir" + + if ! temp_dir=$(mktemp -d "${TMPDIR:-/tmp}/pasarguard_backup.XXXXXX"); then + colorized_echo red "Failed to create backup temp directory." + return 1 + fi + + if ! log_file=$(mktemp "${TMPDIR:-/tmp}/pasarguard_backup_error.XXXXXX.log"); then + colorized_echo red "Failed to create backup log file." + rm -rf "$temp_dir" + return 1 + fi + + if command -v flock >/dev/null 2>&1; then + eval "exec ${lock_fd}>\"$lock_file\"" + if ! flock -n "$lock_fd"; then + colorized_echo yellow "Another backup process is already running." + rm -rf "$temp_dir" + rm -f "$log_file" + return 1 + fi + elif ! mkdir "$lock_dir" 2>/dev/null; then + colorized_echo yellow "Another backup process is already running." + rm -rf "$temp_dir" + rm -f "$log_file" + return 1 + else + printf '%s\n' "$$" >"$lock_dir/pid" + fi + + cleanup_backup_command() { + trap - RETURN + rm -rf "$temp_dir" + if [ "$keep_log_file" != true ] && [ -n "$log_file" ]; then + rm -f "$log_file" + fi + if command -v flock >/dev/null 2>&1; then + eval "exec ${lock_fd}>&-" + else + rm -rf "$lock_dir" + fi + } + + trap cleanup_backup_command RETURN + >"$log_file" echo "Backup Log - $(date)" >>"$log_file" @@ -741,16 +830,6 @@ backup_command() { install_package zip fi - # Remove old backups before creating new one (keep only latest) - rm -f "$backup_dir"/backup_*.tar.gz - rm -f "$backup_dir"/backup_*.zip - rm -f "$backup_dir"/backup_*.z[0-9][0-9] 2>/dev/null || true - mkdir -p "$backup_dir" - - # Clean up temp directory completely before starting - rm -rf "$temp_dir" - mkdir -p "$temp_dir" - if [ -f "$ENV_FILE" ]; then while IFS='=' read -r key value; do if [[ -z "$key" || "$key" =~ ^# ]]; then @@ -770,7 +849,8 @@ backup_command() { error_messages+=("Environment file (.env) not found.") echo "Environment file (.env) not found." >>"$log_file" send_backup_error_to_telegram "${error_messages[*]}" "$log_file" - exit 1 + keep_log_file=true + return 1 fi local db_type="" @@ -1273,6 +1353,11 @@ backup_command() { error_messages+=("Failed to create backup archive.") echo "Failed to create backup archive." >>"$log_file" else + find "$backup_dir" -maxdepth 1 -type f \ + \( -name "backup_*.tar.gz" -o -name "backup_*.zip" -o -name "backup_*.z[0-9][0-9]" \) \ + ! -name "backup_${timestamp}.zip" \ + ! -name "backup_${timestamp}.z[0-9][0-9]" \ + -delete 2>/dev/null || true local backup_size=$(du -h "$backup_file" | cut -f1) colorized_echo green "Backup archive created: $backup_file (Size: $backup_size)" fi @@ -1284,10 +1369,8 @@ backup_command() { final_backup_paths+=("$backup_file") fi - # Clean up temp directory after archive is created - rm -rf "$temp_dir" - if [ ${#error_messages[@]} -gt 0 ]; then + keep_log_file=true colorized_echo red "Backup completed with errors:" for error in "${error_messages[@]}"; do colorized_echo red " - $error" @@ -1300,6 +1383,7 @@ backup_command() { fi if [ ${#final_backup_paths[@]} -eq 0 ]; then + keep_log_file=true colorized_echo red "Backup file was not created. Check log file: $log_file" return 1 fi diff --git a/lib/pasarguard-restore.sh b/lib/pasarguard-restore.sh index 9566975..052398d 100644 --- a/lib/pasarguard-restore.sh +++ b/lib/pasarguard-restore.sh @@ -22,6 +22,17 @@ restore_command() { local current_sqlalchemy_url="" local current_mysql_root_password="" + redact_database_url() { + local url="$1" + + if [ -z "$url" ]; then + printf '%s\n' "not set" + return 0 + fi + + printf '%s\n' "$url" | sed -E 's#^([^:]+://)([^@/]+)@#\1REDACTED@#' + } + if [ -f "$ENV_FILE" ]; then set +e while IFS='=' read -r key value || [ -n "$key" ]; do @@ -398,7 +409,7 @@ restore_command() { exit 1 fi - colorized_echo green "✓ Found SQLALCHEMY_DATABASE_URL: ${SQLALCHEMY_DATABASE_URL:0:50}..." + colorized_echo green "✓ Found SQLALCHEMY_DATABASE_URL: $(redact_database_url "$SQLALCHEMY_DATABASE_URL")" # Parse database configuration (similar to backup function) colorized_echo blue "Detecting database type..." @@ -688,9 +699,10 @@ restore_command() { colorized_echo blue "Restoring $db_type database from container: $container_name" - # Prepare restore credentials - local restore_user="${db_user:-${DB_USER:-postgres}}" - local restore_password="${db_password:-${DB_PASSWORD:-}}" + # Prepare restore credentials, preferring the current installation values. + local restore_user="${current_db_user:-${db_user:-${DB_USER:-postgres}}}" + local restore_password="${current_db_password:-${db_password:-${DB_PASSWORD:-}}}" + local restore_db_name="${current_db_name:-${db_name:-${DB_NAME:-postgres}}}" if [ -z "$restore_password" ]; then colorized_echo red "No database password found for restore." @@ -714,7 +726,7 @@ restore_command() { # Use target installation's identity when available, falling back to backup values. # This ensures cross-server restores work correctly when the local DB user/name # differs from the backup source. - local target_db_name="${current_db_name:-$db_name}" + local target_db_name="$restore_db_name" local target_db_owner="${current_db_user:-$restore_user}" # Drop and recreate the target database for a clean slate @@ -771,8 +783,8 @@ restore_command() { fi else # Plain PostgreSQL restore with ON_ERROR_STOP so psql exits non-zero on SQL errors - colorized_echo blue "Attempting restore using app user '$restore_user' to database '$db_name'..." - if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U "$restore_user" -d "$db_name" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then + colorized_echo blue "Attempting restore using app user '$restore_user' to database '$restore_db_name'..." + if docker exec -i "$container_name" psql -v ON_ERROR_STOP=1 -U "$restore_user" -d "$restore_db_name" < "$temp_restore_dir/db_backup.sql" 2>>"$log_file"; then colorized_echo green "$db_type database restored successfully." restore_success=true else @@ -852,6 +864,9 @@ restore_command() { fi if [ "$preserve_db_credentials" = true ]; then colorized_echo yellow "Database credentials in backup differ from current installation; preserving current database credentials." + if [ -n "$current_mysql_root_password" ]; then + replace_or_append_env_var "MYSQL_ROOT_PASSWORD" "$current_mysql_root_password" true "$ENV_FILE" + fi if [ -n "$current_db_user" ]; then replace_or_append_env_var "DB_USER" "$current_db_user" false "$ENV_FILE" fi