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 VERY_LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; if file_size > VERY_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 read_pb = progress::file_progress_bar(file_size, "Reading");
117 let spinner_pb = if read_pb.is_hidden() {
118 Some(progress::spinner(&format!("Reading {}", filename)))
119 } else {
120 None
121 };
122
123 let data = tokio::fs::read(&path).await?;
125 let bytes_data = Bytes::from(data);
126
127 read_pb.inc(file_size);
129 read_pb.finish_and_clear();
130
131 if let Some(ref pb) = spinner_pb {
132 progress::finish_spinner_success(
133 pb,
134 &format!("Read {} ({})", filename, format_bytes(file_size)),
135 );
136 }
137
138 let pb = progress::spinner("Creating block");
140 let block = Block::new(bytes_data)?;
141 let cid = *block.cid();
142 progress::finish_spinner_success(&pb, "Block created");
143
144 let config = BlockStoreConfig::default();
146 let store = SledBlockStore::new(config)?;
147
148 let pb = progress::spinner("Storing block");
150 store.put(&block).await?;
151 progress::finish_spinner_success(&pb, "Block stored");
152
153 match format {
154 "json" => {
155 println!("{{");
156 println!(" \"path\": \"{}\",", path);
157 println!(" \"cid\": \"{}\",", cid);
158 println!(" \"size\": {}", block.size());
159 println!("}}");
160 }
161 _ => {
162 success(&format!("Added {}", filename));
163 print_cid("CID", &cid.to_string());
164 print_kv("Size", &format_bytes(block.size()));
165 }
166 }
167
168 Ok(())
169}
170
171pub async fn get_file(cid_str: String, output: Option<String>, timeout_secs: u64) -> Result<()> {
176 use ipfrs_core::Cid;
177 use ipfrs_storage::{BlockStoreConfig, BlockStoreTrait, SledBlockStore};
178 use std::time::Duration;
179 use tokio::fs;
180
181 let cid = cid_str.parse::<Cid>().map_err(|e| {
183 anyhow::anyhow!(
184 "Invalid CID format: {}\n\nExpected format: QmXXXXXXXXXX or bafyXXXXXXXXXX",
185 e
186 )
187 })?;
188
189 let pb = progress::spinner(&format!("Retrieving {}", cid));
190
191 let config = BlockStoreConfig::default();
193 let store = SledBlockStore::new(config)?;
194
195 let fetch_result = if timeout_secs > 0 {
197 tokio::time::timeout(Duration::from_secs(timeout_secs), store.get(&cid))
198 .await
199 .map_err(|_| {
200 anyhow::anyhow!(
201 "Timeout after {}s fetching {}\n\nThe block may be available on the network but \
202 is unreachable right now.\nTry increasing --timeout or checking connectivity.",
203 timeout_secs, cid
204 )
205 })??
206 } else {
207 store.get(&cid).await?
208 };
209
210 match fetch_result {
211 Some(block) => {
212 progress::finish_spinner_success(&pb, "Block retrieved");
213
214 let output_path = output.unwrap_or_else(|| cid_str.clone());
215
216 if std::path::Path::new(&output_path).exists() {
218 output::warning(&format!("Overwriting existing file: {}", output_path));
219 }
220
221 let write_pb = progress::file_progress_bar(block.size(), "Saving");
223 fs::write(&output_path, block.data()).await?;
224 write_pb.inc(block.size());
225 write_pb.finish_and_clear();
226
227 success(&format!("Saved to: {}", output_path));
228 print_kv("Size", &format_bytes(block.size()));
229 Ok(())
230 }
231 None => {
232 progress::finish_spinner_error(&pb, "Block not found");
233 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
241pub async fn cat_file(cid_str: String, timeout_secs: u64) -> Result<()> {
246 use ipfrs_core::Cid;
247 use ipfrs_storage::{BlockStoreConfig, BlockStoreTrait, SledBlockStore};
248 use std::time::Duration;
249
250 let cid = cid_str.parse::<Cid>().map_err(|e| {
252 anyhow::anyhow!(
253 "Invalid CID format: {}\n\nExpected format: QmXXXXXXXXXX or bafyXXXXXXXXXX",
254 e
255 )
256 })?;
257
258 let config = BlockStoreConfig::default();
260 let store = SledBlockStore::new(config)?;
261
262 let fetch_result = if timeout_secs > 0 {
264 tokio::time::timeout(Duration::from_secs(timeout_secs), store.get(&cid))
265 .await
266 .map_err(|_| {
267 anyhow::anyhow!(
268 "Timeout after {}s fetching {}\n\nThe block may be available on the network but \
269 is unreachable right now.\nTry increasing --timeout or checking connectivity.",
270 timeout_secs, cid
271 )
272 })??
273 } else {
274 store.get(&cid).await?
275 };
276
277 match fetch_result {
278 Some(block) => {
279 use std::io::Write;
281 std::io::stdout().write_all(block.data())?;
282 std::io::stdout().flush()?;
283 Ok(())
284 }
285 None => Err(anyhow::anyhow!(
286 "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",
287 cid, cid
288 )),
289 }
290}
291
292#[derive(Debug)]
294pub struct DirectoryEntry {
295 pub name: String,
296 pub cid: String,
297 pub size: u64,
298 pub entry_type: String,
299}
300
301pub async fn ls_directory(cid_str: String, format: &str) -> Result<()> {
303 use ipfrs::{Node, NodeConfig};
304 use ipfrs_core::Cid;
305
306 let cid = cid_str
307 .parse::<Cid>()
308 .map_err(|e| anyhow::anyhow!("Invalid CID: {}", e))?;
309
310 let pb = progress::spinner(&format!("Listing directory {}", cid));
311 let mut node = Node::new(NodeConfig::default())?;
312 node.start().await?;
313
314 match node.dag_get(&cid).await? {
315 Some(ipld) => {
316 progress::finish_spinner_success(&pb, "Directory retrieved");
317
318 let entries = extract_directory_entries(&ipld)?;
320
321 match format {
322 "json" => {
323 println!("[");
324 for (i, entry) in entries.iter().enumerate() {
325 print!(" {{");
326 print!("\"name\": \"{}\", ", entry.name);
327 print!("\"cid\": \"{}\", ", entry.cid);
328 print!("\"size\": {}, ", entry.size);
329 print!("\"type\": \"{}\"", entry.entry_type);
330 print!("}}");
331 if i < entries.len() - 1 {
332 println!(",");
333 } else {
334 println!();
335 }
336 }
337 println!("]");
338 }
339 _ => {
340 if entries.is_empty() {
341 output::info("Empty directory");
342 } else {
343 print_header(&format!("Directory: {}", cid));
344 for entry in entries {
345 println!(
346 " {} {} {}",
347 entry.entry_type,
348 format_bytes(entry.size),
349 entry.name
350 );
351 println!(" CID: {}", entry.cid);
352 }
353 }
354 }
355 }
356 }
357 None => {
358 progress::finish_spinner_error(&pb, "Directory not found");
359 error(&format!("Directory not found: {}", cid));
360 std::process::exit(1);
361 }
362 }
363
364 node.stop().await?;
365 Ok(())
366}
367
368pub fn extract_directory_entries(ipld: &ipfrs_core::Ipld) -> Result<Vec<DirectoryEntry>> {
370 use ipfrs_core::Ipld;
371
372 let mut entries = Vec::new();
373
374 match ipld {
376 Ipld::Map(map) => {
377 if let Some(Ipld::List(links)) = map.get("Links") {
379 for link in links {
380 if let Ipld::Map(link_map) = link {
381 let name = link_map
382 .get("Name")
383 .and_then(|v| match v {
384 Ipld::String(s) => Some(s.clone()),
385 _ => None,
386 })
387 .unwrap_or_else(|| "<unnamed>".to_string());
388
389 let cid = link_map
390 .get("Hash")
391 .and_then(|v| match v {
392 Ipld::Link(c) => Some(c.to_string()),
393 Ipld::String(s) => Some(s.clone()),
394 _ => None,
395 })
396 .unwrap_or_else(|| "<unknown>".to_string());
397
398 let size = link_map
399 .get("Size")
400 .and_then(|v| match v {
401 Ipld::Integer(n) => Some(*n as u64),
402 _ => None,
403 })
404 .unwrap_or(0);
405
406 let entry_type = if link_map.contains_key("Links") {
408 "dir"
409 } else {
410 "file"
411 };
412
413 entries.push(DirectoryEntry {
414 name,
415 cid,
416 size,
417 entry_type: entry_type.to_string(),
418 });
419 }
420 }
421 } else {
422 for (key, value) in map {
424 let (cid_str, size, entry_type) = match value {
425 Ipld::Link(c) => (c.to_string(), 0, "link"),
426 Ipld::Map(m) => {
427 let has_links = m.contains_key("Links");
428 let size = m
429 .get("Size")
430 .and_then(|v| match v {
431 Ipld::Integer(n) => Some(*n as u64),
432 _ => None,
433 })
434 .unwrap_or(0);
435 let typ = if has_links { "dir" } else { "file" };
436 ("<embedded>".to_string(), size, typ)
437 }
438 _ => (format!("{:?}", value), 0, "unknown"),
439 };
440
441 entries.push(DirectoryEntry {
442 name: key.clone(),
443 cid: cid_str,
444 size,
445 entry_type: entry_type.to_string(),
446 });
447 }
448 }
449 }
450 Ipld::List(list) => {
451 for (i, item) in list.iter().enumerate() {
453 let (cid_str, size, entry_type) = match item {
454 Ipld::Link(c) => (c.to_string(), 0, "link"),
455 Ipld::Map(m) => {
456 let has_links = m.contains_key("Links");
457 let size = m
458 .get("Size")
459 .and_then(|v| match v {
460 Ipld::Integer(n) => Some(*n as u64),
461 _ => None,
462 })
463 .unwrap_or(0);
464 let typ = if has_links { "dir" } else { "file" };
465 ("<embedded>".to_string(), size, typ)
466 }
467 _ => (format!("{:?}", item), 0, "unknown"),
468 };
469
470 entries.push(DirectoryEntry {
471 name: format!("item-{}", i),
472 cid: cid_str,
473 size,
474 entry_type: entry_type.to_string(),
475 });
476 }
477 }
478 _ => {
479 return Err(anyhow::anyhow!(
480 "Not a directory: expected Map or List structure"
481 ));
482 }
483 }
484
485 Ok(entries)
486}