use crate::config::{backups_dir, Environment, DB_USER};
use crate::docker;
use crate::ui;
use anyhow::Result;
use chrono::Local;
use clap::Subcommand;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Write};
use std::process::{Command, Stdio};
#[derive(Subcommand)]
pub enum DbCommand {
Shell {
#[arg(long)]
prod: bool,
},
Backup {
#[arg(long)]
prod: bool,
},
Restore {
file: String,
#[arg(long)]
prod: bool,
},
}
pub fn run(cmd: DbCommand) -> Result<()> {
docker::check_docker()?;
match cmd {
DbCommand::Shell { prod } => shell(if prod {
Environment::Prod
} else {
Environment::Dev
})?,
DbCommand::Backup { prod } => backup(if prod {
Environment::Prod
} else {
Environment::Dev
})?,
DbCommand::Restore { file, prod } => {
restore(
if prod {
Environment::Prod
} else {
Environment::Dev
},
&file,
)?;
}
}
Ok(())
}
fn shell(env: Environment) -> Result<()> {
let db_name = env.db_name();
docker::exec(env, "database", &["psql", "-U", DB_USER, "-d", db_name])?;
Ok(())
}
fn backup(env: Environment) -> Result<()> {
let backups = backups_dir()?;
let timestamp = Local::now().format("%Y%m%d-%H%M%S");
let env_prefix = match env {
Environment::Dev => "dev",
Environment::Prod => "prod",
};
let filename = format!("{env_prefix}-backup-{timestamp}.sql");
let filepath = backups.join(&filename);
ui::info(&format!("Backing up {env} database..."));
let output = Command::new("docker")
.args([
"compose",
"-f",
&env.compose_file(),
"exec",
"-T",
"database",
"pg_dump",
"-U",
DB_USER,
env.db_name(),
])
.output()?;
if output.status.success() {
let mut file = BufWriter::new(File::create(&filepath)?);
file.write_all(&output.stdout)?;
ui::success(&format!("Backup saved to backups/{filename}"));
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
ui::error(&format!("Backup failed: {stderr}"));
}
Ok(())
}
fn restore(env: Environment, file: &str) -> Result<()> {
let filepath = std::path::Path::new(file);
if !filepath.exists() {
anyhow::bail!("File not found: {file}");
}
if env == Environment::Prod {
if !ui::confirm_strict("This will overwrite PRODUCTION data!") {
ui::info("Aborted.");
return Ok(());
}
} else if !ui::confirm_dangerous("This will overwrite the database!") {
ui::info("Aborted.");
return Ok(());
}
ui::info(&format!("Restoring {env} database from {file}..."));
let backup_file = File::open(filepath)?;
let mut reader = BufReader::new(backup_file);
let mut contents = Vec::new();
reader.read_to_end(&mut contents)?;
let mut child = Command::new("docker")
.args([
"compose",
"-f",
&env.compose_file(),
"exec",
"-T",
"database",
"psql",
"-U",
DB_USER,
env.db_name(),
])
.stdin(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(&contents)?;
}
let status = child.wait()?;
if status.success() {
ui::success("Database restored!");
} else {
ui::error("Restore may have failed");
}
Ok(())
}