Skip to main content

ipfrs_cli/commands/
file.rs

1//! File operation commands
2//!
3//! This module provides file-related operations:
4//! - `init_repo` - Initialize repository
5//! - `add_file` - Add file to IPFRS
6//! - `get_file` - Retrieve file from IPFRS
7//! - `cat_file` - Output file contents to stdout
8//! - `ls_directory` - List directory contents
9
10use 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
16/// Initialize IPFRS repository
17pub 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        // Check if already initialized
31        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    // Create directory structure
42    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    // Generate default configuration
47    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
73/// Add file to IPFRS
74pub 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    // Validate file exists
82    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    // Validate it's a file (not a directory)
90    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    // Get file size for progress
103    let metadata = tokio::fs::metadata(&path).await?;
104    let file_size = metadata.len();
105
106    // Warn about very large files that will consume significant memory.
107    const VERY_LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; // 100 MB
108    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    // For files >= 10 MB on a TTY, show a progress bar; otherwise use a spinner.
116    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    // Read file
124    let data = tokio::fs::read(&path).await?;
125    let bytes_data = Bytes::from(data);
126
127    // Advance the bar to completion (we read in one shot).
128    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    // Create block
139    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    // Initialize storage
145    let config = BlockStoreConfig::default();
146    let store = SledBlockStore::new(config)?;
147
148    // Store block
149    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
171/// Get file from IPFRS and save to disk.
172///
173/// `timeout_secs` bounds the entire block-fetch operation.  A value of `0`
174/// disables the timeout (waits indefinitely).
175pub 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    // Parse CID
182    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    // Initialize storage
192    let config = BlockStoreConfig::default();
193    let store = SledBlockStore::new(config)?;
194
195    // Retrieve block, wrapping with an optional timeout.
196    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            // Check if output file already exists
217            if std::path::Path::new(&output_path).exists() {
218                output::warning(&format!("Overwriting existing file: {}", output_path));
219            }
220
221            // Show a progress bar for large blocks being written to disk.
222            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
241/// Output file contents to stdout.
242///
243/// `timeout_secs` bounds the entire block-fetch operation.  A value of `0`
244/// disables the timeout (waits indefinitely).
245pub 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    // Parse CID
251    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    // Initialize storage
259    let config = BlockStoreConfig::default();
260    let store = SledBlockStore::new(config)?;
261
262    // Retrieve block, wrapping with an optional timeout.
263    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            // Write to stdout
280            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/// Directory entry for ls command
293#[derive(Debug)]
294pub struct DirectoryEntry {
295    pub name: String,
296    pub cid: String,
297    pub size: u64,
298    pub entry_type: String,
299}
300
301/// List directory contents
302pub 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            // Extract links from IPLD node (UnixFS directory structure)
319            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
368/// Extract directory entries from IPLD structure
369pub 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    // Try to extract links from the IPLD structure
375    match ipld {
376        Ipld::Map(map) => {
377            // Check if this is a UnixFS directory with "Links" field
378            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                        // Try to determine type from the structure
407                        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                // Fallback: treat all map entries as directory entries
423                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            // If it's a list, enumerate entries
452            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}