#!/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 export SSHPASS="$REMOTE_PASSWORD" rsync_cmd=(sshpass -e "${rsync_cmd[@]}") fi local rc=0 "${rsync_cmd[@]}" || rc=$? if (( rc == 0 )); then log_debug "rsync succeeded on attempt $attempt" return 0 fi 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" }