use anyhow::Result; use serde_json::Value; use sqlx::PgPool; use std::collections::BTreeMap; use crate::commands::add::{self, AddArgs}; use crate::models::ExportFormat; use crate::output::OutputMode; pub struct ImportArgs<'a> { pub file: &'a str, /// Overwrite existing records when there is a conflict (upsert). pub force: bool, /// Check and preview operations without writing to the database. pub dry_run: bool, pub output: OutputMode, } pub async fn run(pool: &PgPool, args: ImportArgs<'_>, master_key: &[u8; 32]) -> Result<()> { let format = ExportFormat::from_extension(args.file)?; let content = std::fs::read_to_string(args.file) .map_err(|e| anyhow::anyhow!("Cannot read file '{}': {}", args.file, e))?; let data = format.deserialize(&content)?; if data.version != 1 { anyhow::bail!( "Unsupported export version {}. Only version 1 is supported.", data.version ); } let total = data.entries.len(); let mut inserted = 0usize; let mut skipped = 0usize; let mut failed = 0usize; for entry in &data.entries { // Check if record already exists. let exists: bool = sqlx::query_scalar( "SELECT EXISTS(SELECT 1 FROM entries \ WHERE namespace = $1 AND kind = $2 AND name = $3)", ) .bind(&entry.namespace) .bind(&entry.kind) .bind(&entry.name) .fetch_one(pool) .await .unwrap_or(false); if exists && !args.force { let msg = format!( "[{}/{}/{}] conflict — record already exists (use --force to overwrite)", entry.namespace, entry.kind, entry.name ); match args.output { OutputMode::Json | OutputMode::JsonCompact => { let v = serde_json::json!({ "action": "conflict", "namespace": entry.namespace, "kind": entry.kind, "name": entry.name, }); let s = if args.output == OutputMode::Json { serde_json::to_string_pretty(&v)? } else { serde_json::to_string(&v)? }; eprintln!("{}", s); } _ => eprintln!("{}", msg), } return Err(anyhow::anyhow!( "Import aborted: conflict on [{}/{}/{}]", entry.namespace, entry.kind, entry.name )); } let action = if exists { "upsert" } else { "insert" }; if args.dry_run { match args.output { OutputMode::Json | OutputMode::JsonCompact => { let v = serde_json::json!({ "action": action, "namespace": entry.namespace, "kind": entry.kind, "name": entry.name, "dry_run": true, }); let s = if args.output == OutputMode::Json { serde_json::to_string_pretty(&v)? } else { serde_json::to_string(&v)? }; println!("{}", s); } _ => println!( "[dry-run] {} [{}/{}/{}]", action, entry.namespace, entry.kind, entry.name ), } if exists { skipped += 1; } else { inserted += 1; } continue; } // Build secret_entries: convert BTreeMap to Vec ("key:=json") let secret_entries = build_secret_entries(entry.secrets.as_ref()); // Build meta_entries from metadata JSON object. let meta_entries = build_meta_entries(&entry.metadata); match add::run( pool, AddArgs { namespace: &entry.namespace, kind: &entry.kind, name: &entry.name, tags: &entry.tags, meta_entries: &meta_entries, secret_entries: &secret_entries, output: OutputMode::Text, }, master_key, ) .await { Ok(()) => { match args.output { OutputMode::Json | OutputMode::JsonCompact => { let v = serde_json::json!({ "action": action, "namespace": entry.namespace, "kind": entry.kind, "name": entry.name, }); let s = if args.output == OutputMode::Json { serde_json::to_string_pretty(&v)? } else { serde_json::to_string(&v)? }; println!("{}", s); } _ => println!( "Imported [{}/{}/{}]", entry.namespace, entry.kind, entry.name ), } inserted += 1; } Err(e) => { eprintln!( "Error importing [{}/{}/{}]: {}", entry.namespace, entry.kind, entry.name, e ); failed += 1; } } } match args.output { OutputMode::Json | OutputMode::JsonCompact => { let v = serde_json::json!({ "total": total, "inserted": inserted, "skipped": skipped, "failed": failed, "dry_run": args.dry_run, }); let s = if args.output == OutputMode::Json { serde_json::to_string_pretty(&v)? } else { serde_json::to_string(&v)? }; println!("{}", s); } _ => { if args.dry_run { println!( "\n[dry-run] {} total: {} would insert, {} would skip, {} would fail", total, inserted, skipped, failed ); } else { println!( "\nImport done: {} total — {} inserted, {} skipped, {} failed", total, inserted, skipped, failed ); } } } if failed > 0 { anyhow::bail!("{} record(s) failed to import", failed); } Ok(()) } /// Convert metadata JSON object into Vec of "key:=json_value" entries. fn build_meta_entries(metadata: &Value) -> Vec { let mut entries = Vec::new(); if let Some(obj) = metadata.as_object() { for (k, v) in obj { entries.push(value_to_kv_entry(k, v)); } } entries } /// Convert a BTreeMap (secrets) into Vec of "key:=json_value" entries. fn build_secret_entries(secrets: Option<&BTreeMap>) -> Vec { let mut entries = Vec::new(); if let Some(map) = secrets { for (k, v) in map { entries.push(value_to_kv_entry(k, v)); } } entries } /// Convert a key/value pair to a CLI-style entry string. /// Strings use `key=value`; everything else uses `key:=`. fn value_to_kv_entry(key: &str, value: &Value) -> String { match value { Value::String(s) => format!("{}={}", key, s), other => format!("{}:={}", key, other), } }