1#![allow(dead_code)]
6
7use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9use std::path::{Path, PathBuf};
10use std::sync::OnceLock;
11
12static CACHED_CONFIG: OnceLock<Config> = OnceLock::new();
14
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
17pub struct Config {
18 #[serde(default)]
20 pub general: GeneralConfig,
21
22 #[serde(default)]
24 pub storage: StorageConfig,
25
26 #[serde(default)]
28 pub network: NetworkConfig,
29
30 #[serde(default)]
32 pub gateway: GatewayConfig,
33
34 #[serde(default)]
36 pub api: ApiConfig,
37
38 #[serde(default)]
40 pub shell: ShellConfig,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct GeneralConfig {
46 #[serde(default = "default_data_dir")]
48 pub data_dir: PathBuf,
49
50 #[serde(default = "default_log_level")]
52 pub log_level: String,
53
54 #[serde(default = "default_true")]
56 pub color: bool,
57
58 #[serde(default = "default_format")]
60 pub format: String,
61}
62
63impl Default for GeneralConfig {
64 fn default() -> Self {
65 Self {
66 data_dir: default_data_dir(),
67 log_level: default_log_level(),
68 color: true,
69 format: default_format(),
70 }
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct StorageConfig {
77 #[serde(default = "default_blocks_path")]
79 pub blocks_path: String,
80
81 #[serde(default = "default_cache_size")]
83 pub cache_size: u64,
84
85 #[serde(default = "default_true")]
87 pub wal_enabled: bool,
88
89 #[serde(default = "default_gc_interval")]
91 pub gc_interval: u64,
92}
93
94impl Default for StorageConfig {
95 fn default() -> Self {
96 Self {
97 blocks_path: default_blocks_path(),
98 cache_size: default_cache_size(),
99 wal_enabled: true,
100 gc_interval: default_gc_interval(),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct NetworkConfig {
108 #[serde(default = "default_listen_addrs")]
110 pub listen_addrs: Vec<String>,
111
112 #[serde(default)]
114 pub bootstrap_peers: Vec<String>,
115
116 #[serde(default = "default_max_connections")]
118 pub max_connections: u32,
119
120 #[serde(default = "default_true")]
122 pub dht_enabled: bool,
123
124 #[serde(default = "default_true")]
126 pub mdns_enabled: bool,
127
128 #[serde(default = "default_timeout")]
130 pub timeout: u64,
131}
132
133impl Default for NetworkConfig {
134 fn default() -> Self {
135 Self {
136 listen_addrs: default_listen_addrs(),
137 bootstrap_peers: Vec::new(),
138 max_connections: default_max_connections(),
139 dht_enabled: true,
140 mdns_enabled: true,
141 timeout: default_timeout(),
142 }
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct GatewayConfig {
149 #[serde(default = "default_gateway_addr")]
151 pub listen_addr: String,
152
153 #[serde(default = "default_true")]
155 pub cors_enabled: bool,
156
157 #[serde(default)]
159 pub cors_origins: Vec<String>,
160
161 #[serde(default)]
163 pub tls_enabled: bool,
164
165 #[serde(default)]
167 pub tls_cert_path: Option<String>,
168
169 #[serde(default)]
171 pub tls_key_path: Option<String>,
172}
173
174impl Default for GatewayConfig {
175 fn default() -> Self {
176 Self {
177 listen_addr: default_gateway_addr(),
178 cors_enabled: true,
179 cors_origins: Vec::new(),
180 tls_enabled: false,
181 tls_cert_path: None,
182 tls_key_path: None,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct ApiConfig {
190 #[serde(default = "default_api_addr")]
192 pub listen_addr: String,
193
194 #[serde(default)]
198 pub remote_url: Option<String>,
199
200 #[serde(default)]
202 pub auth_enabled: bool,
203
204 #[serde(default)]
206 pub api_token: Option<String>,
207
208 #[serde(default = "default_api_timeout")]
210 pub timeout: u64,
211}
212
213impl Default for ApiConfig {
214 fn default() -> Self {
215 Self {
216 listen_addr: default_api_addr(),
217 remote_url: None,
218 auth_enabled: false,
219 api_token: None,
220 timeout: default_api_timeout(),
221 }
222 }
223}
224
225fn default_data_dir() -> PathBuf {
227 PathBuf::from(".ipfrs")
228}
229
230fn default_log_level() -> String {
231 "info".to_string()
232}
233
234fn default_true() -> bool {
235 true
236}
237
238fn default_format() -> String {
239 "text".to_string()
240}
241
242fn default_blocks_path() -> String {
243 "blocks".to_string()
244}
245
246fn default_cache_size() -> u64 {
247 100 * 1024 * 1024 }
249
250fn default_gc_interval() -> u64 {
251 3600 }
253
254fn default_listen_addrs() -> Vec<String> {
255 vec![
256 "/ip4/0.0.0.0/tcp/4001".to_string(),
257 "/ip6/::/tcp/4001".to_string(),
258 ]
259}
260
261fn default_max_connections() -> u32 {
262 256
263}
264
265fn default_timeout() -> u64 {
266 30
267}
268
269fn default_gateway_addr() -> String {
270 "127.0.0.1:8080".to_string()
271}
272
273fn default_api_addr() -> String {
274 "127.0.0.1:5001".to_string()
275}
276
277fn default_api_timeout() -> u64 {
278 60 }
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct ShellConfig {
284 #[serde(default)]
286 pub aliases: std::collections::HashMap<String, String>,
287
288 #[serde(default = "default_true")]
290 pub hints_enabled: bool,
291
292 #[serde(default = "default_true")]
294 pub highlighting_enabled: bool,
295
296 #[serde(default = "default_history_size")]
298 pub history_size: usize,
299}
300
301impl Default for ShellConfig {
302 fn default() -> Self {
303 Self {
304 aliases: std::collections::HashMap::new(),
305 hints_enabled: true,
306 highlighting_enabled: true,
307 history_size: default_history_size(),
308 }
309 }
310}
311
312fn default_history_size() -> usize {
313 1000
314}
315
316impl Config {
317 pub fn load() -> Result<Self> {
327 Ok(CACHED_CONFIG
328 .get_or_init(|| Self::load_uncached().unwrap_or_default())
329 .clone())
330 }
331
332 pub fn load_uncached() -> Result<Self> {
347 let local_config = PathBuf::from(".ipfrs/config.toml");
349 let mut config = if local_config.exists() {
350 Self::load_from(&local_config)?
351 } else if let Some(home) = dirs::home_dir() {
352 let user_config = home.join(".ipfrs/config.toml");
354 if user_config.exists() {
355 Self::load_from(&user_config)?
356 } else {
357 let system_config = PathBuf::from("/etc/ipfrs/config.toml");
359 if system_config.exists() {
360 Self::load_from(&system_config)?
361 } else {
362 Self::default()
363 }
364 }
365 } else {
366 Self::default()
367 };
368
369 config.apply_env_overrides();
371
372 Ok(config)
373 }
374
375 fn apply_env_overrides(&mut self) {
383 use std::env;
384
385 if let Ok(path) = env::var("IPFRS_PATH") {
387 self.general.data_dir = PathBuf::from(path);
388 }
389
390 if let Ok(level) = env::var("IPFRS_LOG_LEVEL") {
392 self.general.log_level = level;
393 }
394
395 if let Ok(url) = env::var("IPFRS_API_URL") {
397 self.api.remote_url = Some(url);
398 }
399
400 if let Ok(token) = env::var("IPFRS_API_TOKEN") {
402 self.api.api_token = Some(token);
403 self.api.auth_enabled = true;
404 }
405 }
406
407 pub fn api_url(&self) -> String {
412 self.api
413 .remote_url
414 .clone()
415 .unwrap_or_else(|| format!("http://{}", self.api.listen_addr))
416 }
417
418 pub fn is_remote(&self) -> bool {
420 self.api.remote_url.is_some()
421 }
422
423 pub fn clear_cache() {
429 }
432
433 pub fn load_from(path: &Path) -> Result<Self> {
435 let content = std::fs::read_to_string(path)
436 .with_context(|| format!("Failed to read config file: {}", path.display()))?;
437
438 let config: Config = toml::from_str(&content)
439 .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
440
441 Ok(config)
442 }
443
444 pub fn save(&self, path: &Path) -> Result<()> {
446 let content = toml::to_string_pretty(self).context("Failed to serialize config")?;
447
448 if let Some(parent) = path.parent() {
450 std::fs::create_dir_all(parent)
451 .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
452 }
453
454 std::fs::write(path, content)
455 .with_context(|| format!("Failed to write config file: {}", path.display()))?;
456
457 Ok(())
458 }
459
460 pub fn default_path() -> Result<PathBuf> {
464 let home = dirs::home_dir()
465 .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
466 Ok(home.join(".ipfrs/config.toml"))
467 }
468
469 pub fn generate_default_config() -> String {
471 r#"# IPFRS Configuration File
472# Generated by ipfrs init
473
474[general]
475# Data directory path
476data_dir = ".ipfrs"
477# Log level: error, warn, info, debug, trace
478log_level = "info"
479# Enable colored output
480color = true
481# Default output format: text, json
482format = "text"
483
484[storage]
485# Block store path (relative to data_dir)
486blocks_path = "blocks"
487# Maximum cache size in bytes (default: 100MB)
488cache_size = 104857600
489# Enable write-ahead logging
490wal_enabled = true
491# Garbage collection interval in seconds
492gc_interval = 3600
493
494[network]
495# Listen addresses
496listen_addrs = [
497 "/ip4/0.0.0.0/tcp/4001",
498 "/ip6/::/tcp/4001"
499]
500# Bootstrap peers (add your own or use public IPFS bootstrap)
501bootstrap_peers = []
502# Maximum number of connections
503max_connections = 256
504# Enable DHT for peer/content discovery
505dht_enabled = true
506# Enable mDNS for local peer discovery
507mdns_enabled = true
508# Connection timeout in seconds
509timeout = 30
510
511[gateway]
512# HTTP Gateway listen address
513listen_addr = "127.0.0.1:8080"
514# Enable CORS
515cors_enabled = true
516# CORS allowed origins (empty = all)
517cors_origins = []
518# Enable TLS
519tls_enabled = false
520# TLS certificate path
521# tls_cert_path = "/path/to/cert.pem"
522# TLS key path
523# tls_key_path = "/path/to/key.pem"
524
525[api]
526# API server listen address (for local daemon)
527listen_addr = "127.0.0.1:5001"
528# Remote API URL (for connecting to remote daemon)
529# Format: http://hostname:port or https://hostname:port
530# When set, overrides listen_addr for client commands
531# Example: remote_url = "http://192.168.1.100:5001"
532# remote_url = ""
533# Connection timeout for remote API in seconds
534timeout = 60
535# Enable API authentication
536auth_enabled = false
537# API token (required if auth_enabled = true or connecting to authenticated remote)
538# api_token = "your-secret-token"
539
540[shell]
541# Enable command hints (suggestions as you type)
542hints_enabled = true
543# Enable syntax highlighting
544highlighting_enabled = true
545# Maximum number of history entries
546history_size = 1000
547# User-defined command aliases
548# Example: aliases = { "myalias" = "full command here" }
549# [shell.aliases]
550# ll = "ls -la"
551# gs = "git status"
552"#
553 .to_string()
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560
561 #[test]
562 fn test_default_config() {
563 let config = Config::default();
564 assert_eq!(config.general.log_level, "info");
565 assert!(config.general.color);
566 assert_eq!(config.storage.blocks_path, "blocks");
567 }
568
569 #[test]
570 fn test_config_serialization() {
571 let config = Config::default();
572 let toml_str = toml::to_string_pretty(&config).unwrap();
573 let parsed: Config = toml::from_str(&toml_str).unwrap();
574 assert_eq!(parsed.general.log_level, config.general.log_level);
575 }
576
577 #[test]
578 fn test_generate_default_config() {
579 let config_str = Config::generate_default_config();
580 assert!(config_str.contains("[general]"));
581 assert!(config_str.contains("[storage]"));
582 assert!(config_str.contains("[network]"));
583 assert!(config_str.contains("[shell]"));
584 }
585
586 #[test]
587 fn test_shell_config_default() {
588 let shell_config = super::ShellConfig::default();
589 assert!(shell_config.hints_enabled);
590 assert!(shell_config.highlighting_enabled);
591 assert_eq!(shell_config.history_size, 1000);
592 assert!(shell_config.aliases.is_empty());
593 }
594
595 #[test]
596 fn test_shell_config_serialization() {
597 let mut shell_config = super::ShellConfig::default();
598 shell_config
599 .aliases
600 .insert("ll".to_string(), "ls -la".to_string());
601 shell_config
602 .aliases
603 .insert("gs".to_string(), "git status".to_string());
604
605 let toml_str = toml::to_string_pretty(&shell_config).unwrap();
606 let parsed: super::ShellConfig = toml::from_str(&toml_str).unwrap();
607
608 assert_eq!(parsed.hints_enabled, shell_config.hints_enabled);
609 assert_eq!(parsed.history_size, shell_config.history_size);
610 assert_eq!(parsed.aliases.len(), 2);
611 assert_eq!(parsed.aliases.get("ll"), Some(&"ls -la".to_string()));
612 assert_eq!(parsed.aliases.get("gs"), Some(&"git status".to_string()));
613 }
614
615 #[test]
616 fn test_config_caching() {
617 let config1 = Config::load().unwrap();
619 let config2 = Config::load().unwrap();
620
621 assert_eq!(config1.general.log_level, config2.general.log_level);
623 assert_eq!(config1.storage.cache_size, config2.storage.cache_size);
624 }
625
626 #[test]
627 fn test_config_uncached_load() {
628 let config1 = Config::load_uncached().unwrap();
630 let config2 = Config::load_uncached().unwrap();
631
632 assert_eq!(config1.general.log_level, config2.general.log_level);
634 assert_eq!(config1.storage.cache_size, config2.storage.cache_size);
635 }
636
637 #[test]
638 fn test_clear_cache() {
639 Config::clear_cache();
641
642 let config = Config::load().unwrap();
644 assert_eq!(config.general.log_level, "info");
645 }
646}