- CLI binary: bin/gniza -> bin/gniza4cp - Install path: /usr/local/gniza4cp/ - Config path: /etc/gniza4cp/ - Log path: /var/log/gniza4cp/ - WHM plugin: gniza4cp-whm/ - cPanel plugin: cpanel/gniza4cp/ - AdminBin: Gniza4cp::Restore - Perl modules: Gniza4cpWHM::*, Gniza4cpCPanel::* - DaisyUI theme: gniza4cp - All internal references, branding, paths updated - Git remote updated to gniza4cp repo
1436 lines
55 KiB
Bash
1436 lines
55 KiB
Bash
#!/usr/bin/env bash
|
|
# gniza4cp/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
|
|
export SSHPASS="$REMOTE_PASSWORD"
|
|
sshpass -e 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..."
|
|
local removeacct_output
|
|
if ! removeacct_output=$(whmapi1 removeacct user="$user" 2>&1); then
|
|
log_error "Failed to terminate account $user"
|
|
log_debug "removeacct output: $removeacct_output"
|
|
rm -rf "$restore_dir"
|
|
return 1
|
|
fi
|
|
log_debug "removeacct output: $removeacct_output"
|
|
# Check whmapi1 result field for failure
|
|
if echo "$removeacct_output" | grep -q 'result: 0'; then
|
|
local reason
|
|
reason=$(echo "$removeacct_output" | grep 'reason:' | sed 's/.*reason: //')
|
|
log_error "Failed to terminate account $user: $reason"
|
|
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
|
|
}
|