- Rename namespace/kind to folder/type on entries, audit_log, and history tables; add notes. Unique key is (user_id, folder, name). - Service layer and MCP tools support name-first lookup with optional folder when multiple entries share the same name. - secrets_delete dry_run uses the same disambiguation as real deletes. - Add scripts/migrate-v0.3.0.sql for manual DB migration. Refresh README and AGENTS.md. Made-with: Cursor
141 lines
4.0 KiB
Rust
141 lines
4.0 KiB
Rust
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<Uuid>,
|
|
}
|
|
|
|
pub async fn export(
|
|
pool: &PgPool,
|
|
params: ExportParams<'_>,
|
|
master_key: Option<&[u8; 32]>,
|
|
) -> Result<ExportData> {
|
|
let entries = fetch_entries(
|
|
pool,
|
|
params.folder,
|
|
params.entry_type,
|
|
params.name,
|
|
params.tags,
|
|
params.query,
|
|
params.user_id,
|
|
)
|
|
.await?;
|
|
|
|
let entry_ids: Vec<Uuid> = entries.iter().map(|e| e.id).collect();
|
|
let secrets_map: HashMap<Uuid, Vec<_>> = if !params.no_secrets && !entry_ids.is_empty() {
|
|
fetch_secrets_for_entries(pool, &entry_ids).await?
|
|
} else {
|
|
HashMap::new()
|
|
};
|
|
|
|
let mut export_entries: Vec<ExportEntry> = 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.field_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<usize> {
|
|
let format = if let Some(f) = format_override {
|
|
f.parse::<ExportFormat>()?
|
|
} 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<String> {
|
|
let fmt = format.parse::<ExportFormat>()?;
|
|
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<String> {
|
|
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<String, Value>>) -> Vec<String> {
|
|
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),
|
|
}
|
|
}
|