From 2f4c25979d9f27aca4d8a35a35a63145155efbd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rog=C3=A9rio=20de=20Lima=20Sousa?= Date: Tue, 23 Jun 2026 11:23:28 -0300 Subject: [PATCH] Atualiza fluxo de restauracao de dumps --- .env.example | 15 ----- Cargo.lock | 56 ++++++++-------- Cargo.toml | 2 +- README.md | 28 ++++---- restoredb.log | 10 +++ src/config.rs | 8 ++- src/dumps.rs | 53 +++++++++++++++ src/main.rs | 73 +++++++++++++++++--- src/minio.rs | 2 +- src/postgres_restore.rs | 64 ++++++++++++++---- src/tui.rs | 145 +++++++++++++++++++++++++++++++++++----- 11 files changed, 358 insertions(+), 98 deletions(-) delete mode 100644 .env.example create mode 100644 restoredb.log diff --git a/.env.example b/.env.example deleted file mode 100644 index 33c72bd..0000000 --- a/.env.example +++ /dev/null @@ -1,15 +0,0 @@ -MINIO_ENDPOINT=http://localhost:9000 -MINIO_ACCESS_KEY=minioadmin -MINIO_SECRET_KEY=minioadmin -MINIO_BUCKET=backups -MINIO_REGION=us-east-1 - -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_DATABASE=app_db -POSTGRES_SSLMODE=prefer - -DUMPS_DIR=./dumps - diff --git a/Cargo.lock b/Cargo.lock index 1ac3ccb..008d91b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2083,6 +2083,34 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "restoredb" +version = "0.1.0" +dependencies = [ + "anyhow", + "aws-config", + "aws-credential-types", + "aws-sdk-s3", + "bytesize", + "chrono", + "clap", + "crossterm", + "dotenvy", + "flate2", + "futures-util", + "ratatui", + "rpassword", + "serde", + "serde_yaml", + "thiserror", + "tokio", + "toml", + "tracing", + "tracing-appender", + "tracing-subscriber", + "zstd", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -2118,34 +2146,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rsdb" -version = "0.1.0" -dependencies = [ - "anyhow", - "aws-config", - "aws-credential-types", - "aws-sdk-s3", - "bytesize", - "chrono", - "clap", - "crossterm", - "dotenvy", - "flate2", - "futures-util", - "ratatui", - "rpassword", - "serde", - "serde_yaml", - "thiserror", - "tokio", - "toml", - "tracing", - "tracing-appender", - "tracing-subscriber", - "zstd", -] - [[package]] name = "rtoolbox" version = "0.0.5" diff --git a/Cargo.toml b/Cargo.toml index c40bfdc..b30f134 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rsdb" +name = "restoredb" version = "0.1.0" edition = "2021" diff --git a/README.md b/README.md index b4e4990..36e994e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# rsdb +# restoredb Assistente CLI/TUI em Rust para listar dumps em MinIO/S3, baixar arquivos para uma pasta local e restaurar um banco PostgreSQL com `DROP DATABASE` e recriação controlados por confirmação explícita. @@ -19,7 +19,7 @@ cargo build --release O binário ficará em: ```bash -target/release/rsdb +target/release/restoredb ``` ## Configuração @@ -51,7 +51,7 @@ cp config.example.toml config.toml Exemplo `config.yml`: ```bash -cp config.example.yml "$(dirname "$(realpath target/release/rsdb)")/config.yml" +cp config.example.yml "$(dirname "$(realpath target/release/restoredb)")/config.yml" ``` Variáveis obrigatórias: @@ -79,51 +79,51 @@ Senhas nunca são exibidas. A senha do PostgreSQL é passada para `psql` e `pg_r Abrir a TUI: ```bash -rsdb +restoredb ``` Listar dumps remotos e locais: ```bash -rsdb list +restoredb list ``` Restaurar diretamente um dump local ou remoto, ainda com confirmação: ```bash -rsdb restore backup.sql.gz +restoredb restore backup.sql.gz ``` Simular uma restauração sem baixar, apagar ou restaurar: ```bash -rsdb restore backup.sql.gz --dry-run +restoredb restore backup.sql.gz --dry-run ``` Validar configuração, pasta local, conexão S3 e ferramentas PostgreSQL: ```bash -rsdb doctor +restoredb doctor ``` -`rsdb config-check` continua disponível como comando compatível. +`restoredb config-check` continua disponível como comando compatível. Mostrar a configuração carregada, com senhas mascaradas: ```bash -rsdb config show +restoredb config show ``` Apagar dumps locais, após confirmação: ```bash -rsdb clean +restoredb clean ``` Apagar apenas dumps locais mais antigos que 30 dias: ```bash -rsdb clean --older-than-days 30 +restoredb clean --older-than-days 30 ``` ## Controles da TUI @@ -136,7 +136,7 @@ rsdb clean --older-than-days 30 - `Enter`: selecionar/restaurar - `q`: sair -Antes da restauração, a TUI mostra o host, banco de destino e dump selecionado, com aviso de que o banco será apagado. A restauração só ocorre após confirmação. +Antes da restauração, a TUI sugere o banco de destino a partir do nome do dump, permite editar esse valor e então mostra host, banco de destino e dump selecionado, com aviso de que o banco será apagado. A restauração só ocorre após confirmação. ## Formatos suportados @@ -150,7 +150,7 @@ Antes da restauração, a TUI mostra o host, banco de destino e dump selecionado Logs técnicos são gravados em: ```bash -rsdb.log +restoredb.log ``` O histórico de restaurações é salvo em: diff --git a/restoredb.log b/restoredb.log new file mode 100644 index 0000000..dcb0351 --- /dev/null +++ b/restoredb.log @@ -0,0 +1,10 @@ +2026-06-23T14:19:02.498663Z INFO restoredb::minio: baixando dump remoto: heteroidentificacao/heteroidentificacao_20260623_050003.sql +2026-06-23T14:23:42.294012Z INFO restoredb::tui: Apagando banco de dados... +2026-06-23T14:23:42.434218Z INFO restoredb::tui: Recriando banco de dados... +2026-06-23T14:23:42.746784Z INFO restoredb::tui: Restaurando dump... +2026-06-23T14:23:46.010945Z INFO restoredb::tui: Restauração finalizada. +2026-06-23T14:23:52.728367Z INFO restoredb::minio: baixando dump remoto: matricula/matricula_20260623_045855.sql +2026-06-23T14:24:12.411620Z INFO restoredb::tui: Apagando banco de dados... +2026-06-23T14:24:12.941459Z INFO restoredb::tui: Recriando banco de dados... +2026-06-23T14:24:13.090076Z INFO restoredb::tui: Restaurando dump... +2026-06-23T14:24:13.132939Z ERROR restoredb::tui: psql retornou erro: psql:dumps/matricula_20260623_045855.sql:47: ERROR: role "dsi" does not exist diff --git a/src/config.rs b/src/config.rs index b0acecd..980c712 100644 --- a/src/config.rs +++ b/src/config.rs @@ -52,7 +52,7 @@ impl Config { && !PathBuf::from("config.toml").exists() && !has_required_env() { - println!("Configuração inicial do rsdb"); + println!("Configuração inicial do restoredb"); println!( "As respostas serão salvas em {}", executable_yaml_path.display() @@ -138,6 +138,12 @@ impl Config { }; serde_yaml::to_string(&masked).context("falha ao gerar YAML da configuração") } + + pub fn with_postgres_database(&self, postgres_database: String) -> Self { + let mut config = self.clone(); + config.postgres_database = postgres_database; + config + } } fn merge_file_config(base: FileConfig, override_with: FileConfig) -> FileConfig { diff --git a/src/dumps.rs b/src/dumps.rs index 714861d..ccbb079 100644 --- a/src/dumps.rs +++ b/src/dumps.rs @@ -137,6 +137,41 @@ pub fn is_supported_dump(name: &str) -> bool { SUPPORTED_EXTENSIONS.iter().any(|ext| lower.ends_with(ext)) } +pub fn suggest_database_name(name: &str) -> String { + let basename = Path::new(name) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(name); + let without_extension = strip_supported_extension(basename).unwrap_or(basename); + strip_timestamp_suffix(without_extension) + .unwrap_or(without_extension) + .to_string() +} + +fn strip_supported_extension(name: &str) -> Option<&str> { + let lower = name.to_ascii_lowercase(); + SUPPORTED_EXTENSIONS + .iter() + .filter(|ext| lower.ends_with(**ext)) + .max_by_key(|ext| ext.len()) + .map(|ext| &name[..name.len() - ext.len()]) +} + +fn strip_timestamp_suffix(name: &str) -> Option<&str> { + let (prefix, time) = name.rsplit_once('_')?; + let (prefix, date) = prefix.rsplit_once('_')?; + if date.len() == 8 + && time.len() == 6 + && date.bytes().all(|byte| byte.is_ascii_digit()) + && time.bytes().all(|byte| byte.is_ascii_digit()) + && !prefix.is_empty() + { + Some(prefix) + } else { + None + } +} + pub fn find_dump_by_name(dumps: &[DumpEntry], name: &str) -> Option { dumps .iter() @@ -144,6 +179,24 @@ pub fn find_dump_by_name(dumps: &[DumpEntry], name: &str) -> Option { .cloned() } +#[cfg(test)] +mod tests { + use super::suggest_database_name; + + #[test] + fn suggests_database_name_from_common_dump_names() { + assert_eq!( + suggest_database_name("heteroidentificacao_20260623_050003.sql"), + "heteroidentificacao" + ); + assert_eq!( + suggest_database_name("backup/minha_base_20260623_050003.dump.gz"), + "minha_base" + ); + assert_eq!(suggest_database_name("app_db.sql.zst"), "app_db"); + } +} + pub async fn materialize_dump( config: &Config, minio: &MinioClient, diff --git a/src/main.rs b/src/main.rs index 245655a..5c0231e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,17 +6,19 @@ mod minio; mod postgres_restore; mod tui; +use std::io::Write; + use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use tracing::{error, info}; use crate::config::Config; -use crate::dumps::{collect_dumps, ensure_dumps_dir, find_dump_by_name}; +use crate::dumps::{collect_dumps, ensure_dumps_dir, find_dump_by_name, suggest_database_name}; use crate::minio::MinioClient; use crate::postgres_restore::{confirm_in_terminal, restore_dump}; #[derive(Parser, Debug)] -#[command(name = "rsdb")] +#[command(name = "restoredb")] #[command(about = "Assistente para restaurar PostgreSQL a partir de dumps MinIO/S3")] struct Cli { #[command(subcommand)] @@ -56,6 +58,9 @@ async fn main() -> Result<()> { let cli = Cli::parse(); let config = Config::load().context("falha ao carregar configuração")?; ensure_dumps_dir(&config.dumps_dir)?; + postgres_restore::check_tools() + .await + .context("ferramentas necessárias indisponíveis")?; let minio = MinioClient::new(&config).await?; let result = match cli.command { @@ -83,14 +88,14 @@ async fn main() -> Result<()> { } fn init_logging() -> Result { - let file_appender = tracing_appender::rolling::never(".", "rsdb.log"); + let file_appender = tracing_appender::rolling::never(".", "restoredb.log"); let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); tracing_subscriber::fmt() .with_writer(non_blocking) .with_ansi(false) .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "rsdb=info".into()), + .unwrap_or_else(|_| "restoredb=info".into()), ) .init(); Ok(guard) @@ -129,6 +134,7 @@ async fn command_restore( let dump = find_dump_by_name(&dumps, name).with_context(|| format!("dump não encontrado: {name}"))?; if dry_run { + let suggested_database = suggest_database_name(&dump.name); println!("Dry-run: nenhuma alteração será executada."); println!("Dump: {}", dump.name); println!("Origem: {}", dump.kind.label()); @@ -141,7 +147,7 @@ async fn command_restore( ); println!( "Destino: {}:{} / {}", - config.postgres_host, config.postgres_port, config.postgres_database + config.postgres_host, config.postgres_port, suggested_database ); if dump.local_remote_size_mismatch() { println!("Aviso: o tamanho local difere do tamanho remoto."); @@ -155,17 +161,45 @@ async fn command_restore( println!("A cópia local será usada. Apague o arquivo local para baixar novamente."); } let local_path = dumps::materialize_dump(config, minio, &dump, false).await?; + let target_database = prompt_target_database(&dump.name)?; + let restore_config = config.with_postgres_database(target_database); - if !confirm_in_terminal(config, &local_path)? { + if !confirm_in_terminal(&restore_config, &local_path)? { println!("Operação cancelada."); return Ok(()); } - let result = restore_dump(config, &local_path, |message| { - println!("{message}"); - }) + let active_connections = postgres_restore::count_active_connections(&restore_config).await?; + let terminate_connections = if active_connections > 0 { + print!( + "Existem {active_connections} conexão(ões) ativa(s) no banco \"{}\". Encerrar para continuar? (s/N): ", + restore_config.postgres_database + ); + std::io::stdout().flush()?; + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + if input.trim().eq_ignore_ascii_case("s") { + true + } else { + println!("Operação cancelada."); + return Ok(()); + } + } else { + false + }; + + let result = restore_dump( + &restore_config, + &local_path, + terminate_connections, + |message| { + println!("{message}"); + }, + ) .await; - if let Err(err) = history::record_restore(config, &local_path, result.as_ref().err()).await { + if let Err(err) = + history::record_restore(&restore_config, &local_path, result.as_ref().err()).await + { error!("{err:?}"); } result?; @@ -229,3 +263,22 @@ fn confirm_text(prompt: &str, expected: &str) -> Result { std::io::stdin().read_line(&mut input)?; Ok(input.trim() == expected) } + +fn prompt_target_database(dump_name: &str) -> Result { + let suggested = suggest_database_name(dump_name); + loop { + print!("Banco de destino [{suggested}]: "); + std::io::stdout().flush()?; + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + let database = if input.trim().is_empty() { + suggested.clone() + } else { + input.trim().to_string() + }; + if !database.is_empty() { + return Ok(database); + } + println!("Banco de destino não pode ficar vazio."); + } +} diff --git a/src/minio.rs b/src/minio.rs index ff918cf..8cfdb52 100644 --- a/src/minio.rs +++ b/src/minio.rs @@ -26,7 +26,7 @@ impl MinioClient { config.minio_secret_key.clone(), None, None, - "rsdb", + "restoredb", ); let shared_config = aws_config::defaults(BehaviorVersion::latest()) .endpoint_url(config.minio_endpoint.clone()) diff --git a/src/postgres_restore.rs b/src/postgres_restore.rs index 62a3724..76b9f2c 100644 --- a/src/postgres_restore.rs +++ b/src/postgres_restore.rs @@ -26,17 +26,27 @@ pub async fn check_admin_connection(config: &Config) -> Result<()> { } async fn check_tool(name: &str) -> Result<()> { - let output = Command::new(name) - .arg("--version") - .output() - .await - .with_context(|| format!("{name} não encontrado no PATH"))?; - if !output.status.success() { - bail!("{name} retornou erro em --version"); + let result = Command::new(name).arg("--version").output().await; + let ok = matches!(result, Ok(output) if output.status.success()); + if !ok { + bail!( + "{name} não encontrado no PATH ou não pôde ser executado. {}", + install_hint() + ); } Ok(()) } +fn install_hint() -> String { + if cfg!(target_os = "macos") { + "Instale com: brew install libpq && brew link --force libpq".to_string() + } else if cfg!(target_os = "windows") { + "Instale o PostgreSQL (inclui psql e pg_restore) em https://www.postgresql.org/download/windows/ e adicione o diretório bin ao PATH".to_string() + } else { + "Instale o pacote cliente do PostgreSQL: sudo apt install postgresql-client (Debian/Ubuntu) ou sudo dnf install postgresql (Fedora/RHEL)".to_string() + } +} + pub fn confirm_in_terminal(config: &Config, dump: &Path) -> Result { println!("ATENÇÃO: esta operação apagará e recriará o banco de dados."); println!("Host: {}", config.postgres_host); @@ -49,7 +59,25 @@ pub fn confirm_in_terminal(config: &Config, dump: &Path) -> Result { Ok(input.trim() == "RESTAURAR") } -pub async fn restore_dump(config: &Config, dump_path: &Path, mut status: F) -> Result<()> +pub async fn count_active_connections(config: &Config) -> Result { + let sql = format!( + "SELECT count(*) FROM pg_stat_activity WHERE datname = {} AND pid <> pg_backend_pid();", + quote_literal(&config.postgres_database) + ); + let output = + run_psql_admin_output(config, &["-t", "-A", "-v", "ON_ERROR_STOP=1", "-c", &sql]).await?; + output + .trim() + .parse::() + .context("falha ao interpretar contagem de conexões ativas") +} + +pub async fn restore_dump( + config: &Config, + dump_path: &Path, + terminate_connections: bool, + mut status: F, +) -> Result<()> where F: FnMut(&str), { @@ -64,8 +92,10 @@ where return Err(RestoreAssistantError::UnsupportedDumpFormat(name.to_string()).into()); } - status("Encerrando conexões ativas..."); - terminate_connections(config).await?; + if terminate_connections { + status("Encerrando conexões ativas..."); + terminate_active_connections(config).await?; + } status("Apagando banco de dados..."); drop_database(config).await?; status("Recriando banco de dados..."); @@ -76,7 +106,7 @@ where Ok(()) } -async fn terminate_connections(config: &Config) -> Result<()> { +async fn terminate_active_connections(config: &Config) -> Result<()> { let sql = format!( "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = {} AND pid <> pg_backend_pid();", quote_literal(&config.postgres_database) @@ -218,7 +248,7 @@ fn decompress_pg_restore_archive_to_temp( let file = std::fs::File::open(source) .with_context(|| format!("falha ao abrir {}", source.display()))?; let temp_path = std::env::temp_dir().join(format!( - "rsdb-{}-{}.tar", + "restoredb-{}-{}.tar", std::process::id(), chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default() )); @@ -275,6 +305,10 @@ async fn run_psql_admin(config: &Config, args: &[&str]) -> Result<()> { run_command(base_psql_command(config, "postgres").args(args), "psql").await } +async fn run_psql_admin_output(config: &Config, args: &[&str]) -> Result { + run_command_output(base_psql_command(config, "postgres").args(args), "psql").await +} + async fn run_psql_target(config: &Config, args: &[&str]) -> Result<()> { run_command( base_psql_command(config, &config.postgres_database).args(args), @@ -284,6 +318,10 @@ async fn run_psql_target(config: &Config, args: &[&str]) -> Result<()> { } async fn run_command(command: &mut Command, label: &str) -> Result<()> { + run_command_output(command, label).await.map(|_| ()) +} + +async fn run_command_output(command: &mut Command, label: &str) -> Result { debug!("executando comando externo: {label}"); let output = command .output() @@ -293,7 +331,7 @@ async fn run_command(command: &mut Command, label: &str) -> Result<()> { let stderr = String::from_utf8_lossy(&output.stderr); bail!("{label} retornou erro: {}", stderr.trim()); } - Ok(()) + Ok(String::from_utf8_lossy(&output.stdout).to_string()) } fn base_psql_command(config: &Config, database: &str) -> Command { diff --git a/src/tui.rs b/src/tui.rs index 11266c1..6cb4151 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -14,7 +14,7 @@ use tracing::{error, info}; use crate::{ config::Config, - dumps::{self, DumpEntry, DumpKind}, + dumps::{self, suggest_database_name, DumpEntry, DumpKind}, errors::RestoreAssistantError, history, minio::MinioClient, @@ -24,7 +24,19 @@ use crate::{ enum Mode { Browsing, Filtering, - Confirming(PathBuf), + EditingDatabase { + path: PathBuf, + input: String, + }, + Confirming { + path: PathBuf, + database: String, + }, + ConfirmTerminate { + path: PathBuf, + database: String, + count: i64, + }, ConfirmDelete(DumpEntry), Working, Error(String), @@ -105,10 +117,39 @@ impl App { } _ => {} }, - Mode::Confirming(path) => match key.code { + Mode::EditingDatabase { .. } => match key.code { + KeyCode::Enter => { + if let Mode::EditingDatabase { path, input } = &self.mode { + let path = path.clone(); + let database = input.trim().to_string(); + if database.is_empty() { + self.status = "Banco de destino não pode ficar vazio.".to_string(); + } else { + self.mode = Mode::Confirming { path, database }; + } + } + } + KeyCode::Backspace => { + if let Mode::EditingDatabase { input, .. } = &mut self.mode { + input.pop(); + } + } + KeyCode::Char(c) => { + if let Mode::EditingDatabase { input, .. } = &mut self.mode { + input.push(c); + } + } + KeyCode::Esc => { + self.status = "Restauração cancelada.".to_string(); + self.mode = Mode::Browsing; + } + _ => {} + }, + Mode::Confirming { path, database } => match key.code { KeyCode::Char('y') | KeyCode::Char('s') => { let path = path.clone(); - self.restore(path).await; + let database = database.clone(); + self.check_connections(path, database).await; } KeyCode::Esc | KeyCode::Char('n') => { self.status = "Restauração cancelada.".to_string(); @@ -116,6 +157,19 @@ impl App { } _ => {} }, + Mode::ConfirmTerminate { path, database, .. } => match key.code { + KeyCode::Char('y') | KeyCode::Char('s') => { + let path = path.clone(); + let database = database.clone(); + self.restore(path, database, true).await; + } + KeyCode::Esc | KeyCode::Char('n') => { + self.status = + "Restauração cancelada: conexões ativas não encerradas.".to_string(); + self.mode = Mode::Browsing; + } + _ => {} + }, Mode::ConfirmDelete(dump) => match key.code { KeyCode::Char('y') | KeyCode::Char('s') => { let dump = dump.clone(); @@ -196,7 +250,10 @@ impl App { } match dumps::materialize_dump(&self.config, &self.minio, &dump, false).await { Ok(path) => { - self.mode = Mode::Confirming(path); + self.mode = Mode::EditingDatabase { + path, + input: suggest_database_name(&dump.name), + }; } Err(err) => { error!("{err:?}"); @@ -231,14 +288,39 @@ impl App { } } - async fn restore(&mut self, path: PathBuf) { + async fn check_connections(&mut self, path: PathBuf, database: String) { + let restore_config = self.config.with_postgres_database(database.clone()); + match postgres_restore::count_active_connections(&restore_config).await { + Ok(count) if count > 0 => { + self.mode = Mode::ConfirmTerminate { + path, + database, + count, + }; + } + Ok(_) => self.restore(path, database, false).await, + Err(err) => { + error!("{err:?}"); + self.mode = Mode::Error(format!("Falha ao verificar conexões ativas: {err}")); + } + } + } + + async fn restore(&mut self, path: PathBuf, database: String, terminate_connections: bool) { self.mode = Mode::Working; self.status = "Iniciando restauração...".to_string(); - let result = postgres_restore::restore_dump(&self.config, &path, |message| { - info!("{message}"); - }) + let restore_config = self.config.with_postgres_database(database); + let result = postgres_restore::restore_dump( + &restore_config, + &path, + terminate_connections, + |message| { + info!("{message}"); + }, + ) .await; - if let Err(err) = history::record_restore(&self.config, &path, result.as_ref().err()).await + if let Err(err) = + history::record_restore(&restore_config, &path, result.as_ref().err()).await { error!("{err:?}"); } @@ -264,8 +346,8 @@ impl App { .split(area); let title = format!( - "rsdb | destino: {}:{} / {}", - self.config.postgres_host, self.config.postgres_port, self.config.postgres_database + "restoredb | PostgreSQL: {}:{}", + self.config.postgres_host, self.config.postgres_port ); frame.render_widget( Paragraph::new(title).block(Block::default().borders(Borders::ALL)), @@ -323,7 +405,15 @@ impl App { ); match &self.mode { - Mode::Confirming(path) => self.draw_confirm(frame, area, path), + Mode::EditingDatabase { path, input } => { + self.draw_database_input(frame, area, path, input) + } + Mode::Confirming { path, database } => self.draw_confirm(frame, area, path, database), + Mode::ConfirmTerminate { + path, + database, + count, + } => self.draw_terminate_confirm(frame, area, path, database, *count), Mode::ConfirmDelete(dump) => self.draw_delete_confirm(frame, area, dump), Mode::Working => self.draw_popup( frame, @@ -336,16 +426,41 @@ impl App { } } - fn draw_confirm(&self, frame: &mut Frame, area: Rect, path: &PathBuf) { + fn draw_database_input(&self, frame: &mut Frame, area: Rect, path: &PathBuf, input: &str) { + let text = format!( + "Informe o banco de destino.\n\nDump: {}\nBanco: {}\n\nEnter confirma, Esc cancela.", + path.display(), + input + ); + self.draw_popup(frame, area, "Banco de destino", &text); + } + + fn draw_confirm(&self, frame: &mut Frame, area: Rect, path: &PathBuf, database: &str) { let text = format!( "Esta operação apagará e recriará o banco.\n\nHost: {}\nBanco: {}\nDump: {}\n\nPressione y/s para confirmar ou Esc/n para cancelar.", self.config.postgres_host, - self.config.postgres_database, + database, path.display() ); self.draw_popup(frame, area, "Confirmar restauração", &text); } + fn draw_terminate_confirm( + &self, + frame: &mut Frame, + area: Rect, + path: &PathBuf, + database: &str, + count: i64, + ) { + let text = format!( + "Existem {count} conexão(ões) ativa(s) no banco \"{}\".\nDump: {}\n\nEncerrar as conexões para continuar?\n\nPressione y/s para confirmar ou Esc/n para cancelar.", + database, + path.display() + ); + self.draw_popup(frame, area, "Conexões ativas", &text); + } + fn draw_delete_confirm(&self, frame: &mut Frame, area: Rect, dump: &DumpEntry) { let path = dump .local_path