Bump secrets-mcp to 0.3.8 (tag 0.3.7 already used). - Junction table entry_secrets; secrets user-scoped with type - Per-user unique secrets.name; link_secret_names on add - Manual migrations + migrate script; MCP/tool and Web updates Made-with: Cursor
121 lines
3.1 KiB
Rust
121 lines
3.1 KiB
Rust
use anyhow::Result;
|
|
use sqlx::PgPool;
|
|
use uuid::Uuid;
|
|
|
|
use crate::models::ExportFormat;
|
|
use crate::service::add::{AddParams, run as add_run};
|
|
use crate::service::export::{build_meta_entries, build_secret_entries};
|
|
|
|
#[derive(Debug, serde::Serialize)]
|
|
pub struct ImportSummary {
|
|
pub total: usize,
|
|
pub inserted: usize,
|
|
pub skipped: usize,
|
|
pub failed: usize,
|
|
pub dry_run: bool,
|
|
}
|
|
|
|
pub struct ImportParams<'a> {
|
|
pub file: &'a str,
|
|
pub force: bool,
|
|
pub dry_run: bool,
|
|
pub user_id: Option<Uuid>,
|
|
}
|
|
|
|
pub async fn run(
|
|
pool: &PgPool,
|
|
params: ImportParams<'_>,
|
|
master_key: &[u8; 32],
|
|
) -> Result<ImportSummary> {
|
|
let format = ExportFormat::from_extension(params.file)?;
|
|
let content = std::fs::read_to_string(params.file)
|
|
.map_err(|e| anyhow::anyhow!("Cannot read file '{}': {}", params.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 {
|
|
let exists: bool = sqlx::query_scalar(
|
|
"SELECT EXISTS(SELECT 1 FROM entries \
|
|
WHERE folder = $1 AND name = $2 AND user_id IS NOT DISTINCT FROM $3)",
|
|
)
|
|
.bind(&entry.folder)
|
|
.bind(&entry.name)
|
|
.bind(params.user_id)
|
|
.fetch_one(pool)
|
|
.await
|
|
.unwrap_or(false);
|
|
|
|
if exists && !params.force {
|
|
return Err(anyhow::anyhow!(
|
|
"Import aborted: conflict on '{}'",
|
|
entry.name
|
|
));
|
|
}
|
|
|
|
if params.dry_run {
|
|
if exists {
|
|
skipped += 1;
|
|
} else {
|
|
inserted += 1;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let secret_entries = build_secret_entries(entry.secrets.as_ref());
|
|
let meta_entries = build_meta_entries(&entry.metadata);
|
|
|
|
match add_run(
|
|
pool,
|
|
AddParams {
|
|
name: &entry.name,
|
|
folder: &entry.folder,
|
|
entry_type: &entry.entry_type,
|
|
notes: &entry.notes,
|
|
tags: &entry.tags,
|
|
meta_entries: &meta_entries,
|
|
secret_entries: &secret_entries,
|
|
link_secret_names: &[],
|
|
user_id: params.user_id,
|
|
},
|
|
master_key,
|
|
)
|
|
.await
|
|
{
|
|
Ok(_) => {
|
|
inserted += 1;
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(
|
|
name = entry.name,
|
|
error = %e,
|
|
"failed to import entry"
|
|
);
|
|
failed += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if failed > 0 {
|
|
return Err(anyhow::anyhow!("{} record(s) failed to import", failed));
|
|
}
|
|
|
|
Ok(ImportSummary {
|
|
total,
|
|
inserted,
|
|
skipped,
|
|
failed,
|
|
dry_run: params.dry_run,
|
|
})
|
|
}
|