use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use cyphr_storage::{CommitEntry, Genesis, load_principal_from_commits};
use super::common::{extract_genesis_from_commits, parse_principal_genesis, parse_store};
use crate::keystore::JsonKeyStore;
use crate::{Cli, Error, OutputFormat};
pub fn export(cli: &Cli, identity: &str, output: &Path) -> crate::Result<()> {
let store = parse_store(&cli.store)?;
let pr = parse_principal_genesis(identity)?;
let commits = store.get_commits(&pr)?;
if commits.is_empty() {
return Err(Error::Storage(
"no commits found for identity (genesis-only state cannot be exported)".into(),
));
}
let file = File::create(output)?;
let mut writer = BufWriter::new(file);
for commit in &commits {
let line = serde_json::to_string(commit)?;
writeln!(writer, "{}", line)?;
}
writer.flush()?;
match cli.output {
OutputFormat::Json => {
let result = serde_json::json!({
"identity": identity,
"output": output.display().to_string(),
"commits": commits.len(),
});
println!("{}", serde_json::to_string_pretty(&result)?);
},
OutputFormat::Table => {
println!("Exported identity to {}", output.display());
println!(" identity: {identity}");
println!(" commits: {}", commits.len());
},
}
Ok(())
}
pub fn import(cli: &Cli, input: &Path) -> crate::Result<()> {
let keystore = JsonKeyStore::open(&cli.keystore)?;
let store = parse_store(&cli.store)?;
let file = File::open(input)?;
let reader = BufReader::new(file);
let mut commits: Vec<CommitEntry> = Vec::new();
for (line_num, line_result) in reader.lines().enumerate() {
let line = line_result?;
if line.trim().is_empty() {
continue;
}
let commit: CommitEntry = serde_json::from_str(&line)
.map_err(|e| Error::Storage(format!("line {}: {}", line_num + 1, e)))?;
commits.push(commit);
}
if commits.is_empty() {
return Err(Error::Storage("no commits found in file".into()));
}
let genesis = extract_genesis_from_commits(&commits, Some(&keystore))?;
let principal = load_principal_from_commits(genesis.clone(), &commits)?;
let pr = match principal.pg() {
Some(pr) => pr.clone(),
None => match &genesis {
Genesis::Implicit(k) => cyphr::PrincipalGenesis::from_bytes(k.tmb.as_bytes().to_vec()),
Genesis::Explicit(_) => {
return Err(Error::Storage(
"explicit genesis must establish a PR".into(),
));
},
},
};
let existing = store.get_commits(&pr).unwrap_or_default();
if !existing.is_empty() {
use base64ct::{Base64UrlUnpadded, Encoding};
let pr_b64 = pr
.as_multihash()
.first_variant()
.map(Base64UrlUnpadded::encode_string)
.map_err(|e| Error::Storage(format!("PR empty: {e}")))?;
return Err(Error::Storage(format!(
"identity {} already exists in storage",
pr_b64
)));
}
for commit in &commits {
store.append_commit(&pr, commit)?;
}
match cli.output {
OutputFormat::Json => {
use coz::base64ct::{Base64UrlUnpadded, Encoding};
let pr_b64 = pr
.as_multihash()
.first_variant()
.map(Base64UrlUnpadded::encode_string)
.map_err(|e| Error::Storage(format!("PR empty: {e}")))?;
let result = serde_json::json!({
"identity": pr_b64,
"input": input.display().to_string(),
"commits": commits.len(),
"verified": true,
});
println!("{}", serde_json::to_string_pretty(&result)?);
},
OutputFormat::Table => {
use coz::base64ct::{Base64UrlUnpadded, Encoding};
let pr_b64 = pr
.as_multihash()
.first_variant()
.map(Base64UrlUnpadded::encode_string)
.map_err(|e| Error::Storage(format!("PR empty: {e}")))?;
println!("Imported identity from {}", input.display());
println!(" identity: {}", pr_b64);
println!(" commits: {}", commits.len());
println!(" verified: OK");
},
}
Ok(())
}