1use std::fs::File;
4use std::io::{BufRead, BufReader, BufWriter, Write};
5use std::path::Path;
6
7use cyphr_storage::{CommitEntry, Genesis, load_principal_from_commits};
8
9use super::common::{extract_genesis_from_commits, parse_principal_genesis, parse_store};
10use crate::keystore::JsonKeyStore;
11use crate::{Cli, Error, OutputFormat};
12
13pub fn export(cli: &Cli, identity: &str, output: &Path) -> crate::Result<()> {
15 let store = parse_store(&cli.store)?;
16 let pr = parse_principal_genesis(identity)?;
17
18 let commits = store.get_commits(&pr)?;
20
21 if commits.is_empty() {
22 return Err(Error::Storage(
23 "no commits found for identity (genesis-only state cannot be exported)".into(),
24 ));
25 }
26
27 let file = File::create(output)?;
29 let mut writer = BufWriter::new(file);
30
31 for commit in &commits {
32 let line = serde_json::to_string(commit)?;
33 writeln!(writer, "{}", line)?;
34 }
35 writer.flush()?;
36
37 match cli.output {
38 OutputFormat::Json => {
39 let result = serde_json::json!({
40 "identity": identity,
41 "output": output.display().to_string(),
42 "commits": commits.len(),
43 });
44 println!("{}", serde_json::to_string_pretty(&result)?);
45 },
46 OutputFormat::Table => {
47 println!("Exported identity to {}", output.display());
48 println!(" identity: {identity}");
49 println!(" commits: {}", commits.len());
50 },
51 }
52
53 Ok(())
54}
55
56pub fn import(cli: &Cli, input: &Path) -> crate::Result<()> {
58 let keystore = JsonKeyStore::open(&cli.keystore)?;
59 let store = parse_store(&cli.store)?;
60
61 let file = File::open(input)?;
63 let reader = BufReader::new(file);
64 let mut commits: Vec<CommitEntry> = Vec::new();
65
66 for (line_num, line_result) in reader.lines().enumerate() {
67 let line = line_result?;
68 if line.trim().is_empty() {
69 continue;
70 }
71 let commit: CommitEntry = serde_json::from_str(&line)
72 .map_err(|e| Error::Storage(format!("line {}: {}", line_num + 1, e)))?;
73 commits.push(commit);
74 }
75
76 if commits.is_empty() {
77 return Err(Error::Storage("no commits found in file".into()));
78 }
79
80 let genesis = extract_genesis_from_commits(&commits, Some(&keystore))?;
82
83 let principal = load_principal_from_commits(genesis.clone(), &commits)?;
85 let pr = match principal.pg() {
87 Some(pr) => pr.clone(),
88 None => match &genesis {
89 Genesis::Implicit(k) => cyphr::PrincipalGenesis::from_bytes(k.tmb.as_bytes().to_vec()),
90 Genesis::Explicit(_) => {
91 return Err(Error::Storage(
92 "explicit genesis must establish a PR".into(),
93 ));
94 },
95 },
96 };
97
98 let existing = store.get_commits(&pr).unwrap_or_default();
100 if !existing.is_empty() {
101 use base64ct::{Base64UrlUnpadded, Encoding};
102 let pr_b64 = pr
103 .as_multihash()
104 .first_variant()
105 .map(Base64UrlUnpadded::encode_string)
106 .map_err(|e| Error::Storage(format!("PR empty: {e}")))?;
107 return Err(Error::Storage(format!(
108 "identity {} already exists in storage",
109 pr_b64
110 )));
111 }
112
113 for commit in &commits {
115 store.append_commit(&pr, commit)?;
116 }
117
118 match cli.output {
119 OutputFormat::Json => {
120 use coz::base64ct::{Base64UrlUnpadded, Encoding};
121 let pr_b64 = pr
122 .as_multihash()
123 .first_variant()
124 .map(Base64UrlUnpadded::encode_string)
125 .map_err(|e| Error::Storage(format!("PR empty: {e}")))?;
126 let result = serde_json::json!({
127 "identity": pr_b64,
128 "input": input.display().to_string(),
129 "commits": commits.len(),
130 "verified": true,
131 });
132 println!("{}", serde_json::to_string_pretty(&result)?);
133 },
134 OutputFormat::Table => {
135 use coz::base64ct::{Base64UrlUnpadded, Encoding};
136 let pr_b64 = pr
137 .as_multihash()
138 .first_variant()
139 .map(Base64UrlUnpadded::encode_string)
140 .map_err(|e| Error::Storage(format!("PR empty: {e}")))?;
141 println!("Imported identity from {}", input.display());
142 println!(" identity: {}", pr_b64);
143 println!(" commits: {}", commits.len());
144 println!(" verified: OK");
145 },
146 }
147
148 Ok(())
149}