use anyhow::Result; use serde_json::Value; use sqlx::PgPool; use std::collections::{BTreeMap, HashMap}; use uuid::Uuid; use crate::crypto; use crate::models::{ExportData, ExportEntry, ExportFormat}; use crate::service::search::{fetch_entries, fetch_secrets_for_entries}; pub struct ExportParams<'a> { pub folder: Option<&'a str>, pub entry_type: Option<&'a str>, pub name: Option<&'a str>, pub tags: &'a [String], pub query: Option<&'a str>, pub no_secrets: bool, pub user_id: Option, } pub async fn export( pool: &PgPool, params: ExportParams<'_>, master_key: Option<&[u8; 32]>, ) -> Result { let entries = fetch_entries( pool, params.folder, params.entry_type, params.name, params.tags, params.query, params.user_id, ) .await?; let entry_ids: Vec = entries.iter().map(|e| e.id).collect(); let secrets_map: HashMap> = if !params.no_secrets && !entry_ids.is_empty() { fetch_secrets_for_entries(pool, &entry_ids).await? } else { HashMap::new() }; let mut export_entries: Vec = Vec::with_capacity(entries.len()); for entry in &entries { let secrets = if params.no_secrets { None } else { let fields = secrets_map.get(&entry.id).map(Vec::as_slice).unwrap_or(&[]); if fields.is_empty() { Some(BTreeMap::new()) } else { let mk = master_key .ok_or_else(|| anyhow::anyhow!("master key required to decrypt secrets"))?; let mut map = BTreeMap::new(); for f in fields { let decrypted = crypto::decrypt_json(mk, &f.encrypted)?; map.insert(f.name.clone(), decrypted); } Some(map) } }; export_entries.push(ExportEntry { name: entry.name.clone(), folder: entry.folder.clone(), entry_type: entry.entry_type.clone(), notes: entry.notes.clone(), tags: entry.tags.clone(), metadata: entry.metadata.clone(), secrets, }); } Ok(ExportData { version: 1, exported_at: chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(), entries: export_entries, }) } pub async fn export_to_file( pool: &PgPool, params: ExportParams<'_>, master_key: Option<&[u8; 32]>, file_path: &str, format_override: Option<&str>, ) -> Result { let format = if let Some(f) = format_override { f.parse::()? } else { ExportFormat::from_extension(file_path).unwrap_or(ExportFormat::Json) }; let data = export(pool, params, master_key).await?; let count = data.entries.len(); let serialized = format.serialize(&data)?; std::fs::write(file_path, &serialized)?; Ok(count) } pub async fn export_to_string( pool: &PgPool, params: ExportParams<'_>, master_key: Option<&[u8; 32]>, format: &str, ) -> Result { let fmt = format.parse::()?; let data = export(pool, params, master_key).await?; fmt.serialize(&data) } // ── Build helpers for re-encoding values as CLI-style entries ───────────────── pub 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 } pub 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 } pub fn value_to_kv_entry(key: &str, value: &Value) -> String { match value { Value::String(s) => format!("{}={}", key, s), other => format!("{}:={}", key, other), } }