1use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use camino::Utf8PathBuf;
7
8use crate::collab::Identity;
9use crate::crypto::KeyVault;
10use crate::support::void_context::{
11 CryptoContext, NetworkConfig, RepoPaths, RepoMeta, SealConfig,
12};
13use crate::{config, Result, VoidContext, VoidError};
14
15use super::Repo;
16
17pub struct RepoBuilder {
22 path: PathBuf,
23 key: Option<[u8; 32]>,
24 content_key: Option<[u8; 32]>,
25 token: Option<crate::crypto::machine_token::MachineToken>,
26 identity: Option<Identity>,
27 signing_key: Option<ed25519_dalek::SigningKey>,
28 target_shard_size: Option<u64>,
29 remote: Option<Arc<dyn crate::store::RemoteStore>>,
30}
31
32impl RepoBuilder {
33 pub(crate) fn new(path: impl AsRef<Path>) -> Self {
34 Self {
35 path: path.as_ref().to_path_buf(),
36 key: None,
37 content_key: None,
38 token: None,
39 identity: None,
40 signing_key: None,
41 target_shard_size: None,
42 remote: None,
43 }
44 }
45
46 pub fn key(mut self, key: &[u8; 32]) -> Self {
48 self.key = Some(*key);
49 self
50 }
51
52 pub fn content_key(mut self, key: &[u8; 32]) -> Self {
54 self.content_key = Some(*key);
55 self
56 }
57
58 pub fn token(mut self, token: crate::crypto::machine_token::MachineToken) -> Self {
60 self.token = Some(token);
61 self
62 }
63
64 pub fn identity(mut self, identity: Identity) -> Self {
69 self.identity = Some(identity);
70 self
71 }
72
73 pub fn signing_key(mut self, key: ed25519_dalek::SigningKey) -> Self {
75 self.signing_key = Some(key);
76 self
77 }
78
79 pub fn target_shard_size(mut self, size: u64) -> Self {
81 self.target_shard_size = Some(size);
82 self
83 }
84
85 pub fn remote(mut self, remote: Arc<dyn crate::store::RemoteStore>) -> Self {
91 self.remote = Some(remote);
92 self
93 }
94
95 pub fn build(self) -> Result<Repo> {
97 let void_dir = find_void_dir(&self.path)?;
99 let root = void_dir
100 .parent()
101 .ok_or_else(|| VoidError::NotFound("void_dir has no parent".into()))?
102 .to_path_buf();
103
104 let key_sources = [
106 self.key.is_some(),
107 self.content_key.is_some(),
108 self.token.is_some(),
109 self.identity.is_some(),
110 ]
111 .iter()
112 .filter(|x| **x)
113 .count();
114
115 if key_sources == 0 {
116 return Err(VoidError::Unauthorized(
117 "no key provided — use key(), content_key(), token(), or identity()".into(),
118 ));
119 }
120 if key_sources > 1 {
121 return Err(VoidError::Unauthorized(
122 "multiple key sources provided — use exactly one".into(),
123 ));
124 }
125
126 let (vault, signing_key) = if let Some(key) = self.key {
127 let vault = KeyVault::new(key)?;
128 (vault, self.signing_key.map(Arc::new))
129 } else if let Some(ck) = self.content_key {
130 let vault = KeyVault::from_content_key_bytes(ck);
131 (vault, self.signing_key.map(Arc::new))
132 } else if let Some(ref token) = self.token {
133 let vault = token.to_vault()?;
134 let sk = token.to_signing_key()?;
135 (vault, Some(Arc::new(sk)))
136 } else if let Some(ref identity) = self.identity {
137 let repo_key = crate::collab::manifest::load_repo_key(&void_dir, Some(identity))?;
139 let vault = KeyVault::new(*repo_key.as_bytes())?;
140 let sk = identity.signing_key().clone();
141 (vault, Some(Arc::new(sk)))
142 } else {
143 unreachable!()
144 };
145
146 let vault = Arc::new(vault);
147
148 let root_utf8 = Utf8PathBuf::try_from(root)
150 .map_err(|e| VoidError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
151 let void_dir_utf8 = Utf8PathBuf::try_from(void_dir)
152 .map_err(|e| VoidError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
153
154 let cfg = config::load(void_dir_utf8.as_std_path())?;
155
156 let secret = config::load_repo_secret(void_dir_utf8.as_std_path(), &vault)
157 .unwrap_or_else(|_| crate::crypto::RepoSecret::new([0u8; 32]));
158
159 let mut seal_cfg = SealConfig::from(&cfg);
160 if let Some(size) = self.target_shard_size {
161 seal_cfg.target_shard_size = size;
162 seal_cfg.max_shard_size = size * 3 / 2;
163 }
164
165 let ctx = VoidContext {
166 paths: RepoPaths {
167 root: root_utf8,
168 void_dir: void_dir_utf8.clone(),
169 workspace_dir: void_dir_utf8,
170 },
171 crypto: CryptoContext {
172 vault,
173 epoch: 0,
174 signing_key,
175 nostr_pubkey: None,
176 },
177 repo: RepoMeta {
178 id: cfg.repo_id.clone(),
179 name: cfg.repo_name.clone(),
180 secret,
181 },
182 seal: seal_cfg,
183 network: NetworkConfig::from(&cfg),
184 user: cfg.user.clone(),
185 };
186
187 Ok(Repo::from_context_with_remote(ctx, self.remote))
188 }
189}
190
191fn find_void_dir(start: &Path) -> Result<PathBuf> {
193 let mut current = if start.is_absolute() {
194 start.to_path_buf()
195 } else {
196 std::env::current_dir()
197 .map_err(|e| VoidError::Io(e))?
198 .join(start)
199 };
200
201 loop {
202 let candidate = current.join(".void");
203 if candidate.is_dir() {
204 return Ok(candidate);
205 }
206 if !current.pop() {
207 return Err(VoidError::NotFound(
208 "no .void directory found in any parent directory".into(),
209 ));
210 }
211 }
212}