1729 lines
61 KiB
Bash
Executable File
1729 lines
61 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gniza — cPanel Backup, Restore & Disaster Recovery
|
|
# CLI entrypoint and command routing
|
|
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
# Resolve install directory
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
if [[ -L "${BASH_SOURCE[0]}" ]]; then
|
|
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
|
fi
|
|
BASE_DIR="$(dirname "$SCRIPT_DIR")"
|
|
LIB_DIR="$BASE_DIR/lib"
|
|
|
|
# Source libraries
|
|
source "$LIB_DIR/constants.sh"
|
|
source "$LIB_DIR/utils.sh"
|
|
source "$LIB_DIR/logging.sh"
|
|
source "$LIB_DIR/config.sh"
|
|
source "$LIB_DIR/locking.sh"
|
|
source "$LIB_DIR/ssh.sh"
|
|
source "$LIB_DIR/accounts.sh"
|
|
source "$LIB_DIR/pkgacct.sh"
|
|
source "$LIB_DIR/snapshot.sh"
|
|
source "$LIB_DIR/transfer.sh"
|
|
source "$LIB_DIR/retention.sh"
|
|
source "$LIB_DIR/verify.sh"
|
|
source "$LIB_DIR/notify.sh"
|
|
source "$LIB_DIR/restore.sh"
|
|
source "$LIB_DIR/remotes.sh"
|
|
source "$LIB_DIR/rclone.sh"
|
|
source "$LIB_DIR/schedule.sh"
|
|
source "$LIB_DIR/sysbackup.sh"
|
|
source "$LIB_DIR/sysrestore.sh"
|
|
|
|
# ── Argument parsing helpers ───────────────────────────────────
|
|
|
|
get_opt() {
|
|
local name="$1"; shift
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--${name}=*) echo "${arg#*=}"; return 0 ;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|
|
|
|
has_flag() {
|
|
local name="$1"; shift
|
|
for arg in "$@"; do
|
|
[[ "$arg" == "--$name" ]] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# ── Commands ───────────────────────────────────────────────────
|
|
|
|
# Transfer + finalize + retention for a single account on the current remote.
|
|
# Globals REMOTE_* must already be set via load_remote().
|
|
# Returns 0 on success, 1 on failure.
|
|
_backup_to_current_remote() {
|
|
local user="$1"
|
|
local ts="$2"
|
|
local remote_label="$CURRENT_REMOTE_NAME"
|
|
|
|
# Clean any leftover partials
|
|
clean_partial_snapshots "$user"
|
|
|
|
# Find previous snapshot for hardlinking
|
|
local prev; prev=$(get_latest_snapshot "$user") || prev=""
|
|
|
|
# Transfer pkgacct data
|
|
if ! transfer_pkgacct "$user" "$ts" "$prev"; then
|
|
log_error "[$remote_label] Transfer pkgacct failed for $user"
|
|
return 1
|
|
fi
|
|
|
|
# Transfer homedir
|
|
if ! transfer_homedir "$user" "$ts" "$prev"; then
|
|
log_error "[$remote_label] Transfer homedir failed for $user"
|
|
return 1
|
|
fi
|
|
|
|
# Finalize snapshot
|
|
if ! finalize_snapshot "$user" "$ts"; then
|
|
log_error "[$remote_label] Finalize failed for $user"
|
|
return 1
|
|
fi
|
|
|
|
# Enforce retention
|
|
enforce_retention "$user"
|
|
return 0
|
|
}
|
|
|
|
cmd_backup() {
|
|
require_root
|
|
require_cmd rsync
|
|
require_cmd ssh
|
|
require_cmd hostname
|
|
require_cmd gzip
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local dry_run=false
|
|
has_flag dry-run "$@" && dry_run=true
|
|
|
|
local run_sysbackup=false
|
|
has_flag sysbackup "$@" && run_sysbackup=true
|
|
|
|
local skip_suspended=false
|
|
has_flag skip-suspended "$@" && skip_suspended=true
|
|
|
|
local single_account=""
|
|
single_account=$(get_opt account "$@" 2>/dev/null) || true
|
|
|
|
local remote_flag=""
|
|
remote_flag=$(get_opt remote "$@" 2>/dev/null) || true
|
|
|
|
acquire_lock
|
|
trap 'cleanup_all_temp; release_lock' EXIT
|
|
|
|
# Resolve target remotes
|
|
local remotes=""
|
|
remotes=$(get_target_remotes "$remote_flag") || die "Invalid remote specification"
|
|
|
|
# Save original globals (will be restored after each load_remote)
|
|
_save_remote_globals
|
|
|
|
# Test connectivity to all targets upfront
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname" || die "Failed to load remote: $rname"
|
|
if _is_rclone_mode; then
|
|
test_rclone_connection || die "Cannot connect to remote '$rname' (${REMOTE_TYPE})"
|
|
else
|
|
test_ssh_connection || die "Cannot connect to remote '$rname' (${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT})"
|
|
fi
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
|
|
local start_time; start_time=$(date +%s)
|
|
local ts; ts=$(timestamp)
|
|
|
|
# Determine accounts
|
|
local accounts
|
|
if [[ -n "$single_account" ]]; then
|
|
account_exists "$single_account" || die "Account does not exist: $single_account"
|
|
accounts="$single_account"
|
|
else
|
|
accounts=$(get_backup_accounts "$skip_suspended")
|
|
fi
|
|
|
|
if [[ -z "$accounts" ]]; then
|
|
die "No accounts found to backup"
|
|
fi
|
|
|
|
local total=0 succeeded=0 failed=0
|
|
local failed_accounts=""
|
|
|
|
while IFS= read -r user; do
|
|
[[ -z "$user" ]] && continue
|
|
((total++)) || true
|
|
|
|
log_info "=== Backing up account: $user ($total) ==="
|
|
|
|
if [[ "$dry_run" == "true" ]]; then
|
|
log_info "[DRY RUN] Would backup account: $user"
|
|
log_info "[DRY RUN] pkgacct --nocomp --skiphomedir -> temp"
|
|
log_info "[DRY RUN] gzip SQL files"
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname"
|
|
local prev; prev=$(get_latest_snapshot "$user") || prev=""
|
|
log_info "[DRY RUN] [$rname] rsync pkgacct to ${REMOTE_HOST} (link-dest: ${prev:-none})"
|
|
log_info "[DRY RUN] [$rname] rsync homedir to ${REMOTE_HOST} (link-dest: ${prev:-none})"
|
|
log_info "[DRY RUN] [$rname] finalize snapshot: $ts"
|
|
log_info "[DRY RUN] [$rname] enforce retention: keep $RETENTION_COUNT"
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
((succeeded++)) || true
|
|
continue
|
|
fi
|
|
|
|
# Run pkgacct ONCE
|
|
if ! run_pkgacct "$user"; then
|
|
log_error "Backup failed for $user: pkgacct error"
|
|
((failed++)) || true
|
|
failed_accounts+=" - $user (pkgacct failed)"$'\n'
|
|
cleanup_pkgacct "$user"
|
|
continue
|
|
fi
|
|
|
|
# Gzip SQL files ONCE
|
|
if ! gzip_sql_files "$user"; then
|
|
log_warn "SQL gzip had issues for $user, continuing..."
|
|
fi
|
|
|
|
# Transfer to each remote
|
|
local user_failed=false
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname"
|
|
log_info "--- Transferring $user to remote '$rname' ---"
|
|
if ! _backup_to_current_remote "$user" "$ts"; then
|
|
log_error "Backup to remote '$rname' failed for $user"
|
|
failed_accounts+=" - $user ($rname: transfer failed)"$'\n'
|
|
user_failed=true
|
|
fi
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
|
|
if [[ "$user_failed" == "true" ]]; then
|
|
((failed++)) || true
|
|
else
|
|
((succeeded++)) || true
|
|
log_info "Backup completed for $user (all remotes)"
|
|
fi
|
|
|
|
# Cleanup local temp
|
|
cleanup_pkgacct "$user"
|
|
done <<< "$accounts"
|
|
|
|
local end_time; end_time=$(date +%s)
|
|
local duration=$(( end_time - start_time ))
|
|
|
|
# Print summary
|
|
echo ""
|
|
echo "============================================"
|
|
echo "Backup Summary"
|
|
echo "============================================"
|
|
echo "Timestamp: $ts"
|
|
echo "Duration: $(human_duration $duration)"
|
|
echo "Remotes: $(echo "$remotes" | tr '\n' ' ')"
|
|
echo "Total: $total"
|
|
echo "Succeeded: ${C_GREEN}${succeeded}${C_RESET}"
|
|
if (( failed > 0 )); then
|
|
echo "Failed: ${C_RED}${failed}${C_RESET}"
|
|
echo ""
|
|
echo "Failed accounts:"
|
|
echo "$failed_accounts"
|
|
else
|
|
echo "Failed: 0"
|
|
fi
|
|
echo "============================================"
|
|
|
|
# Run system backup if --sysbackup was requested
|
|
if [[ "$run_sysbackup" == "true" ]]; then
|
|
echo ""
|
|
log_info "=== Running system backup (--sysbackup) ==="
|
|
# Release lock so sysbackup can acquire its own
|
|
release_lock
|
|
local sysbackup_args=()
|
|
[[ -n "$remote_flag" ]] && sysbackup_args+=(--remote="$remote_flag")
|
|
[[ "$dry_run" == "true" ]] && sysbackup_args+=(--dry-run)
|
|
# Run as subprocess so its exit doesn't kill our process
|
|
/usr/local/bin/gniza sysbackup "${sysbackup_args[@]}" || log_error "System backup failed"
|
|
acquire_lock
|
|
fi
|
|
|
|
# Send notification
|
|
send_backup_report "$total" "$succeeded" "$failed" "$duration" "$failed_accounts"
|
|
|
|
if (( failed > 0 && succeeded > 0 )); then
|
|
exit "$EXIT_PARTIAL"
|
|
elif (( failed > 0 )); then
|
|
exit "$EXIT_FATAL"
|
|
fi
|
|
exit "$EXIT_OK"
|
|
}
|
|
|
|
# Helper: load remote context for restore commands.
|
|
# --remote=NAME is always required for restore.
|
|
_restore_load_remote() {
|
|
local remote_flag="$1"
|
|
[[ -z "$remote_flag" ]] && die "Specify --remote=NAME for restore."$'\n'"Available remotes: $(list_remotes | tr '\n' ' ')"
|
|
_save_remote_globals
|
|
load_remote "$remote_flag" || die "Failed to load remote: $remote_flag"
|
|
}
|
|
|
|
# Helper: test connection to current remote (SSH or rclone).
|
|
_test_connection() {
|
|
if _is_rclone_mode; then
|
|
test_rclone_connection || die "Cannot connect to remote (${REMOTE_TYPE})"
|
|
else
|
|
test_ssh_connection || die "Cannot connect to remote server"
|
|
fi
|
|
}
|
|
|
|
cmd_restore() {
|
|
require_root
|
|
require_cmd rsync
|
|
require_cmd ssh
|
|
require_cmd hostname
|
|
local subcommand="${1:-}"
|
|
shift 2>/dev/null || true
|
|
|
|
case "$subcommand" in
|
|
account)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore account <name> [--remote=NAME] [--timestamp=TS] [--terminate]"
|
|
validate_account_name "$name" || die "Invalid account name"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
[[ -n "$timestamp" ]] && { validate_timestamp "$timestamp" || die "Invalid timestamp"; }
|
|
local exclude; exclude=$(get_opt exclude "$@" 2>/dev/null) || exclude=""
|
|
local terminate_val; terminate_val=$(get_opt terminate "$@" 2>/dev/null) || terminate_val=""
|
|
local terminate=false
|
|
[[ "$terminate_val" == "1" ]] && terminate=true
|
|
|
|
_test_connection
|
|
restore_full_account "$name" "$timestamp" "$terminate" "$exclude"
|
|
;;
|
|
files)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore files <name> [--remote=NAME] [--path=subpath] [--timestamp=TS]"
|
|
validate_account_name "$name" || die "Invalid account name"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local subpath; subpath=$(get_opt path "$@" 2>/dev/null) || subpath=""
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
[[ -n "$timestamp" ]] && { validate_timestamp "$timestamp" || die "Invalid timestamp"; }
|
|
local exclude; exclude=$(get_opt exclude "$@" 2>/dev/null) || exclude=""
|
|
|
|
_test_connection
|
|
restore_files "$name" "$subpath" "$timestamp" "$exclude"
|
|
;;
|
|
database)
|
|
local name="${1:-}"
|
|
local dbname="${2:-}"
|
|
[[ -z "$name" ]] && die "Usage: gniza restore database <name> [<dbname>] [--remote=NAME] [--timestamp=TS]"
|
|
validate_account_name "$name" || die "Invalid account name"
|
|
shift 2>/dev/null || true
|
|
# If dbname looks like a flag, it's not a dbname
|
|
if [[ -n "$dbname" && "$dbname" != --* ]]; then
|
|
shift 2>/dev/null || true
|
|
else
|
|
dbname=""
|
|
fi
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
[[ -n "$timestamp" ]] && { validate_timestamp "$timestamp" || die "Invalid timestamp"; }
|
|
|
|
_test_connection
|
|
if [[ -n "$dbname" ]]; then
|
|
restore_database "$name" "$dbname" "$timestamp"
|
|
else
|
|
restore_all_databases "$name" "$timestamp"
|
|
fi
|
|
;;
|
|
mailbox)
|
|
local name="${1:-}"
|
|
local email="${2:-}"
|
|
[[ -z "$name" ]] && die "Usage: gniza restore mailbox <name> [<email@domain>] [--remote=NAME] [--timestamp=TS]"
|
|
validate_account_name "$name" || die "Invalid account name"
|
|
shift 2>/dev/null || true
|
|
# If email looks like a flag, it's not an email
|
|
if [[ -n "$email" && "$email" != --* ]]; then
|
|
shift 2>/dev/null || true
|
|
else
|
|
email=""
|
|
fi
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
[[ -n "$timestamp" ]] && { validate_timestamp "$timestamp" || die "Invalid timestamp"; }
|
|
|
|
_test_connection
|
|
if [[ -n "$email" ]]; then
|
|
restore_mailbox "$name" "$email" "$timestamp"
|
|
else
|
|
restore_all_mailboxes "$name" "$timestamp"
|
|
fi
|
|
;;
|
|
list-databases)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore list-databases <name> [--remote=NAME] [--timestamp=TS]"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
list_snapshot_databases "$name" "$timestamp"
|
|
;;
|
|
list-mailboxes)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore list-mailboxes <name> [--remote=NAME] [--timestamp=TS]"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
list_snapshot_mailboxes "$name" "$timestamp"
|
|
;;
|
|
list-files)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore list-files <name> [--remote=NAME] [--timestamp=TS] [--path=subdir]"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
local subpath; subpath=$(get_opt path "$@" 2>/dev/null) || subpath=""
|
|
|
|
_test_connection
|
|
list_snapshot_files "$name" "$timestamp" "$subpath"
|
|
;;
|
|
list-dbusers)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore list-dbusers <name> [--remote=NAME] [--timestamp=TS]"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
list_snapshot_dbusers "$name" "$timestamp"
|
|
;;
|
|
list-cron)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore list-cron <name> [--remote=NAME] [--timestamp=TS]"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
list_snapshot_cron "$name" "$timestamp"
|
|
;;
|
|
list-dns)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore list-dns <name> [--remote=NAME] [--timestamp=TS]"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
list_snapshot_dns "$name" "$timestamp"
|
|
;;
|
|
list-ssl)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore list-ssl <name> [--remote=NAME] [--timestamp=TS]"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
list_snapshot_ssl "$name" "$timestamp"
|
|
;;
|
|
cron)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore cron <name> [--remote=NAME] [--timestamp=TS]"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
restore_cron "$name" "$timestamp"
|
|
;;
|
|
dbusers)
|
|
local name="${1:-}"
|
|
local specific_dbuser="${2:-}"
|
|
[[ -z "$name" ]] && die "Usage: gniza restore dbusers <name> [<dbuser>] [--remote=NAME] [--timestamp=TS]"
|
|
shift 2>/dev/null || true
|
|
if [[ -n "$specific_dbuser" && "$specific_dbuser" != --* ]]; then
|
|
shift 2>/dev/null || true
|
|
else
|
|
specific_dbuser=""
|
|
fi
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
restore_dbusers "$name" "$specific_dbuser" "$timestamp"
|
|
;;
|
|
cpconfig)
|
|
local name="${1:-}"
|
|
shift 2>/dev/null || true
|
|
[[ -z "$name" ]] && die "Usage: gniza restore cpconfig <name> [--remote=NAME] [--timestamp=TS]"
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
restore_cpconfig "$name" "$timestamp"
|
|
;;
|
|
domains)
|
|
local name="${1:-}"
|
|
local specific_domain="${2:-}"
|
|
[[ -z "$name" ]] && die "Usage: gniza restore domains <name> [<domain>] [--remote=NAME] [--timestamp=TS]"
|
|
shift 2>/dev/null || true
|
|
if [[ -n "$specific_domain" && "$specific_domain" != --* ]]; then
|
|
shift 2>/dev/null || true
|
|
else
|
|
specific_domain=""
|
|
fi
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
restore_domains "$name" "$specific_domain" "$timestamp"
|
|
;;
|
|
ssl)
|
|
local name="${1:-}"
|
|
local specific_cert="${2:-}"
|
|
[[ -z "$name" ]] && die "Usage: gniza restore ssl <name> [<cert>] [--remote=NAME] [--timestamp=TS]"
|
|
shift 2>/dev/null || true
|
|
if [[ -n "$specific_cert" && "$specific_cert" != --* ]]; then
|
|
shift 2>/dev/null || true
|
|
else
|
|
specific_cert=""
|
|
fi
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
restore_ssl "$name" "$specific_cert" "$timestamp"
|
|
;;
|
|
server)
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag; remote_flag=$(get_opt remote "$@" 2>/dev/null) || remote_flag=""
|
|
_restore_load_remote "$remote_flag"
|
|
|
|
local timestamp; timestamp=$(get_opt timestamp "$@" 2>/dev/null) || timestamp=""
|
|
|
|
_test_connection
|
|
restore_server "$timestamp"
|
|
;;
|
|
*)
|
|
die "Unknown restore subcommand: $subcommand"$'\n'"Usage: gniza restore {account|files|database|mailbox|cron|dbusers|cpconfig|domains|ssl|list-databases|list-mailboxes|list-files|list-dbusers|list-cron|list-dns|list-ssl|server}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Display listing for the current remote context (globals already set).
|
|
_list_current_remote() {
|
|
local single_account="$1"
|
|
local hostname; hostname=$(hostname -f)
|
|
|
|
if [[ -n "$single_account" ]]; then
|
|
local snapshots; snapshots=$(list_remote_snapshots "$single_account")
|
|
if [[ -z "$snapshots" ]]; then
|
|
echo " (none)"
|
|
else
|
|
while IFS= read -r snap; do
|
|
local snap_dir; snap_dir=$(get_snapshot_dir "$single_account")
|
|
local size; size=$(remote_exec "du -sb '$snap_dir/$snap' 2>/dev/null | cut -f1" 2>/dev/null)
|
|
printf " %-25s %s\n" "$snap" "$(human_size "${size:-0}")"
|
|
done <<< "$snapshots"
|
|
fi
|
|
else
|
|
local accounts; accounts=$(list_remote_accounts)
|
|
if [[ -z "$accounts" ]]; then
|
|
echo " (no backups)"
|
|
return 0
|
|
fi
|
|
|
|
printf " %-20s %-10s %-25s\n" "ACCOUNT" "SNAPSHOTS" "LATEST"
|
|
printf " %-20s %-10s %-25s\n" "-------" "---------" "------"
|
|
|
|
while IFS= read -r user; do
|
|
[[ -z "$user" ]] && continue
|
|
local count; count=$(list_remote_snapshots "$user" | wc -l)
|
|
local latest; latest=$(get_latest_snapshot "$user")
|
|
printf " %-20s %-10s %-25s\n" "$user" "$count" "${latest:-(none)}"
|
|
done <<< "$accounts"
|
|
fi
|
|
}
|
|
|
|
cmd_list() {
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
# list accounts subcommand
|
|
if [[ "${1:-}" == "accounts" ]]; then
|
|
shift
|
|
local remote_flag=""
|
|
remote_flag=$(get_opt remote "$@" 2>/dev/null) || true
|
|
[[ -z "$remote_flag" ]] && die "Usage: gniza list accounts --remote=NAME"
|
|
local remotes; remotes=$(get_target_remotes "$remote_flag") || die "Invalid remote"
|
|
local rname; rname=$(head -1 <<< "$remotes")
|
|
_save_remote_globals
|
|
load_remote "$rname" || die "Failed to load remote: $rname"
|
|
_test_connection || die "Connection failed to remote: $rname"
|
|
list_remote_accounts
|
|
_restore_remote_globals
|
|
return 0
|
|
fi
|
|
|
|
local single_account=""
|
|
single_account=$(get_opt account "$@" 2>/dev/null) || true
|
|
|
|
local remote_flag=""
|
|
remote_flag=$(get_opt remote "$@" 2>/dev/null) || true
|
|
|
|
local remotes=""
|
|
remotes=$(get_target_remotes "$remote_flag") || die "Invalid remote specification"
|
|
|
|
local hostname; hostname=$(hostname -f)
|
|
|
|
_save_remote_globals
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname" || { log_error "Failed to load remote: $rname"; continue; }
|
|
|
|
echo ""
|
|
if [[ -n "$single_account" ]]; then
|
|
echo "Snapshots for ${C_BOLD}$single_account${C_RESET} on ${C_BOLD}[$rname]${C_RESET} (${REMOTE_HOST}):"
|
|
else
|
|
echo "Backups on ${C_BOLD}[$rname]${C_RESET} (${REMOTE_HOST}):"
|
|
fi
|
|
echo ""
|
|
local conn_ok=false
|
|
if _is_rclone_mode; then
|
|
test_rclone_connection 2>/dev/null && conn_ok=true
|
|
else
|
|
test_ssh_connection 2>/dev/null && conn_ok=true
|
|
fi
|
|
if [[ "$conn_ok" == "true" ]]; then
|
|
_list_current_remote "$single_account"
|
|
else
|
|
echo " ${C_RED}Connection failed${C_RESET}"
|
|
fi
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
}
|
|
|
|
cmd_verify() {
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local single_account=""
|
|
single_account=$(get_opt account "$@" 2>/dev/null) || true
|
|
|
|
local remote_flag=""
|
|
remote_flag=$(get_opt remote "$@" 2>/dev/null) || true
|
|
|
|
local remotes=""
|
|
remotes=$(get_target_remotes "$remote_flag") || die "Invalid remote specification"
|
|
|
|
_save_remote_globals
|
|
local any_failed=false
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname" || { log_error "Failed to load remote: $rname"; any_failed=true; continue; }
|
|
|
|
echo ""
|
|
echo "${C_BOLD}[$rname]${C_RESET} (${REMOTE_TYPE:-ssh}):"
|
|
local conn_ok=false
|
|
if _is_rclone_mode; then
|
|
test_rclone_connection 2>/dev/null && conn_ok=true
|
|
else
|
|
test_ssh_connection 2>/dev/null && conn_ok=true
|
|
fi
|
|
if [[ "$conn_ok" == "true" ]]; then
|
|
if [[ -n "$single_account" ]]; then
|
|
verify_account_backup "$single_account" || any_failed=true
|
|
else
|
|
verify_all_accounts || any_failed=true
|
|
fi
|
|
else
|
|
echo " ${C_RED}Connection failed${C_RESET}"
|
|
any_failed=true
|
|
fi
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
[[ "$any_failed" == "true" ]] && return 1
|
|
}
|
|
|
|
_status_ssh_and_disk() {
|
|
if _is_rclone_mode; then
|
|
echo -n " Connection: "
|
|
if test_rclone_connection 2>/dev/null; then
|
|
echo "${C_GREEN}OK${C_RESET} (${REMOTE_TYPE})"
|
|
echo -n " Remote storage: "
|
|
local size_json; size_json=$(_rclone_cmd about --json "$(_rclone_remote_path "")" 2>/dev/null) || true
|
|
if [[ -n "$size_json" ]]; then
|
|
local used; used=$(echo "$size_json" | grep -oP '"used":\s*\K[0-9]+' || echo "")
|
|
local total; total=$(echo "$size_json" | grep -oP '"total":\s*\K[0-9]+' || echo "")
|
|
if [[ -n "$used" && -n "$total" && "$total" -gt 0 ]]; then
|
|
echo "$(human_size "$used") used of $(human_size "$total")"
|
|
elif [[ -n "$used" ]]; then
|
|
echo "$(human_size "$used") used"
|
|
else
|
|
echo "available"
|
|
fi
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
else
|
|
echo "${C_RED}FAILED${C_RESET}"
|
|
fi
|
|
else
|
|
echo -n " SSH connection: "
|
|
if test_ssh_connection 2>/dev/null; then
|
|
echo "${C_GREEN}OK${C_RESET}"
|
|
echo -n " Remote disk: "
|
|
local disk_info; disk_info=$(remote_exec "df -h '${REMOTE_BASE}' 2>/dev/null | tail -1" 2>/dev/null)
|
|
if [[ -n "$disk_info" ]]; then
|
|
echo "$disk_info" | awk '{printf "%s used of %s (%s)\n", $3, $2, $5}'
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
else
|
|
echo "${C_RED}FAILED${C_RESET}"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
cmd_status() {
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
|
|
local hostname; hostname=$(hostname -f)
|
|
|
|
echo "${C_BOLD}gniza v${GNIZA_VERSION}${C_RESET}"
|
|
echo ""
|
|
echo "Hostname: $hostname"
|
|
echo "Log level: ${LOG_LEVEL}"
|
|
echo "Notifications: ${NOTIFY_ON}${NOTIFY_EMAIL:+ ($NOTIFY_EMAIL)}"
|
|
|
|
echo "Mode: multi-remote"
|
|
echo ""
|
|
|
|
if has_remotes; then
|
|
_save_remote_globals
|
|
local remotes; remotes=$(list_remotes)
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname" 2>/dev/null || { echo " ${C_RED}Failed to load${C_RESET}"; continue; }
|
|
echo "${C_BOLD}[$rname]${C_RESET} (${REMOTE_TYPE:-ssh})"
|
|
if [[ "${REMOTE_TYPE:-ssh}" == "ssh" ]]; then
|
|
echo " Remote: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}"
|
|
elif [[ "$REMOTE_TYPE" == "s3" ]]; then
|
|
echo " Bucket: ${S3_BUCKET}"
|
|
echo " Region: ${S3_REGION}"
|
|
[[ -n "${S3_ENDPOINT:-}" ]] && echo " Endpoint: ${S3_ENDPOINT}"
|
|
elif [[ "$REMOTE_TYPE" == "gdrive" ]]; then
|
|
echo " Service acct: ${GDRIVE_SERVICE_ACCOUNT_FILE}"
|
|
[[ -n "${GDRIVE_ROOT_FOLDER_ID:-}" ]] && echo " Root folder: ${GDRIVE_ROOT_FOLDER_ID}"
|
|
fi
|
|
echo " Remote base: ${REMOTE_BASE}"
|
|
echo " Retention: ${RETENTION_COUNT} snapshots"
|
|
if (( BWLIMIT == 0 )); then echo " Bandwidth: unlimited"; else echo " Bandwidth: ${BWLIMIT} KB/s"; fi
|
|
_status_ssh_and_disk
|
|
echo ""
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
else
|
|
echo "No remotes configured. Run 'gniza init remote <name>' to add one."
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# Check lock
|
|
echo -n "Lock status: "
|
|
if [[ -f "${LOCK_FILE:-$DEFAULT_LOCK_FILE}" ]]; then
|
|
local pid; pid=$(cat "${LOCK_FILE:-$DEFAULT_LOCK_FILE}" 2>/dev/null)
|
|
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
|
|
echo "${C_YELLOW}LOCKED (PID $pid)${C_RESET}"
|
|
else
|
|
echo "stale lock file (no running process)"
|
|
fi
|
|
else
|
|
echo "unlocked"
|
|
fi
|
|
|
|
# Last log
|
|
local log_dir="${LOG_DIR:-$DEFAULT_LOG_DIR}"
|
|
echo -n "Last log: "
|
|
local last_log; last_log=$(ls -1t "$log_dir"/gniza-*.log 2>/dev/null | head -1)
|
|
if [[ -n "$last_log" ]]; then
|
|
echo "$(basename "$last_log")"
|
|
else
|
|
echo "(none)"
|
|
fi
|
|
}
|
|
|
|
cmd_remote() {
|
|
local subcommand="${1:-}"
|
|
shift 2>/dev/null || true
|
|
|
|
case "$subcommand" in
|
|
list|ls|"")
|
|
if ! has_remotes; then
|
|
echo "No remotes configured."
|
|
echo "Run 'gniza init remote <name>' to add one."
|
|
return 0
|
|
fi
|
|
|
|
local remotes; remotes=$(list_remotes)
|
|
local count; count=$(echo "$remotes" | wc -l)
|
|
|
|
echo "${C_BOLD}Configured remotes ($count):${C_RESET}"
|
|
echo ""
|
|
printf " %-15s %-8s %-30s %s\n" "NAME" "TYPE" "DESTINATION" "BASE"
|
|
printf " %-15s %-8s %-30s %s\n" "----" "----" "-----------" "----"
|
|
|
|
_save_remote_globals
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname" 2>/dev/null || { printf " %-15s %s\n" "$rname" "${C_RED}(failed to load)${C_RESET}"; continue; }
|
|
|
|
local dest_str
|
|
case "${REMOTE_TYPE:-ssh}" in
|
|
ssh) dest_str="${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}" ;;
|
|
s3) dest_str="s3://${S3_BUCKET}" ;;
|
|
gdrive) dest_str="gdrive:${GDRIVE_SERVICE_ACCOUNT_FILE##*/}" ;;
|
|
esac
|
|
printf " %-15s %-8s %-30s %s\n" \
|
|
"$rname" "${REMOTE_TYPE:-ssh}" "$dest_str" "$REMOTE_BASE"
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
echo ""
|
|
;;
|
|
delete|rm|remove)
|
|
require_root
|
|
local name="${1:-}"
|
|
[[ -z "$name" ]] && die "Usage: gniza remote delete <name>"
|
|
|
|
local conf="$REMOTES_DIR/${name}.conf"
|
|
if [[ ! -f "$conf" ]]; then
|
|
die "Remote not found: $name (expected $conf)"
|
|
fi
|
|
|
|
# Show what will be deleted
|
|
local config_file; config_file=$(get_opt config "${@:2}" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
load_remote "$name" 2>/dev/null
|
|
echo "Remote: $name"
|
|
echo "Host: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}"
|
|
echo "Base: ${REMOTE_BASE}"
|
|
echo ""
|
|
read -rp "Delete remote '$name'? [y/N] " answer
|
|
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
|
|
|
rm -f "$conf"
|
|
echo "Remote '$name' deleted."
|
|
;;
|
|
*)
|
|
die "Unknown remote subcommand: $subcommand"$'\n'"Usage: gniza remote {list|delete <name>}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_schedule() {
|
|
require_root
|
|
local subcommand="${1:-show}"
|
|
shift 2>/dev/null || true
|
|
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
init_logging
|
|
|
|
case "$subcommand" in
|
|
add)
|
|
local name="${1:-}"
|
|
[[ -z "$name" ]] && die "Usage: gniza schedule add <name>"
|
|
_schedule_add "$name"
|
|
;;
|
|
delete|rm|remove-schedule)
|
|
local name="${1:-}"
|
|
[[ -z "$name" ]] && die "Usage: gniza schedule delete <name>"
|
|
_schedule_delete "$name"
|
|
;;
|
|
run)
|
|
local name="${1:-}"
|
|
[[ -z "$name" ]] && die "Usage: gniza schedule run <name>"
|
|
_schedule_run "$name"
|
|
;;
|
|
list|ls)
|
|
_schedule_list
|
|
;;
|
|
install) install_schedules ;;
|
|
show) show_schedules ;;
|
|
remove) remove_schedules ;;
|
|
*) die "Unknown schedule subcommand: $subcommand"$'\n'"Usage: gniza schedule {add|delete|run|list|install|show|remove}" ;;
|
|
esac
|
|
}
|
|
|
|
_schedule_add() {
|
|
local name="$1"
|
|
|
|
if ! [[ "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
die "Schedule name must be alphanumeric (hyphens and underscores allowed): $name"
|
|
fi
|
|
|
|
local config_file="$SCHEDULES_DIR/${name}.conf"
|
|
|
|
if [[ -f "$config_file" ]]; then
|
|
echo "Schedule config already exists: $config_file"
|
|
read -rp "Overwrite? [y/N] " answer
|
|
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
|
fi
|
|
|
|
echo "${C_BOLD}gniza schedule add${C_RESET} — New schedule: ${C_BOLD}$name${C_RESET}"
|
|
echo ""
|
|
|
|
echo "Schedule options: hourly, daily, weekly, monthly, custom"
|
|
read -rp "Schedule type [daily]: " sched_type
|
|
sched_type="${sched_type:-daily}"
|
|
|
|
local sched_time="02:00"
|
|
local sched_day=""
|
|
local sched_cron=""
|
|
|
|
read -rp "Schedule time HH:MM [02:00]: " sched_time
|
|
sched_time="${sched_time:-02:00}"
|
|
|
|
case "$sched_type" in
|
|
weekly)
|
|
read -rp "Day of week (0=Sun..6=Sat): " sched_day
|
|
[[ -z "$sched_day" ]] && die "Day of week is required for weekly schedule"
|
|
;;
|
|
monthly)
|
|
read -rp "Day of month (1-28): " sched_day
|
|
[[ -z "$sched_day" ]] && die "Day of month is required for monthly schedule"
|
|
;;
|
|
custom)
|
|
read -rp "Full cron expression (5 fields): " sched_cron
|
|
[[ -z "$sched_cron" ]] && die "Cron expression is required for custom schedule"
|
|
;;
|
|
hourly)
|
|
read -rp "Hours between backups (1-23) [1]: " sched_day
|
|
sched_day="${sched_day:-1}"
|
|
;;
|
|
daily) ;;
|
|
*) die "Invalid schedule type: $sched_type" ;;
|
|
esac
|
|
|
|
# Select remotes
|
|
local sched_remotes=""
|
|
if has_remotes; then
|
|
echo ""
|
|
echo "Available remotes:"
|
|
local remotes; remotes=$(list_remotes)
|
|
local i=1
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
echo " $i) $rname"
|
|
((i++)) || true
|
|
done <<< "$remotes"
|
|
echo ""
|
|
read -rp "Target remotes (comma-separated names, empty=all): " sched_remotes
|
|
fi
|
|
|
|
# Write config
|
|
mkdir -p "$SCHEDULES_DIR"
|
|
cat > "$config_file" <<CONF
|
|
# gniza schedule config: $name
|
|
# Generated by 'gniza schedule add $name'
|
|
# $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
|
|
|
SCHEDULE="$sched_type"
|
|
SCHEDULE_TIME="$sched_time"
|
|
SCHEDULE_DAY="$sched_day"
|
|
SCHEDULE_CRON="$sched_cron"
|
|
|
|
REMOTES="$sched_remotes"
|
|
CONF
|
|
|
|
echo ""
|
|
echo "${C_GREEN}Schedule '$name' created: $config_file${C_RESET}"
|
|
echo "Run 'gniza schedule install' to activate cron entries."
|
|
}
|
|
|
|
_schedule_delete() {
|
|
local name="$1"
|
|
local config_file="$SCHEDULES_DIR/${name}.conf"
|
|
|
|
if [[ ! -f "$config_file" ]]; then
|
|
die "Schedule not found: $name (expected $config_file)"
|
|
fi
|
|
|
|
load_schedule "$name"
|
|
echo "Schedule: $name"
|
|
echo "Type: ${SCHEDULE:-none}"
|
|
echo "Time: ${SCHEDULE_TIME:-02:00}"
|
|
echo "Remotes: ${SCHEDULE_REMOTES:-(all)}"
|
|
echo ""
|
|
read -rp "Delete schedule '$name'? [y/N] " answer
|
|
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
|
|
|
rm -f "$config_file"
|
|
echo "Schedule '$name' deleted."
|
|
echo "Run 'gniza schedule install' to update cron entries."
|
|
}
|
|
|
|
_schedule_run() {
|
|
local name="$1"
|
|
local config_file="$SCHEDULES_DIR/${name}.conf"
|
|
|
|
if [[ ! -f "$config_file" ]]; then
|
|
die "Schedule not found: $name (expected $config_file)"
|
|
fi
|
|
|
|
load_schedule "$name"
|
|
|
|
local args=()
|
|
if [[ -n "${SCHEDULE_REMOTES:-}" ]]; then
|
|
args+=(--remote="$SCHEDULE_REMOTES")
|
|
fi
|
|
if [[ "${SCHEDULE_SYSBACKUP:-}" == "yes" ]]; then
|
|
args+=(--sysbackup)
|
|
fi
|
|
if [[ "${SCHEDULE_SKIP_SUSPENDED:-}" == "yes" ]]; then
|
|
args+=(--skip-suspended)
|
|
fi
|
|
|
|
echo "Running schedule '$name'..."
|
|
echo " Remotes: ${SCHEDULE_REMOTES:-(all)}"
|
|
echo " Sysbackup: ${SCHEDULE_SYSBACKUP:-no}"
|
|
echo " Skip suspended: ${SCHEDULE_SKIP_SUSPENDED:-no}"
|
|
echo ""
|
|
|
|
# Exec replaces this process with the backup command
|
|
exec /usr/local/bin/gniza backup "${args[@]}"
|
|
}
|
|
|
|
_schedule_list() {
|
|
if ! has_schedules; then
|
|
echo "No schedules configured."
|
|
echo "Run 'gniza schedule add <name>' to create one."
|
|
return 0
|
|
fi
|
|
|
|
local schedules; schedules=$(list_schedules)
|
|
local count; count=$(echo "$schedules" | wc -l)
|
|
|
|
echo "${C_BOLD}Configured schedules ($count):${C_RESET}"
|
|
echo ""
|
|
printf " %-15s %-10s %-8s %-6s %s\n" "NAME" "TYPE" "TIME" "DAY" "REMOTES"
|
|
printf " %-15s %-10s %-8s %-6s %s\n" "----" "----" "----" "---" "-------"
|
|
|
|
while IFS= read -r sname; do
|
|
[[ -z "$sname" ]] && continue
|
|
load_schedule "$sname" 2>/dev/null || { printf " %-15s %s\n" "$sname" "${C_RED}(failed to load)${C_RESET}"; continue; }
|
|
|
|
printf " %-15s %-10s %-8s %-6s %s\n" \
|
|
"$sname" "${SCHEDULE:-none}" "${SCHEDULE_TIME:-02:00}" "${SCHEDULE_DAY:--}" "${SCHEDULE_REMOTES:-(all)}"
|
|
done <<< "$schedules"
|
|
echo ""
|
|
}
|
|
|
|
cmd_init() {
|
|
local subcommand="${1:-}"
|
|
|
|
if [[ "$subcommand" == "remote" ]]; then
|
|
shift
|
|
_init_remote "$@"
|
|
return
|
|
fi
|
|
|
|
local config_dir="/etc/gniza"
|
|
local config_file="$config_dir/gniza.conf"
|
|
|
|
echo "${C_BOLD}gniza init${C_RESET} — Setup wizard"
|
|
echo ""
|
|
|
|
# Step 1: Create main config with local settings
|
|
if [[ -f "$config_file" ]]; then
|
|
echo "Config file already exists: $config_file"
|
|
read -rp "Overwrite? [y/N] " answer
|
|
[[ "$answer" =~ ^[Yy]$ ]] || {
|
|
echo "Skipping main config."
|
|
echo ""
|
|
# Still offer to add a remote
|
|
echo "Add a remote destination?"
|
|
read -rp "Remote name (e.g. nas, offsite): " init_remote_name
|
|
if [[ -n "$init_remote_name" ]]; then
|
|
_init_remote "$init_remote_name"
|
|
fi
|
|
return
|
|
}
|
|
fi
|
|
|
|
read -rp "Notification email (empty to disable): " init_email
|
|
|
|
# Create config
|
|
mkdir -p "$config_dir"
|
|
mkdir -p "$config_dir/remotes.d"
|
|
cat > "$config_file" <<CONF
|
|
# gniza configuration — generated by 'gniza init'
|
|
# $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
|
#
|
|
# Remote destinations are configured in /etc/gniza/remotes.d/<name>.conf
|
|
# Run 'gniza init remote <name>' to add one.
|
|
|
|
TEMP_DIR="/usr/local/gniza/workdir"
|
|
INCLUDE_ACCOUNTS=""
|
|
EXCLUDE_ACCOUNTS="nobody"
|
|
|
|
LOG_DIR="/var/log/gniza"
|
|
LOG_LEVEL="info"
|
|
LOG_RETAIN=90
|
|
|
|
NOTIFY_EMAIL="$init_email"
|
|
NOTIFY_ON="failure"
|
|
|
|
LOCK_FILE="/var/run/gniza.lock"
|
|
SSH_TIMEOUT=30
|
|
SSH_RETRIES=3
|
|
RSYNC_EXTRA_OPTS=""
|
|
CONF
|
|
|
|
echo ""
|
|
echo "Config written to $config_file"
|
|
|
|
# Create log directory
|
|
mkdir -p "${LOG_DIR:-$DEFAULT_LOG_DIR}"
|
|
|
|
# Step 2: Create first remote
|
|
echo ""
|
|
echo "Now let's set up your first remote destination."
|
|
read -rp "Remote name (e.g. nas, offsite): " init_remote_name
|
|
[[ -z "$init_remote_name" ]] && die "Remote name is required"
|
|
|
|
_init_remote "$init_remote_name"
|
|
}
|
|
|
|
_init_remote() {
|
|
local name="${1:-}"
|
|
[[ -z "$name" ]] && die "Usage: gniza init remote <name>"
|
|
|
|
# Validate name (alphanumeric, hyphens, underscores)
|
|
if ! [[ "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
die "Remote name must be alphanumeric (hyphens and underscores allowed): $name"
|
|
fi
|
|
|
|
local config_dir="/etc/gniza/remotes.d"
|
|
local config_file="$config_dir/${name}.conf"
|
|
|
|
echo "${C_BOLD}gniza init remote${C_RESET} — Remote setup: ${C_BOLD}$name${C_RESET}"
|
|
echo ""
|
|
|
|
if [[ -f "$config_file" ]]; then
|
|
echo "Remote config already exists: $config_file"
|
|
read -rp "Overwrite? [y/N] " answer
|
|
[[ "$answer" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
|
|
fi
|
|
|
|
read -rp "Remote host: " init_host
|
|
[[ -z "$init_host" ]] && die "Remote host is required"
|
|
|
|
read -rp "Remote port [22]: " init_port
|
|
init_port="${init_port:-22}"
|
|
|
|
read -rp "Remote user [root]: " init_user
|
|
init_user="${init_user:-root}"
|
|
|
|
read -rp "SSH key path [/root/.ssh/id_rsa]: " init_key
|
|
init_key="${init_key:-/root/.ssh/id_rsa}"
|
|
|
|
read -rp "Remote base directory [/backups]: " init_base
|
|
init_base="${init_base:-/backups}"
|
|
|
|
read -rp "Retention count [30]: " init_retention
|
|
init_retention="${init_retention:-30}"
|
|
|
|
read -rp "Bandwidth limit in KB/s [0 = unlimited]: " init_bwlimit
|
|
init_bwlimit="${init_bwlimit:-0}"
|
|
|
|
# Create config
|
|
mkdir -p "$config_dir"
|
|
cat > "$config_file" <<CONF
|
|
# gniza remote config: $name
|
|
# Generated by 'gniza init remote $name'
|
|
# $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
|
|
|
REMOTE_HOST="$init_host"
|
|
REMOTE_PORT=$init_port
|
|
REMOTE_USER="$init_user"
|
|
REMOTE_KEY="$init_key"
|
|
REMOTE_BASE="$init_base"
|
|
|
|
BWLIMIT=$init_bwlimit
|
|
RETENTION_COUNT=$init_retention
|
|
RSYNC_EXTRA_OPTS=""
|
|
CONF
|
|
|
|
echo ""
|
|
echo "Remote config written to $config_file"
|
|
echo ""
|
|
|
|
# Test SSH
|
|
# Load main config first for defaults, then load remote
|
|
local main_config="/etc/gniza/gniza.conf"
|
|
if [[ -f "$main_config" ]]; then
|
|
load_config "$main_config"
|
|
fi
|
|
load_remote "$name"
|
|
|
|
echo "Testing SSH connection to $name..."
|
|
if test_ssh_connection 2>/dev/null; then
|
|
echo "${C_GREEN}SSH connection successful!${C_RESET}"
|
|
echo "Creating remote base directory..."
|
|
ensure_remote_dir "${REMOTE_BASE}/$(hostname -f)/accounts"
|
|
echo "${C_GREEN}Remote directory created.${C_RESET}"
|
|
else
|
|
echo "${C_YELLOW}SSH connection failed. Check your settings in $config_file${C_RESET}"
|
|
fi
|
|
|
|
echo ""
|
|
echo "${C_GREEN}Remote '$name' configured!${C_RESET}"
|
|
echo "Run 'gniza schedule add <name>' to set up a backup schedule."
|
|
echo "Run 'gniza backup --remote=$name --dry-run' to test."
|
|
}
|
|
|
|
# ── System Backup / Restore ───────────────────────────────────
|
|
|
|
# Transfer + finalize + retention for system backup on the current remote.
|
|
# Globals REMOTE_* must already be set via load_remote().
|
|
_sysbackup_to_current_remote() {
|
|
local stage_dir="$1"
|
|
local ts="$2"
|
|
local remote_label="$CURRENT_REMOTE_NAME"
|
|
|
|
clean_partial_system_snapshots
|
|
|
|
local prev; prev=$(get_latest_system_snapshot) || prev=""
|
|
|
|
if ! transfer_system_backup "$stage_dir" "$ts" "$prev"; then
|
|
log_error "[$remote_label] System backup transfer failed"
|
|
return 1
|
|
fi
|
|
|
|
if ! finalize_system_snapshot "$ts"; then
|
|
log_error "[$remote_label] System snapshot finalize failed"
|
|
return 1
|
|
fi
|
|
|
|
enforce_system_retention
|
|
return 0
|
|
}
|
|
|
|
cmd_sysbackup() {
|
|
require_root
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local dry_run=false
|
|
has_flag dry-run "$@" && dry_run=true
|
|
|
|
local remote_flag=""
|
|
remote_flag=$(get_opt remote "$@" 2>/dev/null) || true
|
|
|
|
acquire_lock
|
|
trap 'cleanup_system_stage; release_lock' EXIT
|
|
|
|
local remotes=""
|
|
remotes=$(get_target_remotes "$remote_flag") || die "Invalid remote specification"
|
|
|
|
_save_remote_globals
|
|
|
|
# Test connectivity upfront
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname" || die "Failed to load remote: $rname"
|
|
if _is_rclone_mode; then
|
|
test_rclone_connection || die "Cannot connect to remote '$rname' (${REMOTE_TYPE})"
|
|
else
|
|
test_ssh_connection || die "Cannot connect to remote '$rname' (${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT})"
|
|
fi
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
|
|
local start_time; start_time=$(date +%s)
|
|
local ts; ts=$(timestamp)
|
|
local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}"
|
|
local stage_dir="$temp_dir/system/$ts"
|
|
|
|
if [[ "$dry_run" == "true" ]]; then
|
|
log_info "[DRY RUN] System backup preview"
|
|
log_info "[DRY RUN] Would export: packages, tweaksettings, DNS zones, IPs, PHP config"
|
|
log_info "[DRY RUN] Would stage: ${#_SYSBACKUP_PATHS[@]} system paths + ea-php configs"
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname"
|
|
log_info "[DRY RUN] [$rname] Would transfer system backup to ${REMOTE_HOST:-$REMOTE_TYPE}"
|
|
log_info "[DRY RUN] [$rname] Would finalize system snapshot: $ts"
|
|
log_info "[DRY RUN] [$rname] Would enforce retention: keep $RETENTION_COUNT"
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
echo ""
|
|
echo "System backup dry run complete. No changes made."
|
|
exit "$EXIT_OK"
|
|
fi
|
|
|
|
# Stage ONCE
|
|
log_info "=== System Backup ==="
|
|
if ! run_system_backup "$stage_dir"; then
|
|
log_error "System backup staging failed"
|
|
cleanup_system_stage
|
|
exit "$EXIT_FATAL"
|
|
fi
|
|
|
|
# Transfer to each remote
|
|
local failed=0
|
|
local succeeded=0
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname"
|
|
log_info "--- Transferring system backup to remote '$rname' ---"
|
|
if _sysbackup_to_current_remote "$stage_dir" "$ts"; then
|
|
((succeeded++)) || true
|
|
log_info "System backup to '$rname' completed"
|
|
else
|
|
((failed++)) || true
|
|
log_error "System backup to '$rname' failed"
|
|
fi
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
|
|
cleanup_system_stage
|
|
|
|
local end_time; end_time=$(date +%s)
|
|
local duration=$(( end_time - start_time ))
|
|
|
|
echo ""
|
|
echo "============================================"
|
|
echo "System Backup Summary"
|
|
echo "============================================"
|
|
echo "Timestamp: $ts"
|
|
echo "Duration: $(human_duration $duration)"
|
|
echo "Remotes: $(echo "$remotes" | tr '\n' ' ')"
|
|
echo "Succeeded: ${C_GREEN}${succeeded}${C_RESET}"
|
|
if (( failed > 0 )); then
|
|
echo "Failed: ${C_RED}${failed}${C_RESET}"
|
|
else
|
|
echo "Failed: 0"
|
|
fi
|
|
echo "============================================"
|
|
|
|
if (( failed > 0 && succeeded > 0 )); then
|
|
exit "$EXIT_PARTIAL"
|
|
elif (( failed > 0 )); then
|
|
exit "$EXIT_FATAL"
|
|
fi
|
|
exit "$EXIT_OK"
|
|
}
|
|
|
|
cmd_sysrestore() {
|
|
require_root
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local remote_flag=""
|
|
remote_flag=$(get_opt remote "$@" 2>/dev/null) || true
|
|
[[ -z "$remote_flag" ]] && die "Specify --remote=NAME for sysrestore."$'\n'"Available remotes: $(list_remotes | tr '\n' ' ')"
|
|
|
|
_save_remote_globals
|
|
load_remote "$remote_flag" || die "Failed to load remote: $remote_flag"
|
|
|
|
local timestamp=""
|
|
timestamp=$(get_opt timestamp "$@" 2>/dev/null) || true
|
|
|
|
local dry_run=false
|
|
has_flag dry-run "$@" && dry_run=true
|
|
|
|
local phases="1,2,3,4"
|
|
phases=$(get_opt phase "$@" 2>/dev/null) || phases="1,2,3,4"
|
|
|
|
# Test connectivity
|
|
if _is_rclone_mode; then
|
|
test_rclone_connection || die "Cannot connect to remote '$remote_flag' (${REMOTE_TYPE})"
|
|
else
|
|
test_ssh_connection || die "Cannot connect to remote '$remote_flag' (${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT})"
|
|
fi
|
|
|
|
# Resolve timestamp
|
|
local ts; ts=$(resolve_system_snapshot_timestamp "$timestamp") || die "No system snapshot found on remote '$remote_flag'"
|
|
[[ -z "$ts" ]] && die "No system snapshot found on remote '$remote_flag'"
|
|
|
|
log_info "System restore from remote '$remote_flag', snapshot: $ts"
|
|
log_info "Phases: $phases, Dry-run: $dry_run"
|
|
|
|
local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}"
|
|
local stage_dir="$temp_dir/system-restore/$ts"
|
|
|
|
# Download snapshot (needed for both dry-run and real restore)
|
|
if ! download_system_snapshot "$ts" "$stage_dir"; then
|
|
die "Failed to download system snapshot: $ts"
|
|
fi
|
|
|
|
# Run restore
|
|
local rc=0
|
|
run_system_restore "$stage_dir" "$phases" "$dry_run" || rc=$?
|
|
|
|
# Cleanup staging
|
|
if [[ -d "$stage_dir" ]]; then
|
|
rm -rf "$stage_dir"
|
|
log_debug "Cleaned up restore staging: $stage_dir"
|
|
fi
|
|
|
|
_restore_remote_globals
|
|
|
|
if (( rc != 0 )); then
|
|
echo ""
|
|
echo "${C_YELLOW}System restore completed with errors. Check log for details.${C_RESET}"
|
|
exit "$EXIT_PARTIAL"
|
|
fi
|
|
|
|
echo ""
|
|
echo "${C_GREEN}System restore completed successfully.${C_RESET}"
|
|
exit "$EXIT_OK"
|
|
}
|
|
|
|
cmd_stats() {
|
|
local config_file; config_file=$(get_opt config "$@" 2>/dev/null) || config_file="$DEFAULT_CONFIG_FILE"
|
|
load_config "$config_file"
|
|
validate_config || die "Invalid configuration"
|
|
init_logging
|
|
|
|
local log_dir="${LOG_DIR:-$DEFAULT_LOG_DIR}"
|
|
local stats_file="$log_dir/stats.json"
|
|
local remotes; remotes=$(list_remotes) || true
|
|
|
|
if [[ -z "$remotes" ]]; then
|
|
log_error "No remotes configured"
|
|
return 1
|
|
fi
|
|
|
|
local total_accounts=0
|
|
local total_snapshots=0
|
|
local remote_json_parts=()
|
|
local -A seen_accounts
|
|
|
|
_save_remote_globals
|
|
while IFS= read -r rname; do
|
|
[[ -z "$rname" ]] && continue
|
|
load_remote "$rname" || { log_warn "Failed to load remote: $rname"; continue; }
|
|
|
|
# Test connectivity
|
|
local conn_ok=false
|
|
if _is_rclone_mode; then
|
|
test_rclone_connection 2>/dev/null && conn_ok=true
|
|
else
|
|
test_ssh_connection 2>/dev/null && conn_ok=true
|
|
fi
|
|
|
|
local r_accounts=0
|
|
local r_snapshots=0
|
|
|
|
if $conn_ok; then
|
|
local accounts_list; accounts_list=$(list_remote_accounts 2>/dev/null) || true
|
|
if [[ -n "$accounts_list" ]]; then
|
|
while IFS= read -r acc; do
|
|
[[ -z "$acc" ]] && continue
|
|
((r_accounts++)) || true
|
|
seen_accounts["$acc"]=1
|
|
local snaps; snaps=$(list_remote_snapshots "$acc" 2>/dev/null) || true
|
|
if [[ -n "$snaps" ]]; then
|
|
local snap_count; snap_count=$(echo "$snaps" | wc -l)
|
|
((r_snapshots += snap_count)) || true
|
|
fi
|
|
done <<< "$accounts_list"
|
|
fi
|
|
else
|
|
log_warn "Cannot connect to remote: $rname (skipping)"
|
|
fi
|
|
|
|
((total_snapshots += r_snapshots)) || true
|
|
remote_json_parts+=("\"$rname\":{\"accounts\":$r_accounts,\"snapshots\":$r_snapshots}")
|
|
done <<< "$remotes"
|
|
_restore_remote_globals
|
|
|
|
total_accounts=${#seen_accounts[@]}
|
|
|
|
# Parse last backup status from most recent log file
|
|
local last_status="UNKNOWN"
|
|
local last_log=""
|
|
local latest_log=""
|
|
if [[ -d "$log_dir" ]]; then
|
|
latest_log=$(ls -1t "$log_dir"/gniza-[0-9]*-[0-9]*.log 2>/dev/null | head -1) || true
|
|
fi
|
|
if [[ -n "$latest_log" && -f "$latest_log" ]]; then
|
|
last_log=$(basename "$latest_log")
|
|
if tail -20 "$latest_log" | grep -qi "Backup completed successfully"; then
|
|
last_status="SUCCESS"
|
|
elif tail -20 "$latest_log" | grep -qi "Backup failed\|partial failure\|PARTIAL"; then
|
|
last_status="FAILURE"
|
|
fi
|
|
fi
|
|
|
|
# Build JSON
|
|
local updated; updated=$(date -u +"%d/%m/%Y %H:%M:%S")
|
|
local remotes_json; remotes_json=$(IFS=','; echo "${remote_json_parts[*]}")
|
|
local json="{\"updated\":\"$updated\",\"backed_up_accounts\":$total_accounts,\"snapshots\":$total_snapshots,\"remotes\":{$remotes_json},\"last_backup\":{\"status\":\"$last_status\",\"log\":\"$last_log\"}}"
|
|
|
|
echo "$json" > "$stats_file"
|
|
log_info "Stats written to $stats_file"
|
|
echo "$json"
|
|
}
|
|
|
|
cmd_usage() {
|
|
cat <<EOF
|
|
${C_BOLD}gniza v${GNIZA_VERSION}${C_RESET} — cPanel Backup, Restore & Disaster Recovery
|
|
|
|
${C_BOLD}Usage:${C_RESET}
|
|
gniza <command> [options]
|
|
|
|
${C_BOLD}Commands:${C_RESET}
|
|
backup [--account=NAME] [--remote=NAME[,NAME2]] [--dry-run] [--sysbackup] [--skip-suspended]
|
|
restore account <name> [--remote=NAME] [--timestamp=TS] [--force]
|
|
restore files <name> [--remote=NAME] [--path=subpath] [--timestamp=TS]
|
|
restore database <name> [<dbname>] [--remote=NAME] [--timestamp=TS]
|
|
restore mailbox <name> [<email@domain>] [--remote=NAME] [--timestamp=TS]
|
|
restore cron <name> [--remote=NAME] [--timestamp=TS]
|
|
restore dbusers <name> [--remote=NAME] [--timestamp=TS]
|
|
restore cpconfig <name> [--remote=NAME] [--timestamp=TS]
|
|
restore domains <name> [--remote=NAME] [--timestamp=TS]
|
|
restore ssl <name> [--remote=NAME] [--timestamp=TS]
|
|
restore list-databases <name> [--remote=NAME] [--timestamp=TS]
|
|
restore list-mailboxes <name> [--remote=NAME] [--timestamp=TS]
|
|
restore list-files <name> [--remote=NAME] [--timestamp=TS] [--path=subdir]
|
|
restore server [--remote=NAME] [--timestamp=TS]
|
|
sysbackup [--remote=NAME[,NAME2]] [--dry-run] Backup system/WHM config
|
|
sysrestore --remote=NAME [--timestamp=TS] [--phase=N] [--dry-run]
|
|
list [--account=NAME] [--remote=NAME] List remote snapshots
|
|
verify [--account=NAME] [--remote=NAME] Verify backup integrity
|
|
status Show configuration and status
|
|
remote list List configured remotes
|
|
remote delete <name> Remove a remote destination
|
|
schedule add <name> Create a backup schedule
|
|
schedule delete <name> Remove a schedule
|
|
schedule run <name> Run a schedule now
|
|
schedule list Show configured schedules
|
|
schedule {install|show|remove} Manage cron entries
|
|
stats Collect backup statistics
|
|
init Setup config + first remote
|
|
init remote <name> Add a remote destination
|
|
version Show version
|
|
|
|
${C_BOLD}Global Options:${C_RESET}
|
|
--config=PATH Use alternate config file (default: /etc/gniza/gniza.conf)
|
|
--remote=NAME Target specific remote(s), comma-separated
|
|
--debug Enable debug logging
|
|
|
|
${C_BOLD}Examples:${C_RESET}
|
|
gniza init
|
|
gniza backup --dry-run
|
|
gniza backup --account=johndoe
|
|
gniza backup --remote=nas
|
|
gniza backup --remote=nas,offsite
|
|
gniza list --remote=offsite
|
|
gniza restore files johndoe --remote=nas --path=public_html
|
|
gniza restore database johndoe johndoe_wp --remote=nas
|
|
gniza restore mailbox johndoe info@example.com --remote=nas
|
|
gniza schedule add nightly
|
|
gniza schedule list
|
|
gniza schedule install
|
|
gniza remote list
|
|
gniza init remote nas
|
|
gniza sysbackup --dry-run
|
|
gniza sysbackup --remote=nas
|
|
gniza sysrestore --remote=nas
|
|
gniza sysrestore --remote=nas --phase=1 --dry-run
|
|
EOF
|
|
}
|
|
|
|
# ── Main routing ───────────────────────────────────────────────
|
|
|
|
main() {
|
|
# Global --debug flag (used by config.sh load_config)
|
|
# shellcheck disable=SC2034
|
|
has_flag debug "$@" && GNIZA_DEBUG=true || GNIZA_DEBUG=false
|
|
|
|
local command="${1:-}"
|
|
shift 2>/dev/null || true
|
|
|
|
case "$command" in
|
|
backup) cmd_backup "$@" ;;
|
|
sysbackup) cmd_sysbackup "$@" ;;
|
|
sysrestore) cmd_sysrestore "$@" ;;
|
|
restore) cmd_restore "$@" ;;
|
|
list) cmd_list "$@" ;;
|
|
verify) cmd_verify "$@" ;;
|
|
status) cmd_status "$@" ;;
|
|
remote) cmd_remote "$@" ;;
|
|
schedule) cmd_schedule "$@" ;;
|
|
stats) cmd_stats "$@" ;;
|
|
init) cmd_init "$@" ;;
|
|
version) echo "gniza v${GNIZA_VERSION}" ;;
|
|
help|-h|--help) cmd_usage ;;
|
|
"") cmd_usage ;;
|
|
*) die "Unknown command: $command"$'\n'"Run 'gniza help' for usage" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|