1use anyhow::Result;
11
12use crate::config::Config;
13use crate::output::{self, error, format_bytes, print_cid, print_header, print_kv, success};
14use crate::progress;
15
16pub async fn init_repo(data_dir: String) -> Result<()> {
18 use std::fs;
19
20 let path = std::path::Path::new(&data_dir);
21
22 if path.exists() {
23 if path.is_file() {
24 return Err(anyhow::anyhow!(
25 "Path exists as a file, not a directory: {}\nPlease choose a different location or remove the file.",
26 data_dir
27 ));
28 }
29
30 if path.join("config.toml").exists() {
32 output::warning(&format!("Repository already initialized at: {}", data_dir));
33 println!("\nTo reinitialize, first remove the existing repository:");
34 println!(" rm -rf {}", data_dir);
35 return Ok(());
36 }
37 }
38
39 let pb = progress::spinner("Initializing repository");
40
41 fs::create_dir_all(path.join("blocks"))?;
43 fs::create_dir_all(path.join("keystore"))?;
44 fs::create_dir_all(path.join("datastore"))?;
45
46 let config_path = path.join("config.toml");
48 let config_content = Config::generate_default_config();
49 fs::write(&config_path, config_content)?;
50
51 progress::finish_spinner_success(&pb, "Repository initialized");
52
53 success(&format!("Initialized IPFRS repository at: {}", data_dir));
54
55 println!();
56 print_header("Repository Structure");
57 print_kv("blocks", &path.join("blocks").display().to_string());
58 print_kv("keystore", &path.join("keystore").display().to_string());
59 print_kv("datastore", &path.join("datastore").display().to_string());
60 print_kv("config", &config_path.display().to_string());
61
62 println!();
63 output::print_section("Next Steps");
64 println!(" 1. Review configuration: {}", config_path.display());
65 println!(" 2. Start the daemon: ipfrs daemon");
66 println!(" 3. Add content: ipfrs add <file>");
67 println!();
68 output::info("Repository ready to use!");
69
70 Ok(())
71}
72
73pub async fn add_file(path: String, format: &str) -> Result<()> {
75 use bytes::Bytes;
76 use ipfrs_core::Block;
77 use ipfrs_storage::{BlockStoreConfig, BlockStoreTrait, SledBlockStore};
78
79 let file_path = std::path::Path::new(&path);
80
81 if !file_path.exists() {
83 return Err(anyhow::anyhow!(
84 "File not found: {}\nPlease check the path and try again.",
85 path
86 ));
87 }
88
89 if !file_path.is_file() {
91 return Err(anyhow::anyhow!(
92 "Path is not a file: {}\nTo add a directory, use 'ipfrs add -r <directory>'",
93 path
94 ));
95 }
96
97 let filename = file_path
98 .file_name()
99 .map(|s| s.to_string_lossy().to_string())
100 .unwrap_or_else(|| path.clone());
101
102 let metadata = tokio::fs::metadata(&path).await?;
104 let file_size = metadata.len();
105
106 const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; if file_size > LARGE_FILE_THRESHOLD {
109 output::warning(&format!(
110 "Large file detected: {}. This may take a while.",
111 format_bytes(file_size)
112 ));
113 }
114
115 let pb = progress::spinner(&format!("Reading {}", filename));
117
118 let data = tokio::fs::read(&path).await?;
120 let bytes_data = Bytes::from(data);
121
122 progress::finish_spinner_success(
123 &pb,
124 &format!("Read {} ({})", filename, format_bytes(file_size)),
125 );
126
127 let pb = progress::spinner("Creating block");
129 let block = Block::new(bytes_data)?;
130 let cid = *block.cid();
131 progress::finish_spinner_success(&pb, "Block created");
132
133 let config = BlockStoreConfig::default();
135 let store = SledBlockStore::new(config)?;
136
137 let pb = progress::spinner("Storing block");
139 store.put(&block).await?;
140 progress::finish_spinner_success(&pb, "Block stored");
141
142 match format {
143 "json" => {
144 println!("{{");
145 println!(" \"path\": \"{}\",", path);
146 println!(" \"cid\": \"{}\",", cid);
147 println!(" \"size\": {}", block.size());
148 println!("}}");
149 }
150 _ => {
151 success(&format!("Added {}", filename));
152 print_cid("CID", &cid.to_string());
153 print_kv("Size", &format_bytes(block.size()));
154 }
155 }
156
157 Ok(())
158}
159
160pub async fn get_file(cid_str: String, output: Option<String>) -> Result<()> {
162 use ipfrs_core::Cid;
163 use ipfrs_storage::{BlockStoreConfig, BlockStoreTrait, SledBlockStore};
164 use tokio::fs;
165
166 let cid = cid_str.parse::<Cid>().map_err(|e| {
168 anyhow::anyhow!(
169 "Invalid CID format: {}\n\nExpected format: QmXXXXXXXXXX or bafyXXXXXXXXXX",
170 e
171 )
172 })?;
173
174 let pb = progress::spinner(&format!("Retrieving {}", cid));
175
176 let config = BlockStoreConfig::default();
178 let store = SledBlockStore::new(config)?;
179
180 match store.get(&cid).await? {
182 Some(block) => {
183 progress::finish_spinner_success(&pb, "Block retrieved");
184
185 let output_path = output.unwrap_or_else(|| cid_str.clone());
186
187 if std::path::Path::new(&output_path).exists() {
189 output::warning(&format!("Overwriting existing file: {}", output_path));
190 }
191
192 fs::write(&output_path, block.data()).await?;
193 success(&format!("Saved to: {}", output_path));
194 print_kv("Size", &format_bytes(block.size()));
195 Ok(())
196 }
197 None => {
198 progress::finish_spinner_error(&pb, "Block not found");
199 Err(anyhow::anyhow!(
200 "Block not found: {}\n\nPossible reasons:\n • Content was never added to IPFRS\n • Content was garbage collected\n • Wrong CID format\n\nTry: ipfrs dht findprovs {} to find providers",
201 cid, cid
202 ))
203 }
204 }
205}
206
207pub async fn cat_file(cid_str: String) -> Result<()> {
209 use ipfrs_core::Cid;
210 use ipfrs_storage::{BlockStoreConfig, BlockStoreTrait, SledBlockStore};
211
212 let cid = cid_str.parse::<Cid>().map_err(|e| {
214 anyhow::anyhow!(
215 "Invalid CID format: {}\n\nExpected format: QmXXXXXXXXXX or bafyXXXXXXXXXX",
216 e
217 )
218 })?;
219
220 let config = BlockStoreConfig::default();
222 let store = SledBlockStore::new(config)?;
223
224 match store.get(&cid).await? {
226 Some(block) => {
227 use std::io::Write;
229 std::io::stdout().write_all(block.data())?;
230 std::io::stdout().flush()?;
231 Ok(())
232 }
233 None => Err(anyhow::anyhow!(
234 "Block not found: {}\n\nPossible reasons:\n • Content was never added to IPFRS\n • Content was garbage collected\n • Wrong CID format\n\nTry: ipfrs dht findprovs {} to find providers",
235 cid, cid
236 )),
237 }
238}
239
240#[derive(Debug)]
242pub struct DirectoryEntry {
243 pub name: String,
244 pub cid: String,
245 pub size: u64,
246 pub entry_type: String,
247}
248
249pub async fn ls_directory(cid_str: String, format: &str) -> Result<()> {
251 use ipfrs::{Node, NodeConfig};
252 use ipfrs_core::Cid;
253
254 let cid = cid_str
255 .parse::<Cid>()
256 .map_err(|e| anyhow::anyhow!("Invalid CID: {}", e))?;
257
258 let pb = progress::spinner(&format!("Listing directory {}", cid));
259 let mut node = Node::new(NodeConfig::default())?;
260 node.start().await?;
261
262 match node.dag_get(&cid).await? {
263 Some(ipld) => {
264 progress::finish_spinner_success(&pb, "Directory retrieved");
265
266 let entries = extract_directory_entries(&ipld)?;
268
269 match format {
270 "json" => {
271 println!("[");
272 for (i, entry) in entries.iter().enumerate() {
273 print!(" {{");
274 print!("\"name\": \"{}\", ", entry.name);
275 print!("\"cid\": \"{}\", ", entry.cid);
276 print!("\"size\": {}, ", entry.size);
277 print!("\"type\": \"{}\"", entry.entry_type);
278 print!("}}");
279 if i < entries.len() - 1 {
280 println!(",");
281 } else {
282 println!();
283 }
284 }
285 println!("]");
286 }
287 _ => {
288 if entries.is_empty() {
289 output::info("Empty directory");
290 } else {
291 print_header(&format!("Directory: {}", cid));
292 for entry in entries {
293 println!(
294 " {} {} {}",
295 entry.entry_type,
296 format_bytes(entry.size),
297 entry.name
298 );
299 println!(" CID: {}", entry.cid);
300 }
301 }
302 }
303 }
304 }
305 None => {
306 progress::finish_spinner_error(&pb, "Directory not found");
307 error(&format!("Directory not found: {}", cid));
308 std::process::exit(1);
309 }
310 }
311
312 node.stop().await?;
313 Ok(())
314}
315
316pub fn extract_directory_entries(ipld: &ipfrs_core::Ipld) -> Result<Vec<DirectoryEntry>> {
318 use ipfrs_core::Ipld;
319
320 let mut entries = Vec::new();
321
322 match ipld {
324 Ipld::Map(map) => {
325 if let Some(Ipld::List(links)) = map.get("Links") {
327 for link in links {
328 if let Ipld::Map(link_map) = link {
329 let name = link_map
330 .get("Name")
331 .and_then(|v| match v {
332 Ipld::String(s) => Some(s.clone()),
333 _ => None,
334 })
335 .unwrap_or_else(|| "<unnamed>".to_string());
336
337 let cid = link_map
338 .get("Hash")
339 .and_then(|v| match v {
340 Ipld::Link(c) => Some(c.to_string()),
341 Ipld::String(s) => Some(s.clone()),
342 _ => None,
343 })
344 .unwrap_or_else(|| "<unknown>".to_string());
345
346 let size = link_map
347 .get("Size")
348 .and_then(|v| match v {
349 Ipld::Integer(n) => Some(*n as u64),
350 _ => None,
351 })
352 .unwrap_or(0);
353
354 let entry_type = if link_map.contains_key("Links") {
356 "dir"
357 } else {
358 "file"
359 };
360
361 entries.push(DirectoryEntry {
362 name,
363 cid,
364 size,
365 entry_type: entry_type.to_string(),
366 });
367 }
368 }
369 } else {
370 for (key, value) in map {
372 let (cid_str, size, entry_type) = match value {
373 Ipld::Link(c) => (c.to_string(), 0, "link"),
374 Ipld::Map(m) => {
375 let has_links = m.contains_key("Links");
376 let size = m
377 .get("Size")
378 .and_then(|v| match v {
379 Ipld::Integer(n) => Some(*n as u64),
380 _ => None,
381 })
382 .unwrap_or(0);
383 let typ = if has_links { "dir" } else { "file" };
384 ("<embedded>".to_string(), size, typ)
385 }
386 _ => (format!("{:?}", value), 0, "unknown"),
387 };
388
389 entries.push(DirectoryEntry {
390 name: key.clone(),
391 cid: cid_str,
392 size,
393 entry_type: entry_type.to_string(),
394 });
395 }
396 }
397 }
398 Ipld::List(list) => {
399 for (i, item) in list.iter().enumerate() {
401 let (cid_str, size, entry_type) = match item {
402 Ipld::Link(c) => (c.to_string(), 0, "link"),
403 Ipld::Map(m) => {
404 let has_links = m.contains_key("Links");
405 let size = m
406 .get("Size")
407 .and_then(|v| match v {
408 Ipld::Integer(n) => Some(*n as u64),
409 _ => None,
410 })
411 .unwrap_or(0);
412 let typ = if has_links { "dir" } else { "file" };
413 ("<embedded>".to_string(), size, typ)
414 }
415 _ => (format!("{:?}", item), 0, "unknown"),
416 };
417
418 entries.push(DirectoryEntry {
419 name: format!("item-{}", i),
420 cid: cid_str,
421 size,
422 entry_type: entry_type.to_string(),
423 });
424 }
425 }
426 _ => {
427 return Err(anyhow::anyhow!(
428 "Not a directory: expected Map or List structure"
429 ));
430 }
431 }
432
433 Ok(entries)
434}