#!/usr/bin/env bash # gniza/lib/restore.sh — Full account, files, database, mailbox, server restores # Helper: build rsync download command args for SSH mode _rsync_download() { local remote_path="$1" local local_path="$2" local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd) if _is_password_mode; then sshpass -p "$REMOTE_PASSWORD" rsync -aHAX --numeric-ids --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}" \ "$local_path" else rsync -aHAX --numeric-ids --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${remote_path}" \ "$local_path" fi } # Helper: build --exclude args from comma-separated exclude string. # Outputs one arg per line (--exclude then pattern). _build_exclude_args() { local exclude="$1" local -a args=() if [[ -n "$exclude" ]]; then IFS=',' read -ra patterns <<< "$exclude" for p in "${patterns[@]}"; do p="${p## }"; p="${p%% }" [[ -n "$p" ]] && args+=(--exclude "$p") done fi printf '%s\n' "${args[@]}" } # Helper: detect pkgacct base (old vs new format) for rclone or SSH _detect_pkgacct_base() { local snap_path="$1" local snap_subpath="${2:-}" if _is_rclone_mode; then if rclone_exists "${snap_subpath}/pkgacct/"; then echo "${snap_subpath}/pkgacct" else echo "$snap_subpath" fi else if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then echo "$snap_path/pkgacct" else echo "$snap_path" fi fi } restore_full_account() { local user="$1" local timestamp="${2:-}" local terminate="${3:-}" local exclude="${4:-}" local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}" # Resolve timestamp local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" log_info "Restoring full account: $user from snapshot $ts" local restore_dir="$temp_dir/restore-$user" mkdir -p "$restore_dir" || { log_error "Failed to create restore temp directory" return 1 } # Download pkgacct data from remote log_info "Downloading pkgacct data for $user..." if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") if [[ "$pkgacct_sub" == *"/pkgacct" ]]; then rclone_from_remote "${pkgacct_sub}" "$restore_dir/$user" || { log_error "Failed to download pkgacct data for $user" rm -rf "$restore_dir" return 1 } else # Download snapshot root excluding homedir rclone_from_remote "${snap_subpath}" "$restore_dir/$user" || { log_error "Failed to download pkgacct data for $user" rm -rf "$restore_dir" return 1 } rm -rf "$restore_dir/$user/homedir" fi else # Detect old format (pkgacct/ subdir) vs new format (content at root) local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd) if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then # Old format: pkgacct data in subdir if ! rsync -aHAX --numeric-ids --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${snap_path}/pkgacct/" \ "$restore_dir/$user/"; then log_error "Failed to download pkgacct data for $user" rm -rf "$restore_dir" return 1 fi else # New format: pkgacct content at snapshot root, exclude homedir/ if ! rsync -aHAX --numeric-ids --rsync-path="rsync --fake-super" --exclude=/homedir/ \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${snap_path}/" \ "$restore_dir/$user/"; then log_error "Failed to download pkgacct data for $user" rm -rf "$restore_dir" return 1 fi fi fi # Gunzip SQL files log_info "Decompressing SQL files..." local mysql_dir="$restore_dir/$user/mysql" if [[ -d "$mysql_dir" ]]; then find "$mysql_dir" -name "*.sql.gz" -exec gunzip -f {} \; fi # Terminate existing account if requested if [[ "$terminate" == "true" ]] && account_exists "$user"; then log_info "Terminating existing account $user before restore..." if ! /scripts/removeacct --skipbw --force "$user"; then log_error "Failed to terminate account $user" rm -rf "$restore_dir" return 1 fi fi # Run restorepkg (--force to merge into existing account if present) log_info "Running restorepkg for $user..." local -a restorepkg_args=() account_exists "$user" && restorepkg_args+=(--force) if ! /scripts/restorepkg "${restorepkg_args[@]}" "$restore_dir/$user"; then log_error "restorepkg failed for $user" rm -rf "$restore_dir" return 1 fi # Build exclude args for homedir phase local -a exclude_args=() if [[ -n "$exclude" ]]; then while IFS= read -r arg; do exclude_args+=("$arg") done < <(_build_exclude_args "$exclude") fi # Restore homedir local homedir; homedir=$(get_account_homedir "$user") if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" if rclone_exists "${snap_subpath}/homedir/"; then log_info "Restoring homedir for $user..." if [[ ${#exclude_args[@]} -gt 0 ]]; then if ! rclone_from_remote_filtered "${snap_subpath}/homedir" "$homedir" "${exclude_args[@]}"; then log_error "Failed to restore homedir for $user" rm -rf "$restore_dir" return 1 fi else if ! rclone_from_remote "${snap_subpath}/homedir" "$homedir"; then log_error "Failed to restore homedir for $user" rm -rf "$restore_dir" return 1 fi fi log_info "Fixing ownership for $homedir..." chown -R "$user":"$user" "$homedir" fi elif remote_exec "test -d '$snap_path/homedir'" 2>/dev/null; then log_info "Restoring homedir for $user..." local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd) if ! rsync -aHAX --numeric-ids --rsync-path="rsync --fake-super" \ "${exclude_args[@]}" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${snap_path}/homedir/" \ "$homedir/"; then log_error "Failed to restore homedir for $user" rm -rf "$restore_dir" return 1 fi # Fix ownership log_info "Fixing ownership for $homedir..." chown -R "$user":"$user" "$homedir" fi rm -rf "$restore_dir" log_info "Full account restore completed for $user" return 0 } restore_files() { local user="$1" local subpath="${2:-}" local timestamp="${3:-}" local exclude="${4:-}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 local homedir; homedir=$(get_account_homedir "$user") local local_dest="$homedir/" if [[ -n "$subpath" ]]; then local_dest="${homedir}/${subpath}" mkdir -p "$(dirname "$local_dest")" fi log_info "Restoring files for $user: ${subpath:-entire homedir} from snapshot $ts" # Build exclude args local -a exclude_args=() if [[ -n "$exclude" ]]; then while IFS= read -r arg; do exclude_args+=("$arg") done < <(_build_exclude_args "$exclude") fi if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local remote_sub="${snap_subpath}/homedir" [[ -n "$subpath" ]] && remote_sub="${snap_subpath}/homedir/${subpath}" if [[ ${#exclude_args[@]} -gt 0 ]]; then if ! rclone_from_remote_filtered "$remote_sub" "$local_dest" "${exclude_args[@]}"; then log_error "Failed to restore files for $user" return 1 fi else if ! rclone_from_remote "$remote_sub" "$local_dest"; then log_error "Failed to restore files for $user" return 1 fi fi else local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd) local remote_source="${snap_path}/homedir/" [[ -n "$subpath" ]] && remote_source="${snap_path}/homedir/${subpath}" if ! rsync -aHAX --numeric-ids --rsync-path="rsync --fake-super" \ "${exclude_args[@]}" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${remote_source}" \ "$local_dest"; then log_error "Failed to restore files for $user" return 1 fi fi # Fix ownership chown -R "$user":"$user" "$local_dest" log_info "File restore completed for $user" return 0 } restore_database() { local user="$1" local dbname="$2" local timestamp="${3:-}" local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd) local restore_dir="$temp_dir/restore-db-$user" mkdir -p "$restore_dir" || { log_error "Failed to create restore temp directory" return 1 } log_info "Restoring database $dbname for $user from snapshot $ts" local local_sql="$restore_dir/${dbname}.sql.gz" if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") if ! rclone_from_remote "${pkgacct_sub}/mysql/${dbname}.sql.gz" "$restore_dir"; then log_error "SQL dump not found for database: $dbname" rm -rf "$restore_dir" return 1 fi else # Detect old format (pkgacct/ subdir) vs new format (content at root) local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local remote_sql="${pkgacct_base}/mysql/${dbname}.sql.gz" if ! rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${remote_sql}" \ "$local_sql" 2>/dev/null; then log_error "SQL dump not found for database: $dbname" rm -rf "$restore_dir" return 1 fi fi # Decompress gunzip -f "$local_sql" || { log_error "Failed to decompress SQL file" rm -rf "$restore_dir" return 1 } # Download mysql.sql (grants) if it exists local local_grants="$restore_dir/mysql.sql" if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") rclone_from_remote "${pkgacct_sub}/mysql.sql" "$restore_dir" 2>/dev/null || true else local remote_grants="${pkgacct_base}/mysql.sql" rsync -aHAX --rsync-path="rsync --fake-super" -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${remote_grants}" \ "$local_grants" 2>/dev/null fi # Get cPanel DB prefix for this account local db_prefix db_prefix=$(uapi --user="$user" Mysql get_restrictions 2>/dev/null | grep -oP '^\s+prefix:\s+\K\S+') || db_prefix="" # Create database via cPanel UAPI log_info "Creating database $dbname via UAPI..." if [[ -n "$db_prefix" && "$dbname" != "${db_prefix}"* ]]; then log_error "Database $dbname does not match cPanel prefix '${db_prefix}' — cannot create via UAPI. Rename the database to start with '${db_prefix}'" rm -rf "$restore_dir" return 1 fi local uapi_out; uapi_out=$(uapi --user="$user" Mysql create_database name="$dbname" 2>&1) || true if echo "$uapi_out" | grep -q 'status: 1'; then log_info "Database $dbname created via UAPI" elif echo "$uapi_out" | grep -qi 'already exists'; then log_debug "Database $dbname already exists" else log_error "UAPI create_database failed for $dbname: $uapi_out" rm -rf "$restore_dir" return 1 fi # Import SQL dump — no cPanel API exists for SQL import; mysql binary is required log_info "Importing SQL dump for $dbname..." if ! mysql --defaults-file=/root/.my.cnf "$dbname" < "$restore_dir/${dbname}.sql"; then log_error "Failed to import SQL dump for $dbname" rm -rf "$restore_dir" return 1 fi # Restore database users and privileges via UAPI if [[ -f "$local_grants" ]]; then log_info "Restoring database users and privileges via UAPI..." # Build grep pattern that matches both escaped (\_) and unescaped (_) underscores local dbname_grep; dbname_grep="${dbname//_/\\\\?_}" local grant_users; grant_users=$(grep -iE "\`${dbname_grep}\`" "$local_grants" | grep -oP "TO\s+'?\K[^'@]+(?='@)" | sort -u) || true if [[ -n "$grant_users" ]]; then while IFS= read -r dbuser; do [[ -z "$dbuser" ]] && continue if [[ -n "$db_prefix" && "$dbuser" != "${db_prefix}"* ]]; then log_warn "Skipping DB user $dbuser — does not match cPanel prefix '${db_prefix}'" continue fi # Create user via UAPI local cu_out; cu_out=$(uapi --user="$user" Mysql create_user name="$dbuser" password="$(openssl rand -base64 24)" 2>&1) || true if echo "$cu_out" | grep -q 'status: 1'; then log_debug "Created DB user $dbuser via UAPI" elif echo "$cu_out" | grep -qi 'exists'; then log_debug "DB user $dbuser already exists" else log_error "UAPI create_user failed for $dbuser: $cu_out" rm -rf "$restore_dir" return 1 fi # Grant privileges via UAPI local sp_out; sp_out=$(uapi --user="$user" Mysql set_privileges_on_database user="$dbuser" database="$dbname" privileges=ALL 2>&1) || true if ! echo "$sp_out" | grep -q 'status: 1'; then log_error "UAPI set_privileges_on_database failed for $dbuser on $dbname: $sp_out" rm -rf "$restore_dir" return 1 fi log_debug "Registered DB user $dbuser for $dbname via UAPI" done <<< "$grant_users" fi fi rm -rf "$restore_dir" log_info "Database restore completed for $dbname" return 0 } restore_all_databases() { local user="$1" local timestamp="${2:-}" log_info "Restoring all databases for $user" local db_list; db_list=$(list_snapshot_databases "$user" "$timestamp") if [[ -z "$db_list" ]]; then log_warn "No databases found in snapshot for $user" return 0 fi local total=0 succeeded=0 failed=0 while IFS= read -r dbname; do [[ -z "$dbname" ]] && continue ((total++)) || true log_info "--- Restoring database $dbname ($total) ---" if restore_database "$user" "$dbname" "$timestamp"; then ((succeeded++)) || true else ((failed++)) || true log_error "Failed to restore database: $dbname" fi done <<< "$db_list" log_info "Restored $succeeded/$total databases for $user" (( failed > 0 )) && return 1 return 0 } restore_mailbox() { local user="$1" local email="$2" local timestamp="${3:-}" # Parse email into domain and mailbox user local mailbox_user="${email%%@*}" local domain="${email##*@}" if [[ -z "$mailbox_user" || -z "$domain" || "$mailbox_user" == "$email" ]]; then log_error "Invalid email format: $email (expected user@domain)" return 1 fi log_info "Restoring mailbox $email for account $user" # Ensure mailbox account exists in cPanel via UAPI local pop_out; pop_out=$(uapi --user="$user" Email add_pop \ email="$mailbox_user" domain="$domain" \ password="$(openssl rand -base64 24)" quota=0 2>&1) || true if echo "$pop_out" | grep -q 'status: 1'; then log_info "Created mailbox $email via UAPI" elif echo "$pop_out" | grep -qi 'exists'; then log_debug "Mailbox $email already exists" else log_error "UAPI Email add_pop failed for $email: $pop_out" return 1 fi # Restore mailbox data files local mail_subpath="mail/${domain}/${mailbox_user}/" restore_files "$user" "$mail_subpath" "$timestamp" } restore_all_mailboxes() { local user="$1" local timestamp="${2:-}" log_info "Restoring all mailboxes for $user" local mailbox_list; mailbox_list=$(list_snapshot_mailboxes "$user" "$timestamp") if [[ -z "$mailbox_list" ]]; then log_warn "No mailboxes found in snapshot for $user" return 0 fi local total=0 succeeded=0 failed=0 while IFS= read -r email; do [[ -z "$email" ]] && continue ((total++)) || true log_info "--- Restoring mailbox $email ($total) ---" if restore_mailbox "$user" "$email" "$timestamp"; then ((succeeded++)) || true else ((failed++)) || true log_error "Failed to restore mailbox: $email" fi done <<< "$mailbox_list" log_info "Restored $succeeded/$total mailboxes for $user" (( failed > 0 )) && return 1 return 0 } # List database names available in a snapshot's mysql/ directory. # Outputs one database name per line (without .sql.gz extension). list_snapshot_databases() { local user="$1" local timestamp="${2:-}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") rclone_list_files "${pkgacct_sub}/mysql" 2>/dev/null | while IFS= read -r f; do [[ "$f" == *.sql.gz ]] || continue echo "${f%.sql.gz}" done return fi local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" # Detect old format vs new format local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local mysql_dir="${pkgacct_base}/mysql" remote_exec "ls -1 '$mysql_dir'/*.sql.gz 2>/dev/null" 2>/dev/null | while IFS= read -r f; do local base; base=$(basename "$f" .sql.gz) echo "$base" done } # List mailbox addresses available in a snapshot's homedir/mail/ directory. # Outputs one email address per line (user@domain format). list_snapshot_mailboxes() { local user="$1" local timestamp="${2:-}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local domains; domains=$(rclone_list_dirs "${snap_subpath}/homedir/mail" 2>/dev/null) || true [[ -z "$domains" ]] && return 0 while IFS= read -r domain; do [[ -z "$domain" || "$domain" == .* ]] && continue local users; users=$(rclone_list_dirs "${snap_subpath}/homedir/mail/${domain}" 2>/dev/null) || true [[ -z "$users" ]] && continue while IFS= read -r mailbox_user; do [[ -z "$mailbox_user" || "$mailbox_user" == .* ]] && continue echo "${mailbox_user}@${domain}" done <<< "$users" done <<< "$domains" return fi local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local mail_dir="${snap_path}/homedir/mail" remote_exec "find '$mail_dir' -mindepth 2 -maxdepth 2 -type d 2>/dev/null" 2>/dev/null | while IFS= read -r d; do local mailbox_user; mailbox_user=$(basename "$d") local domain; domain=$(basename "$(dirname "$d")") # Skip Maildir system directories (not real domains/users) [[ "$domain" == .* || "$mailbox_user" == .* ]] && continue echo "${mailbox_user}@${domain}" done } # List files/directories in a snapshot's homedir/ directory. # Outputs one entry per line. Directories end with '/'. # Optional subpath argument for navigating subdirectories. list_snapshot_files() { local user="$1" local timestamp="${2:-}" local subpath="${3:-}" # Guard against path traversal if [[ "$subpath" == *".."* ]]; then log_error "Path traversal not allowed: $subpath" return 1 fi local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}/homedir" [[ -n "$subpath" ]] && snap_subpath="${snap_subpath}/${subpath}" rclone_list_files "$snap_subpath" 2>/dev/null || true return fi local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local target_dir="${snap_path}/homedir" if [[ -n "$subpath" ]]; then target_dir="${target_dir}/${subpath}" fi remote_exec "ls -1ap '$target_dir' 2>/dev/null" 2>/dev/null | while IFS= read -r entry; do # Filter out . and .. [[ "$entry" == "." || "$entry" == ".." || "$entry" == "./" || "$entry" == "../" ]] && continue echo "$entry" done } # List database users found in a snapshot's mysql.sql grant file. # Outputs one username per line. list_snapshot_dbusers() { local user="$1" local timestamp="${2:-}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") local grants_content; grants_content=$(rclone_cat "${pkgacct_sub}/mysql.sql" 2>/dev/null) || true [[ -z "$grants_content" ]] && return 0 echo "$grants_content" | grep -oP "TO\s+'?\K[^'@]+" | sort -u | while IFS= read -r dbuser; do [[ "$dbuser" == "$user" ]] && continue [[ -z "$dbuser" ]] && continue echo "$dbuser" done return fi local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local grants_file="${pkgacct_base}/mysql.sql" remote_exec "grep -oP \"TO\\\\s+'?\\\\K[^'@]+\" '$grants_file' 2>/dev/null | sort -u" 2>/dev/null | while IFS= read -r dbuser; do # Skip the account's default DB user (same as cPanel username) [[ "$dbuser" == "$user" ]] && continue [[ -z "$dbuser" ]] && continue echo "$dbuser" done } # List cron job entries found in a snapshot's cron/ directory. # Outputs one cron line per line (skips comments, empty lines, env vars). list_snapshot_cron() { local user="$1" local timestamp="${2:-}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") local files; files=$(rclone_list_files "${pkgacct_sub}/cron" 2>/dev/null) || true [[ -z "$files" ]] && return 0 local first_file; first_file=$(echo "$files" | head -1) [[ -z "$first_file" ]] && return 0 rclone_cat "${pkgacct_sub}/cron/${first_file}" 2>/dev/null | grep -vE '^\s*#|^\s*$|^\s*[A-Za-z_]+=' || true return fi local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local cron_dir="${pkgacct_base}/cron" remote_exec "f=\$(find '$cron_dir' -type f 2>/dev/null | head -1); [ -f \"\$f\" ] && grep -vE '^\s*#|^\s*$|^\s*[A-Za-z_]+=' \"\$f\" || true" 2>/dev/null } # List DNS zone names found in a snapshot's dnszones/ directory. # Outputs one domain name per line. list_snapshot_dns() { local user="$1" local timestamp="${2:-}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") rclone_list_files "${pkgacct_sub}/dnszones" 2>/dev/null | while IFS= read -r f; do [[ "$f" == *.db ]] || continue echo "${f%.db}" done return fi local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local zones_dir="${pkgacct_base}/dnszones" remote_exec "ls -1 '$zones_dir'/*.db 2>/dev/null" 2>/dev/null | while IFS= read -r f; do local base; base=$(basename "$f" .db) echo "$base" done } # List SSL certificate domains found in a snapshot's ssl/ directory. # Outputs one cert filename (without extension) per line. list_snapshot_ssl() { local user="$1" local timestamp="${2:-}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") { rclone_list_files "${pkgacct_sub}/ssl/certs" 2>/dev/null || true rclone_list_files "${pkgacct_sub}/ssl/installed/certs" 2>/dev/null || true rclone_list_files "${pkgacct_sub}/sslcerts" 2>/dev/null || true rclone_list_files "${snap_subpath}/homedir/ssl/certs" 2>/dev/null || true } | while IFS= read -r f; do [[ "$f" == *.crt ]] || continue echo "${f%.crt}" done | sort -u return fi local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local homedir_base="${snap_path}/homedir" remote_exec "ls -1 '$pkgacct_base/ssl/certs/'*.crt '$pkgacct_base/ssl/installed/certs/'*.crt '$pkgacct_base/sslcerts/'*.crt '$homedir_base/ssl/certs/'*.crt 2>/dev/null || true" 2>/dev/null | while IFS= read -r f; do [[ -z "$f" ]] && continue basename "$f" .crt done | sort -u } restore_cron() { local user="$1" local timestamp="${2:-}" local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd) # Detect old format vs new format local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local restore_dir="$temp_dir/restore-cron-$user" mkdir -p "$restore_dir" || { log_error "Failed to create restore temp directory" return 1 } log_info "Restoring cron jobs for $user from snapshot $ts" # Download cron/ directory from snapshot if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") if ! rclone_from_remote "${pkgacct_sub}/cron" "$restore_dir/cron"; then log_error "No cron data found in snapshot for $user" rm -rf "$restore_dir" return 1 fi else if ! rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${pkgacct_base}/cron/" \ "$restore_dir/cron/" 2>/dev/null; then log_error "No cron data found in snapshot for $user" rm -rf "$restore_dir" return 1 fi fi # Find the crontab file local crontab_file crontab_file=$(find "$restore_dir/cron" -type f | head -1) if [[ -z "$crontab_file" ]]; then log_warn "Cron directory exists but no crontab file found for $user" rm -rf "$restore_dir" return 0 fi # Restore cron jobs via cpapi2 (registers with cPanel's cron manager) log_info "Restoring cron jobs via cPanel API..." # Remove existing cron entries via cpapi2 local existing_lines; existing_lines=$(cpapi2 --user="$user" Cron listcron 2>&1) if [[ $? -ne 0 ]]; then log_error "cpapi2 Cron listcron failed for $user: $existing_lines" rm -rf "$restore_dir" return 1 fi local linekeys; linekeys=$(echo "$existing_lines" | grep -oP 'linekey:\s*\K\S+') || true if [[ -n "$linekeys" ]]; then while IFS= read -r linekey; do [[ -z "$linekey" ]] && continue cpapi2 --user="$user" Cron remove_line linekey="$linekey" 2>/dev/null || true done <<< "$linekeys" fi # Parse each cron line and add via cpapi2 while IFS= read -r line; do # Skip empty lines, comments, and environment variables (KEY=value) [[ -z "$line" || "$line" =~ ^[[:space:]]*# || "$line" =~ ^[[:space:]]*[A-Za-z_]+= ]] && continue # Split into 5 cron fields + command local minute hour day month weekday command read -r minute hour day month weekday command <<< "$line" [[ -z "$command" ]] && continue local add_out; add_out=$(cpapi2 --user="$user" Cron add_line \ minute="$minute" hour="$hour" day="$day" \ month="$month" weekday="$weekday" command="$command" 2>&1) || true if ! echo "$add_out" | grep -q 'status: 1'; then log_error "cpapi2 Cron add_line failed for '$line': $add_out" rm -rf "$restore_dir" return 1 fi done < "$crontab_file" rm -rf "$restore_dir" log_info "Cron restore completed for $user" return 0 } restore_dbusers() { local user="$1" local specific_dbuser="${2:-}" local timestamp="${3:-}" local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd) # Detect old format vs new format local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local restore_dir="$temp_dir/restore-dbusers-$user" mkdir -p "$restore_dir" || { log_error "Failed to create restore temp directory" return 1 } log_info "Restoring DB users & grants for $user from snapshot $ts" # Download mysql.sql (grant statements) local local_grants="$restore_dir/mysql.sql" if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") if ! rclone_from_remote "${pkgacct_sub}/mysql.sql" "$restore_dir"; then log_error "No mysql.sql (grants) found in snapshot for $user" rm -rf "$restore_dir" return 1 fi else local remote_grants="${pkgacct_base}/mysql.sql" if ! rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${remote_grants}" \ "$local_grants" 2>/dev/null; then log_error "No mysql.sql (grants) found in snapshot for $user" rm -rf "$restore_dir" return 1 fi fi # Get cPanel DB prefix for this account local db_prefix db_prefix=$(uapi --user="$user" Mysql get_restrictions 2>/dev/null | grep -oP '^\s+prefix:\s+\K\S+') || db_prefix="" log_info "Registering DB users & privileges (cPanel prefix: ${db_prefix:-none})..." # Extract unique db users from GRANT statements local grant_users; grant_users=$(grep -oP "TO\s+'?\K[^'@]+(?='@)" "$local_grants" | sort -u) || true if [[ -n "$grant_users" ]]; then while IFS= read -r dbuser; do [[ -z "$dbuser" ]] && continue # Skip account default DB user — it always exists with the account [[ "$dbuser" == "$user" ]] && continue # If a specific DB user was requested, skip all others if [[ -n "$specific_dbuser" && "$dbuser" != "$specific_dbuser" ]]; then continue fi if [[ -n "$db_prefix" && "$dbuser" != "${db_prefix}"* ]]; then log_warn "Skipping DB user $dbuser — does not match cPanel prefix '${db_prefix}'" continue fi # Create user via UAPI local uapi_out; uapi_out=$(uapi --user="$user" Mysql create_user name="$dbuser" password="$(openssl rand -base64 24)" 2>&1) || true if echo "$uapi_out" | grep -q 'status: 1'; then log_debug "Created DB user $dbuser via UAPI" elif echo "$uapi_out" | grep -qi 'exists'; then log_debug "DB user $dbuser already exists" else log_error "UAPI create_user failed for $dbuser: $uapi_out" rm -rf "$restore_dir" return 1 fi # Extract databases this user has grants on and set privileges via UAPI # Strip backslash escapes (GRANT syntax uses \_ for literal underscores) local dbs; dbs=$(grep -i "TO '$dbuser'@" "$local_grants" | grep -oP "ON\s+\`\K[^\`]+" | sed 's/\\//g' | sort -u) || true if [[ -n "$dbs" ]]; then while IFS= read -r dbname; do [[ -z "$dbname" || "$dbname" == "*" ]] && continue if [[ -n "$db_prefix" && "$dbname" != "${db_prefix}"* ]]; then log_warn "Skipping privileges for $dbuser on $dbname — database does not match cPanel prefix" continue fi local sp_out; sp_out=$(uapi --user="$user" Mysql set_privileges_on_database user="$dbuser" database="$dbname" privileges=ALL 2>&1) || true if ! echo "$sp_out" | grep -q 'status: 1'; then log_error "UAPI set_privileges_on_database failed for $dbuser on $dbname: $sp_out" rm -rf "$restore_dir" return 1 fi log_debug "Set privileges for $dbuser on $dbname via UAPI" done <<< "$dbs" fi done <<< "$grant_users" fi rm -rf "$restore_dir" log_info "DB users & grants restore completed for $user" return 0 } restore_cpconfig() { local user="$1" local timestamp="${2:-}" local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd) # Detect old format vs new format local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local restore_dir="$temp_dir/restore-cpconfig-$user" mkdir -p "$restore_dir" || { log_error "Failed to create restore temp directory" return 1 } log_info "Restoring panel config for $user from snapshot $ts" # Download cp/ directory if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") if ! rclone_from_remote "${pkgacct_sub}/cp" "$restore_dir/cp"; then log_error "No cPanel config data found in snapshot for $user" rm -rf "$restore_dir" return 1 fi else if ! rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${pkgacct_base}/cp/" \ "$restore_dir/cp/" 2>/dev/null; then log_error "No cPanel config data found in snapshot for $user" rm -rf "$restore_dir" return 1 fi fi # Copy to cPanel user config location local cp_user_file="$restore_dir/cp/$user" if [[ -f "$cp_user_file" ]]; then cp -f "$cp_user_file" "/var/cpanel/users/$user" || { log_error "Failed to copy cPanel config for $user" rm -rf "$restore_dir" return 1 } else log_warn "No user config file found in cp/ directory for $user" fi # Rebuild cPanel caches log_info "Rebuilding cPanel user domain caches..." /usr/local/cpanel/scripts/updateuserdomains 2>/dev/null || log_warn "updateuserdomains returned non-zero" rm -rf "$restore_dir" log_info "Panel config restore completed for $user" return 0 } restore_domains() { local user="$1" local specific_domain="${2:-}" local timestamp="${3:-}" local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd) # Detect old format vs new format local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local restore_dir="$temp_dir/restore-domains-$user" mkdir -p "$restore_dir" || { log_error "Failed to create restore temp directory" return 1 } log_info "Restoring domains & DNS for $user from snapshot $ts" # Download userdata/, addons, dnszones/ if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") rclone_from_remote "${pkgacct_sub}/userdata" "$restore_dir/userdata" 2>/dev/null || log_warn "No userdata found in snapshot" rclone_from_remote "${pkgacct_sub}/addons" "$restore_dir" 2>/dev/null || log_debug "No addons file in snapshot" rclone_from_remote "${pkgacct_sub}/dnszones" "$restore_dir/dnszones" 2>/dev/null || log_warn "No DNS zones found in snapshot" else rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${pkgacct_base}/userdata/" \ "$restore_dir/userdata/" 2>/dev/null || log_warn "No userdata found in snapshot" rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${pkgacct_base}/addons" \ "$restore_dir/addons" 2>/dev/null || log_debug "No addons file in snapshot" rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${pkgacct_base}/dnszones/" \ "$restore_dir/dnszones/" 2>/dev/null || log_warn "No DNS zones found in snapshot" fi # Restore userdata if [[ -d "$restore_dir/userdata" ]]; then log_info "Restoring userdata for $user..." mkdir -p "/var/cpanel/userdata/$user" cp -af "$restore_dir/userdata/"* "/var/cpanel/userdata/$user/" || { log_error "Failed to restore userdata for $user" rm -rf "$restore_dir" return 1 } fi # Rebuild cPanel caches and Apache config log_info "Rebuilding cPanel domain caches..." /usr/local/cpanel/scripts/updateuserdomains 2>/dev/null || log_warn "updateuserdomains returned non-zero" /usr/local/cpanel/scripts/rebuildhttpdconf 2>/dev/null || log_warn "rebuildhttpdconf returned non-zero" # Restore DNS zones via whmapi1 if [[ -d "$restore_dir/dnszones" ]]; then log_info "Restoring DNS zones via whmapi1..." for zone_file in "$restore_dir/dnszones"/*.db; do [[ -f "$zone_file" ]] || continue local zone_name; zone_name=$(basename "$zone_file" .db) [[ -z "$zone_name" ]] && continue # If a specific domain was requested, skip all others if [[ -n "$specific_domain" && "$zone_name" != "$specific_domain" ]]; then continue fi # Extract main IP from the zone's A record for adddns local zone_ip; zone_ip=$(grep -m1 -P "^${zone_name}\.\s+\d+\s+(IN\s+)?A\s+" "$zone_file" \ | grep -oP 'A\s+\K[0-9.]+') || zone_ip="" # Ensure zone exists in cPanel local adddns_out; adddns_out=$(whmapi1 adddns domain="$zone_name" ip="$zone_ip" trueowner="$user" 2>&1) || true if echo "$adddns_out" | grep -q 'result: 1'; then log_debug "Created DNS zone for $zone_name via whmapi1" elif echo "$adddns_out" | grep -qi 'already exists\|exists on\|already has\|has dns'; then log_debug "DNS zone $zone_name already exists" else log_warn "whmapi1 adddns failed for $zone_name: $(echo "$adddns_out" | grep -oP 'reason:\s*\K.*' | head -1)" fi # Parse zone file and add records via whmapi1 addzonerecord # Skip SOA, NS, and comment lines — cPanel manages those local add_ok=0 add_fail=0 while IFS= read -r line; do [[ -z "$line" || "$line" =~ ^[[:space:]]*\; || "$line" =~ ^\$ ]] && continue # Match: name [ttl] [IN] type data local rec_name rec_ttl rec_type rec_data if [[ "$line" =~ ^([^[:space:]]+)[[:space:]]+([0-9]+)[[:space:]]+(IN[[:space:]]+)?([A-Z]+)[[:space:]]+(.+)$ ]]; then rec_name="${BASH_REMATCH[1]}" rec_ttl="${BASH_REMATCH[2]}" rec_type="${BASH_REMATCH[4]}" rec_data="${BASH_REMATCH[5]}" elif [[ "$line" =~ ^([^[:space:]]+)[[:space:]]+(IN[[:space:]]+)?([A-Z]+)[[:space:]]+(.+)$ ]]; then rec_name="${BASH_REMATCH[1]}" rec_ttl="14400" rec_type="${BASH_REMATCH[3]}" rec_data="${BASH_REMATCH[4]}" else continue fi # Skip SOA, NS — cPanel manages these [[ "$rec_type" == "SOA" || "$rec_type" == "NS" ]] && continue # Strip trailing dot from name rec_name="${rec_name%.}" # Strip trailing comments and whitespace rec_data="${rec_data%%;*}" rec_data="${rec_data%"${rec_data##*[![:space:]]}"}" # Strip trailing dots from data (FQDN format) rec_data="${rec_data%.}" local rec_out="" case "$rec_type" in A|AAAA) rec_out=$(whmapi1 addzonerecord domain="$zone_name" name="$rec_name" type="$rec_type" address="$rec_data" ttl="$rec_ttl" 2>&1) || true ;; CNAME) rec_out=$(whmapi1 addzonerecord domain="$zone_name" name="$rec_name" type=CNAME cname="$rec_data" ttl="$rec_ttl" 2>&1) || true ;; MX) local mx_prio="${rec_data%%[[:space:]]*}" local mx_host="${rec_data#*[[:space:]]}" mx_host="${mx_host%.}" rec_out=$(whmapi1 addzonerecord domain="$zone_name" name="$rec_name" type=MX exchange="$mx_host" preference="$mx_prio" ttl="$rec_ttl" 2>&1) || true ;; TXT) # Strip surrounding quotes rec_data="${rec_data#\"}" rec_data="${rec_data%\"}" rec_out=$(whmapi1 addzonerecord domain="$zone_name" name="$rec_name" type=TXT txtdata="$rec_data" ttl="$rec_ttl" 2>&1) || true ;; SRV) # SRV: priority weight port target local srv_prio srv_weight srv_port srv_target read -r srv_prio srv_weight srv_port srv_target <<< "$rec_data" srv_target="${srv_target%.}" rec_out=$(whmapi1 addzonerecord domain="$zone_name" name="$rec_name" type=SRV priority="$srv_prio" weight="$srv_weight" port="$srv_port" target="$srv_target" ttl="$rec_ttl" 2>&1) || true ;; CAA) rec_out=$(whmapi1 addzonerecord domain="$zone_name" name="$rec_name" type=CAA record="$rec_data" ttl="$rec_ttl" 2>&1) || true ;; *) log_debug "Skipping unsupported record type $rec_type for $rec_name" continue ;; esac if echo "$rec_out" | grep -q 'result: 1'; then ((add_ok++)) || true else local reason; reason=$(echo "$rec_out" | grep -oP 'reason:\s*\K.*' | head -1) if echo "$reason" | grep -qi 'already has\|duplicate'; then log_debug "Record $rec_name $rec_type already exists in $zone_name" else log_debug "addzonerecord failed for $rec_name $rec_type in $zone_name: $reason" ((add_fail++)) || true fi fi done < "$zone_file" log_info "Restored DNS zone $zone_name via whmapi1 ($add_ok records added, $add_fail failed)" done fi rm -rf "$restore_dir" log_info "Domains & DNS restore completed for $user" return 0 } restore_ssl() { local user="$1" local specific_cert="${2:-}" local timestamp="${3:-}" local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}" local ts; ts=$(resolve_snapshot_timestamp "$user" "$timestamp") || return 1 local snap_dir; snap_dir=$(get_snapshot_dir "$user") local snap_path="$snap_dir/$ts" local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd) # Detect old format vs new format local pkgacct_base="$snap_path" if remote_exec "test -d '$snap_path/pkgacct'" 2>/dev/null; then pkgacct_base="$snap_path/pkgacct" fi local restore_dir="$temp_dir/restore-ssl-$user" mkdir -p "$restore_dir" || { log_error "Failed to create restore temp directory" return 1 } log_info "Restoring SSL certificates for $user from snapshot $ts" local found_any=false # Download ssl/, sslcerts/, autossl.json if _is_rclone_mode; then local snap_subpath="accounts/${user}/snapshots/${ts}" local pkgacct_sub; pkgacct_sub=$(_detect_pkgacct_base "" "$snap_subpath") rclone_from_remote "${pkgacct_sub}/ssl" "$restore_dir/ssl" 2>/dev/null && found_any=true rclone_from_remote "${pkgacct_sub}/sslcerts" "$restore_dir/sslcerts" 2>/dev/null && found_any=true rclone_from_remote "${pkgacct_sub}/autossl.json" "$restore_dir" 2>/dev/null && found_any=true rclone_from_remote "${snap_subpath}/homedir/ssl" "$restore_dir/homedir-ssl" 2>/dev/null && found_any=true else if rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${pkgacct_base}/ssl/" \ "$restore_dir/ssl/" 2>/dev/null; then found_any=true fi if rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${pkgacct_base}/sslcerts/" \ "$restore_dir/sslcerts/" 2>/dev/null; then found_any=true fi if rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${pkgacct_base}/autossl.json" \ "$restore_dir/autossl.json" 2>/dev/null; then found_any=true fi # Also download homedir/ssl/ (user-managed certs) local homedir_base="${snap_path}/homedir" if rsync -aHAX --rsync-path="rsync --fake-super" \ -e "$rsync_ssh" \ "${REMOTE_USER}@${REMOTE_HOST}:${homedir_base}/ssl/" \ "$restore_dir/homedir-ssl/" 2>/dev/null; then found_any=true fi fi if [[ "$found_any" == "false" ]]; then log_error "No SSL data found in snapshot for $user" rm -rf "$restore_dir" return 1 fi # Install certs on domains via whmapi1 installssl (no file copies) log_info "Installing SSL certificates via whmapi1..." local installed_count=0 # Find cert files in ssl/ and sslcerts/ directories local cert_dirs=() [[ -d "$restore_dir/ssl/certs" ]] && cert_dirs+=("$restore_dir/ssl/certs") [[ -d "$restore_dir/sslcerts" ]] && cert_dirs+=("$restore_dir/sslcerts") [[ -d "$restore_dir/ssl/installed/certs" ]] && cert_dirs+=("$restore_dir/ssl/installed/certs") [[ -d "$restore_dir/homedir-ssl/certs" ]] && cert_dirs+=("$restore_dir/homedir-ssl/certs") for cert_search_dir in "${cert_dirs[@]}"; do for cert_file in "$cert_search_dir"/*.crt "$cert_search_dir"/*.pem; do [[ -f "$cert_file" ]] || continue # If a specific cert was requested, skip all others if [[ -n "$specific_cert" ]]; then local cert_base; cert_base=$(basename "$cert_file" .crt) [[ "$cert_base" == "$cert_file" ]] && cert_base=$(basename "$cert_file" .pem) if [[ "$cert_base" != "$specific_cert" ]]; then continue fi fi # Extract domain from certificate local cert_domain; cert_domain=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null \ | grep -oP 'CN\s*=\s*\K[^\s/]+') || continue [[ -z "$cert_domain" ]] && continue # Read cert content local cert_content; cert_content=$(cat "$cert_file") # Find matching key file local key_content="" local cert_basename; cert_basename=$(basename "$cert_file") local key_basename="${cert_basename%.*}.key" # Search for key in ssl/keys/ directory for key_search_dir in "$restore_dir/ssl/keys" "$restore_dir/ssl/installed/keys" "$restore_dir/homedir-ssl/keys"; do if [[ -f "$key_search_dir/$key_basename" ]]; then key_content=$(cat "$key_search_dir/$key_basename") break fi done # If no exact match, try to find any key that matches the cert if [[ -z "$key_content" ]]; then local cert_modulus; cert_modulus=$(openssl x509 -in "$cert_file" -noout -modulus 2>/dev/null) || continue for key_search_dir in "$restore_dir/ssl/keys" "$restore_dir/ssl/installed/keys" "$restore_dir/homedir-ssl/keys"; do [[ -d "$key_search_dir" ]] || continue for key_file in "$key_search_dir"/*.key; do [[ -f "$key_file" ]] || continue local key_modulus; key_modulus=$(openssl rsa -in "$key_file" -noout -modulus 2>/dev/null) || continue if [[ "$cert_modulus" == "$key_modulus" ]]; then key_content=$(cat "$key_file") break 2 fi done done fi [[ -z "$key_content" ]] && { log_debug "No matching key found for cert $cert_domain, skipping"; continue; } # Find CA bundle if available local cab_content="" for cab_search_dir in "$restore_dir/ssl/cabundles" "$restore_dir/ssl/installed/cabundles" "$restore_dir/homedir-ssl/cabundles"; do local cab_file="$cab_search_dir/${cert_basename%.*}.cabundle" if [[ -f "$cab_file" ]]; then cab_content=$(cat "$cab_file") break fi done # Install via whmapi1 local install_out if [[ -n "$cab_content" ]]; then install_out=$(whmapi1 installssl domain="$cert_domain" crt="$cert_content" key="$key_content" cab="$cab_content" 2>&1) || true else install_out=$(whmapi1 installssl domain="$cert_domain" crt="$cert_content" key="$key_content" 2>&1) || true fi if echo "$install_out" | grep -q 'result: 1'; then log_info "Installed SSL cert for $cert_domain via whmapi1" ((installed_count++)) || true else log_warn "whmapi1 installssl failed for $cert_domain: $(echo "$install_out" | grep -oP 'reason:\s*\K.*' | head -1)" fi done done if (( installed_count == 0 )); then log_warn "No SSL certificates were installed via whmapi1" else log_info "Installed $installed_count SSL certificate(s) via whmapi1" fi rm -rf "$restore_dir" log_info "SSL restore completed for $user" return 0 } restore_server() { local timestamp="${1:-}" log_info "Starting full server rebuild..." local accounts; accounts=$(list_remote_accounts) if [[ -z "$accounts" ]]; then log_error "No accounts found on remote for restore" return 1 fi local total=0 succeeded=0 failed=0 local failed_accounts="" while IFS= read -r user; do [[ -z "$user" ]] && continue ((total++)) || true log_info "--- Restoring account $user ($total) ---" if restore_full_account "$user" "$timestamp" "" ""; then ((succeeded++)) || true else ((failed++)) || true failed_accounts+=" - $user"$'\n' fi done <<< "$accounts" echo "" echo "============================================" echo "Server Restore Summary" echo "============================================" echo "Total accounts: $total" echo "Succeeded: $succeeded" echo "Failed: $failed" if [[ -n "$failed_accounts" ]]; then echo "" echo "Failed accounts:" echo "$failed_accounts" fi echo "============================================" (( failed > 0 )) && return 1 return 0 }