Files
AliasVault/install.sh
2025-12-02 13:59:36 +01:00

3265 lines
119 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# @version 0.25.2
# Repository information used for downloading files and images from GitHub
REPO_OWNER="aliasvault"
REPO_NAME="aliasvault"
GITHUB_RAW_URL_REPO="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}"
GITHUB_CONTAINER_REGISTRY="ghcr.io/$(echo "$REPO_OWNER" | tr '[:upper:]' '[:lower:]')"
# Required files and directories
REQUIRED_DIRS=(
"certificates/ssl"
"certificates/letsencrypt"
"certificates/letsencrypt/www"
"database"
"database/postgres"
"logs"
"logs/msbuild"
"secrets"
)
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
MAGENTA='\033[0;35m'
ORANGE='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
# File paths
ENV_FILE=".env"
ENV_EXAMPLE_FILE=".env.example"
SECRETS_DIR="./secrets"
# Minimum required versions
MIN_DOCKER_VERSION="20.10.0"
MIN_COMPOSE_VERSION="2.0.0"
MIN_DISK_SPACE_GB=5
# Global cache for latest version to avoid rate limiting
CACHED_LATEST_VERSION=""
# Function to show usage
show_usage() {
print_logo
printf "Usage: $0 [COMMAND] [OPTIONS]\n"
printf "\n"
printf "Commands:\n"
printf " install Install AliasVault by pulling pre-built images from GitHub Container Registry (recommended)\n"
printf " build Build AliasVault containers locally from source (takes longer and requires sufficient specs)\n"
printf " start Start AliasVault containers\n"
printf " restart Restart AliasVault containers\n"
printf " stop Stop AliasVault containers\n"
printf "\n"
printf " configure-hostname Configure the hostname where AliasVault can be accessed from\n"
printf " configure-ssl Configure SSL certificates (Let's Encrypt or self-signed)\n"
printf " configure-email Configure email domains for receiving emails\n"
printf " configure-registration Configure new account registration (enable or disable)\n"
printf " configure-ip-logging Configure IP address logging (enable or disable)\n"
printf " reset-admin-password Reset admin password\n"
printf " uninstall Uninstall AliasVault\n"
printf "\n"
printf " update Update AliasVault including install.sh script to the latest version\n"
printf " update-installer Update install.sh script if newer version is available\n"
printf "\n"
printf " db-export Export database to file\n"
printf " db-import Import database from file\n"
printf "\n"
printf " configure-dev-db Enable/disable development database (for local development only)\n"
printf "\n"
printf "Options:\n"
printf " --verbose Show detailed output\n"
printf " -y, --yes Automatic yes to prompts\n"
printf " --dev Target development database for db import/export operations\n"
printf " --parallel=N Use pigz with N threads for faster compression (db-export only, max: 32)\n"
printf "\n"
}
# Function to print the logo
print_logo() {
printf "${MAGENTA}" >&2
printf "==================================================\n" >&2
printf " _ _ _ __ __ _ _ \n" >&2
printf " / \ | (_) __ _ ___ \ \ / /_ _ _ _| | |_\n" >&2
printf " / _ \ | | |/ _\` / __| \ \/\/ / _\` | | | | | __|\n" >&2
printf " / ___ \| | | (_| \__ \ \ / / (_| | |_| | | |_ \n" >&2
printf "/_/ \_\_|_|\__,_|___/ \/ \__,__|\__,_|_|\__|\n" >&2
printf "\n" >&2
printf "==================================================\n${NC}" >&2
}
# Function to parse command line arguments
parse_args() {
COMMAND=""
VERBOSE=false
FORCE_YES=false
COMMAND_ARG=""
DEV_DB=false
PARALLEL_JOBS=0 # 0 = use standard gzip, >0 = use pigz with N threads
if [ $# -eq 0 ]; then
show_usage
exit 0
fi
# First argument is always the command
case $1 in
install|i)
COMMAND="install"
shift
# Check for version argument
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then
COMMAND_ARG="$1"
shift
fi
;;
build|b)
COMMAND="build"
shift
# Check for additional operation argument
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then
case $1 in
start|stop|restart)
COMMAND_ARG="$1"
shift
;;
*)
echo "Invalid build operation: $1"
echo "Valid operations are: start, stop, restart"
exit 1
;;
esac
fi
;;
uninstall|u)
COMMAND="uninstall"
shift
;;
reset-password|reset-admin-password|rp)
COMMAND="reset-admin-password"
shift
;;
configure-hostname|hostname)
COMMAND="configure-hostname"
shift
;;
configure-ssl|ssl)
COMMAND="configure-ssl"
shift
;;
configure-email|email)
COMMAND="configure-email"
shift
;;
configure-registration|registration)
COMMAND="configure-registration"
shift
;;
configure-ip-logging|ip-logging)
COMMAND="configure-ip-logging"
shift
;;
start|s)
COMMAND="start"
shift
;;
stop|st)
COMMAND="stop"
shift
;;
restart|r)
COMMAND="restart"
shift
;;
update|up)
COMMAND="update"
shift
;;
update-installer|cs)
COMMAND="update-installer"
shift
;;
configure-dev-db|dev-db)
COMMAND="configure-dev-db"
shift
# Check for direct option argument
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then
COMMAND_ARG="$1"
shift
fi
;;
migrate-db|migrate)
COMMAND="migrate-db"
shift
;;
db-export)
COMMAND="db-export"
shift
;;
db-import)
COMMAND="db-import"
shift
;;
--help)
show_usage
exit 0
;;
*)
echo "Unknown option: $1"
show_usage
exit 1
;;
esac
# Parse remaining flags
while [[ $# -gt 0 ]]; do
case $1 in
--verbose)
VERBOSE=true
shift
;;
-y|--yes)
FORCE_YES=true
shift
;;
--dev)
DEV_DB=true
shift
;;
--parallel=*)
PARALLEL_JOBS="${1#*=}"
if ! [[ "$PARALLEL_JOBS" =~ ^[0-9]+$ ]] || [ "$PARALLEL_JOBS" -lt 1 ] || [ "$PARALLEL_JOBS" -gt 32 ]; then
echo "Error: Invalid --parallel value '$PARALLEL_JOBS'. Must be a number between 1 and 32"
exit 1
fi
shift
;;
*)
echo "Unknown option: $1"
show_usage
exit 1
;;
esac
done
}
# Progress and animation functions
show_spinner() {
local pid=$1
local message="$2"
local delay=0.1
local spinstr='|/-\\'
local i=0
printf "${CYAN} %s${NC} " "$message"
while kill -0 "$pid" 2>/dev/null; do
printf "\b%c" "${spinstr:$i:1}"
sleep $delay
((i = (i + 1) % 4))
done
printf "\b ${GREEN}${NC}\n"
}
log_info() {
printf "${CYAN} ${NC}%s\n" "$1"
}
log_warning() {
printf "${YELLOW}${NC}%s\n" "$1"
}
log_error() {
printf "${RED}${NC}%s\n" "$1" >&2
}
# Version comparison function
version_ge() {
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"
}
# Network connectivity check
check_connectivity() {
printf "${CYAN} Checking network connectivity...${NC} "
local test_urls=(
"https://api.github.com"
"https://raw.githubusercontent.com"
"https://ghcr.io"
)
local spinstr='|/-\\'
local i=0
for url in "${test_urls[@]}"; do
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
if ! curl -Ls --connect-timeout 10 --max-time 30 "$url" > /dev/null 2>&1; then
printf "\b ${RED}${NC}\n"
log_error "Cannot reach $url. Please check your internet connection."
return 1
fi
done
printf "\b ${GREEN}${NC}\n"
return 0
}
# Disk space check
check_disk_space() {
printf "${CYAN} Checking disk space...${NC} "
local available_gb=""
local spinstr='|/-\\'
local i=0
if command -v df >/dev/null 2>&1; then
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
# Use portable df command and parse available size in KB
local available_kb
available_kb=$(df -k . 2>/dev/null | awk 'NR==2 {print $4}')
if [ -n "$available_kb" ] && [ "$available_kb" -gt 0 ] 2>/dev/null; then
available_gb=$((available_kb / 1024 / 1024))
fi
if [ -n "$available_gb" ] && [ "$available_gb" -gt 0 ] 2>/dev/null; then
if [ "$available_gb" -lt "$MIN_DISK_SPACE_GB" ]; then
printf "\b ${RED}${NC}\n"
log_error "Insufficient disk space. Required: ${MIN_DISK_SPACE_GB}GB, Available: ${available_gb}GB"
return 1
fi
printf "\b ${GREEN}${NC}\n"
printf " ${GREEN}✓ Disk space verified (${available_gb}GB available)${NC}\n"
else
printf "\b ${YELLOW}${NC}\n"
log_warning "Cannot determine available disk space, skipping check"
fi
else
printf "\b ${YELLOW}${NC}\n"
log_warning "Cannot check disk space (df command not available)"
fi
return 0
}
# Read port configuration from .env file with fallbacks
get_port_config() {
local http_port=80
local https_port=443
local smtp_port=25
if [ -f "$ENV_FILE" ]; then
# Read ports from .env file if it exists
local env_http_port
local env_https_port
local env_smtp_port
env_http_port=$(grep -E "^HTTP_PORT=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
env_https_port=$(grep -E "^HTTPS_PORT=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
env_smtp_port=$(grep -E "^SMTP_PORT=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
# Use .env values if they're valid numbers
if [[ "$env_http_port" =~ ^[0-9]+$ ]] && [ "$env_http_port" -gt 0 ] && [ "$env_http_port" -le 65535 ]; then
http_port="$env_http_port"
fi
if [[ "$env_https_port" =~ ^[0-9]+$ ]] && [ "$env_https_port" -gt 0 ] && [ "$env_https_port" -le 65535 ]; then
https_port="$env_https_port"
fi
if [[ "$env_smtp_port" =~ ^[0-9]+$ ]] && [ "$env_smtp_port" -gt 0 ] && [ "$env_smtp_port" -le 65535 ]; then
smtp_port="$env_smtp_port"
fi
fi
# Return the ports as space-separated values
echo "$http_port $https_port $smtp_port"
}
# Check if ports are available (not in use by non-AliasVault processes)
check_port_availability() {
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
printf "${CYAN} Checking port availability...${NC} "
local ports_config
ports_config=$(get_port_config)
read -r http_port https_port smtp_port <<< "$ports_config"
local ports_to_check=("$http_port" "$https_port" "$smtp_port")
local port_issues=()
local has_issues=false
local spinstr='|/-\\'
local i=0
# Get current directory name as potential project name
local current_project_name
current_project_name=$(basename "$(pwd)" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]//g')
# Get list of running AliasVault containers to exclude from checks
local aliasvault_containers=()
local aliasvault_project_containers=()
if command -v docker > /dev/null 2>&1 && docker info > /dev/null 2>&1; then
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
# Get all running containers
while IFS= read -r container_info; do
if [ -n "$container_info" ]; then
# Parse container name and project
local container_name
local project_name
container_name=$(echo "$container_info" | cut -d'|' -f1)
project_name=$(echo "$container_info" | cut -d'|' -f2)
# Check if it's an AliasVault container by name or project
if [[ "$container_name" =~ aliasvault ]] || [[ "$project_name" =~ aliasvault ]] || [[ "$project_name" == "$current_project_name" ]]; then
aliasvault_containers+=("$container_name")
aliasvault_project_containers+=("$project_name")
fi
fi
done < <(docker ps --format "{{.Names}}|{{.Label \"com.docker.compose.project\"}}" 2>/dev/null | grep -v "^$" || true)
fi
for port in "${ports_to_check[@]}"; do
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
local port_in_use=false
local blocking_process=""
local is_aliasvault_port=false
# Check if port is in use using netstat/ss
if command -v ss > /dev/null 2>&1; then
# Use ss (more modern)
local ss_output
ss_output=$(ss -tuln 2>/dev/null | grep ":${port} " || true)
if [ -n "$ss_output" ]; then
port_in_use=true
# Try to get process info
local process_info
process_info=$(ss -tulpn 2>/dev/null | grep ":${port} " | head -n1 || true)
if [ -n "$process_info" ]; then
blocking_process=$(echo "$process_info" | sed -n 's/.*users:((\"\([^\"]*\)\".*/\1/p' || true)
fi
fi
elif command -v netstat > /dev/null 2>&1; then
# Fallback to netstat
local netstat_output
netstat_output=$(netstat -tuln 2>/dev/null | grep ":${port} " || true)
if [ -n "$netstat_output" ]; then
port_in_use=true
# Try to get process info with netstat -tulpn if available
local process_info
process_info=$(netstat -tulpn 2>/dev/null | grep ":${port} " | head -n1 || true)
if [ -n "$process_info" ]; then
blocking_process=$(echo "$process_info" | awk '{print $7}' | cut -d'/' -f2 || true)
fi
fi
else
# Last resort: try to bind to the port temporarily
if command -v nc > /dev/null 2>&1; then
if ! nc -z localhost "$port" 2>/dev/null; then
port_in_use=false
else
port_in_use=true
fi
else
log_warning "Cannot check port $port availability (no netstat, ss, or nc available)"
continue
fi
fi
# If port is in use, check if it's used by AliasVault containers
if [ "$port_in_use" = true ]; then
# First, check if any AliasVault containers are using this port directly
for container in "${aliasvault_containers[@]}"; do
if command -v docker > /dev/null 2>&1; then
local container_ports
container_ports=$(docker port "$container" 2>/dev/null || true)
if echo "$container_ports" | grep -q ":${port}$" || echo "$container_ports" | grep -q ":${port}->" ; then
is_aliasvault_port=true
break
fi
fi
done
# If not found in direct container ports, check if it's docker-proxy for AliasVault
if [ "$is_aliasvault_port" = false ] && [ "$blocking_process" = "docker-proxy" ]; then
# Check if there are any AliasVault containers running
if [ ${#aliasvault_containers[@]} -gt 0 ]; then
# If we have AliasVault containers and docker-proxy is using the port,
# it's likely for AliasVault (especially during installation/updates)
is_aliasvault_port=true
fi
fi
# Additional check: if we're in an AliasVault directory and docker-proxy is using the port,
# it's very likely for AliasVault
if [ "$is_aliasvault_port" = false ] && [ "$blocking_process" = "docker-proxy" ]; then
# Check if we're in an AliasVault project directory
if [ -f "docker-compose.yml" ] || [ -f ".env" ]; then
# If we have docker-compose.yml or .env file, this is likely an AliasVault project
is_aliasvault_port=true
fi
fi
# Only report as an issue if it's not used by AliasVault
if [ "$is_aliasvault_port" = false ]; then
has_issues=true
local port_name=""
case "$port" in
"$http_port") port_name="HTTP" ;;
"$https_port") port_name="HTTPS" ;;
"$smtp_port") port_name="SMTP" ;;
esac
if [ -n "$blocking_process" ]; then
port_issues+=("Port $port ($port_name) is in use by: $blocking_process")
else
port_issues+=("Port $port ($port_name) is in use by an unknown process")
fi
fi
fi
done
if [ "$has_issues" = true ]; then
printf "\b ${RED}${NC}\n"
log_error "Port availability issues detected:"
for issue in "${port_issues[@]}"; do
printf " ${RED}${NC} %s\n" "$issue"
done
printf "\n${YELLOW}Common solutions:${NC}\n"
# Show specific help based on which ports are in use
local smtp_in_use=false
local http_https_in_use=false
for issue in "${port_issues[@]}"; do
if [[ "$issue" == *"SMTP"* ]]; then
smtp_in_use=true
fi
if [[ "$issue" == *"HTTP"* ]] || [[ "$issue" == *"HTTPS"* ]]; then
http_https_in_use=true
fi
done
if [ "$smtp_in_use" = true ]; then
printf " ${YELLOW}${NC} Try disabling the postfix service with 'sudo systemctl stop postfix && sudo systemctl disable postfix'\n"
fi
if [ "$http_https_in_use" = true ]; then
printf " ${YELLOW}${NC} Try stopping the existing local webserver (e.g. nginx, apache, httpd etc.)\n"
printf " ${YELLOW}${NC} Change the default AliasVault ports (80, 443) by editing the .env file\n"
fi
printf "\nIf this still doesn't work, try finding out which services are running on the specified ports and read documentation for your distribution on how to disable them.\n"
printf "\n"
return 1
fi
printf "\b ${GREEN}${NC}\n"
printf " ${GREEN}✓ Port availability verified (HTTP:$http_port, HTTPS:$https_port, SMTP:$smtp_port)${NC}\n"
return 0
}
# Comprehensive dependency checks
check_dependencies() {
printf "${CYAN} Checking dependencies...${NC} "
local missing_deps=()
local version_issues=()
local has_issues=false
local spinstr='|/-\\'
local i=0
# Check if OS is 64-bit
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
local arch=$(uname -m)
case "$arch" in
x86_64|amd64|arm64|aarch64)
# 64-bit architecture - continue
;;
*)
printf "\b ${RED}${NC}\n"
log_error "AliasVault requires a 64-bit operating system."
printf "\n"
printf "${RED}${BOLD}Unsupported architecture detected: $arch${NC}\n"
printf "\n"
printf "AliasVault only supports 64-bit operating systems. Your current architecture ($arch) is not supported.\n"
printf "\n"
printf "${CYAN}Supported architectures:${NC}\n"
printf " ${GREEN}${NC} x86_64 (Intel/AMD 64-bit)\n"
printf " ${GREEN}${NC} arm64/aarch64 (ARM 64-bit)\n"
printf "\n"
printf "${YELLOW}Please upgrade your operating system to a 64-bit version.${NC}\n"
printf "\n"
return 1
;;
esac
# Check Docker
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
if ! command -v docker > /dev/null 2>&1; then
missing_deps+=("docker")
has_issues=true
else
local docker_version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
if [ -n "$docker_version" ]; then
if ! version_ge "$docker_version" "$MIN_DOCKER_VERSION"; then
version_issues+=("Docker version $docker_version is below minimum required $MIN_DOCKER_VERSION")
has_issues=true
fi
fi
# Check if Docker daemon is running
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
if ! docker info > /dev/null 2>&1; then
printf "\b ${RED}${NC}\n"
log_error "Docker daemon cannot be reached."
printf "\n"
printf "${CYAN}To resolve this issue:${NC}\n"
printf "\n"
printf "${YELLOW}Step 1:${NC} Manually test Docker daemon status:\n"
printf " ${DIM}docker info${NC}\n"
printf "\n"
printf "${YELLOW}Step 2:${NC} If Docker is not installed or not running, follow the official installation instructions:\n"
printf " ${CYAN}https://docs.docker.com/engine/install/${NC}\n"
printf "\n"
printf "Please install the latest version of Docker and ensure the Docker daemon is running.\n"
printf "\n"
return 1
fi
# Test if Docker can actually run containers (lightweight test)
if [ "$has_issues" != true ]; then
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
local docker_test_output
docker_test_output=$(docker run --rm alpine:latest echo "test" 2>&1 >/dev/null)
if [ $? -ne 0 ]; then
printf "\b ${RED}${NC}\n"
log_error "Docker cannot run containers properly. Error output:"
printf " ${RED}%s${NC}\\n\\n" "$docker_test_output"
printf " Possible causes:\\n"
printf " ${YELLOW}${NC} Docker daemon configuration issues\\n"
printf " ${YELLOW}${NC} Insufficient permissions\\n"
printf " ${YELLOW}${NC} SELinux/AppArmor restrictions\\n"
printf " ${YELLOW}${NC} Storage driver problems\\n"
printf " ${YELLOW}${NC} Kernel compatibility issues\\n"
printf "\\n"
printf " ${CYAN}To debug, try running:${NC}\\n"
printf " ${DIM}docker run --rm alpine:latest echo \"test\"${NC}\\n"
printf "\\n"
return 1
fi
fi
fi
# Check Docker Compose
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
if docker compose version > /dev/null 2>&1; then
local compose_version=$(docker compose version --short 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
if [ -n "$compose_version" ]; then
if ! version_ge "$compose_version" "$MIN_COMPOSE_VERSION"; then
version_issues+=("Docker Compose version $compose_version is below minimum required $MIN_COMPOSE_VERSION")
has_issues=true
fi
fi
elif command -v docker-compose > /dev/null 2>&1; then
printf "\b ${RED}${NC}\n"
local compose_version=$(docker-compose --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
log_error "Docker Compose v1 detected ($compose_version). AliasVault requires Docker Compose v2."
printf "\n"
printf "${RED}${BOLD}Docker Compose v1 is not supported.${NC}\n"
printf "\n"
printf "${CYAN}To upgrade to Docker Compose v2:${NC}\n"
printf " ${YELLOW}${NC} Uninstall docker-compose v1: sudo apt remove docker-compose (Ubuntu/Debian)\n"
printf " ${YELLOW}${NC} Install Docker Compose v2 plugin: https://docs.docker.com/compose/install/linux/#install-using-the-repository\n"
printf "\n"
printf "${CYAN}After installation, verify with:${NC}\n"
printf " docker compose version\n"
printf "\n"
return 1
else
missing_deps+=("docker-compose")
has_issues=true
fi
# Check essential tools
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
for tool in curl openssl grep sed; do
if ! command -v "$tool" > /dev/null 2>&1; then
missing_deps+=("$tool")
has_issues=true
fi
done
# Show final result
if [ "$has_issues" = true ]; then
printf "\b ${RED}${NC}\n"
printf "\n"
if [ ${#missing_deps[@]} -gt 0 ]; then
printf "${RED}${BOLD}Missing required dependencies:${NC}\n"
for dep in "${missing_deps[@]}"; do
case $dep in
"docker")
printf " ${RED}${NC} %s (install manual: https://docs.docker.com/engine/install/)\n" "$dep"
;;
"docker-compose")
printf " ${RED}${NC} %s (install manual: https://docs.docker.com/compose/install/linux/#install-using-the-repository)\n" "$dep"
;;
*)
printf " ${RED}${NC} %s\n" "$dep"
;;
esac
done
printf "\n"
fi
if [ ${#version_issues[@]} -gt 0 ]; then
printf "${YELLOW}${BOLD}Version compatibility warnings:${NC}\n"
for issue in "${version_issues[@]}"; do
printf " ${YELLOW}${NC} %s\n" "$issue"
done
printf "\n"
printf "${YELLOW}AliasVault may still work, but upgrading is recommended.${NC}\n\n"
fi
if [ ${#missing_deps[@]} -gt 0 ]; then
return 1
fi
else
printf "\b ${GREEN}${NC}\n"
fi
return 0
}
# Enhanced error handling with retries
retry_command() {
local max_attempts=$1
local delay=$2
shift 2
local command=("$@")
local attempt=1
while [ $attempt -le $max_attempts ]; do
if "${command[@]}"; then
return 0
fi
if [ $attempt -lt $max_attempts ]; then
log_warning "Attempt $attempt failed, retrying in ${delay}s..."
sleep $delay
fi
((attempt++))
done
log_error "Command failed after $max_attempts attempts: ${command[*]}"
return 1
}
# Enhanced docker pull with progress
enhanced_docker_pull() {
local image="$1"
local image_name=$(basename "$image")
if [ "$VERBOSE" = true ]; then
docker pull "$image"
else
(
docker pull "$image" > /tmp/docker_pull_${image_name//[:\/]/_}.log 2>&1 &
local pull_pid=$!
show_spinner $pull_pid "Pulling $image_name "
wait $pull_pid
local exit_code=$?
if [ $exit_code -ne 0 ]; then
cat /tmp/docker_pull_${image_name//[:\/]/_}.log >&2
fi
rm -f /tmp/docker_pull_${image_name//[:\/]/_}.log
return $exit_code
)
fi
}
# Function to validate semver format
validate_semver() {
local version="$1"
# Check if version is "latest" (special case)
if [ "$version" = "latest" ]; then
return 0
fi
# Validate semver format: x.y.z where x, y, z are non-negative integers
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
return 0
else
return 1
fi
}
# Main function
main() {
parse_args "$@"
# Check if command is empty (should not happen with updated parse_args)
if [ -z "$COMMAND" ]; then
show_usage
exit 1
fi
print_logo
# Skip dependency checks for certain commands that don't require Docker
case $COMMAND in
"update-installer")
# Only check basic network connectivity for installer updates
if ! check_connectivity; then
exit 1
fi
;;
"install"|"build"|"update"|"configure-dev-db")
# Full dependency check for operations that require Docker
if ! check_dependencies; then
exit 1
fi
# Run migrations
migrate_secrets_to_files # 0.22.0+
migrate_docker_image_urls # 0.23.0+
# Additional checks for installation/build operations
if [[ "$COMMAND" == "install" || "$COMMAND" == "build" || "$COMMAND" == "update" ]]; then
if ! check_connectivity; then
exit 1
fi
if ! check_disk_space; then
exit 1
fi
if ! check_port_availability; then
exit 1
fi
fi
;;
esac
case $COMMAND in
"build")
handle_build
;;
"install")
handle_install "$COMMAND_ARG"
;;
"uninstall")
handle_uninstall
;;
"reset-admin-password")
generate_admin_password
if [ $? -eq 0 ]; then
printf "${CYAN}> Restarting admin container...${NC}\n"
if [ "$VERBOSE" = true ]; then
eval "$(get_docker_compose_command) up -d --force-recreate admin"
else
eval "$(get_docker_compose_command) up -d --force-recreate admin" > /dev/null 2>&1
fi
print_password_reset_message
fi
;;
"configure-ssl")
handle_ssl_configuration
;;
"configure-email")
handle_email_configuration
;;
"configure-registration")
handle_registration_configuration
;;
"configure-hostname")
handle_hostname_configuration
;;
"configure-ip-logging")
handle_ip_logging_configuration
;;
"start")
handle_start
;;
"stop")
handle_stop
;;
"restart")
handle_restart
;;
"update")
handle_update
;;
"update-installer")
check_install_script_update
exit $?
;;
"configure-dev-db")
configure_dev_database
;;
"db-export")
handle_db_export
;;
"db-import")
handle_db_import
;;
esac
}
# Function to get the latest release version from GitHub (with session caching)
get_latest_version() {
# Check if we have a cached version for this session
if [ -n "${CACHED_LATEST_VERSION:-}" ]; then
echo "$CACHED_LATEST_VERSION"
return 0
fi
local attempt=1
local max_attempts=5
local wait_time=1
while [ $attempt -le $max_attempts ]; do
local latest_version=$(curl -Ls "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
if [ -n "$latest_version" ]; then
# Cache the version for this session
CACHED_LATEST_VERSION="$latest_version"
echo "$latest_version"
return 0
fi
printf "${YELLOW}> Attempt ${attempt}/${max_attempts}: Failed to get latest version from GitHub. Retrying in ${wait_time}s...${NC}\n" >&2
sleep $wait_time
# Exponential backoff - double the wait time for next attempt
wait_time=$((wait_time * 2))
attempt=$((attempt + 1))
done
printf "${RED}> Failed to get latest version from GitHub after ${max_attempts} attempts.${NC}\n" >&2
return 1
}
# Function to initialize workspace and create required directories
initialize_workspace() {
printf "${CYAN} Checking workspace...${NC} ${GREEN}${NC}\n"
local dirs_needed=false
for dir in "${REQUIRED_DIRS[@]}"; do
if [ ! -d "$dir" ]; then
if [ "$dirs_needed" = false ]; then
printf " ${GREEN}> Creating required directories...${NC}\n"
dirs_needed=true
fi
mkdir -p "$dir"
chmod -R 755 "$dir"
if [ $? -ne 0 ]; then
printf " ${RED}> Failed to create directory: $dir${NC}\n"
exit 1
fi
fi
done
}
# Function to handle docker-compose.yml
handle_docker_compose() {
local version_tag="$1"
printf "${CYAN} Downloading docker-compose files for version ${version_tag}...${NC} "
local files_to_download=(
"docker-compose.yml"
"docker-compose.letsencrypt.yml"
)
local spinstr='|/-\\'
local i=0
for file in "${files_to_download[@]}"; do
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
local temp_file="${file}.tmp"
local download_url="${GITHUB_RAW_URL_REPO}/${version_tag}/${file}"
# First, check if the file exists by making a HEAD request
local http_status
http_status=$(curl -Ls -o /dev/null -w "%{http_code}" "$download_url" 2>/dev/null)
if [ "$http_status" = "404" ]; then
printf "\b ${RED}${NC}\n"
log_error "Version '${version_tag}' does not exist or is not available."
log_error "The requested version may not have been released yet or may be invalid."
printf "\n"
printf "${CYAN}Available options:${NC}\n"
printf " ${YELLOW}${NC} Check available versions at: https://github.com/${REPO_OWNER}/${REPO_NAME}/releases\n"
printf " ${YELLOW}${NC} Use a different version: ./install.sh install <version>\n"
printf "\n"
rm -f "$temp_file"
exit 1
fi
if ! retry_command 3 2 curl -LsSf "$download_url" -o "$temp_file"; then
printf "\b ${RED}${NC}\n"
log_error "Failed to download $file from $download_url"
log_error "Please check your internet connection and try again."
log_info "Alternatively, download manually and place in the current directory."
rm -f "$temp_file"
exit 1
fi
# Special handling for docker-compose.yml version replacement
if [ "$file" = "docker-compose.yml" ]; then
if [ -n "$version_tag" ] && [ "$version_tag" != "latest" ]; then
sed "s/:latest/:$version_tag/g" "$temp_file" > "$file"
rm "$temp_file"
else
mv "$temp_file" "$file"
fi
else
mv "$temp_file" "$file"
fi
done
printf "\b ${GREEN}${NC}\n"
return 0
}
# Function to check and update install.sh for specific version
check_install_script_version() {
local target_version="$1"
printf "${CYAN} Checking install script version for ${target_version}...${NC} ${GREEN}${NC}\n"
# First, check if the install.sh file exists for this version
local install_url="${GITHUB_RAW_URL_REPO}/${target_version}/install.sh"
local http_status
http_status=$(curl -Ls -o /dev/null -w "%{http_code}" "$install_url" 2>/dev/null)
if [ "$http_status" = "404" ]; then
printf " > Install script not found for version ${target_version}. Continuing with current version.\n"
return 1
fi
# Get remote install.sh for target version
if ! curl -LsSf "$install_url" -o "install.sh.tmp"; then
printf "${RED}> Failed to check install script version. Continuing with current version.${NC}\n"
rm -f install.sh.tmp
return 1
fi
# Get versions
local current_version=$(extract_version "install.sh")
local target_script_version=$(extract_version "install.sh.tmp")
# Check if versions could be extracted
if [ -z "$current_version" ] || [ -z "$target_script_version" ]; then
printf "\n${YELLOW}> Could not determine script versions. Falling back to file comparison...${NC}\n"
if ! cmp -s "install.sh" "install.sh.tmp"; then
printf "${YELLOW}> Install script needs updating to match version ${target_version}${NC}\n"
printf " ${GREEN}${NC}\n"
return 2
fi
else
if [ "$current_version" != "$target_script_version" ]; then
printf "${YELLOW}> Install script needs updating to match version ${target_version}${NC}\n"
printf " ${GREEN}${NC}\n"
return 2
fi
fi
rm -f install.sh.tmp
return 0
}
# Function to create .env file
create_env_file() {
printf "${CYAN} Checking .env file...${NC} ${GREEN}${NC}\n"
if [ ! -f "$ENV_FILE" ]; then
if [ ! -f "$ENV_EXAMPLE_FILE" ]; then
# Get latest release version
local latest_version=$(get_latest_version) || {
printf "\n ${YELLOW}> Failed to check latest version. Creating blank .env file.${NC}\n"
touch "$ENV_FILE"
return 0
}
printf " ${CYAN}> Downloading .env.example...${NC}"
# Check if .env.example exists for this version
local env_example_url="${GITHUB_RAW_URL_REPO}/${latest_version}/.env.example"
local http_status
http_status=$(curl -Ls -o /dev/null -w "%{http_code}" "$env_example_url" 2>/dev/null)
if [ "$http_status" = "404" ]; then
printf "\n ${YELLOW}> .env.example not found for version ${latest_version}. Creating blank .env file.${NC}\n"
touch "$ENV_FILE"
return 0
fi
if curl -LsSf "$env_example_url" -o "$ENV_EXAMPLE_FILE" > /dev/null 2>&1; then
printf "\n ${GREEN}> .env.example downloaded successfully.${NC}\n"
else
printf "\n ${YELLOW}> Failed to download .env.example. Creating blank .env file.${NC}\n"
touch "$ENV_FILE"
return 0
fi
fi
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
printf " ${GREEN}> New .env file created from .env.example.${NC}\n"
else
# Check if .env file contains POSTGRES_DB or POSTGRES_USER, remove if present and show confirmation
local removed_vars=()
if grep -q "^POSTGRES_DB=" "$ENV_FILE"; then
sed -i.bak "/^POSTGRES_DB=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
removed_vars+=("POSTGRES_DB")
fi
if grep -q "^POSTGRES_USER=" "$ENV_FILE"; then
sed -i.bak "/^POSTGRES_USER=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
removed_vars+=("POSTGRES_USER")
fi
if [ ${#removed_vars[@]} -gt 0 ]; then
printf " ${GREEN}> Removed obsolete variable(s) from .env: %s${NC}\n" "${removed_vars[*]}"
fi
fi
}
populate_hostname() {
if ! grep -q "^HOSTNAME=" "$ENV_FILE" || [ -z "$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
while true; do
read -p "Enter the hostname where this AliasVault server can be accessed (e.g. aliasvault.example.com): " USER_HOSTNAME
if [ -n "$USER_HOSTNAME" ]; then
HOSTNAME="$USER_HOSTNAME"
break
else
printf "${YELLOW}> Hostname cannot be empty. Please enter a valid hostname.${NC}\n"
fi
done
update_env_var "HOSTNAME" "$HOSTNAME"
else
HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
fi
}
# Function to generate admin password
generate_admin_password() {
printf "${CYAN} Generating admin password...${NC} ${GREEN}${NC}\n"
PASSWORD=$(openssl rand -base64 12)
# Build locally if in build mode or if pre-built image is not available
if grep -q "^DEPLOYMENT_MODE=build" "$ENV_FILE" 2>/dev/null || ! docker pull ${GITHUB_CONTAINER_REGISTRY}/installcli:latest > /dev/null 2>&1; then
log_info "Building InstallCli locally..."
if [ "$VERBOSE" = true ]; then
if ! docker build -t installcli -f apps/server/Utilities/AliasVault.InstallCli/Dockerfile .; then
log_error "Failed to build InstallCli Docker image"
exit 1
fi
else
(
docker build -t installcli -f apps/server/Utilities/AliasVault.InstallCli/Dockerfile . > install_build_output.log 2>&1 &
BUILD_PID=$!
show_spinner $BUILD_PID "Building InstallCli image "
wait $BUILD_PID
BUILD_EXIT_CODE=$?
if [ $BUILD_EXIT_CODE -ne 0 ]; then
log_error "Failed to build InstallCli Docker image. Build output:"
cat install_build_output.log >&2
exit $BUILD_EXIT_CODE
fi
rm -f install_build_output.log
)
fi
HASH=$(docker run --rm installcli hash-password "$PASSWORD")
else
HASH=$(docker run --rm ${GITHUB_CONTAINER_REGISTRY}/installcli:latest hash-password "$PASSWORD")
fi
if [ -z "$HASH" ]; then
printf "\n${RED}> Error: Failed to generate password hash${NC}\n"
exit 1
fi
# Write admin password hash to secret file with timestamp
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
write_secret_to_file "admin_password_hash" "${HASH}|${TIMESTAMP}"
}
# Helper function to update environment variables
update_env_var() {
local key=$1
local value=$2
if [ -f "$ENV_FILE" ]; then
# Check if key exists
if grep -q "^${key}=" "$ENV_FILE"; then
# Update existing key inline
sed -i.bak "s|^${key}=.*|${key}=${value}|" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
else
# Key doesn't exist, append it
echo "$key=$value" >> "$ENV_FILE"
fi
else
# File doesn't exist, create it with the key-value pair
echo "$key=$value" > "$ENV_FILE"
fi
printf " ${GREEN}> $key has been set in $ENV_FILE.${NC}\n"
}
# Helper function to write secrets to files instead of .env
write_secret_to_file() {
local secret_name=$1
local secret_value=$2
local secret_file="${SECRETS_DIR}/${secret_name}"
# Create secrets directory if it doesn't exist
if [ ! -d "$SECRETS_DIR" ]; then
mkdir -p "$SECRETS_DIR"
chmod 700 "$SECRETS_DIR"
fi
# Write secret to file
echo -n "$secret_value" > "$secret_file"
chmod 600 "$secret_file"
printf " ${GREEN}> Secret $secret_name has been written to $secret_file${NC}\n"
}
# Helper function to read secret from file
read_secret_from_file() {
local secret_name=$1
local secret_file="${SECRETS_DIR}/${secret_name}"
if [ -f "$secret_file" ]; then
cat "$secret_file"
else
echo ""
fi
}
# Function to migrate secrets from .env to files
# Used when migrating from 0.21.0 or lower to 0.22.0+
migrate_secrets_to_files() {
# Check if migration is needed
if [ ! -f "$ENV_FILE" ]; then
return 0
fi
# Check if any secrets exist in .env file
if ! grep -q "^JWT_KEY=\|^DATA_PROTECTION_CERT_PASS=\|^POSTGRES_PASSWORD=\|^ADMIN_PASSWORD_HASH=" "$ENV_FILE" 2>/dev/null; then
return 0
fi
printf "${CYAN} Migrating secrets from .env to secret files...${NC}\n"
local migrated=false
local confirm_overwrite=false
# Migrate PRIVATE_EMAIL_DOMAINS from DISABLED.TLD to empty string (v0.22.0+)
if grep -q "^PRIVATE_EMAIL_DOMAINS=DISABLED.TLD" "$ENV_FILE"; then
update_env_var "PRIVATE_EMAIL_DOMAINS" ""
printf " Migrated PRIVATE_EMAIL_DOMAINS (DISABLED.TLD → empty string, v0.22.0+)\n"
fi
# Check if any secret files already exist
if [ -f "${SECRETS_DIR}/jwt_key" ] || [ -f "${SECRETS_DIR}/data_protection_cert_pass" ] ||
[ -f "${SECRETS_DIR}/postgres_password" ] || [ -f "${SECRETS_DIR}/admin_password_hash" ]; then
printf "${YELLOW}⚠ Some secret files already exist. Do you want to overwrite them with values from .env? (y/n):${NC} "
read -r response
if [ "$response" != "y" ] && [ "$response" != "Y" ]; then
printf "${CYAN} Skipping secret migration (keeping existing secret files)${NC}\n"
# Still remove secrets from .env if they exist there
remove_secrets_from_env
return 0
fi
confirm_overwrite=true
fi
# Create secrets directory if it doesn't exist
if [ ! -d "$SECRETS_DIR" ]; then
mkdir -p "$SECRETS_DIR"
chmod 700 "$SECRETS_DIR"
fi
# Migrate JWT_KEY
if grep -q "^JWT_KEY=" "$ENV_FILE" 2>/dev/null; then
JWT_KEY=$(grep "^JWT_KEY=" "$ENV_FILE" | cut -d '=' -f2-)
if [ -n "$JWT_KEY" ]; then
write_secret_to_file "jwt_key" "$JWT_KEY"
migrated=true
fi
fi
# Migrate DATA_PROTECTION_CERT_PASS
if grep -q "^DATA_PROTECTION_CERT_PASS=" "$ENV_FILE" 2>/dev/null; then
CERT_PASS=$(grep "^DATA_PROTECTION_CERT_PASS=" "$ENV_FILE" | cut -d '=' -f2-)
if [ -n "$CERT_PASS" ]; then
write_secret_to_file "data_protection_cert_pass" "$CERT_PASS"
migrated=true
fi
fi
# Migrate POSTGRES_PASSWORD
if grep -q "^POSTGRES_PASSWORD=" "$ENV_FILE" 2>/dev/null; then
POSTGRES_PASS=$(grep "^POSTGRES_PASSWORD=" "$ENV_FILE" | cut -d '=' -f2-)
if [ -n "$POSTGRES_PASS" ]; then
write_secret_to_file "postgres_password" "$POSTGRES_PASS"
migrated=true
fi
fi
# Migrate ADMIN_PASSWORD_HASH (with timestamp)
if grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" 2>/dev/null; then
ADMIN_HASH=$(grep "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" | cut -d '=' -f2-)
if [ -n "$ADMIN_HASH" ]; then
# Get the timestamp if it exists
TIMESTAMP=""
if grep -q "^ADMIN_PASSWORD_GENERATED=" "$ENV_FILE" 2>/dev/null; then
TIMESTAMP=$(grep "^ADMIN_PASSWORD_GENERATED=" "$ENV_FILE" | cut -d '=' -f2-)
else
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
fi
# Write hash with timestamp appended
write_secret_to_file "admin_password_hash" "${ADMIN_HASH}|${TIMESTAMP}"
migrated=true
fi
fi
if [ "$migrated" = true ]; then
printf " ${GREEN}✓ Secrets have been migrated from .env to secret files${NC}\n"
# Remove secrets from .env after successful migration
remove_secrets_from_env
elif [ "$confirm_overwrite" = false ]; then
printf " ${CYAN} No secrets found in .env to migrate${NC}\n"
fi
}
# Function to remove secrets from .env file. This is used to migrate pre-0.22.0 .env files to the new secret file format.
# Can be removed later when v1.0 is launched.
remove_secrets_from_env() {
local removed=false
# Only proceed if .env file exists
if [ ! -f "$ENV_FILE" ]; then
return 0
fi
# Verify that secret files exist before removing from .env
local all_secrets_migrated=true
if grep -q "^JWT_KEY=" "$ENV_FILE" 2>/dev/null && [ ! -f "${SECRETS_DIR}/jwt_key" ]; then
all_secrets_migrated=false
fi
if grep -q "^DATA_PROTECTION_CERT_PASS=" "$ENV_FILE" 2>/dev/null && [ ! -f "${SECRETS_DIR}/data_protection_cert_pass" ]; then
all_secrets_migrated=false
fi
if grep -q "^POSTGRES_PASSWORD=" "$ENV_FILE" 2>/dev/null && [ ! -f "${SECRETS_DIR}/postgres_password" ]; then
all_secrets_migrated=false
fi
if grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" 2>/dev/null && [ ! -f "${SECRETS_DIR}/admin_password_hash" ]; then
all_secrets_migrated=false
fi
if [ "$all_secrets_migrated" = false ]; then
printf "${YELLOW}⚠ Not all secrets have been migrated to files. Keeping secrets in .env${NC}\n"
return 1
fi
# Remove secrets from .env
if grep -q "^JWT_KEY=" "$ENV_FILE"; then
sed -i.bak "/^JWT_KEY=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
removed=true
fi
if grep -q "^DATA_PROTECTION_CERT_PASS=" "$ENV_FILE"; then
sed -i.bak "/^DATA_PROTECTION_CERT_PASS=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
removed=true
fi
if grep -q "^POSTGRES_PASSWORD=" "$ENV_FILE"; then
sed -i.bak "/^POSTGRES_PASSWORD=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
removed=true
fi
if grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE"; then
sed -i.bak "/^ADMIN_PASSWORD_HASH=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
removed=true
fi
if grep -q "^ADMIN_PASSWORD_GENERATED=" "$ENV_FILE"; then
sed -i.bak "/^ADMIN_PASSWORD_GENERATED=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
removed=true
fi
if [ "$removed" = true ]; then
printf " ${GREEN}✓ Secrets have been removed from .env file${NC}\n"
fi
}
# Helper function to delete environment variables
delete_env_var() {
local key=$1
if [ -f "$ENV_FILE" ]; then
sed -i.bak "/^${key}=/d" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
printf " ${GREEN}> $key has been removed from $ENV_FILE.${NC}\n"
fi
}
# Function to migrate Docker image URLs from old namespace to new namespace
# Used when migrating from 0.22.0 or lower to 0.23.0+
migrate_docker_image_urls() {
if [ ! -f "docker-compose.yml" ] || ! grep -q "ghcr.io/lanedirt/aliasvault-" "docker-compose.yml" 2>/dev/null; then
return 0
fi
printf "${CYAN} Migrating Docker image URLs to new official namespace aliasvault/*...${NC}\n"
# Process docker-compose.yml
if [ -f "docker-compose.yml" ] && grep -q "ghcr.io/lanedirt/aliasvault-" "docker-compose.yml"; then
# Create backup before modifying
cp docker-compose.yml "docker-compose.yml.backup.$(date +%Y%m%d_%H%M%S)"
# Update all image URLs
sed -i.tmp 's|ghcr.io/lanedirt/aliasvault-postgres:|ghcr.io/aliasvault/postgres:|g' docker-compose.yml
sed -i.tmp 's|ghcr.io/lanedirt/aliasvault-client:|ghcr.io/aliasvault/client:|g' docker-compose.yml
sed -i.tmp 's|ghcr.io/lanedirt/aliasvault-api:|ghcr.io/aliasvault/api:|g' docker-compose.yml
sed -i.tmp 's|ghcr.io/lanedirt/aliasvault-admin:|ghcr.io/aliasvault/admin:|g' docker-compose.yml
sed -i.tmp 's|ghcr.io/lanedirt/aliasvault-smtp:|ghcr.io/aliasvault/smtp:|g' docker-compose.yml
sed -i.tmp 's|ghcr.io/lanedirt/aliasvault-task-runner:|ghcr.io/aliasvault/task-runner:|g' docker-compose.yml
sed -i.tmp 's|ghcr.io/lanedirt/aliasvault-reverse-proxy:|ghcr.io/aliasvault/reverse-proxy:|g' docker-compose.yml
# Clean up temp files
rm -f docker-compose.yml.tmp docker-compose.yml.backup.*
printf " ${GREEN}✓ Updated docker-compose.yml${NC}\n"
fi
printf " ${GREEN}✓ Docker image URL migration completed${NC}\n"
}
# Function to print a success box
print_success_box() {
local message="$1"
local width=70
local header="╔══════════════════════════════════════════════════════════════════════╗"
local footer="╚══════════════════════════════════════════════════════════════════════╝"
local success="✓ SUCCESS!"
local success_line
local message_line
local padding
# Print header
printf "${MAGENTA}%s${NC}\n" "$header"
# Construct second line with centered success text
local success_padding=$(( (width - ${#success}) / 2 ))
printf "${MAGENTA}${NC}%*s${GREEN}%s${NC}%*s${MAGENTA}${NC}\n" \
"$success_padding" "" "$success" "$((width - success_padding - ${#success}))" ""
# Construct third line with centered message
local msg_len=${#message}
local msg_padding=$(( (width - msg_len) / 2 ))
printf "${MAGENTA}${NC}%*s${BOLD}%s${NC}%*s${MAGENTA}${NC}\n" \
"$msg_padding" "" "$message" "$((width - msg_padding - msg_len))" ""
# Print footer
printf "${MAGENTA}%s${NC}\n" "$footer"
}
# Function to clean up old Docker images
cleanup_docker_images() {
printf "\n${YELLOW}+++ Cleaning up old Docker images +++${NC}\n"
# Only clean up AliasVault-related images and dangling images
local current_version="$1"
local removed_count=0
local total_count=0
if [ "$VERBOSE" = true ]; then
printf "${CYAN} Removing old AliasVault images...${NC}\n"
# Remove dangling images (untagged)
docker image prune -f
# Get all images containing "aliasvault" (case-insensitive) in the repository name
docker images --format "{{.Repository}}:{{.Tag}}" | grep -i "aliasvault" | \
while read -r image_full; do
# Extract just the tag from the full image name
local tag="${image_full##*:}"
# Skip if image is currently being used by a container
if ! docker ps -a --format "{{.Image}}" | grep -q "^${image_full}$"; then
# Skip if it's the current version or latest
if [ "$tag" != "$current_version" ] && [ "$tag" != "latest" ]; then
((total_count++))
printf " Removing old image: $image_full"
# Remove by full repository:tag name, not by ID
if docker rmi "$image_full" > /dev/null 2>&1; then
printf " ${GREEN}${NC}\n"
((removed_count++))
else
printf " ${YELLOW}(skipped - may share layers)${NC}\n"
fi
fi
fi
done
if [ $total_count -eq 0 ]; then
printf " ${CYAN}No old images to remove${NC}\n"
elif [ $removed_count -eq $total_count ]; then
printf " ${GREEN}Successfully removed all $removed_count old image(s)${NC}\n"
else
printf " ${GREEN}Removed $removed_count of $total_count old image(s)${NC}\n"
fi
printf "${GREEN}✓ Docker image cleanup completed${NC}\n"
else
(
# Silent cleanup with spinner
{
# Remove dangling images
docker image prune -f > /dev/null 2>&1
# Remove old AliasVault images by full name
docker images --format "{{.Repository}}:{{.Tag}}" | grep -i "aliasvault" | \
while read -r image_full; do
# Extract just the tag from the full image name
local tag="${image_full##*:}"
# Skip if image is currently being used by a container
if ! docker ps -a --format "{{.Image}}" | grep -q "^${image_full}$"; then
# Skip if it's the current version or latest
if [ "$tag" != "$current_version" ] && [ "$tag" != "latest" ]; then
# Remove by full repository:tag name
docker rmi "$image_full" > /dev/null 2>&1 || true
fi
fi
done
} &
CLEANUP_PID=$!
show_spinner $CLEANUP_PID "Cleaning up old Docker images "
wait $CLEANUP_PID
)
printf "${GREEN}✓ Docker image cleanup completed${NC}\n"
fi
}
# Function to print success message
print_install_success_message() {
printf "\n"
print_success_box "AliasVault is successfully installed!"
printf "\n"
printf "${BOLD}To configure the server, login to the admin panel:${NC}\n"
printf "\n"
if [ -n "$PASSWORD" ]; then
printf " ${CYAN}Admin Panel:${NC} https://localhost/admin\n"
printf " ${CYAN}Username:${NC} admin\n"
printf " ${CYAN}Password:${NC} $PASSWORD\n"
printf "\n"
printf "${YELLOW}⚠ IMPORTANT: Make sure to backup the above credentials in a safe place,${NC}\n"
printf "${YELLOW} they won't be shown again!${NC}\n"
else
printf " ${CYAN}Admin Panel:${NC} https://localhost/admin\n"
printf " ${CYAN}Username:${NC} admin\n"
printf " ${CYAN}Password:${NC} (Previously set. Use ./install.sh reset-admin-password to generate new one.)\n"
fi
printf "\n"
printf "${BOLD}To start using AliasVault, log into the client website:${NC}\n"
printf "\n"
printf " ${CYAN}Client Website:${NC} https://localhost/\n"
}
# Function to check if AliasVault is responding on configured ports
check_aliasvault_health() {
printf "${CYAN} Verifying AliasVault is responding...${NC} "
local ports_config
ports_config=$(get_port_config)
read -r http_port https_port smtp_port <<< "$ports_config"
local max_attempts=30
local attempt=1
local spinstr='|/-\\'
local i=0
local all_healthy=false
while [ $attempt -le $max_attempts ]; do
printf "\b%c" "${spinstr:$i:1}"
((i = (i + 1) % 4))
local http_ok=false
local https_ok=false
# Check HTTP port (should redirect to HTTPS)
if curl -Ls -o /dev/null -w "%{http_code}" "http://localhost:$http_port" 2>/dev/null | grep -q "301"; then
http_ok=true
fi
# Check HTTPS port (should return status page or main app)
local https_status
https_status=$(curl -Ls -k -o /dev/null -w "%{http_code}" "https://localhost:$https_port" 2>/dev/null)
if [ "$https_status" = "200" ] || [ "$https_status" = "503" ]; then
https_ok=true
fi
if [ "$http_ok" = true ] && [ "$https_ok" = true ]; then
all_healthy=true
break
fi
sleep 2
((attempt++))
done
if [ "$all_healthy" = true ]; then
printf "\b ${GREEN}${NC}\n"
printf " ${GREEN}✓ AliasVault is responding on HTTP:$http_port and HTTPS:$https_port${NC}\n"
return 0
else
printf "\b ${RED}${NC}\n"
printf "\n${RED}❌ Health Check Failed${NC}\n"
printf "AliasVault containers started but the service is not responding properly.\n"
printf "\n"
printf "${YELLOW}Troubleshooting steps:${NC}\n"
printf "1. Check container status: ${DIM}docker compose ps${NC}\n"
printf "2. Check container logs: ${DIM}docker compose logs${NC}\n"
printf "3. Verify ports are not blocked by firewall\n"
printf "4. Wait a few more minutes for services to fully initialize\n"
printf "\n"
printf "You can manually check if AliasVault is working by visiting:\n"
printf " ${CYAN}https://localhost:$https_port${NC}\n"
printf "\n"
return 1
fi
}
# Function to recreate (restart) Docker containers
recreate_docker_containers() {
if [ "$VERBOSE" = true ]; then
printf "${CYAN} (Re)creating Docker containers...${NC}\n"
printf "\b${NC}\n"
if ! eval "$(get_docker_compose_command) up -d --force-recreate"; then
log_error "Failed to recreate Docker containers"
exit 1
fi
else
(
eval "$(get_docker_compose_command) up -d --force-recreate" > /tmp/docker_recreate.log 2>&1 &
RECREATE_PID=$!
show_spinner $RECREATE_PID "Recreating Docker containers "
wait $RECREATE_PID
RECREATE_EXIT_CODE=$?
if [ $RECREATE_EXIT_CODE -ne 0 ]; then
log_error "Failed to recreate Docker containers. Output:"
cat /tmp/docker_recreate.log >&2
exit 1
fi
rm -f /tmp/docker_recreate.log
)
fi
printf "${GREEN}✓ Docker containers (re)created successfully${NC}\n"
}
# Function to print password reset success message
print_password_reset_message() {
printf "\n"
print_success_box "The admin password has been successfully reset!"
printf "\n"
printf "${BOLD}New admin credentials:${NC}\n"
printf " ${CYAN}Admin Panel:${NC} https://localhost/admin\n"
printf " ${CYAN}Username:${NC} admin\n"
printf " ${CYAN}Password:${NC} $PASSWORD\n"
printf "\n"
printf "${YELLOW}⚠ IMPORTANT: Make sure to backup the above credentials in a safe place,${NC}\n"
printf "${YELLOW} they won't be shown again!${NC}\n"
}
# Function to get docker compose command with appropriate config files
get_docker_compose_command() {
local base_command="docker compose -f docker-compose.yml"
# Check if using build configuration
if grep -q "^DEPLOYMENT_MODE=build" "$ENV_FILE" 2>/dev/null; then
base_command="$base_command -f dockerfiles/docker-compose.build.yml"
fi
# Check if Let's Encrypt is enabled
if grep -q "^LETSENCRYPT_ENABLED=true" "$ENV_FILE" 2>/dev/null; then
base_command="$base_command -f docker-compose.letsencrypt.yml"
fi
echo "$base_command"
}
# Add this new function for handling registration configuration
handle_registration_configuration() {
printf "${YELLOW}+++ Public Registration Configuration +++${NC}\n"
printf "\n"
# Check if AliasVault is installed
if [ ! -f "docker-compose.yml" ]; then
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
exit 1
fi
# Get current registration setting
CURRENT_SETTING=$(grep "^PUBLIC_REGISTRATION_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)
printf "${CYAN}About Public Registration:${NC}\n"
printf "Public registration allows new users to create their own accounts on your AliasVault server.\n"
printf "When disabled, no new accounts can be created.\n"
printf "\n"
printf "${CYAN}Current Configuration:${NC}\n"
if [ "$CURRENT_SETTING" = "true" ]; then
printf "Public Registration: ${GREEN}Enabled${NC}\n"
else
printf "Public Registration: ${RED}Disabled${NC}\n"
fi
printf "\n"
printf "Options:\n"
printf "1) Enable public registration\n"
printf "2) Disable public registration\n"
printf "3) Cancel\n"
printf "\n"
read -p "Select an option [1-3]: " reg_option
case $reg_option in
1)
update_env_var "PUBLIC_REGISTRATION_ENABLED" "true"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
handle_restart
# Print success message
printf "\n"
print_success_box "Public registration has been enabled!"
;;
2)
update_env_var "PUBLIC_REGISTRATION_ENABLED" "false"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
handle_restart
# Print success message
printf "\n"
print_success_box "Public registration has been disabled!"
;;
3)
printf "${YELLOW}Registration configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
}
# Function to handle initial installation or reinstallation
handle_install() {
local specified_version="$1"
# If version specified, install that version directly
if [ -n "$specified_version" ]; then
handle_install_version "$specified_version"
return
fi
# Check if .env exists before reading
if [ -f "$ENV_FILE" ]; then
if grep -q "^ALIASVAULT_VERSION=" "$ENV_FILE"; then
current_version=$(grep "^ALIASVAULT_VERSION=" "$ENV_FILE" | cut -d '=' -f2)
printf "${CYAN}> Current AliasVault version: ${current_version}${NC}\n"
printf "\n"
printf "==================================================\n"
printf "AliasVault is already installed.\n"
printf "==================================================\n"
printf "1. To reinstall the current version (${current_version}), continue with this script\n"
printf "2. To check for updates and to install the latest version, use: ./install.sh update\n"
printf "3. To install a specific version, use: ./install.sh install <version>\n\n"
read -p "Would you like to reinstall the current version? [y/N]: " REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
printf "${YELLOW}> Installation cancelled.${NC}\n"
exit 0
fi
handle_install_version "$current_version"
return
fi
fi
handle_install_version "latest"
}
# Function to handle build
handle_build() {
printf "\n${YELLOW}+++ Building AliasVault from source +++${NC}\n"
# Set deployment mode to build to ensure container lifecycle uses build configuration
set_deployment_mode "build"
# Initialize workspace which makes sure all required directories and files exist
initialize_workspace
# Check for required build files
if [ ! -f "dockerfiles/docker-compose.build.yml" ] || [ ! -d "apps/server" ]; then
printf "${RED}Error: Required files for building from source are missing.${NC}\n"
printf "\n"
printf "To build AliasVault from source, you need:\n"
printf "1. dockerfiles/docker-compose.build.yml file\n"
printf "2. apps/server/ directory with the complete source code\n"
printf "\n"
printf "Please clone the complete repository using:\n"
printf "git clone https://github.com/${REPO_OWNER}/${REPO_NAME}.git\n"
printf "\n"
printf "Alternatively, you can use './install.sh install' to pull pre-built images.\n"
exit 1
fi
# Initialize environment with proper error handling
check_and_populate_env
# Only generate admin password if not already set
if [ ! -f "${SECRETS_DIR}/admin_password_hash" ] || [ -z "$(cat "${SECRETS_DIR}/admin_password_hash" 2>/dev/null)" ]; then
generate_admin_password || { printf "${RED}> Failed to generate admin password${NC}\n"; exit 1; }
fi
printf "\n${YELLOW}+++ Building and starting services +++${NC}\n"
if [ "$VERBOSE" = true ]; then
printf "${CYAN} Building Docker Compose stack...${NC}\n"
printf "\b${NC}\n"
if ! eval "$(get_docker_compose_command) build"; then
log_error "Failed to build Docker Compose stack"
exit 1
fi
else
(
eval "$(get_docker_compose_command) build" > install_compose_build_output.log 2>&1 &
BUILD_PID=$!
show_spinner $BUILD_PID "Building Docker Compose stack "
wait $BUILD_PID
BUILD_EXIT_CODE=$?
if [ $BUILD_EXIT_CODE -ne 0 ]; then
log_error "Failed to build Docker Compose stack. Build output:"
cat install_compose_build_output.log >&2
exit 1
fi
rm -f install_compose_build_output.log
)
fi
printf "${GREEN}✓ Docker Compose stack built successfully${NC}\n"
recreate_docker_containers
# Check if AliasVault is actually responding before showing success
if ! check_aliasvault_health; then
printf "${YELLOW}Installation completed but AliasVault health check failed.${NC}\n"
printf "${YELLOW}Please check the troubleshooting steps above.${NC}\n"
exit 1
fi
# Clean up old Docker images
cleanup_docker_images "$target_version"
# Only show success message if we made it here without errors and health check passed
print_install_success_message
}
# Function to handle uninstall
handle_uninstall() {
printf "${YELLOW}+++ Uninstalling AliasVault +++${NC}\n"
printf "\n"
# Check if -y flag was passed
if [ "$FORCE_YES" != "true" ]; then
# Ask for confirmation before proceeding
read -p "Are you sure you want to uninstall AliasVault? This will remove all containers and images. [y/N]: " REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
printf "${YELLOW}> Uninstall cancelled.${NC}\n"
exit 0
fi
fi
printf "${CYAN}> Stopping and removing Docker containers...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose -f docker-compose.yml down -v || {
printf "${RED}> Failed to stop and remove Docker containers${NC}\n"
exit 1
}
else
docker compose -f docker-compose.yml down -v > /dev/null 2>&1 || {
printf "${RED}> Failed to stop and remove Docker containers${NC}\n"
exit 1
}
fi
printf "${GREEN}> Docker containers stopped and removed.${NC}\n"
# Remove version from .env
delete_env_var "ALIASVAULT_VERSION" ""
printf "${CYAN}> Removing Docker images...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose -f docker-compose.yml down --rmi all || {
printf "${RED}> Failed to remove Docker images${NC}\n"
exit 1
}
else
docker compose -f docker-compose.yml down --rmi all > /dev/null 2>&1 || {
printf "${RED}> Failed to remove Docker images${NC}\n"
exit 1
}
fi
printf "${GREEN}> Docker images removed.${NC}\n"
printf "${CYAN}> Pruning Docker system...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker system prune -af || {
printf "${RED}> Failed to prune Docker system${NC}\n"
exit 1
}
else
docker system prune -af > /dev/null 2>&1 || {
printf "${RED}> Failed to prune Docker system${NC}\n"
exit 1
}
fi
printf "${GREEN}> Docker system pruned.${NC}\n"
# Only show success message if we made it here without errors
printf "\n"
print_success_box "AliasVault has been successfully uninstalled!"
printf "\n"
printf "All Docker containers and images related to AliasVault have been removed.\n"
printf "The current directory, including database, logs and .env files, has been left intact.\n"
printf "\n"
printf "If you wish to remove the remaining files, it's safe to do so now.\n"
}
# Function to handle SSL configuration
handle_ssl_configuration() {
printf "${YELLOW}+++ SSL Certificate Configuration +++${NC}\n"
printf "\n"
# Check if AliasVault is installed
if [ ! -f "docker-compose.yml" ]; then
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
exit 1
fi
populate_hostname || { printf "${RED}> Failed to set hostname${NC}\n"; exit 1; }
# Get the current hostname and SSL config from .env
CURRENT_HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
LETSENCRYPT_ENABLED=$(grep "^LETSENCRYPT_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)
printf "${CYAN}SSL Certificate Options:${NC}\n"
printf "AliasVault uses a self-signed SSL certificate by default.\n"
printf "This provides encryption but may trigger browser warnings.\n"
printf "\n"
printf "You can switch to a trusted Let's Encrypt certificate, which:\n"
printf " - Avoids browser warnings\n"
printf " - Requires a public domain (not localhost)\n"
printf " - Needs ports 80 and 443 open to the internet\n"
printf "\n"
printf "Let's Encrypt certificates auto-renew before expiry.\n"
printf "\n"
printf "${CYAN}Current Configuration:${NC}\n"
if [ "$LETSENCRYPT_ENABLED" = "true" ]; then
printf "Using: ${GREEN}Let's Encrypt${NC}\n"
else
printf "Using: ${YELLOW}Self-signed${NC}\n"
fi
printf "Hostname: ${CYAN}${CURRENT_HOSTNAME}${NC} (change via: ./install.sh configure-hostname)\n"
printf "\n"
printf "Choose an option:\n"
printf "1) Use Let's Encrypt certificate (recommended for public domains)\n"
printf "2) Use self-signed certificate\n"
printf "3) Cancel\n"
printf "\n"
read -p "Select an option [1-3]: " ssl_option
case $ssl_option in
1)
configure_letsencrypt
;;
2)
generate_self_signed_cert
;;
3)
printf "${YELLOW}SSL configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
}
# Function to handle email server configuration
handle_email_configuration() {
# Setup trap for Ctrl+C and other interrupts
trap 'printf "\n${YELLOW}Configuration cancelled by user.${NC}\n"; exit 1' INT TERM
printf "${YELLOW}+++ Email Server Configuration +++${NC}\n"
printf "\n"
# Check if AliasVault is installed
if [ ! -f "docker-compose.yml" ]; then
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
exit 1
fi
# Get current email domains from .env
CURRENT_DOMAINS=$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
printf "${CYAN}About Email Server:${NC}\n"
printf "AliasVault includes a built-in email server for handling virtual email addresses.\n"
printf "When enabled, it can receive emails for one or more configured domains.\n"
printf "Each domain must have an MX DNS record pointing to this server's hostname.\n"
printf "\n"
printf "${CYAN}Current Configuration:${NC}\n"
if [ -z "$CURRENT_DOMAINS" ] || [ "$CURRENT_DOMAINS" = "DISABLED.TLD" ]; then
printf "Email Server Status: ${RED}Disabled${NC}\n"
else
printf "Email Server Status: ${GREEN}Enabled${NC}\n"
printf "Active Domains: ${CYAN}${CURRENT_DOMAINS}${NC}\n"
fi
printf "\n"
printf "Email Server Options:\n"
printf "1) Enable email server / Update domains\n"
printf "2) Disable email server\n"
printf "3) Cancel\n"
printf "\n"
read -p "Select an option [1-3]: " email_option
case $email_option in
1)
while true; do
printf "\n${CYAN}Enter domain(s) for email server${NC}\n"
printf "For multiple domains, separate with commas (e.g. domain1.com,domain2.com)\n"
printf "IMPORTANT: Each domain must have an MX record in DNS pointing to this server.\n"
read -p "Domains: " new_domains
if [ -z "$new_domains" ]; then
printf "${RED}Error: Domains cannot be empty${NC}\n"
continue
fi
printf "\n${CYAN}You entered the following domains:${NC}\n"
IFS=',' read -ra DOMAIN_ARRAY <<< "$new_domains"
for domain in "${DOMAIN_ARRAY[@]}"; do
printf " - ${GREEN}${domain}${NC}\n"
done
printf "\n"
read -p "Are these domains correct? (y/n): " confirm
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
break
fi
done
# Update .env file and restart
if ! update_env_var "PRIVATE_EMAIL_DOMAINS" "$new_domains"; then
printf "${RED}Failed to update configuration.${NC}\n"
exit 1
fi
printf "${GREEN}Email server configuration updated${NC}\n"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
if ! handle_restart; then
printf "${RED}Failed to restart services.${NC}\n"
exit 1
fi
# Print success message
printf "\n"
print_success_box "The email server is now successfully configured!"
# Print next steps
printf "\n"
printf "To test the email server:\n"
printf " a. Log in to your AliasVault account\n"
printf " b. Create a new alias using one of your configured private domains\n"
printf " c. Send a test email from an external email service (e.g., Gmail)\n"
printf " d. Check if the email appears in your AliasVault inbox\n"
printf "\n"
printf "If emails don't arrive, please verify:\n"
printf " > DNS MX records are correctly configured\n"
printf " > Your server's firewall allows incoming traffic on port 25 and 587\n"
printf " > Your ISP/hosting provider doesn't block SMTP traffic\n"
printf "\n"
;;
2)
printf "\n${YELLOW}Warning: Docker containers need to be restarted after disabling the email server.${NC}\n"
read -p "Continue with disable and restart? (y/n): " disable_confirm
if [ "$disable_confirm" != "y" ] && [ "$disable_confirm" != "Y" ]; then
printf "${YELLOW}Configuration cancelled.${NC}\n"
exit 0
fi
# Disable email server
if ! update_env_var "PRIVATE_EMAIL_DOMAINS" ""; then
printf "${RED}Failed to update configuration.${NC}\n"
exit 1
fi
if ! handle_restart; then
printf "${RED}Failed to restart services.${NC}\n"
exit 1
fi
# Print success message
printf "\n"
print_success_box "The email server has been disabled successfully!"
;;
3)
printf "${YELLOW}Email configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
# Remove the trap before normal exit
trap - INT TERM
}
# Function to configure Let's Encrypt
configure_letsencrypt() {
printf "${CYAN}> Configuring Let's Encrypt SSL certificate...${NC}\n"
# Check if hostname is localhost
if [ "$CURRENT_HOSTNAME" = "localhost" ]; then
printf "${RED}Error: Let's Encrypt certificates cannot be issued for 'localhost'.${NC}\n"
printf "${YELLOW}Please configure a valid publically resolvable domain name (e.g. mydomain.com) before setting up Let's Encrypt.${NC}\n"
exit 1
fi
# Check if hostname is a valid domain
if ! [[ "$CURRENT_HOSTNAME" =~ \.[a-zA-Z]{2,}$ ]]; then
printf "${RED}Error: Invalid hostname '${CURRENT_HOSTNAME}'.${NC}\n"
printf "${YELLOW}Please configure a valid publically resolvable domain name (e.g. mydomain.com) before setting up Let's Encrypt.${NC}\n"
exit 1
fi
# Verify DNS is properly configured
printf "\n${YELLOW}Important: Before proceeding, ensure that:${NC}\n"
printf "1. AliasVault is currently running and accessible at ${CYAN}https://${CURRENT_HOSTNAME}${NC}\n"
printf "2. Your domain (${CYAN}${CURRENT_HOSTNAME}${NC}) is externally resolvable to this server's IP address\n"
printf "3. Ports 80 and 443 are open and accessible from the internet\n"
printf "\n"
read -p "Have you completed these steps? [y/N]: " REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
printf "${YELLOW}> Let's Encrypt configuration cancelled.${NC}\n"
exit 0
fi
# Get contact email for Let's Encrypt
SUPPORT_EMAIL=$(grep "^SUPPORT_EMAIL=" "$ENV_FILE" | cut -d '=' -f2)
LETSENCRYPT_EMAIL=""
while true; do
printf "\nPlease enter a valid email address that will be used for Let's Encrypt certificate notifications:\n"
read -p "Email: " LETSENCRYPT_EMAIL
if [[ "$LETSENCRYPT_EMAIL" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
printf "Confirm using ${CYAN}${LETSENCRYPT_EMAIL}${NC} for Let's Encrypt notifications? [y/N] "
read REPLY
if [[ $REPLY =~ ^[Yy]$ ]]; then
break
fi
else
printf "${RED}Invalid email format. Please try again.${NC}\n"
fi
done
# Create certbot directories
printf "${CYAN}> Creating Let's Encrypt directories...${NC}\n"
mkdir -p ./certificates/letsencrypt/www
# Get absolute path for certificates directory for the docker bind mounts
CERTIFICATES_DIR=$(realpath ./certificates)
# Request certificate using a temporary certbot container
printf "${CYAN}> Requesting Let's Encrypt certificate...${NC}\n"
docker run --rm \
-v "${CERTIFICATES_DIR}/letsencrypt:/etc/letsencrypt:rw" \
-v "${CERTIFICATES_DIR}/letsencrypt/www:/var/www/certbot:rw" \
certbot/certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
--email "$LETSENCRYPT_EMAIL" \
--agree-tos \
--no-eff-email \
--non-interactive \
--domains ${CURRENT_HOSTNAME} \
--force-renewal
if [ $? -ne 0 ]; then
printf "${RED}Failed to obtain Let's Encrypt certificate.${NC}\n"
exit 1
fi
# Fix permissions on Let's Encrypt directories and files
sudo chmod -R 755 ./certificates/letsencrypt
# Ensure private keys remain secure
sudo find ./certificates/letsencrypt -type f -name "privkey*.pem" -exec chmod 600 {} \;
sudo find ./certificates/letsencrypt -type f -name "fullchain*.pem" -exec chmod 644 {} \;
# Update .env to indicate Let's Encrypt is enabled
update_env_var "LETSENCRYPT_ENABLED" "true"
# Restart only the reverse proxy with new configuration so it loads the new certificate
printf "${CYAN}> Restarting reverse proxy with Let's Encrypt configuration...${NC}\n"
eval "$(get_docker_compose_command) up -d reverse-proxy --force-recreate"
# Starting certbot container to renew certificates automatically
printf "${CYAN}> Starting new certbot container to renew certificates automatically...${NC}\n"
eval "$(get_docker_compose_command) up -d certbot"
# Print success message
printf "\n"
print_success_box "Let's Encrypt SSL certificate has been configured successfully!"
}
# Function to generate self-signed certificate
generate_self_signed_cert() {
printf "${CYAN}> Generating new self-signed certificate...${NC}\n"
# Disable Let's Encrypt
update_env_var "LETSENCRYPT_ENABLED" "false"
# Get current hostname from .env
HOSTNAME_VALUE=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
if [ -n "$HOSTNAME_VALUE" ] && [ "$HOSTNAME_VALUE" != "localhost" ]; then
printf "${CYAN}> Using configured hostname: ${HOSTNAME_VALUE}${NC}\n"
printf "${CYAN}> The certificate will include:${NC}\n"
printf " ${GREEN}Primary CN:${NC} ${HOSTNAME_VALUE}\n"
printf " ${GREEN}Alternative Names:${NC} localhost, 127.0.0.1\n\n"
fi
# Stop existing containers
printf "${CYAN}> Stopping existing containers...${NC}\n"
docker compose down
# Remove existing certificates and hostname marker
rm -f ./certificates/ssl/cert.pem ./certificates/ssl/key.pem ./certificates/ssl/.hostname_marker
# Remove Let's Encrypt directories
rm -rf ./certificates/letsencrypt
# Start containers (which will generate new self-signed cert with hostname)
printf "${CYAN}> Restarting services...${NC}\n"
docker compose up -d
# Print success message
printf "\n"
print_success_box "New self-signed certificate has been generated successfully!"
}
# New functions to handle container lifecycle:
handle_start() {
printf "${CYAN}> Starting AliasVault containers...${NC}\n"
eval "$(get_docker_compose_command) up -d"
printf "${GREEN}> AliasVault containers started successfully.${NC}\n"
}
handle_stop() {
printf "${CYAN}> Stopping AliasVault containers...${NC}\n"
if ! docker compose ps --quiet 2>/dev/null | grep -q .; then
printf "${YELLOW}> No containers are currently running.${NC}\n"
exit 0
fi
eval "$(get_docker_compose_command) down"
printf "${GREEN}> AliasVault containers stopped successfully.${NC}\n"
}
handle_restart() {
printf "\n${CYAN}> Restarting AliasVault containers...${NC}\n"
eval "$(get_docker_compose_command) down"
eval "$(get_docker_compose_command) up -d"
printf "${GREEN}> AliasVault containers restarted successfully.${NC}\n"
}
# Function to handle updates
handle_update() {
printf "\n${YELLOW}+++ Checking for AliasVault updates +++${NC}\n"
# First check for install.sh updates
check_install_script_update || true
# Check current version
if ! grep -q "^ALIASVAULT_VERSION=" "$ENV_FILE"; then
printf "${CYAN}> No version information found. Running first-time update check...${NC}\n"
printf "\n"
handle_install_version "latest"
return
fi
current_version=$(grep "^ALIASVAULT_VERSION=" "$ENV_FILE" | cut -d '=' -f2)
latest_version=$(get_latest_version) || {
printf "${RED}> Failed to check for updates. Please try again later.${NC}\n"
exit 1
}
if [ -z "$latest_version" ]; then
printf "${RED}> Failed to check for updates. Please try again later.${NC}\n"
exit 1
fi
if [ "$current_version" = "$latest_version" ]; then
printf "\n"
printf "You are already running the latest version of AliasVault (${current_version})!\n"
exit 0
fi
if [ "$FORCE_YES" = true ]; then
printf "${CYAN}> Updating AliasVault to the latest version (${latest_version})...${NC}\n"
handle_install_version "$latest_version"
printf "${GREEN}> Update completed successfully!${NC}\n"
return
fi
printf "\n"
printf "A new version of AliasVault is available (${latest_version})!\n"
printf "\n"
printf "${MAGENTA}Important:${NC}\n"
printf "1. It's recommended to backup your database before updating (./install.sh db-export > backup.sql.gz)\n"
printf "2. The update process will restart all containers\n"
printf "\n"
read -p "Would you like to continue with the update? [y/N]: " REPLY
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
printf "${YELLOW}> Update cancelled.${NC}\n"
exit 0
fi
printf "${CYAN}> Updating AliasVault...${NC}\n"
printf "\n"
handle_install_version "$latest_version"
}
# Function to extract version
extract_version() {
local file="$1"
local version=$(head -n 2 "$file" | grep '@version' | cut -d' ' -f3)
echo "$version"
}
# Function to compare semantic versions
compare_versions() {
local version1="$1"
local version2="$2"
# Split versions into arrays
IFS='.' read -ra v1_parts <<< "$version1"
IFS='.' read -ra v2_parts <<< "$version2"
# Compare each part numerically
for i in {0..2}; do
# Default to 0 if part doesn't exist
local v1_part=${v1_parts[$i]:-0}
local v2_part=${v2_parts[$i]:-0}
# Compare numerically
if [ "$v1_part" -gt "$v2_part" ]; then
echo "1" # version1 is greater
return
elif [ "$v1_part" -lt "$v2_part" ]; then
echo "-1" # version1 is lesser
return
fi
done
echo "0" # versions are equal
}
# Function to check if install.sh needs updating
check_install_script_update() {
printf "${CYAN}> Checking for install script updates...${NC}\n"
# Get latest release version
local latest_version=$(get_latest_version) || {
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
return 1
}
if [ -z "$latest_version" ]; then
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
return 1
fi
if ! curl -LsSf "${GITHUB_RAW_URL_REPO}/${latest_version}/install.sh" -o "install.sh.tmp"; then
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
rm -f install.sh.tmp
return 1
fi
# Get versions
local current_version=$(extract_version "install.sh")
local new_version=$(extract_version "install.sh.tmp")
# Check if versions could be extracted
if [ -z "$current_version" ] || [ -z "$new_version" ]; then
printf "${YELLOW}> Could not determine script versions. Falling back to file comparison...${NC}\n"
# Fall back to file comparison
if ! cmp -s "install.sh" "install.sh.tmp"; then
printf "${YELLOW}> Changes detected in install script.${NC}\n"
else
printf "${GREEN}> Install script is up to date.${NC}\n"
rm -f install.sh.tmp
return 0
fi
else
# Compare versions using semver comparison
if [ "$current_version" = "$new_version" ]; then
printf "${GREEN}> Install script is up to date.${NC}\n"
rm -f install.sh.tmp
return 0
else
local compare_result=$(compare_versions "$current_version" "$new_version")
if [ "$compare_result" -ge "0" ]; then
printf "${GREEN}> Install script is up to date.${NC}\n"
rm -f install.sh.tmp
return 0
fi
fi
fi
# If we get here, an update is available
if [ "$FORCE_YES" = true ]; then
printf "${CYAN}> Updating install script...${NC}\n"
cp "install.sh" "install.sh.backup"
mv "install.sh.tmp" "install.sh"
chmod +x "install.sh"
printf "${GREEN}> Install script updated successfully.${NC}\n"
printf "${GREEN}> Backup of previous version saved as install.sh.backup${NC}\n"
exit 0
fi
printf "${YELLOW}> A new version of the install script is available (${new_version}).${NC}\n"
printf "\n"
printf "Would you like to update the install script? [Y/n]: "
read -r reply
if [[ ! $reply =~ ^[Nn]$ ]]; then
# Create backup of current script
cp "install.sh" "install.sh.backup"
if mv "install.sh.tmp" "install.sh"; then
chmod +x "install.sh"
printf "${GREEN}> Install script updated successfully.${NC}\n"
printf "${GREEN}> Backup of previous version saved as install.sh.backup${NC}\n"
printf "${YELLOW}> Please run the update command again to continue with the update process.${NC}\n"
exit 0
else
printf "${RED}> Failed to update install script. Continuing with current version.${NC}\n"
# Restore from backup if update failed
mv "install.sh.backup" "install.sh"
rm -f install.sh.tmp
return 1
fi
else
printf "${YELLOW}> Continuing with current install script version.${NC}\n"
rm -f install.sh.tmp
return 0
fi
}
# Function to perform the actual installation with specific version
handle_install_version() {
local target_version="$1"
# Validate semver format if a specific version is provided
if [ -n "$target_version" ] && [ "$target_version" != "latest" ]; then
if ! validate_semver "$target_version"; then
printf "${RED}Error: You tried to install AliasVault with version '${target_version}' which is an incorrect value.${NC}\n"
printf "Please check the command you executed and try again.${NC}\n"
printf "\n"
printf "The provided version must follow semantic versioning format (e.g., '0.0.1', '1.0.5') and match an existing version on GitHub.${NC}\n"
printf "Alternatively, you can omit the version to install the latest version.${NC}\n"
exit 1
fi
fi
# If latest, get actual version number from GitHub API
if [ "$target_version" = "latest" ]; then
local actual_version=$(get_latest_version) || {
printf "${RED}> Failed to get latest version. Please try again later.${NC}\n"
exit 1
}
if [ -n "$actual_version" ]; then
target_version="$actual_version"
fi
fi
printf "\n${YELLOW}+++ Installing AliasVault ${target_version} +++${NC}\n"
# Initialize workspace which makes sure all required directories and files exist
initialize_workspace
# Check if install script needs updating for this version
check_install_script_version "$target_version"
local check_result=$?
if [ $check_result -eq 2 ]; then
if [ "$FORCE_YES" = true ]; then
printf "${CYAN}> Updating install script to match version ${target_version}...${NC}\n"
else
printf "${YELLOW}> A different version of the install script is required for installing version ${target_version}.${NC}\n"
read -p "Would you like to self-update the install script before proceeding? [Y/n]: " reply
if [[ $reply =~ ^[Nn]$ ]]; then
printf "${YELLOW}> Continuing with current install script version.${NC}\n"
rm -f install.sh.tmp
fi
fi
if [ "$FORCE_YES" = true ] || [[ ! $reply =~ ^[Nn]$ ]]; then
# Create backup of current script
cp "install.sh" "install.sh.backup"
if mv "install.sh.tmp" "install.sh"; then
chmod +x "install.sh"
printf "${GREEN}> Install script updated successfully.${NC}\n"
printf "${GREEN}> Backup of previous version saved as install.sh.backup${NC}\n"
printf "${YELLOW}> Please run the same install command again to continue with the installation.${NC}\n"
exit 2
else
printf "${RED}> Failed to update install script. Continuing with current version.${NC}\n"
mv "install.sh.backup" "install.sh"
rm -f install.sh.tmp
fi
fi
fi
# Update docker-compose files with correct version so we pull the correct images
if ! handle_docker_compose "$target_version"; then
log_error "Failed to download docker-compose files"
exit 1
fi
# Initialize environment
check_and_populate_env
# Set deployment mode to install to ensure container lifecycle uses install configuration
set_deployment_mode "install"
# Only generate admin password if not already set
if [ ! -f "${SECRETS_DIR}/admin_password_hash" ] || [ -z "$(cat "${SECRETS_DIR}/admin_password_hash" 2>/dev/null)" ]; then
generate_admin_password || { printf "${RED}> Failed to generate admin password${NC}\n"; exit 1; }
fi
# Pull images from GitHub Container Registry
printf "\n${YELLOW}+++ Pulling Docker images +++${NC}\n"
printf "${CYAN} Installing version: ${target_version}${NC}\n"
images=(
"${GITHUB_CONTAINER_REGISTRY}/postgres:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}/reverse-proxy:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}/api:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}/client:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}/admin:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}/smtp:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}/task-runner:${target_version}"
)
for image in "${images[@]}"; do
if ! retry_command 3 5 enhanced_docker_pull "$image"; then
log_warning "Failed to pull image: $image - continuing anyway"
fi
done
printf "${GREEN}✓ Docker image pulling completed${NC}\n"
# Save version to .env
update_env_var "ALIASVAULT_VERSION" "$target_version"
# Start containers
printf "\n${YELLOW}+++ Starting services +++${NC}\n"
if [ "$VERBOSE" = true ]; then
printf "${CYAN} Starting Docker containers...${NC} "
printf "\b${NC}\n"
if ! docker compose up -d --force-recreate; then
log_error "Failed to start Docker containers"
exit 1
fi
else
(
docker compose up -d --force-recreate > /tmp/docker_start.log 2>&1 &
START_PID=$!
show_spinner $START_PID "Starting Docker containers "
wait $START_PID
START_EXIT_CODE=$?
if [ $START_EXIT_CODE -ne 0 ]; then
log_error "Failed to start Docker containers. Output:"
cat /tmp/docker_start.log >&2
exit 1
fi
rm -f /tmp/docker_start.log
)
fi
printf "${GREEN}✓ Docker containers started successfully${NC}\n"
# Check if AliasVault is actually responding before showing success
if ! check_aliasvault_health; then
printf "${YELLOW}Installation completed but AliasVault health check failed.${NC}\n"
printf "${YELLOW}Please check the troubleshooting steps above.${NC}\n"
exit 1
fi
# Clean up old Docker images
cleanup_docker_images "$target_version"
# Only show success message if we made it here without errors and health check passed
print_install_success_message
}
# Function to handle development database configuration
configure_dev_database() {
printf "${YELLOW}+++ Development Database Configuration +++${NC}\n"
printf "\n"
if [ ! -f "dockerfiles/docker-compose.dev.yml" ]; then
printf "${RED}> The dockerfiles/docker-compose.dev.yml file is missing. This file is required to start the development database. Please checkout the full GitHub repository and try again.${NC}\n"
return 1
fi
# Check if direct option was provided
if [ -n "$COMMAND_ARG" ]; then
case $COMMAND_ARG in
1|start)
if docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev ps --status running 2>/dev/null | grep -q postgres-dev; then
printf "${YELLOW}> Development database is already running.${NC}\n"
else
printf "${CYAN}> Starting development database...${NC}\n"
docker compose -p aliasvault-dev -f dockerfiles/docker-compose.dev.yml up -d --wait --wait-timeout 60
printf "${GREEN}> Development database started successfully.${NC}\n"
fi
print_dev_db_details
return
;;
0|stop)
if ! docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev ps --status running 2>/dev/null | grep -q postgres-dev; then
printf "${YELLOW}> Development database is already stopped.${NC}\n"
else
printf "${CYAN}> Stopping development database...${NC}\n"
docker compose -p aliasvault-dev -f dockerfiles/docker-compose.dev.yml down
printf "${GREEN}> Development database stopped successfully.${NC}\n"
fi
return
;;
esac
fi
# Check current status
if docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev ps --status running 2>/dev/null | grep -q postgres-dev; then
DEV_DB_STATUS="running"
else
DEV_DB_STATUS="stopped"
fi
printf "${CYAN}About Development Database:${NC}\n"
printf "A separate PostgreSQL instance for development purposes that:\n"
printf " - Runs on port 5433 (to avoid conflicts)\n"
printf " - Uses simple credentials (password: 'password')\n"
printf " - Stores data separately from production\n"
printf "\n"
printf "${CYAN}Current Status:${NC}\n"
if [ "$DEV_DB_STATUS" = "running" ]; then
printf "Development Database: ${GREEN}Running${NC}\n"
else
printf "Development Database: ${YELLOW}Stopped${NC}\n"
fi
printf "\n"
printf "Options:\n"
printf "1) Start development database\n"
printf "2) Stop development database\n"
printf "3) View connection details\n"
printf "4) Cancel\n"
printf "\n"
read -p "Select an option [1-4]: " dev_db_option
case $dev_db_option in
1)
if [ "$DEV_DB_STATUS" = "running" ]; then
printf "${YELLOW}> Development database is already running.${NC}\n"
else
printf "${CYAN}> Starting development database...${NC}\n"
docker compose -p aliasvault-dev -f dockerfiles/docker-compose.dev.yml up -d --wait --wait-timeout 60
printf "${GREEN}> Development database started successfully.${NC}\n"
fi
print_dev_db_details
;;
2)
if [ "$DEV_DB_STATUS" = "stopped" ]; then
printf "${YELLOW}> Development database is already stopped.${NC}\n"
else
printf "${CYAN}> Stopping development database...${NC}\n"
docker compose -p aliasvault-dev -f dockerfiles/docker-compose.dev.yml down
printf "${GREEN}> Development database stopped successfully.${NC}\n"
fi
;;
3)
print_dev_db_details
;;
4)
printf "${YELLOW}Configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
}
# Function to print development database connection details
print_dev_db_details() {
printf "\n"
printf "${MAGENTA}=========================================================${NC}\n"
printf "\n"
printf "${CYAN}Development Database Connection Details:${NC}\n"
printf "Host: localhost\n"
printf "Port: 5433\n"
printf "Database: aliasvault\n"
printf "Username: aliasvault\n"
printf "Password: password\n"
printf "\n"
printf "Connection string:\n"
printf "Host=localhost;Port=5433;Database=aliasvault;Username=aliasvault;Password=password\n"
printf "\n"
printf "${MAGENTA}=========================================================${NC}\n"
}
# Function to set deployment mode in .env
set_deployment_mode() {
local mode=$1
if [ "$mode" != "build" ] && [ "$mode" != "install" ]; then
printf "${RED}Invalid deployment mode: $mode${NC}\n"
exit 1
fi
update_env_var "DEPLOYMENT_MODE" "$mode" &>/dev/null
}
# Function to handle database export
handle_db_export() {
printf "${YELLOW}+++ Exporting Database +++${NC}\n" >&2
# Check if output redirection is present
if [ -t 1 ]; then
printf "Usage: ./install.sh db-export [OPTIONS] > backup.sql.gz\n" >&2
printf "\n" >&2
printf "Options:\n" >&2
printf " --dev Export from development database\n" >&2
printf " --parallel=N Use pigz with N threads for faster compression (max: 32)\n" >&2
printf "\n" >&2
printf "Examples:\n" >&2
printf " ./install.sh db-export > backup.sql.gz # Standard compression\n" >&2
printf " ./install.sh db-export --parallel=4 > backup.sql.gz # Parallel compression\n" >&2
printf "\n" >&2
printf "Note: Parallel compression runs at lowest priority to minimize system impact.\n" >&2
printf "\n" >&2
exit 1
fi
# Determine docker compose command based on dev/prod
if [ "$DEV_DB" = true ]; then
# Check if dev containers are running
if ! docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev ps postgres-dev --quiet 2>/dev/null | grep -q .; then
printf "${RED}Error: Development database container is not running. Start it first with: ./install.sh configure-dev-db${NC}\n" >&2
exit 1
fi
# Check if postgres-dev container is healthy
if ! docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev ps postgres-dev | grep -q "healthy"; then
printf "${RED}Error: Development PostgreSQL container is not healthy. Please check the logs.${NC}\n" >&2
exit 1
fi
DOCKER_CMD="docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev"
DB_TYPE="development"
else
# Production database export logic
if ! docker compose ps --quiet 2>/dev/null | grep -q .; then
printf "${RED}Error: AliasVault containers are not running. Start them first with: ./install.sh start${NC}\n" >&2
exit 1
fi
if ! docker compose ps postgres | grep -q "healthy"; then
printf "${RED}Error: PostgreSQL container is not healthy. Please check the logs with: docker compose logs postgres${NC}\n" >&2
exit 1
fi
DOCKER_CMD="docker compose exec -T postgres"
DB_TYPE="production"
fi
# Stream export directly to stdout (no temp files)
# Start timing
export_start_time=$(date +%s)
if [ "$PARALLEL_JOBS" -gt 0 ]; then
# Use pigz for parallel compression
# Use nice (lowest CPU priority) and ionice (lowest I/O priority) to minimize impact
printf "${CYAN}> Exporting ${DB_TYPE} database with parallel=${PARALLEL_JOBS} compression...${NC}\n" >&2
$DOCKER_CMD bash -c "
ionice -c 3 nice -n 19 pg_dump -U aliasvault aliasvault | ionice -c 3 nice -n 19 pigz -1 -p ${PARALLEL_JOBS} 2>/dev/null || \
nice -n 19 pg_dump -U aliasvault aliasvault | nice -n 19 pigz -1 -p ${PARALLEL_JOBS}
" 2>/dev/null
else
# Standard gzip
printf "${CYAN}> Exporting ${DB_TYPE} database...${NC}\n" >&2
$DOCKER_CMD nice -n 19 pg_dump -U aliasvault aliasvault | gzip -1
fi
export_status=$?
# End timing
export_end_time=$(date +%s)
export_duration=$((export_end_time - export_start_time))
if [ $export_status -eq 0 ]; then
printf "${GREEN}> Database exported successfully.${NC}\n" >&2
printf "${CYAN}> Export duration: ${export_duration}s${NC}\n" >&2
else
printf "${RED}> Failed to export database.${NC}\n" >&2
exit 1
fi
}
# Function to handle database import
handle_db_import() {
# Save stdin to file descriptor 3 AS THE VERY FIRST THING
# This MUST be first to prevent bash/docker/any command from consuming stdin bytes
exec 3<&0
printf "${YELLOW}+++ Importing Database +++${NC}\n"
# Check if containers are running
if [ "$DEV_DB" = true ]; then
if ! docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev ps postgres-dev | grep -q "healthy"; then
printf "${RED}Error: Development PostgreSQL container is not healthy.${NC}\n"
exit 1
fi
else
if ! docker compose ps postgres | grep -q "healthy"; then
printf "${RED}Error: PostgreSQL container is not healthy.${NC}\n"
exit 1
fi
fi
# Check if we're getting input from a pipe (check fd 3 instead of stdin now)
if [ -t 3 ]; then
printf "Usage: ./install.sh db-import [OPTIONS] < backup_file\n"
printf "\n"
printf "Options:\n"
printf " --dev Import to development database\n"
printf "\n"
printf "Examples:\n"
printf " ./install.sh db-import < backup.sql.gz # Import gzipped SQL\n"
printf " ./install.sh db-import < backup.sql # Import plain SQL\n"
printf " ./install.sh db-import --dev < backup.sql.gz # Import to dev database\n"
printf "\n"
printf "Note: Import uses a temp file for format detection. For large imports,\n"
printf " ensure you have enough disk space (backup size + decompressed size).\n"
exit 1
fi
printf "${RED}Warning: This will DELETE ALL EXISTING DATA in the "
if [ "$DEV_DB" = true ]; then
printf "development database"
else
printf "database"
fi
printf ".${NC}\n"
if [ "$FORCE_YES" != true ]; then
# Use /dev/tty to read from terminal even when stdin is redirected
if [ -t 1 ] && [ -t 2 ] && [ -e /dev/tty ]; then
# Temporarily switch stdin to tty for confirmation
exec < /dev/tty
read -p "Continue? [y/N]: " confirm
# Switch back to original stdin
exec 0<&3
if [[ ! $confirm =~ ^[Yy]$ ]]; then
exec 3<&- # Close fd 3
exit 1
fi
else
printf "${RED}Error: Cannot read confirmation from terminal. Use -y flag to bypass confirmation.${NC}\n"
exec 3<&- # Close fd 3
exit 1
fi
fi
if [ "$DEV_DB" != true ]; then
printf "${CYAN}> Stopping dependent services...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose stop api admin task-runner smtp
else
docker compose stop api admin task-runner smtp > /dev/null 2>&1
fi
fi
printf "${CYAN}> Importing "
if [ "$DEV_DB" = true ]; then
printf "development "
fi
printf "database...${NC}\n"
# Determine docker compose command based on dev/prod
if [ "$DEV_DB" = true ]; then
DOCKER_CMD="docker compose -f dockerfiles/docker-compose.dev.yml -p aliasvault-dev exec -T postgres-dev"
else
DOCKER_CMD="docker compose exec -T postgres"
fi
# Create a temporary file to store the input
temp_file=$(mktemp)
# Set up trap to ensure temp file cleanup on exit
trap 'rm -f "$temp_file"' EXIT INT TERM
cat <&3 > "$temp_file" # Read from fd 3 instead of stdin
exec 3<&- # Close fd 3
# Get input filesize
if [ -f "$temp_file" ]; then
import_filesize=$(wc -c < "$temp_file")
import_filesize_mb=$(awk "BEGIN {printf \"%.2f\", $import_filesize/1024/1024}")
printf "${CYAN}> Input file size: ${import_filesize_mb} MB${NC}\n"
fi
# Detect file format
is_gzipped=false
if gzip -t "$temp_file" 2>/dev/null; then
is_gzipped=true
printf "${CYAN}> Detected gzipped SQL backup${NC}\n"
else
# Check if it looks like SQL (basic validation)
if head -n 10 "$temp_file" | grep -qE '(^--|^CREATE |^INSERT |^ALTER |^DROP |^\\\\connect|^SET |^COMMENT |^GRANT |^REVOKE )'; then
printf "${CYAN}> Detected plain SQL backup${NC}\n"
else
printf "${RED}Error: Input is neither a valid gzip file nor a SQL file${NC}\n"
exit 1
fi
fi
# Start timing
import_start_time=$(date +%s)
if [ "$VERBOSE" = true ]; then
$DOCKER_CMD psql -U aliasvault postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aliasvault' AND pid <> pg_backend_pid();" && \
$DOCKER_CMD psql -U aliasvault postgres -c "DROP DATABASE IF EXISTS aliasvault;" && \
$DOCKER_CMD psql -U aliasvault postgres -c "CREATE DATABASE aliasvault OWNER aliasvault;" && \
if [ "$is_gzipped" = true ]; then
gunzip -c "$temp_file" | $DOCKER_CMD psql -U aliasvault aliasvault
else
cat "$temp_file" | $DOCKER_CMD psql -U aliasvault aliasvault
fi
else
$DOCKER_CMD psql -U aliasvault postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aliasvault' AND pid <> pg_backend_pid();" > /dev/null 2>&1 && \
$DOCKER_CMD psql -U aliasvault postgres -c "DROP DATABASE IF EXISTS aliasvault;" > /dev/null 2>&1 && \
$DOCKER_CMD psql -U aliasvault postgres -c "CREATE DATABASE aliasvault OWNER aliasvault;" > /dev/null 2>&1 && \
if [ "$is_gzipped" = true ]; then
gunzip -c "$temp_file" | $DOCKER_CMD psql -U aliasvault aliasvault > /dev/null 2>&1
else
cat "$temp_file" | $DOCKER_CMD psql -U aliasvault aliasvault > /dev/null 2>&1
fi
fi
import_status=$?
# End timing
import_end_time=$(date +%s)
import_duration=$((import_end_time - import_start_time))
rm "$temp_file"
if [ $import_status -eq 0 ]; then
printf "${GREEN}> Database imported successfully.${NC}\n"
printf "${CYAN}> Import duration: ${import_duration}s${NC}\n"
if [ "$DEV_DB" != true ]; then
printf "${CYAN}> Starting services...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose restart api admin task-runner smtp reverse-proxy
else
docker compose restart api admin task-runner smtp reverse-proxy > /dev/null 2>&1
fi
fi
else
printf "${RED}> Import failed. Please check that your backup file is valid.${NC}\n"
exit 1
fi
}
# Function to handle hostname configuration
handle_hostname_configuration() {
printf "${YELLOW}+++ Hostname Configuration +++${NC}\n"
printf "\n"
# Check if AliasVault is installed
if [ ! -f "docker-compose.yml" ]; then
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
exit 1
fi
printf "The hostname is the domain name where your AliasVault server will be accessible.\n"
printf "This hostname will be used for both Let's Encrypt and self-signed certificates.\n"
printf "\n"
# Get current hostname
CURRENT_HOSTNAME=$(grep "^HOSTNAME=" "$ENV_FILE" | cut -d '=' -f2)
printf "Current hostname: ${CYAN}${CURRENT_HOSTNAME}${NC}\n"
printf "\n"
# Ask for new hostname
while true; do
read -p "Enter new hostname (e.g. aliasvault.example.com): " NEW_HOSTNAME
if [ -n "$NEW_HOSTNAME" ]; then
break
else
printf "${YELLOW}> Hostname cannot be empty. Please enter a valid hostname.${NC}\n"
fi
done
# Check if hostname changed
HOSTNAME_CHANGED=false
if [ "$CURRENT_HOSTNAME" != "$NEW_HOSTNAME" ]; then
HOSTNAME_CHANGED=true
fi
# Update the hostname
update_env_var "HOSTNAME" "$NEW_HOSTNAME"
# If using self-signed cert and hostname changed, offer to regenerate
if [ "$HOSTNAME_CHANGED" = true ]; then
LETSENCRYPT_ENABLED=$(grep "^LETSENCRYPT_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)
if [ "$LETSENCRYPT_ENABLED" != "true" ]; then
printf "\n${YELLOW}Hostname changed. The self-signed certificate needs to be regenerated.${NC}\n"
read -p "Regenerate certificate now? (y/n): " REGEN_CERT
if [ "$REGEN_CERT" = "y" ] || [ "$REGEN_CERT" = "Y" ]; then
# Remove the hostname marker to force regeneration
rm -f ./certificates/ssl/.hostname_marker
printf "\n${YELLOW}Restarting services to regenerate certificate...${NC}\n"
handle_restart
else
printf "${YELLOW}Please restart services manually to apply the new certificate.${NC}\n"
fi
else
printf "\n${YELLOW}Note: You're using Let's Encrypt. Make sure the new hostname has proper DNS records.${NC}\n"
printf "${YELLOW}You may need to reconfigure Let's Encrypt for the new hostname.${NC}\n"
fi
fi
printf "\n"
print_success_box "Hostname updated successfully to ${NEW_HOSTNAME}!"
}
# Function to handle IP logging configuration
handle_ip_logging_configuration() {
# Get current IP logging setting
CURRENT_SETTING=$(grep "^IP_LOGGING_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)
printf "${YELLOW}+++ Configure IP Address Logging +++${NC}\n"
printf "\n"
printf "Current setting: ${CYAN}${CURRENT_SETTING}${NC}\n"
printf "\n"
printf "1) Enable IP address logging\n"
printf "2) Disable IP address logging\n"
printf "\n"
printf "Choose an option (1-2): "
read -r choice
case $choice in
1)
update_env_var "IP_LOGGING_ENABLED" "true"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
handle_restart
# Print success message
printf "\n"
print_success_box "IP address logging has been enabled!"
;;
2)
update_env_var "IP_LOGGING_ENABLED" "false"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
handle_restart
# Print success message
printf "\n"
print_success_box "IP address logging has been disabled!"
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
return 1
;;
esac
}
check_and_populate_env() {
printf "${CYAN} Checking .env values...${NC} ${GREEN}${NC}\n"
# SUPPORT_EMAIL
if ! grep -q "^SUPPORT_EMAIL=" "$ENV_FILE"; then
read -p "Enter server admin support email address that is shown on contact page (optional, press Enter to skip): " SUPPORT_EMAIL
update_env_var "SUPPORT_EMAIL" "$SUPPORT_EMAIL"
printf " Set SUPPORT_EMAIL\n"
fi
# JWT_KEY
if [ ! -f "${SECRETS_DIR}/jwt_key" ] || [ -z "$(cat "${SECRETS_DIR}/jwt_key" 2>/dev/null)" ]; then
JWT_KEY=$(openssl rand -base64 32)
write_secret_to_file "jwt_key" "$JWT_KEY"
printf " Generated JWT_KEY\n"
fi
# DATA_PROTECTION_CERT_PASS
if [ ! -f "${SECRETS_DIR}/data_protection_cert_pass" ] || [ -z "$(cat "${SECRETS_DIR}/data_protection_cert_pass" 2>/dev/null)" ]; then
CERT_PASS=$(openssl rand -base64 32)
write_secret_to_file "data_protection_cert_pass" "$CERT_PASS"
printf " Generated DATA_PROTECTION_CERT_PASS\n"
fi
# POSTGRES_PASSWORD
if [ ! -f "${SECRETS_DIR}/postgres_password" ] || [ -z "$(cat "${SECRETS_DIR}/postgres_password" 2>/dev/null)" ]; then
POSTGRES_PASS=$(openssl rand -base64 32)
write_secret_to_file "postgres_password" "$POSTGRES_PASS"
printf " Generated POSTGRES_PASSWORD\n"
fi
# PRIVATE_EMAIL_DOMAINS
if ! grep -q "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE"; then
update_env_var "PRIVATE_EMAIL_DOMAINS" ""
printf " Set PRIVATE_EMAIL_DOMAINS\n"
fi
# HIDDEN_PRIVATE_EMAIL_DOMAINS
if ! grep -q "^HIDDEN_PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE"; then
update_env_var "HIDDEN_PRIVATE_EMAIL_DOMAINS" ""
printf " Set HIDDEN_PRIVATE_EMAIL_DOMAINS\n"
fi
# HTTP_PORT
if ! grep -q "^HTTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "HTTP_PORT" "80"
printf " Set HTTP_PORT\n"
fi
# HTTPS_PORT
if ! grep -q "^HTTPS_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTPS_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "HTTPS_PORT" "443"
printf " Set HTTPS_PORT\n"
fi
# SMTP_PORT
if ! grep -q "^SMTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^SMTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "SMTP_PORT" "25"
printf " Set SMTP_PORT\n"
fi
# SMTP_TLS_PORT
if ! grep -q "^SMTP_TLS_PORT=" "$ENV_FILE" || [ -z "$(grep "^SMTP_TLS_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "SMTP_TLS_PORT" "587"
printf " Set SMTP_TLS_PORT\n"
fi
}
main "$@"