Files
gniza4cp/lib/transfer.sh
shuki 1459bd1b8b Initial commit: gniza backup & disaster recovery CLI + WHM plugin
Full-featured cPanel backup tool with SSH, S3, and Google Drive support.
Includes WHM plugin with Tailwind/DaisyUI UI, multi-remote management,
decoupled schedules, and account restore workflows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 02:39:39 +02:00

173 lines
5.2 KiB
Bash

#!/usr/bin/env bash
# gniza/lib/transfer.sh — rsync --link-dest to remote, .partial atomicity, retries
rsync_to_remote() {
local source_dir="$1"
local remote_dest="$2"
local link_dest="${3:-}"
local attempt=0
local max_retries="${SSH_RETRIES:-$DEFAULT_SSH_RETRIES}"
local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd)
local rsync_opts=(-aHAX --numeric-ids --delete --rsync-path="rsync --fake-super")
if [[ -n "$link_dest" ]]; then
rsync_opts+=(--link-dest="$link_dest")
fi
if [[ "${BWLIMIT:-0}" -gt 0 ]]; then
rsync_opts+=(--bwlimit="$BWLIMIT")
fi
if [[ -n "${RSYNC_EXTRA_OPTS:-}" ]]; then
# shellcheck disable=SC2206
rsync_opts+=($RSYNC_EXTRA_OPTS)
fi
rsync_opts+=(-e "$rsync_ssh")
# Ensure source ends with /
[[ "$source_dir" != */ ]] && source_dir="$source_dir/"
while (( attempt < max_retries )); do
((attempt++)) || true
log_debug "rsync attempt $attempt/$max_retries: $source_dir -> $remote_dest"
log_debug "CMD: rsync ${rsync_opts[*]} $source_dir ${REMOTE_USER}@${REMOTE_HOST}:${remote_dest}"
local rsync_cmd=(rsync "${rsync_opts[@]}" "$source_dir" "${REMOTE_USER}@${REMOTE_HOST}:${remote_dest}")
if _is_password_mode; then
rsync_cmd=(sshpass -p "$REMOTE_PASSWORD" "${rsync_cmd[@]}")
fi
if "${rsync_cmd[@]}"; then
log_debug "rsync succeeded on attempt $attempt"
return 0
fi
local rc=$?
log_warn "rsync failed (exit $rc), attempt $attempt/$max_retries"
if (( attempt < max_retries )); then
local backoff=$(( attempt * 10 ))
log_info "Retrying in ${backoff}s..."
sleep "$backoff"
fi
done
log_error "rsync failed after $max_retries attempts"
return 1
}
transfer_pkgacct() {
local user="$1"
local timestamp="$2"
local prev_snapshot="${3:-}"
local temp_dir="${TEMP_DIR:-$DEFAULT_TEMP_DIR}"
local source="$temp_dir/$user"
if _is_rclone_mode; then
local snap_subpath="accounts/${user}/snapshots/${timestamp}"
log_info "Transferring pkgacct data for $user (rclone)..."
rclone_to_remote "$source" "$snap_subpath"
return
fi
local snap_dir; snap_dir=$(get_snapshot_dir "$user")
local dest="$snap_dir/${timestamp}.partial/"
local link_dest=""
if [[ -n "$prev_snapshot" ]]; then
# Detect old format (pkgacct/ subdir) vs new format (content at root)
if remote_exec "test -d '$snap_dir/$prev_snapshot/pkgacct'" 2>/dev/null; then
link_dest="$snap_dir/$prev_snapshot/pkgacct"
else
link_dest="$snap_dir/$prev_snapshot"
fi
fi
ensure_remote_dir "$dest" || return 1
log_info "Transferring pkgacct data for $user..."
rsync_to_remote "$source" "$dest" "$link_dest"
}
transfer_homedir() {
local user="$1"
local timestamp="$2"
local prev_snapshot="${3:-}"
local homedir; homedir=$(get_account_homedir "$user")
if [[ ! -d "$homedir" ]]; then
log_warn "Home directory not found for $user: $homedir"
return 1
fi
if _is_rclone_mode; then
local snap_subpath="accounts/${user}/snapshots/${timestamp}/homedir"
log_info "Transferring homedir for $user ($homedir) (rclone)..."
rclone_to_remote "$homedir" "$snap_subpath"
return
fi
local snap_dir; snap_dir=$(get_snapshot_dir "$user")
local dest="$snap_dir/${timestamp}.partial/homedir/"
local link_dest=""
if [[ -n "$prev_snapshot" ]]; then
link_dest="$snap_dir/$prev_snapshot/homedir"
fi
ensure_remote_dir "$dest" || return 1
log_info "Transferring homedir for $user ($homedir)..."
rsync_to_remote "$homedir" "$dest" "$link_dest"
}
finalize_snapshot() {
local user="$1"
local timestamp="$2"
if _is_rclone_mode; then
log_info "Finalizing snapshot for $user: $timestamp (rclone)"
rclone_finalize_snapshot "$user" "$timestamp"
return
fi
local snap_dir; snap_dir=$(get_snapshot_dir "$user")
log_info "Finalizing snapshot for $user: $timestamp"
remote_exec "mv '$snap_dir/${timestamp}.partial' '$snap_dir/$timestamp'" || {
log_error "Failed to finalize snapshot for $user: $timestamp"
return 1
}
update_latest_symlink "$user" "$timestamp"
}
rsync_dry_run() {
if _is_rclone_mode; then
log_info "[DRY RUN] rclone mode — dry run not supported for cloud remotes"
return 0
fi
local source_dir="$1"
local remote_dest="$2"
local link_dest="${3:-}"
local rsync_ssh; rsync_ssh=$(build_rsync_ssh_cmd)
local rsync_opts=(-aHAX --numeric-ids --delete --rsync-path="rsync --fake-super" --dry-run --stats)
if [[ -n "$link_dest" ]]; then
rsync_opts+=(--link-dest="$link_dest")
fi
rsync_opts+=(-e "$rsync_ssh")
[[ "$source_dir" != */ ]] && source_dir="$source_dir/"
if _is_password_mode; then
sshpass -p "$REMOTE_PASSWORD" rsync "${rsync_opts[@]}" "$source_dir" "${REMOTE_USER}@${REMOTE_HOST}:${remote_dest}" 2>&1
else
rsync "${rsync_opts[@]}" "$source_dir" "${REMOTE_USER}@${REMOTE_HOST}:${remote_dest}" 2>&1
fi
}