ipfrs_cli/commands/
config.rs

1//! Configuration management commands
2//!
3//! This module provides functions for managing IPFRS configuration:
4//! - `config_show` - Display current configuration
5//! - `config_export` - Export configuration to file
6//! - `config_import` - Import configuration from file
7//! - `config_edit` - Open configuration in editor
8
9use crate::config::Config;
10use crate::output::{self, error, print_kv, success};
11use anyhow::{Context, Result};
12
13/// Show current configuration
14pub async fn config_show(format: String) -> Result<()> {
15    let config = Config::load()?;
16
17    if format == "json" {
18        let json = serde_json::to_string_pretty(&config)?;
19        println!("{}", json);
20    } else {
21        println!("IPFRS Configuration");
22        println!("===================");
23        println!();
24
25        println!("General:");
26        println!("--------");
27        print_kv(
28            "  Data directory",
29            &config.general.data_dir.display().to_string(),
30        );
31        print_kv("  Log level", &config.general.log_level);
32        print_kv("  Color output", &config.general.color.to_string());
33        print_kv("  Default format", &config.general.format);
34        println!();
35
36        println!("Storage:");
37        println!("--------");
38        print_kv("  Blocks path", &config.storage.blocks_path);
39        print_kv("  Cache size", &format_bytes(config.storage.cache_size));
40        print_kv("  WAL enabled", &config.storage.wal_enabled.to_string());
41        print_kv("  GC interval", &format!("{}s", config.storage.gc_interval));
42        println!();
43
44        println!("Network:");
45        println!("--------");
46        print_kv(
47            "  Max connections",
48            &config.network.max_connections.to_string(),
49        );
50        println!("  Listen addresses:");
51        for addr in &config.network.listen_addrs {
52            println!("    - {}", addr);
53        }
54        println!();
55
56        println!("Gateway:");
57        println!("--------");
58        print_kv("  Listen address", &config.gateway.listen_addr);
59        println!();
60
61        println!("API:");
62        println!("----");
63        print_kv("  Listen address", &config.api.listen_addr);
64        print_kv("  Auth enabled", &config.api.auth_enabled.to_string());
65        print_kv("  Timeout", &format!("{}s", config.api.timeout));
66        if let Some(ref remote_url) = config.api.remote_url {
67            print_kv("  Remote URL", remote_url);
68        }
69        println!();
70    }
71
72    Ok(())
73}
74
75/// Export configuration to file
76pub async fn config_export(output: String, format: String) -> Result<()> {
77    let config = Config::load()?;
78
79    // Export configuration
80    let content = if format == "json" {
81        serde_json::to_string_pretty(&config)?
82    } else if format == "toml" {
83        toml::to_string_pretty(&config)?
84    } else if format == "yaml" {
85        serde_yaml::to_string(&config)?
86    } else {
87        anyhow::bail!(
88            "Unsupported export format: {}. Use json, toml, or yaml.",
89            format
90        );
91    };
92
93    let content_len = content.len();
94
95    std::fs::write(&output, content)
96        .with_context(|| format!("Failed to write configuration to {}", output))?;
97
98    success(&format!("Configuration exported to {}", output));
99    print_kv("Format", &format);
100    print_kv("Size", &format!("{} bytes", content_len));
101
102    Ok(())
103}
104
105/// Import configuration from file
106pub async fn config_import(input: String, dry_run: bool) -> Result<()> {
107    // Read input file
108    let content = std::fs::read_to_string(&input)
109        .with_context(|| format!("Failed to read configuration from {}", input))?;
110
111    // Detect format from file extension
112    let format = if input.ends_with(".json") {
113        "json"
114    } else if input.ends_with(".toml") {
115        "toml"
116    } else if input.ends_with(".yaml") || input.ends_with(".yml") {
117        "yaml"
118    } else {
119        // Try to auto-detect
120        if content.trim().starts_with('{') {
121            "json"
122        } else if content.contains('[') && content.contains(']') {
123            "toml"
124        } else {
125            "yaml"
126        }
127    };
128
129    // Parse configuration
130    let config: Config = match format {
131        "json" => serde_json::from_str(&content)?,
132        "toml" => toml::from_str(&content)?,
133        "yaml" => serde_yaml::from_str(&content)?,
134        _ => anyhow::bail!("Could not detect configuration format"),
135    };
136
137    if dry_run {
138        output::info("Dry run mode - configuration validation only");
139        success("✓ Configuration is valid");
140        print_kv("Format detected", format);
141        println!();
142        println!("Configuration summary:");
143        print_kv(
144            "  Data directory",
145            &config.general.data_dir.display().to_string(),
146        );
147        print_kv("  Log level", &config.general.log_level);
148        print_kv(
149            "  Max connections",
150            &config.network.max_connections.to_string(),
151        );
152        println!();
153        output::info("Use without --dry-run to apply the configuration");
154    } else {
155        // Get default config path
156        let config_path = Config::default_path()?;
157
158        // Backup existing config
159        if config_path.exists() {
160            let backup_path = config_path.with_extension("toml.backup");
161            std::fs::copy(&config_path, &backup_path)?;
162            output::info(&format!("Backed up existing config to {:?}", backup_path));
163        }
164
165        // Write new configuration
166        let toml_content = toml::to_string_pretty(&config)?;
167        std::fs::write(&config_path, toml_content)?;
168
169        success("Configuration imported successfully");
170        print_kv("Source", &input);
171        print_kv("Destination", &config_path.display().to_string());
172        print_kv("Format", format);
173        println!();
174        output::info("Restart the daemon to apply changes: ipfrs daemon restart");
175    }
176
177    Ok(())
178}
179
180/// Edit configuration file
181pub async fn config_edit() -> Result<()> {
182    let config_path = Config::default_path()?;
183
184    if !config_path.exists() {
185        error("Configuration file does not exist");
186        println!();
187        output::info("Initialize a new configuration:");
188        println!("  ipfrs init");
189        return Ok(());
190    }
191
192    // Get editor from environment or use default
193    let editor = std::env::var("EDITOR")
194        .or_else(|_| std::env::var("VISUAL"))
195        .unwrap_or_else(|_| "vi".to_string());
196
197    println!("Opening configuration in {}...", editor);
198    print_kv("Config file", &config_path.display().to_string());
199    println!();
200
201    // Open editor
202    let status = std::process::Command::new(&editor)
203        .arg(&config_path)
204        .status()
205        .with_context(|| format!("Failed to open editor: {}", editor))?;
206
207    if status.success() {
208        success("Configuration editor closed");
209        println!();
210        output::info("Restart the daemon to apply changes: ipfrs daemon restart");
211    } else {
212        error("Editor exited with error");
213    }
214
215    Ok(())
216}
217
218/// Helper function to format bytes
219fn format_bytes(bytes: u64) -> String {
220    const KB: u64 = 1024;
221    const MB: u64 = KB * 1024;
222    const GB: u64 = MB * 1024;
223
224    if bytes >= GB {
225        format!("{:.2} GB", bytes as f64 / GB as f64)
226    } else if bytes >= MB {
227        format!("{:.2} MB", bytes as f64 / MB as f64)
228    } else if bytes >= KB {
229        format!("{:.2} KB", bytes as f64 / KB as f64)
230    } else {
231        format!("{} bytes", bytes)
232    }
233}