use std::io::{self, BufRead, IsTerminal, Write};
use anyhow::{bail, Context, Result};
use serde_json::json;
use crate::api::client::ApiClient;
use crate::api::error::ApiError;
use crate::config::credentials::resolve_api_key;
use crate::config::manager::ConfigManager;
use crate::model::loader::Models;
use super::dispatch::{print_output_with_opts, OutputOpts};
struct CreateOpts {
cluster_id: Option<String>,
backup_type: Option<String>,
db_name: Option<String>,
collection_name: Option<String>,
}
fn take_value(args: &[String], i: &mut usize, flag: &str) -> Result<Option<String>> {
*i += 1;
match args.get(*i) {
Some(v) if v.starts_with("--") => bail!("{} requires a value", flag),
Some(v) => Ok(Some(v.clone())),
None => bail!("{} requires a value", flag),
}
}
fn parse_create_args(raw_args: &[String]) -> Result<CreateOpts> {
let mut opts = CreateOpts {
cluster_id: None,
backup_type: None,
db_name: None,
collection_name: None,
};
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--cluster-id" => opts.cluster_id = take_value(raw_args, &mut i, "--cluster-id")?,
"--backup-type" => opts.backup_type = take_value(raw_args, &mut i, "--backup-type")?,
"--database" => opts.db_name = take_value(raw_args, &mut i, "--database")?,
"--collection" => opts.collection_name = take_value(raw_args, &mut i, "--collection")?,
other => {
bail!("Unknown flag '{}'. Available: --cluster-id, --backup-type, --database, --collection", other);
}
}
i += 1;
}
Ok(opts)
}
pub async fn create(
models: &Models,
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if raw_args.iter().any(|a| a == "-h" || a == "--help") {
print!(
"Create a backup.\n\n\
Usage: zilliz backup create [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:30}Cluster ID (prompted if omitted)\n\
\x20 {:24}{:30}Backup type: CLUSTER or COLLECTION [default: CLUSTER]\n\
\x20 {:24}{:30}Database name (for COLLECTION backup) [default: default]\n\
\x20 {:24}{:30}Collection name (for COLLECTION backup)\n",
"--cluster-id",
"<string>",
"--type",
"<string>",
"--database",
"<string>",
"--collection",
"<string>",
);
return Ok(());
}
let mut opts = parse_create_args(raw_args)?;
let api_key = resolve_api_key(output_opts.api_key, config_mgr).ok_or(ApiError::NoApiKey)?;
let base_url =
super::endpoint::resolve_control_plane_url(config_mgr, &models.control_plane, None);
let client = ApiClient::new(api_key, base_url);
let is_tty = io::stdin().is_terminal();
let cluster_id = match opts.cluster_id.take() {
Some(id) => id,
None if is_tty => prompt_cluster(&client).await?,
None => bail!("Missing required option: --cluster-id"),
};
let backup_type = match opts.backup_type.take() {
Some(t) => {
let upper = t.to_uppercase();
if upper != "CLUSTER" && upper != "COLLECTION" {
bail!("Invalid backup type '{}'. Use: CLUSTER or COLLECTION", t);
}
upper
}
None if opts.collection_name.is_some() => "COLLECTION".to_string(),
None if is_tty => prompt_backup_type()?,
None => "CLUSTER".to_string(),
};
let mut body = json!({
"backupType": backup_type,
});
if backup_type == "COLLECTION" {
let db_name;
let collection_name;
if opts.db_name.is_some() || opts.collection_name.is_some() {
db_name = opts.db_name.take().unwrap_or_else(|| "default".to_string());
collection_name = opts
.collection_name
.take()
.context("--collection is required for COLLECTION backup")?;
} else if is_tty {
eprintln!();
eprintln!("Configure additional options? [y/N]: ");
let mut input = String::new();
let bytes_read = io::stdin().lock().read_line(&mut input)?;
if bytes_read == 0 {
bail!("Unexpected end of input");
}
if input.trim().eq_ignore_ascii_case("y") {
db_name = prompt_string(" database", Some("default"))?;
collection_name = prompt_string(" collection", None)?;
} else {
bail!("--database and --collection are required for COLLECTION backup");
}
} else {
bail!("--database and --collection are required for COLLECTION backup");
}
body["dbCollections"] = json!([{
"dbName": db_name,
"collectionNames": [collection_name],
}]);
}
let path = format!("/v2/clusters/{}/backups/create", cluster_id);
let result = client.call("POST", &path, None, Some(&body)).await?;
print_output_with_opts(&result, output_opts, None);
Ok(())
}
async fn prompt_cluster(client: &ApiClient) -> Result<String> {
let body = json!({ "pageSize": 100, "currentPage": 1 });
let result = client
.call("GET", "/v2/clusters", None, Some(&body))
.await?;
let clusters = result
.get("data")
.or(Some(&result))
.and_then(|v| v.get("clusters").or_else(|| v.as_array().map(|_| v)))
.and_then(|v| v.as_array())
.context("Failed to fetch clusters")?;
if clusters.is_empty() {
bail!("No clusters found.");
}
eprintln!("Select cluster:");
for (i, c) in clusters.iter().enumerate() {
let name = c.get("clusterName").and_then(|v| v.as_str()).unwrap_or("?");
let id = c.get("clusterId").and_then(|v| v.as_str()).unwrap_or("?");
eprintln!(" [{}] {} ({})", i + 1, name, id);
}
eprint!("Select [1-{}]: ", clusters.len());
io::stderr().flush()?;
let mut input = String::new();
let bytes_read = io::stdin().lock().read_line(&mut input)?;
if bytes_read == 0 {
bail!("Unexpected end of input");
}
let idx: usize = input.trim().parse().context("Invalid selection")?;
if idx < 1 || idx > clusters.len() {
bail!("Selection out of range");
}
clusters[idx - 1]
.get("clusterId")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.context("Cluster has no ID")
}
fn prompt_backup_type() -> Result<String> {
let choices = ["CLUSTER", "COLLECTION"];
eprintln!("Select backup type:");
for (i, choice) in choices.iter().enumerate() {
eprintln!(" [{}] {}", i + 1, choice);
}
loop {
eprint!("Select [1-{}]: ", choices.len());
io::stderr().flush()?;
let mut input = String::new();
let bytes_read = io::stdin().lock().read_line(&mut input)?;
if bytes_read == 0 {
bail!("Unexpected end of input");
}
if let Ok(n) = input.trim().parse::<usize>() {
if n >= 1 && n <= choices.len() {
return Ok(choices[n - 1].to_string());
}
}
eprintln!("Invalid selection, please try again.");
}
}
fn prompt_string(label: &str, default: Option<&str>) -> Result<String> {
loop {
if let Some(def) = default {
eprint!("{} [{}]: ", label, def);
} else {
eprint!("{}: ", label);
}
io::stderr().flush()?;
let mut input = String::new();
let bytes_read = io::stdin().lock().read_line(&mut input)?;
if bytes_read == 0 {
bail!("Unexpected end of input");
}
let trimmed = input.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
if let Some(def) = default {
return Ok(def.to_string());
}
eprintln!("Value required, please try again.");
}
}