refactor(cli): move cmd_import into commands/import.rs

This commit is contained in:
adlee-was-taken
2026-05-06 19:37:35 -04:00
parent c2f3c35ac9
commit 615afd7483
3 changed files with 91 additions and 83 deletions

View File

@@ -0,0 +1,88 @@
//! `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<String> = 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.save_manifest(&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);
}

View File

@@ -9,6 +9,7 @@
pub mod backup; pub mod backup;
pub mod generate; pub mod generate;
pub mod get; pub mod get;
pub mod import;
pub mod init; pub mod init;
pub mod list; pub mod list;
pub mod rate; pub mod rate;

View File

@@ -12,7 +12,7 @@ mod session;
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::{bail, Context, Result}; use anyhow::{Context, Result};
use clap::{CommandFactory, Parser, Subcommand}; use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell}; use clap_complete::{generate, Shell};
@@ -438,7 +438,7 @@ fn main() -> Result<()> {
Commands::Purge { query } => commands::trash::cmd_purge(query), Commands::Purge { query } => commands::trash::cmd_purge(query),
Commands::Trash { action } => commands::trash::cmd_trash(action), Commands::Trash { action } => commands::trash::cmd_trash(action),
Commands::Backup { action } => commands::backup::cmd_backup(action), Commands::Backup { action } => commands::backup::cmd_backup(action),
Commands::Import { action } => cmd_import(action), Commands::Import { action } => commands::import::cmd_import(action),
Commands::Attach { query, file } => cmd_attach(query, file), Commands::Attach { query, file } => cmd_attach(query, file),
Commands::Attachments { query } => cmd_attachments(query), Commands::Attachments { query } => cmd_attachments(query),
Commands::Extract { query, aid, out } => cmd_extract(query, aid, out), Commands::Extract { query, aid, out } => cmd_extract(query, aid, out),
@@ -979,87 +979,6 @@ fn push_history(
}); });
} }
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<String> = 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.save_manifest(&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");
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);
}
fn cmd_attach(query: String, file: PathBuf) -> Result<()> { fn cmd_attach(query: String, file: PathBuf) -> Result<()> {
use std::fs; use std::fs;
use relicario_core::{encrypt_attachment, AttachmentRef}; use relicario_core::{encrypt_attachment, AttachmentRef};