oph 0.1.0

OpenHealth CLI - Universal development tool for the OpenHealth team
Documentation
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 {
    /// Open psql shell in database
    Shell {
        /// Use production environment instead of development
        #[arg(long)]
        prod: bool,
    },
    /// Backup database
    Backup {
        /// Use production environment instead of development
        #[arg(long)]
        prod: bool,
    },
    /// Restore database from backup
    Restore {
        /// Backup file path
        file: String,
        /// Use production environment instead of development
        #[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<()> {
    // Check file exists
    let filepath = std::path::Path::new(file);
    if !filepath.exists() {
        anyhow::bail!("File not found: {file}");
    }

    // Extra confirmation for production
    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}..."));

    // Read the backup 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)?;

    // Pipe to psql
    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(())
}