#!/usr/bin/env bash # gniza4linux/lib/backup.sh — Backup orchestration per target [[ -n "${_GNIZA4LINUX_BACKUP_LOADED:-}" ]] && return 0 _GNIZA4LINUX_BACKUP_LOADED=1 # Backup a single target to a remote. # Usage: backup_target [remote_name] backup_target() { local target_name="$1" local remote_name="${2:-}" # 1. Load and validate target load_target "$target_name" || { log_error "Failed to load target: $target_name" return 1 } if [[ "${TARGET_ENABLED:-yes}" != "yes" ]]; then log_info "Target '$target_name' is disabled, skipping" return 0 fi # 2. Determine which remote to use if [[ -z "$remote_name" ]]; then if [[ -n "${TARGET_REMOTE:-}" ]]; then remote_name="$TARGET_REMOTE" else remote_name=$(list_remotes | head -1) fi fi if [[ -z "$remote_name" ]]; then log_error "No remote specified and none configured" return 1 fi # 3. Save/load remote context _save_remote_globals load_remote "$remote_name" || { log_error "Failed to load remote: $remote_name" _restore_remote_globals return 1 } local rc=0 _backup_target_impl "$target_name" "$remote_name" || rc=$? # 15. Restore remote globals _restore_remote_globals return "$rc" } # Internal implementation after remote context is loaded. _backup_target_impl() { local target_name="$1" local remote_name="$2" # 4. Test remote connectivity case "${REMOTE_TYPE:-ssh}" in ssh) test_ssh_connection || { log_error "Cannot connect to remote '$remote_name'" return 1 } ;; local) if [[ ! -d "$REMOTE_BASE" ]]; then log_error "Remote base directory does not exist: $REMOTE_BASE" return 1 fi ;; s3|gdrive) test_rclone_connection || { log_error "Cannot connect to remote '$remote_name' (${REMOTE_TYPE})" return 1 } ;; esac local start_time; start_time=$(date +%s) # 5. Get timestamp local ts; ts=$(timestamp) # 6. Get previous snapshot for --link-dest local prev; prev=$(get_latest_snapshot "$target_name") || prev="" if [[ -n "$prev" ]]; then log_debug "Previous snapshot for $target_name: $prev" fi # 7. Clean partial snapshots clean_partial_snapshots "$target_name" # 8. Run pre-hook if [[ -n "${TARGET_PRE_HOOK:-}" ]]; then log_info "Running pre-hook for $target_name..." if ! bash -c "$TARGET_PRE_HOOK"; then log_error "Pre-hook failed for $target_name" return 1 fi fi # 8.5. Dump MySQL databases (if enabled) local mysql_dump_dir="" if [[ "${TARGET_MYSQL_ENABLED:-no}" == "yes" ]]; then log_info "Dumping MySQL databases for $target_name..." if mysql_dump_databases; then mysql_dump_dir="${MYSQL_DUMP_DIR:-}" else log_error "MySQL dump failed for $target_name" mysql_cleanup_dump return 1 fi fi # 9. Transfer each folder local folder local transfer_failed=false while IFS= read -r folder; do [[ -z "$folder" ]] && continue if ! transfer_folder "$target_name" "$folder" "$ts" "$prev"; then log_error "Transfer failed for folder: $folder" transfer_failed=true fi done < <(get_target_folders) # 9.5. Transfer MySQL dumps if [[ -n "$mysql_dump_dir" && -d "$mysql_dump_dir/_mysql" ]]; then log_info "Transferring MySQL dumps for $target_name..." if ! transfer_folder "$target_name" "$mysql_dump_dir/_mysql" "$ts" "$prev"; then log_error "Transfer failed for MySQL dumps" transfer_failed=true fi fi # Cleanup MySQL temp dir mysql_cleanup_dump if [[ "$transfer_failed" == "true" ]]; then log_error "One or more folder transfers failed for $target_name" return 1 fi # 10. Generate meta.json local end_time; end_time=$(date +%s) local duration=$(( end_time - start_time )) local hostname; hostname=$(hostname -f) local snap_dir; snap_dir=$(get_snapshot_dir "$target_name") local total_size=0 local meta_json meta_json=$(cat < "$snap_dir/${ts}.partial/meta.json" || log_warn "Failed to write meta.json" else echo "$meta_json" | remote_exec "cat > '$snap_dir/${ts}.partial/meta.json'" || log_warn "Failed to write meta.json" fi # 11. Generate manifest.txt if _is_rclone_mode; then local manifest; manifest=$(rclone_list_files "targets/${target_name}/snapshots/${ts}" 2>/dev/null) || manifest="" if [[ -n "$manifest" ]]; then rclone_rcat "targets/${target_name}/snapshots/${ts}/manifest.txt" "$manifest" || log_warn "Failed to write manifest.txt" fi elif [[ "${REMOTE_TYPE:-ssh}" == "local" ]]; then find "$snap_dir/${ts}.partial" -type f 2>/dev/null > "$snap_dir/${ts}.partial/manifest.txt" || log_warn "Failed to write manifest.txt" else remote_exec "find '$snap_dir/${ts}.partial' -type f > '$snap_dir/${ts}.partial/manifest.txt'" 2>/dev/null || log_warn "Failed to write manifest.txt" fi # 12. Finalize snapshot if ! finalize_snapshot "$target_name" "$ts"; then log_error "Failed to finalize snapshot for $target_name" return 1 fi # Calculate total_size after finalization for accurate reporting if _is_rclone_mode; then local size_json; size_json=$(rclone_size "targets/${target_name}/snapshots/${ts}" 2>/dev/null) || true if [[ -n "$size_json" ]]; then total_size=$(echo "$size_json" | grep -oP '"bytes":\s*\K[0-9]+' || echo 0) fi elif [[ "${REMOTE_TYPE:-ssh}" == "local" ]]; then total_size=$(du -sb "$snap_dir/$ts" 2>/dev/null | cut -f1) || total_size=0 else total_size=$(remote_exec "du -sb '$snap_dir/$ts' 2>/dev/null | cut -f1" 2>/dev/null) || total_size=0 fi log_info "Backup completed for $target_name: $ts ($(human_size "${total_size:-0}") in $(human_duration "$duration"))" # 13. Run post-hook if [[ -n "${TARGET_POST_HOOK:-}" ]]; then log_info "Running post-hook for $target_name..." bash -c "$TARGET_POST_HOOK" || log_warn "Post-hook failed for $target_name" fi # 14. Enforce retention enforce_retention "$target_name" return 0 } # Backup all enabled targets. # Usage: backup_all_targets [remote_flag] backup_all_targets() { local remote_flag="${1:-}" local targets; targets=$(list_targets) if [[ -z "$targets" ]]; then log_error "No targets configured" return 1 fi # Resolve remotes local remotes="" remotes=$(get_target_remotes "$remote_flag") || { log_error "Invalid remote specification" return 1 } local start_time; start_time=$(date +%s) local total=0 succeeded=0 failed=0 local failed_targets="" while IFS= read -r target_name; do [[ -z "$target_name" ]] && continue load_target "$target_name" || { log_warn "Cannot load target: $target_name"; continue; } if [[ "${TARGET_ENABLED:-yes}" != "yes" ]]; then log_debug "Target '$target_name' is disabled, skipping" continue fi ((total++)) || true log_info "=== Backing up target: $target_name ($total) ===" local target_failed=false while IFS= read -r rname; do [[ -z "$rname" ]] && continue log_info "--- Transferring $target_name to remote '$rname' ---" if ! backup_target "$target_name" "$rname"; then log_error "Backup to remote '$rname' failed for $target_name" failed_targets+=" - $target_name ($rname: failed)"$'\n' target_failed=true fi done <<< "$remotes" if [[ "$target_failed" == "true" ]]; then ((failed++)) || true else ((succeeded++)) || true log_info "Backup completed for $target_name (all remotes)" fi done <<< "$targets" local end_time; end_time=$(date +%s) local duration=$(( end_time - start_time )) # Print summary echo "" echo "============================================" echo "Backup Summary" echo "============================================" echo "Timestamp: $(timestamp)" 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 targets:" echo "$failed_targets" else echo "Failed: 0" fi echo "============================================" # Send notification send_backup_report "$total" "$succeeded" "$failed" "$duration" "$failed_targets" if (( failed > 0 && succeeded > 0 )); then return "$EXIT_PARTIAL" elif (( failed > 0 )); then return 1 fi return 0 }