From 7e71b5071f4a6a49ebab30bf093d2daa0d59994b Mon Sep 17 00:00:00 2001 From: h7x4 Date: Wed, 7 Aug 2024 23:33:07 +0200 Subject: [PATCH] Report back more detailed results from commands --- src/cli/database_command.rs | 52 +++++++++++++++++++++---------------- src/cli/user_command.rs | 33 ++++++++++++++++------- src/core/common.rs | 25 ++++++++++++++++++ src/main.rs | 26 ++++++++++++++++--- 4 files changed, 99 insertions(+), 37 deletions(-) diff --git a/src/cli/database_command.rs b/src/cli/database_command.rs index 59f4ece..84d6273 100644 --- a/src/cli/database_command.rs +++ b/src/cli/database_command.rs @@ -5,7 +5,7 @@ use prettytable::{Cell, Row, Table}; use sqlx::{Connection, MySqlConnection}; use crate::core::{ - common::{close_database_connection, get_current_unix_user, yn}, + common::{close_database_connection, get_current_unix_user, yn, CommandStatus}, database_operations::*, database_privilege_operations::*, user_operations::user_exists, @@ -142,7 +142,7 @@ pub struct DatabaseEditPrivsArgs { pub async fn handle_command( command: DatabaseCommand, mut connection: MySqlConnection, -) -> anyhow::Result<()> { +) -> anyhow::Result { let result = connection .transaction(|txn| { Box::pin(async move { @@ -165,67 +165,73 @@ pub async fn handle_command( async fn create_databases( args: DatabaseCreateArgs, connection: &mut MySqlConnection, -) -> anyhow::Result<()> { +) -> anyhow::Result { if args.name.is_empty() { anyhow::bail!("No database names provided"); } + let mut result = CommandStatus::SuccessfullyModified; + for name in args.name { // TODO: This can be optimized by fetching all the database privileges in one query. if let Err(e) = create_database(&name, connection).await { eprintln!("Failed to create database '{}': {}", name, e); eprintln!("Skipping..."); + result = CommandStatus::PartiallySuccessfullyModified; } } - Ok(()) + Ok(result) } async fn drop_databases( args: DatabaseDropArgs, connection: &mut MySqlConnection, -) -> anyhow::Result<()> { +) -> anyhow::Result { if args.name.is_empty() { anyhow::bail!("No database names provided"); } + let mut result = CommandStatus::SuccessfullyModified; + for name in args.name { // TODO: This can be optimized by fetching all the database privileges in one query. if let Err(e) = drop_database(&name, connection).await { eprintln!("Failed to drop database '{}': {}", name, e); eprintln!("Skipping..."); + result = CommandStatus::PartiallySuccessfullyModified; } } - Ok(()) + Ok(result) } async fn list_databases( args: DatabaseListArgs, connection: &mut MySqlConnection, -) -> anyhow::Result<()> { +) -> anyhow::Result { let databases = get_database_list(connection).await?; - if databases.is_empty() { - println!("No databases to show."); - return Ok(()); - } - if args.json { println!("{}", serde_json::to_string_pretty(&databases)?); + return Ok(CommandStatus::NoModificationsIntended); + } + + if databases.is_empty() { + println!("No databases to show."); } else { for db in databases { println!("{}", db); } } - Ok(()) + Ok(CommandStatus::NoModificationsIntended) } async fn show_database_privileges( args: DatabaseShowPrivsArgs, connection: &mut MySqlConnection, -) -> anyhow::Result<()> { +) -> anyhow::Result { let database_users_to_show = if args.name.is_empty() { get_all_database_privileges(connection).await? } else { @@ -243,13 +249,13 @@ async fn show_database_privileges( result }; - if database_users_to_show.is_empty() { - println!("No database users to show."); - return Ok(()); - } - if args.json { println!("{}", serde_json::to_string_pretty(&database_users_to_show)?); + return Ok(CommandStatus::NoModificationsIntended); + } + + if database_users_to_show.is_empty() { + println!("No database users to show."); } else { let mut table = Table::new(); table.add_row(Row::new( @@ -280,13 +286,13 @@ async fn show_database_privileges( table.printstd(); } - Ok(()) + Ok(CommandStatus::NoModificationsIntended) } pub async fn edit_privileges( args: DatabaseEditPrivsArgs, connection: &mut MySqlConnection, -) -> anyhow::Result<()> { +) -> anyhow::Result { let privilege_data = if let Some(name) = &args.name { get_database_privileges(name, connection).await? } else { @@ -317,14 +323,14 @@ pub async fn edit_privileges( if diffs.is_empty() { println!("No changes to make."); - return Ok(()); + return Ok(CommandStatus::NoModificationsNeeded); } // TODO: Add confirmation prompt. apply_privilege_diffs(diffs, connection).await?; - Ok(()) + Ok(CommandStatus::SuccessfullyModified) } pub fn parse_privilege_tables_from_args( diff --git a/src/cli/user_command.rs b/src/cli/user_command.rs index b06641a..4412966 100644 --- a/src/cli/user_command.rs +++ b/src/cli/user_command.rs @@ -9,7 +9,7 @@ use serde_json::json; use sqlx::{Connection, MySqlConnection}; use crate::core::{ - common::{close_database_connection, get_current_unix_user}, + common::{close_database_connection, get_current_unix_user, CommandStatus}, database_operations::*, user_operations::*, }; @@ -78,7 +78,7 @@ pub struct UserShowArgs { pub async fn handle_command( command: UserCommand, mut connection: MySqlConnection, -) -> anyhow::Result<()> { +) -> anyhow::Result { let result = connection .transaction(|txn| { Box::pin(async move { @@ -100,15 +100,18 @@ pub async fn handle_command( async fn create_users( args: UserCreateArgs, connection: &mut MySqlConnection, -) -> anyhow::Result<()> { +) -> anyhow::Result { if args.username.is_empty() { anyhow::bail!("No usernames provided"); } + let mut result = CommandStatus::SuccessfullyModified; + for username in args.username { if let Err(e) = create_database_user(&username, connection).await { eprintln!("{}", e); eprintln!("Skipping...\n"); + result = CommandStatus::PartiallySuccessfullyModified; continue; } else { println!("User '{}' created.", username); @@ -133,21 +136,28 @@ async fn create_users( } println!(); } - Ok(()) + Ok(result) } -async fn drop_users(args: UserDeleteArgs, connection: &mut MySqlConnection) -> anyhow::Result<()> { +async fn drop_users( + args: UserDeleteArgs, + connection: &mut MySqlConnection, +) -> anyhow::Result { if args.username.is_empty() { anyhow::bail!("No usernames provided"); } + let mut result = CommandStatus::SuccessfullyModified; + for username in args.username { if let Err(e) = delete_database_user(&username, connection).await { eprintln!("{}", e); eprintln!("Skipping..."); + result = CommandStatus::PartiallySuccessfullyModified; } } - Ok(()) + + Ok(result) } pub fn read_password_from_stdin_with_double_check(username: &str) -> anyhow::Result { @@ -164,7 +174,7 @@ pub fn read_password_from_stdin_with_double_check(username: &str) -> anyhow::Res async fn change_password_for_user( args: UserPasswdArgs, connection: &mut MySqlConnection, -) -> anyhow::Result<()> { +) -> anyhow::Result { // NOTE: although this also is checked in `set_password_for_database_user`, we check it here // to provide a more natural order of error messages. let unix_user = get_current_unix_user()?; @@ -181,10 +191,13 @@ async fn change_password_for_user( set_password_for_database_user(&args.username, &password, connection).await?; - Ok(()) + Ok(CommandStatus::SuccessfullyModified) } -async fn show_users(args: UserShowArgs, connection: &mut MySqlConnection) -> anyhow::Result<()> { +async fn show_users( + args: UserShowArgs, + connection: &mut MySqlConnection, +) -> anyhow::Result { let unix_user = get_current_unix_user()?; let users = if args.username.is_empty() { @@ -251,5 +264,5 @@ async fn show_users(args: UserShowArgs, connection: &mut MySqlConnection) -> any table.printstd(); } - Ok(()) + Ok(CommandStatus::NoModificationsIntended) } diff --git a/src/core/common.rs b/src/core/common.rs index 7d79aef..5c27e43 100644 --- a/src/core/common.rs +++ b/src/core/common.rs @@ -7,6 +7,31 @@ use sqlx::{Connection, MySqlConnection}; #[cfg(not(target_os = "macos"))] use std::ffi::CString; +/// Report the result status of a command. +/// This is used to display a status message to the user. +pub enum CommandStatus { + /// The command was successful, + /// and made modification to the database. + SuccessfullyModified, + + /// The command was mostly successful, + /// and modifications have been made to the database. + /// However, some of the requested modifications failed. + PartiallySuccessfullyModified, + + /// The command was successful, + /// but no modifications were needed. + NoModificationsNeeded, + + /// The command was successful, + /// and made no modification to the database. + NoModificationsIntended, + + /// The command was cancelled, either through a dialog or a signal. + /// No modifications have been made to the database. + Cancelled, +} + pub fn get_current_unix_user() -> anyhow::Result { User::from_uid(getuid()) .context("Failed to look up your UNIX username") diff --git a/src/main.rs b/src/main.rs index 49d6ae5..8a26ee1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate prettytable; +use core::common::CommandStatus; #[cfg(feature = "mysql-admutils-compatibility")] use std::path::PathBuf; @@ -73,9 +74,26 @@ async fn main() -> anyhow::Result<()> { }; match result { - Ok(_) => println!("Changes committed to database"), - Err(_) => println!("Changes reverted due to error"), + Ok(CommandStatus::SuccessfullyModified) => { + println!("Modifications committed successfully"); + Ok(()) + } + Ok(CommandStatus::PartiallySuccessfullyModified) => { + println!("Some modifications committed successfully"); + Ok(()) + } + Ok(CommandStatus::NoModificationsNeeded) => { + println!("No modifications made"); + Ok(()) + } + Ok(CommandStatus::NoModificationsIntended) => { + /* Don't report anything */ + Ok(()) + } + Ok(CommandStatus::Cancelled) => { + println!("Command cancelled successfully"); + Ok(()) + } + Err(e) => Err(e), } - - result }