diff --git a/bin/gniza b/bin/gniza index e10055f..a1534a3 100755 --- a/bin/gniza +++ b/bin/gniza @@ -158,6 +158,14 @@ run_cli() { dest=$(_parse_flag "--dest" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true folder=$(_parse_flag "--folder" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true + # Check for --skip-mysql flag + local skip_mysql="" + local arg + for arg in "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}"; do + [[ "$arg" == "--skip-mysql" ]] && skip_mysql="yes" + done + [[ -n "$skip_mysql" ]] && export SKIP_MYSQL_RESTORE="yes" + [[ -z "$target" ]] && die "restore requires --target=NAME" if [[ -z "$remote" ]]; then diff --git a/lib/backup.sh b/lib/backup.sh index 5e293cf..c225fdc 100644 --- a/lib/backup.sh +++ b/lib/backup.sh @@ -106,6 +106,7 @@ _backup_target_impl() { if [[ "${TARGET_MYSQL_ENABLED:-no}" == "yes" ]]; then log_info "Dumping MySQL databases for $target_name..." if mysql_dump_databases; then + mysql_dump_grants || log_warn "Grants dump failed, continuing with database dumps" mysql_dump_dir="${MYSQL_DUMP_DIR:-}" else log_error "MySQL dump failed for $target_name" diff --git a/lib/mysql.sh b/lib/mysql.sh index 5f78bee..320c0eb 100644 --- a/lib/mysql.sh +++ b/lib/mysql.sh @@ -190,6 +190,164 @@ mysql_dump_databases() { return 0 } +# Dump MySQL user grants to grants.sql in the dump directory. +# Must be called after mysql_dump_databases() sets MYSQL_DUMP_DIR. +mysql_dump_grants() { + local client_cmd + client_cmd=$(_mysql_find_client_cmd) || { + log_error "MySQL/MariaDB client not found — cannot dump grants" + return 1 + } + + mysql_build_conn_args + + local grants_file="$MYSQL_DUMP_DIR/_mysql/grants.sql" + + # System users to skip + local -a skip_users=( + "'root'@'localhost'" + "'mysql.sys'@'localhost'" + "'mysql.infoschema'@'localhost'" + "'mysql.session'@'localhost'" + "'debian-sys-maint'@'localhost'" + "'mariadb.sys'@'localhost'" + ) + + # Get all users + local users_output + users_output=$("$client_cmd" "${MYSQL_CONN_ARGS[@]}" -N -e \ + "SELECT CONCAT(\"'\", user, \"'@'\", host, \"'\") FROM mysql.user" 2>&1) || { + log_error "Failed to list MySQL users: $users_output" + return 1 + } + + local count=0 + { + echo "-- MySQL grants dump" + echo "-- Generated: $(date -Iseconds)" + echo "" + + while IFS= read -r user_host; do + user_host="${user_host#"${user_host%%[![:space:]]*}"}" + user_host="${user_host%"${user_host##*[![:space:]]}"}" + [[ -z "$user_host" ]] && continue + + # Skip system users + local skip=false + local su + for su in "${skip_users[@]}"; do + if [[ "$user_host" == "$su" ]]; then + skip=true + break + fi + done + [[ "$skip" == "true" ]] && continue + + # Try SHOW CREATE USER (MySQL 5.7+/MariaDB 10.2+) + local create_user + create_user=$("$client_cmd" "${MYSQL_CONN_ARGS[@]}" -N -e \ + "SHOW CREATE USER $user_host" 2>/dev/null) || true + if [[ -n "$create_user" ]]; then + echo "$create_user;" + fi + + # SHOW GRANTS + local grants + grants=$("$client_cmd" "${MYSQL_CONN_ARGS[@]}" -N -e \ + "SHOW GRANTS FOR $user_host" 2>/dev/null) || continue + while IFS= read -r grant_line; do + [[ -n "$grant_line" ]] && echo "$grant_line;" + done <<< "$grants" + echo "" + ((count++)) || true + done <<< "$users_output" + } > "$grants_file" + + log_info "MySQL grants dumped: $count user(s) -> grants.sql" + return 0 +} + +# Restore MySQL databases from a directory of .sql.gz files. +# Usage: mysql_restore_databases +# The directory should contain *.sql.gz files and optionally grants.sql. +mysql_restore_databases() { + local mysql_dir="$1" + + if [[ ! -d "$mysql_dir" ]]; then + log_error "MySQL restore dir not found: $mysql_dir" + return 1 + fi + + local client_cmd + client_cmd=$(_mysql_find_client_cmd) || { + log_error "MySQL/MariaDB client not found — cannot restore databases" + return 1 + } + + mysql_build_conn_args + + local errors=0 + + # Restore database dumps + local f + for f in "$mysql_dir"/*.sql.gz; do + [[ -f "$f" ]] || continue + local db_name + db_name=$(basename "$f" .sql.gz) + + # Skip system databases + local skip=false + local sdb + for sdb in $_MYSQL_SYSTEM_DBS; do + if [[ "$db_name" == "$sdb" ]]; then + skip=true + break + fi + done + [[ "$skip" == "true" ]] && continue + + log_info "Restoring MySQL database: $db_name" + + # Create database if not exists + "$client_cmd" "${MYSQL_CONN_ARGS[@]}" -e \ + "CREATE DATABASE IF NOT EXISTS \`$db_name\`" 2>/dev/null || { + log_error "Failed to create database: $db_name" + ((errors++)) || true + continue + } + + # Import dump + if gunzip -c "$f" | "$client_cmd" "${MYSQL_CONN_ARGS[@]}" "$db_name" 2>/dev/null; then + log_info "Restored database: $db_name" + else + log_error "Failed to restore database: $db_name" + ((errors++)) || true + fi + done + + # Restore grants + if [[ -f "$mysql_dir/grants.sql" ]]; then + log_info "Restoring MySQL grants..." + if "$client_cmd" "${MYSQL_CONN_ARGS[@]}" < "$mysql_dir/grants.sql" 2>/dev/null; then + log_info "MySQL grants restored" + "$client_cmd" "${MYSQL_CONN_ARGS[@]}" -e "FLUSH PRIVILEGES" 2>/dev/null || true + else + log_error "Failed to restore some MySQL grants (partial restore may have occurred)" + ((errors++)) || true + fi + fi + + unset MYSQL_PWD 2>/dev/null || true + + if (( errors > 0 )); then + log_error "MySQL restore completed with $errors error(s)" + return 1 + fi + + log_info "MySQL restore completed successfully" + return 0 +} + # Clean up the temporary MySQL dump directory and env vars. mysql_cleanup_dump() { if [[ -n "${MYSQL_DUMP_DIR:-}" && -d "$MYSQL_DUMP_DIR" ]]; then diff --git a/lib/restore.sh b/lib/restore.sh index fa38386..1b236cc 100644 --- a/lib/restore.sh +++ b/lib/restore.sh @@ -99,6 +99,46 @@ restore_target() { fi done < <(get_target_folders) + # Restore MySQL databases if snapshot contains _mysql/ and target has MySQL enabled + if [[ "${TARGET_MYSQL_ENABLED:-no}" == "yes" && "${SKIP_MYSQL_RESTORE:-}" != "yes" ]]; then + log_info "Checking for MySQL dumps in snapshot..." + local mysql_restore_dir + mysql_restore_dir=$(mktemp -d "${WORK_DIR:-/tmp}/gniza-mysql-restore-XXXXXX") + mkdir -p "$mysql_restore_dir/_mysql" + + local mysql_found=false + if _is_rclone_mode; then + local mysql_subpath="targets/${target_name}/snapshots/${ts}/_mysql" + if rclone_from_remote "$mysql_subpath" "$mysql_restore_dir/_mysql" 2>/dev/null; then + mysql_found=true + fi + elif [[ "${REMOTE_TYPE:-ssh}" == "local" ]]; then + local mysql_source="$snap_dir/$ts/_mysql/" + if [[ -d "$mysql_source" ]]; then + rsync -aHAX "$mysql_source" "$mysql_restore_dir/_mysql/" && mysql_found=true + fi + else + local mysql_source="$snap_dir/$ts/_mysql/" + if _rsync_download "$mysql_source" "$mysql_restore_dir/_mysql/" 2>/dev/null; then + mysql_found=true + fi + fi + + if [[ "$mysql_found" == "true" ]] && ls "$mysql_restore_dir/_mysql/"*.sql.gz &>/dev/null || \ + [[ -f "$mysql_restore_dir/_mysql/grants.sql" ]]; then + log_info "Found MySQL dumps in snapshot, restoring..." + if ! mysql_restore_databases "$mysql_restore_dir/_mysql"; then + log_error "MySQL restore had errors" + ((errors++)) || true + fi + else + log_debug "No MySQL dumps found in snapshot" + fi + rm -rf "$mysql_restore_dir" + elif [[ "${SKIP_MYSQL_RESTORE:-}" == "yes" ]]; then + log_info "Skipping MySQL restore (--skip-mysql)" + fi + _restore_remote_globals if (( errors > 0 )); then