//! `relicario import` — currently only LastPass CSV is supported. use std::path::PathBuf; use anyhow::{bail, Context, Result}; use crate::ImportAction; pub fn cmd_import(action: ImportAction) -> Result<()> { match action { ImportAction::Lastpass { csv } => cmd_import_lastpass(csv), } } fn cmd_import_lastpass(csv_path: PathBuf) -> Result<()> { use std::fs; use relicario_core::import_lastpass::parse_lastpass_csv; let csv_bytes = fs::read(&csv_path) .with_context(|| format!("failed to read CSV {}", csv_path.display()))?; let (items, warnings) = parse_lastpass_csv(&csv_bytes)?; if items.is_empty() { // Print all warnings so the user sees why nothing imported. for w in &warnings { print_warning(w); } bail!( "imported 0 items from {} — see warnings above", csv_path.display() ); } let vault = crate::session::UnlockedVault::unlock_interactive()?; let mut manifest = vault.load_manifest()?; let total = items.len(); let mut written_paths: Vec = Vec::with_capacity(items.len() + 1); for (idx, item) in items.iter().enumerate() { vault.save_item(item)?; manifest.upsert(item); written_paths.push(format!("items/{}.enc", item.id.as_str())); let n = idx + 1; if n % 50 == 0 || n == total { eprintln!("[{n}/{total}] importing..."); } } vault.after_manifest_change(&manifest)?; written_paths.push("manifest.enc".into()); let path_refs: Vec<&str> = written_paths.iter().map(String::as_str).collect(); let csv_filename = csv_path .file_name() .and_then(|s| s.to_str()) .unwrap_or("lastpass.csv"); super::commit_paths( &vault, &format!("import: {} items from LastPass ({})", total, csv_filename), &path_refs, )?; for w in &warnings { print_warning(w); } // Counts only true skips, not partial imports. Coupled by convention to // the parser's warning message strings: skip messages end in "— skipped", // partial-import messages say "imported without TOTP" / "imported without URL". // If a future warning uses the word "skipped" in any other sense, this filter // will need to switch to an enum tag (see ImportWarning::message). eprintln!( "Imported {}, skipped {} (see warnings above)", total, warnings.iter().filter(|w| w.message.contains("skipped")).count() ); Ok(()) } fn print_warning(w: &relicario_core::import_lastpass::ImportWarning) { let prefix = match &w.title { Some(t) => format!("row {} ({}):", w.row, t), None => format!("row {}:", w.row), }; eprintln!("warning: {prefix} {}", w.message); }