systemprompt-cli 0.2.2

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use anyhow::{Context, Result, bail};
use std::path::PathBuf;
use std::process::Command;
use systemprompt_logging::CliService;

use super::{BackupFormat, adjust_ssl_mode, find_pg_dump};

const fn backup_extension(format: BackupFormat) -> &'static str {
    match format {
        BackupFormat::Custom => "dump",
        BackupFormat::Sql => "sql",
        BackupFormat::Directory => "dir",
    }
}

const fn pg_dump_format_flag(format: BackupFormat) -> &'static str {
    match format {
        BackupFormat::Custom => "-Fc",
        BackupFormat::Sql => "-Fp",
        BackupFormat::Directory => "-Fd",
    }
}

pub fn execute(
    profile_name: &str,
    database_url: &str,
    format: BackupFormat,
    output: Option<&str>,
) -> Result<()> {
    let pg_dump = find_pg_dump()?;

    let output_path = if let Some(p) = output {
        PathBuf::from(p)
    } else {
        let timestamp = chrono::Utc::now().format("%Y-%m-%d-%H%M%S");
        let ext = backup_extension(format);
        let dir = PathBuf::from("backups");
        std::fs::create_dir_all(&dir)
            .with_context(|| format!("Failed to create {}", dir.display()))?;
        dir.join(format!("{}-{}.{}", profile_name, timestamp, ext))
    };

    CliService::section("Cloud Database Backup");
    CliService::key_value("Profile", profile_name);
    CliService::key_value("Format", &format!("{:?}", format));
    CliService::key_value("Output", &output_path.display().to_string());

    let adjusted_url = adjust_ssl_mode(database_url);
    let spinner = CliService::spinner("Running pg_dump...");

    let result = Command::new(&pg_dump)
        .arg(pg_dump_format_flag(format))
        .arg("--no-owner")
        .arg("--no-privileges")
        .arg("-f")
        .arg(&output_path)
        .arg(&adjusted_url)
        .output()
        .context("Failed to execute pg_dump")?;

    spinner.finish_and_clear();

    if !result.status.success() {
        let stderr = String::from_utf8_lossy(&result.stderr);
        bail!("pg_dump failed:\n{}", stderr);
    }

    let size = if output_path.is_dir() {
        dir_size(&output_path)
    } else {
        std::fs::metadata(&output_path).map_or(0, |m| m.len())
    };

    CliService::success(&format!(
        "Backup complete: {} ({})",
        output_path.display(),
        format_size(size)
    ));

    Ok(())
}

fn dir_size(path: &PathBuf) -> u64 {
    std::fs::read_dir(path).map_or(0, |entries| {
        entries
            .filter_map(std::result::Result::ok)
            .filter_map(|e| e.metadata().ok())
            .map(|m| m.len())
            .sum()
    })
}

fn format_size(bytes: u64) -> String {
    if bytes >= 1_073_741_824 {
        format!("{:.1} GB", bytes as f64 / 1_073_741_824.0)
    } else if bytes >= 1_048_576 {
        format!("{:.1} MB", bytes as f64 / 1_048_576.0)
    } else if bytes >= 1024 {
        format!("{:.1} KB", bytes as f64 / 1024.0)
    } else {
        format!("{} B", bytes)
    }
}