Improve CLI safety and maintenance commands
This commit is contained in:
parent
c66a85b192
commit
ab37755e77
48
Cargo.lock
generated
48
Cargo.lock
generated
@ -1004,27 +1004,6 @@ dependencies = [
|
|||||||
"ctutils",
|
"ctutils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs"
|
|
||||||
version = "6.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
|
||||||
dependencies = [
|
|
||||||
"dirs-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs-sys"
|
|
||||||
version = "0.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"option-ext",
|
|
||||||
"redox_users",
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@ -1761,15 +1740,6 @@ version = "0.2.186"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libredox"
|
|
||||||
version = "0.1.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
@ -1913,12 +1883,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "option-ext"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "outref"
|
name = "outref"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@ -2096,17 +2060,6 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_users"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.17",
|
|
||||||
"libredox",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@ -2177,7 +2130,6 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"dirs",
|
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|||||||
@ -13,7 +13,6 @@ chrono = { version = "0.4", features = ["serde"] }
|
|||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
crossterm = "0.28"
|
crossterm = "0.28"
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
dirs = "6"
|
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
ratatui = "0.29"
|
ratatui = "0.29"
|
||||||
|
|||||||
47
README.md
47
README.md
@ -24,19 +24,17 @@ target/release/rsdb
|
|||||||
|
|
||||||
## Configuração
|
## Configuração
|
||||||
|
|
||||||
Na primeira execução, se nenhuma configuração completa existir, o programa pergunta os campos um a um, no estilo do `rclone`, e salva em:
|
Na primeira execução, se nenhuma configuração completa existir, o programa pergunta os campos um a um, no estilo do `rclone`, e salva em `config.yml` na mesma pasta do executável.
|
||||||
|
|
||||||
```bash
|
|
||||||
~/.config/rsdb/config.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
Senhas são perguntadas sem eco no terminal. O programa também continua aceitando `.env` e `config.toml` na raiz do projeto.
|
Senhas são perguntadas sem eco no terminal. O programa também continua aceitando `.env` e `config.toml` na raiz do projeto.
|
||||||
|
|
||||||
|
Também é possível distribuir esse `config.yml` junto do binário. Ele é usado como fallback para completar valores ausentes.
|
||||||
|
|
||||||
A ordem de precedência é:
|
A ordem de precedência é:
|
||||||
|
|
||||||
1. Variáveis de ambiente / `.env`
|
1. Variáveis de ambiente / `.env`
|
||||||
2. `config.toml` local
|
2. `config.toml` local
|
||||||
3. `~/.config/rsdb/config.yml`
|
3. `config.yml` na mesma pasta do executável
|
||||||
|
|
||||||
Exemplo `.env`:
|
Exemplo `.env`:
|
||||||
|
|
||||||
@ -53,8 +51,7 @@ cp config.example.toml config.toml
|
|||||||
Exemplo `config.yml`:
|
Exemplo `config.yml`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.config/rsdb
|
cp config.example.yml "$(dirname "$(realpath target/release/rsdb)")/config.yml"
|
||||||
cp config.example.yml ~/.config/rsdb/config.yml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Variáveis obrigatórias:
|
Variáveis obrigatórias:
|
||||||
@ -97,10 +94,36 @@ Restaurar diretamente um dump local ou remoto, ainda com confirmação:
|
|||||||
rsdb restore backup.sql.gz
|
rsdb restore backup.sql.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Simular uma restauração sem baixar, apagar ou restaurar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsdb restore backup.sql.gz --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
Validar configuração, pasta local, conexão S3 e ferramentas PostgreSQL:
|
Validar configuração, pasta local, conexão S3 e ferramentas PostgreSQL:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rsdb config-check
|
rsdb doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
`rsdb config-check` continua disponível como comando compatível.
|
||||||
|
|
||||||
|
Mostrar a configuração carregada, com senhas mascaradas:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsdb config show
|
||||||
|
```
|
||||||
|
|
||||||
|
Apagar dumps locais, após confirmação:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsdb clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Apagar apenas dumps locais mais antigos que 30 dias:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsdb clean --older-than-days 30
|
||||||
```
|
```
|
||||||
|
|
||||||
## Controles da TUI
|
## Controles da TUI
|
||||||
@ -130,4 +153,10 @@ Logs técnicos são gravados em:
|
|||||||
rsdb.log
|
rsdb.log
|
||||||
```
|
```
|
||||||
|
|
||||||
|
O histórico de restaurações é salvo em:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
<DUMPS_DIR>/history.yml
|
||||||
|
```
|
||||||
|
|
||||||
Erros exibidos na TUI ou CLI são resumidos para o usuário; detalhes técnicos ficam no log.
|
Erros exibidos na TUI ou CLI são resumidos para o usuário; detalhes técnicos ficam no log.
|
||||||
|
|||||||
@ -43,16 +43,22 @@ struct FileConfig {
|
|||||||
impl Config {
|
impl Config {
|
||||||
pub fn load() -> Result<Self> {
|
pub fn load() -> Result<Self> {
|
||||||
let _ = dotenv();
|
let _ = dotenv();
|
||||||
let yaml_path = default_yaml_path()?;
|
let executable_yaml_path = executable_yaml_path()?;
|
||||||
let yaml = load_yaml(&yaml_path).unwrap_or_default();
|
let yaml = load_yaml(&executable_yaml_path).unwrap_or_default();
|
||||||
let toml = load_toml().unwrap_or_default();
|
let toml = load_toml().unwrap_or_default();
|
||||||
let mut file = merge_file_config(yaml, toml);
|
let mut file = merge_file_config(yaml, toml);
|
||||||
|
|
||||||
if !yaml_path.exists() && !PathBuf::from("config.toml").exists() && !has_required_env() {
|
if !executable_yaml_path.exists()
|
||||||
|
&& !PathBuf::from("config.toml").exists()
|
||||||
|
&& !has_required_env()
|
||||||
|
{
|
||||||
println!("Configuração inicial do rsdb");
|
println!("Configuração inicial do rsdb");
|
||||||
println!("As respostas serão salvas em {}", yaml_path.display());
|
println!(
|
||||||
|
"As respostas serão salvas em {}",
|
||||||
|
executable_yaml_path.display()
|
||||||
|
);
|
||||||
file = prompt_config()?;
|
file = prompt_config()?;
|
||||||
save_yaml(&yaml_path, &file)?;
|
save_yaml(&executable_yaml_path, &file)?;
|
||||||
println!("Configuração salva.");
|
println!("Configuração salva.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +104,40 @@ impl Config {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_masked_yaml(&self) -> Result<String> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct MaskedConfig<'a> {
|
||||||
|
minio_endpoint: &'a str,
|
||||||
|
minio_access_key: &'a str,
|
||||||
|
minio_secret_key: &'a str,
|
||||||
|
minio_bucket: &'a str,
|
||||||
|
minio_region: &'a str,
|
||||||
|
postgres_host: &'a str,
|
||||||
|
postgres_port: u16,
|
||||||
|
postgres_user: &'a str,
|
||||||
|
postgres_password: &'a str,
|
||||||
|
postgres_database: &'a str,
|
||||||
|
postgres_sslmode: &'a str,
|
||||||
|
dumps_dir: &'a PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
let masked = MaskedConfig {
|
||||||
|
minio_endpoint: &self.minio_endpoint,
|
||||||
|
minio_access_key: &self.minio_access_key,
|
||||||
|
minio_secret_key: "***",
|
||||||
|
minio_bucket: &self.minio_bucket,
|
||||||
|
minio_region: &self.minio_region,
|
||||||
|
postgres_host: &self.postgres_host,
|
||||||
|
postgres_port: self.postgres_port,
|
||||||
|
postgres_user: &self.postgres_user,
|
||||||
|
postgres_password: "***",
|
||||||
|
postgres_database: &self.postgres_database,
|
||||||
|
postgres_sslmode: &self.postgres_sslmode,
|
||||||
|
dumps_dir: &self.dumps_dir,
|
||||||
|
};
|
||||||
|
serde_yaml::to_string(&masked).context("falha ao gerar YAML da configuração")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_file_config(base: FileConfig, override_with: FileConfig) -> FileConfig {
|
fn merge_file_config(base: FileConfig, override_with: FileConfig) -> FileConfig {
|
||||||
@ -202,10 +242,13 @@ fn prompt_secret(name: &str) -> Result<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_yaml_path() -> Result<PathBuf> {
|
fn executable_yaml_path() -> Result<PathBuf> {
|
||||||
let base =
|
let executable =
|
||||||
dirs::config_dir().context("não foi possível descobrir o diretório de configuração")?;
|
env::current_exe().context("não foi possível descobrir o caminho do executável")?;
|
||||||
Ok(base.join("rsdb").join("config.yml"))
|
let parent = executable
|
||||||
|
.parent()
|
||||||
|
.context("não foi possível descobrir o diretório do executável")?;
|
||||||
|
Ok(parent.join("config.yml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_yaml(path: &PathBuf) -> Result<FileConfig> {
|
fn load_yaml(path: &PathBuf) -> Result<FileConfig> {
|
||||||
|
|||||||
34
src/dumps.rs
34
src/dumps.rs
@ -36,6 +36,18 @@ pub struct DumpEntry {
|
|||||||
pub kind: DumpKind,
|
pub kind: DumpKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DumpEntry {
|
||||||
|
pub fn local_remote_size_mismatch(&self) -> bool {
|
||||||
|
matches!(self.kind, DumpKind::RemoteAndLocal)
|
||||||
|
&& self
|
||||||
|
.local_path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|path| path.metadata().ok())
|
||||||
|
.map(|metadata| Some(metadata.len()) != self.size)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const SUPPORTED_EXTENSIONS: &[&str] = &[
|
pub const SUPPORTED_EXTENSIONS: &[&str] = &[
|
||||||
".sql",
|
".sql",
|
||||||
".sql.gz",
|
".sql.gz",
|
||||||
@ -185,3 +197,25 @@ pub fn delete_local_dump(dump: &DumpEntry) -> Result<PathBuf> {
|
|||||||
fs::remove_file(path).with_context(|| format!("falha ao apagar {}", path.display()))?;
|
fs::remove_file(path).with_context(|| format!("falha ao apagar {}", path.display()))?;
|
||||||
Ok(path.clone())
|
Ok(path.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clean_candidates(dir: &Path, older_than_days: Option<u64>) -> Result<Vec<PathBuf>> {
|
||||||
|
let cutoff = older_than_days.map(|days| {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.checked_sub(std::time::Duration::from_secs(days * 24 * 60 * 60))
|
||||||
|
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||||
|
});
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
for dump in list_local_dumps(dir)? {
|
||||||
|
let Some(path) = dump.local_path else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(cutoff) = cutoff {
|
||||||
|
let modified = path.metadata()?.modified()?;
|
||||||
|
if modified > cutoff {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates.push(path);
|
||||||
|
}
|
||||||
|
Ok(candidates)
|
||||||
|
}
|
||||||
|
|||||||
48
src/history.rs
Normal file
48
src/history.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
struct HistoryEntry {
|
||||||
|
timestamp: DateTime<Utc>,
|
||||||
|
dump: String,
|
||||||
|
host: String,
|
||||||
|
database: String,
|
||||||
|
status: String,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn record_restore(
|
||||||
|
config: &Config,
|
||||||
|
dump_path: &Path,
|
||||||
|
error: Option<&anyhow::Error>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let history_path = config.dumps_dir.join("history.yml");
|
||||||
|
let mut entries = read_history(&history_path)?;
|
||||||
|
entries.push(HistoryEntry {
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
dump: dump_path.display().to_string(),
|
||||||
|
host: config.postgres_host.clone(),
|
||||||
|
database: config.postgres_database.clone(),
|
||||||
|
status: if error.is_some() { "error" } else { "success" }.to_string(),
|
||||||
|
error: error.map(|err| err.to_string()),
|
||||||
|
});
|
||||||
|
let raw = serde_yaml::to_string(&entries).context("falha ao gerar histórico YAML")?;
|
||||||
|
tokio::fs::write(&history_path, raw)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("falha ao salvar {}", history_path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_history(path: &Path) -> Result<Vec<HistoryEntry>> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let raw = std::fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("falha ao ler {}", path.display()))?;
|
||||||
|
serde_yaml::from_str(&raw).with_context(|| format!("{} inválido", path.display()))
|
||||||
|
}
|
||||||
122
src/main.rs
122
src/main.rs
@ -1,6 +1,7 @@
|
|||||||
mod config;
|
mod config;
|
||||||
mod dumps;
|
mod dumps;
|
||||||
mod errors;
|
mod errors;
|
||||||
|
mod history;
|
||||||
mod minio;
|
mod minio;
|
||||||
mod postgres_restore;
|
mod postgres_restore;
|
||||||
mod tui;
|
mod tui;
|
||||||
@ -25,8 +26,28 @@ struct Cli {
|
|||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
List,
|
List,
|
||||||
Restore { arquivo: String },
|
Restore {
|
||||||
|
arquivo: String,
|
||||||
|
#[arg(long)]
|
||||||
|
dry_run: bool,
|
||||||
|
},
|
||||||
|
Doctor,
|
||||||
ConfigCheck,
|
ConfigCheck,
|
||||||
|
Clean {
|
||||||
|
#[arg(long)]
|
||||||
|
older_than_days: Option<u64>,
|
||||||
|
#[arg(short, long)]
|
||||||
|
yes: bool,
|
||||||
|
},
|
||||||
|
Config {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: ConfigCommands,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum ConfigCommands {
|
||||||
|
Show,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@ -40,8 +61,19 @@ async fn main() -> Result<()> {
|
|||||||
let result = match cli.command {
|
let result = match cli.command {
|
||||||
None => tui::run(config, minio).await,
|
None => tui::run(config, minio).await,
|
||||||
Some(Commands::List) => command_list(&config, &minio).await,
|
Some(Commands::List) => command_list(&config, &minio).await,
|
||||||
Some(Commands::Restore { arquivo }) => command_restore(&config, &minio, &arquivo).await,
|
Some(Commands::Restore { arquivo, dry_run }) => {
|
||||||
Some(Commands::ConfigCheck) => command_config_check(&config, &minio).await,
|
command_restore(&config, &minio, &arquivo, dry_run).await
|
||||||
|
}
|
||||||
|
Some(Commands::Doctor) | Some(Commands::ConfigCheck) => {
|
||||||
|
command_doctor(&config, &minio).await
|
||||||
|
}
|
||||||
|
Some(Commands::Clean {
|
||||||
|
older_than_days,
|
||||||
|
yes,
|
||||||
|
}) => command_clean(&config, older_than_days, yes).await,
|
||||||
|
Some(Commands::Config { command }) => match command {
|
||||||
|
ConfigCommands::Show => command_config_show(&config),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = &result {
|
if let Err(err) = &result {
|
||||||
@ -87,10 +119,41 @@ async fn command_list(config: &Config, minio: &MinioClient) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn command_restore(config: &Config, minio: &MinioClient, name: &str) -> Result<()> {
|
async fn command_restore(
|
||||||
|
config: &Config,
|
||||||
|
minio: &MinioClient,
|
||||||
|
name: &str,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> Result<()> {
|
||||||
let dumps = collect_dumps(config, minio).await?;
|
let dumps = collect_dumps(config, minio).await?;
|
||||||
let dump =
|
let dump =
|
||||||
find_dump_by_name(&dumps, name).with_context(|| format!("dump não encontrado: {name}"))?;
|
find_dump_by_name(&dumps, name).with_context(|| format!("dump não encontrado: {name}"))?;
|
||||||
|
if dry_run {
|
||||||
|
println!("Dry-run: nenhuma alteração será executada.");
|
||||||
|
println!("Dump: {}", dump.name);
|
||||||
|
println!("Origem: {}", dump.kind.label());
|
||||||
|
println!(
|
||||||
|
"Arquivo local: {}",
|
||||||
|
dump.local_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| path.display().to_string())
|
||||||
|
.unwrap_or_else(|| config.dumps_dir.join(&dump.name).display().to_string())
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Destino: {}:{} / {}",
|
||||||
|
config.postgres_host, config.postgres_port, config.postgres_database
|
||||||
|
);
|
||||||
|
if dump.local_remote_size_mismatch() {
|
||||||
|
println!("Aviso: o tamanho local difere do tamanho remoto.");
|
||||||
|
}
|
||||||
|
println!("Ações: baixar se necessário, encerrar conexões, DROP DATABASE, CREATE DATABASE, restaurar.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if dump.local_remote_size_mismatch() {
|
||||||
|
println!("Aviso: existe cópia local com tamanho diferente do remoto.");
|
||||||
|
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 local_path = dumps::materialize_dump(config, minio, &dump, false).await?;
|
||||||
|
|
||||||
if !confirm_in_terminal(config, &local_path)? {
|
if !confirm_in_terminal(config, &local_path)? {
|
||||||
@ -98,20 +161,25 @@ async fn command_restore(config: &Config, minio: &MinioClient, name: &str) -> Re
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
restore_dump(config, &local_path, |message| {
|
let result = restore_dump(config, &local_path, |message| {
|
||||||
println!("{message}");
|
println!("{message}");
|
||||||
})
|
})
|
||||||
.await?;
|
.await;
|
||||||
|
if let Err(err) = history::record_restore(config, &local_path, result.as_ref().err()).await {
|
||||||
|
error!("{err:?}");
|
||||||
|
}
|
||||||
|
result?;
|
||||||
info!("restauração concluída: {}", local_path.display());
|
info!("restauração concluída: {}", local_path.display());
|
||||||
println!("Restauração concluída.");
|
println!("Restauração concluída.");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn command_config_check(config: &Config, minio: &MinioClient) -> Result<()> {
|
async fn command_doctor(config: &Config, minio: &MinioClient) -> Result<()> {
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
ensure_dumps_dir(&config.dumps_dir)?;
|
ensure_dumps_dir(&config.dumps_dir)?;
|
||||||
minio.check().await?;
|
minio.check().await?;
|
||||||
postgres_restore::check_tools().await?;
|
postgres_restore::check_tools().await?;
|
||||||
|
postgres_restore::check_admin_connection(config).await?;
|
||||||
println!("Configuração válida.");
|
println!("Configuração válida.");
|
||||||
println!("Bucket: {}", config.minio_bucket);
|
println!("Bucket: {}", config.minio_bucket);
|
||||||
println!(
|
println!(
|
||||||
@ -121,3 +189,43 @@ async fn command_config_check(config: &Config, minio: &MinioClient) -> Result<()
|
|||||||
println!("Dumps locais: {}", config.dumps_dir.display());
|
println!("Dumps locais: {}", config.dumps_dir.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn command_clean(config: &Config, older_than_days: Option<u64>, yes: bool) -> Result<()> {
|
||||||
|
let candidates = dumps::clean_candidates(&config.dumps_dir, older_than_days)?;
|
||||||
|
if candidates.is_empty() {
|
||||||
|
println!("Nenhum dump local para apagar.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Arquivos locais que serão apagados:");
|
||||||
|
for dump in &candidates {
|
||||||
|
println!("- {}", dump.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !yes && !confirm_text("Digite exatamente APAGAR para continuar: ", "APAGAR")? {
|
||||||
|
println!("Operação cancelada.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for dump in candidates {
|
||||||
|
std::fs::remove_file(&dump)
|
||||||
|
.with_context(|| format!("falha ao apagar {}", dump.display()))?;
|
||||||
|
println!("Apagado: {}", dump.display());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_config_show(config: &Config) -> Result<()> {
|
||||||
|
println!("{}", config.to_masked_yaml()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm_text(prompt: &str, expected: &str) -> Result<bool> {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
print!("{prompt}");
|
||||||
|
std::io::stdout().flush()?;
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input)?;
|
||||||
|
Ok(input.trim() == expected)
|
||||||
|
}
|
||||||
|
|||||||
@ -21,6 +21,10 @@ pub async fn check_tools() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn check_admin_connection(config: &Config) -> Result<()> {
|
||||||
|
run_psql_admin(config, &["-v", "ON_ERROR_STOP=1", "-q", "-c", "SELECT 1;"]).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn check_tool(name: &str) -> Result<()> {
|
async fn check_tool(name: &str) -> Result<()> {
|
||||||
let output = Command::new(name)
|
let output = Command::new(name)
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
|
|||||||
12
src/tui.rs
12
src/tui.rs
@ -16,6 +16,7 @@ use crate::{
|
|||||||
config::Config,
|
config::Config,
|
||||||
dumps::{self, DumpEntry, DumpKind},
|
dumps::{self, DumpEntry, DumpKind},
|
||||||
errors::RestoreAssistantError,
|
errors::RestoreAssistantError,
|
||||||
|
history,
|
||||||
minio::MinioClient,
|
minio::MinioClient,
|
||||||
postgres_restore,
|
postgres_restore,
|
||||||
};
|
};
|
||||||
@ -187,8 +188,11 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if matches!(dump.kind, DumpKind::RemoteAndLocal) {
|
if matches!(dump.kind, DumpKind::RemoteAndLocal) {
|
||||||
self.status =
|
self.status = if dump.local_remote_size_mismatch() {
|
||||||
"Usando cópia local existente. Remova o arquivo para baixar novamente.".to_string();
|
"Aviso: cópia local tem tamanho diferente do remoto; ela será usada.".to_string()
|
||||||
|
} else {
|
||||||
|
"Usando cópia local existente. Remova o arquivo para baixar novamente.".to_string()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
match dumps::materialize_dump(&self.config, &self.minio, &dump, false).await {
|
match dumps::materialize_dump(&self.config, &self.minio, &dump, false).await {
|
||||||
Ok(path) => {
|
Ok(path) => {
|
||||||
@ -234,6 +238,10 @@ impl App {
|
|||||||
info!("{message}");
|
info!("{message}");
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
if let Err(err) = history::record_restore(&self.config, &path, result.as_ref().err()).await
|
||||||
|
{
|
||||||
|
error!("{err:?}");
|
||||||
|
}
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.status = "Restauração concluída.".to_string();
|
self.status = "Restauração concluída.".to_string();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user