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 large files
107    const LARGE_FILE_THRESHOLD: u64 = 100 * 1024 * 1024; // 100 MB
108    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    // Show progress spinner for reading
116    let pb = progress::spinner(&format!("Reading {}", filename));
117
118    // Read file
119    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    // Create block
128    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    // Initialize storage
134    let config = BlockStoreConfig::default();
135    let store = SledBlockStore::new(config)?;
136
137    // Store block
138    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
160/// Get file from IPFRS and save to disk
161pub 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    // Parse CID
167    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    // Initialize storage
177    let config = BlockStoreConfig::default();
178    let store = SledBlockStore::new(config)?;
179
180    // Retrieve block
181    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            // Check if output file already exists
188            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
207/// Output file contents to stdout
208pub async fn cat_file(cid_str: String) -> Result<()> {
209    use ipfrs_core::Cid;
210    use ipfrs_storage::{BlockStoreConfig, BlockStoreTrait, SledBlockStore};
211
212    // Parse CID
213    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    // Initialize storage
221    let config = BlockStoreConfig::default();
222    let store = SledBlockStore::new(config)?;
223
224    // Retrieve block
225    match store.get(&cid).await? {
226        Some(block) => {
227            // Write to stdout
228            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/// Directory entry for ls command
241#[derive(Debug)]
242pub struct DirectoryEntry {
243    pub name: String,
244    pub cid: String,
245    pub size: u64,
246    pub entry_type: String,
247}
248
249/// List directory contents
250pub 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            // Extract links from IPLD node (UnixFS directory structure)
267            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
316/// Extract directory entries from IPLD structure
317pub 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    // Try to extract links from the IPLD structure
323    match ipld {
324        Ipld::Map(map) => {
325            // Check if this is a UnixFS directory with "Links" field
326            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                        // Try to determine type from the structure
355                        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                // Fallback: treat all map entries as directory entries
371                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            // If it's a list, enumerate entries
400            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}