|
| 1 | +use std::collections::BTreeMap; |
| 2 | +use std::fs; |
| 3 | +use std::io::Write; |
| 4 | +use std::path::{Path, PathBuf}; |
| 5 | + |
| 6 | +/// Finds the project .env path (next to Cargo.toml or in working dir) |
| 7 | +pub fn env_path() -> PathBuf { |
| 8 | + // Check if .env exists in current directory |
| 9 | + let cwd = std::env::current_dir().unwrap_or_default(); |
| 10 | + cwd.join(".env") |
| 11 | +} |
| 12 | + |
| 13 | +/// Writes key-value pairs to .env, preserving existing entries |
| 14 | +/// Uses atomic write (write to .env.tmp, then rename) |
| 15 | +pub fn save_keys(keys: &[(&str, &str)]) -> std::io::Result<()> { |
| 16 | + let path = env_path(); |
| 17 | + let mut existing = load_existing(&path); |
| 18 | + |
| 19 | + // Merge new keys (overwrite if already present) |
| 20 | + for (k, v) in keys { |
| 21 | + if !v.trim().is_empty() { |
| 22 | + existing.insert(k.to_string(), v.to_string()); |
| 23 | + } |
| 24 | + } |
| 25 | + |
| 26 | + // Write to temporary file first (atomic write) |
| 27 | + let tmp_path = path.with_extension("env.tmp"); |
| 28 | + { |
| 29 | + let mut file = fs::File::create(&tmp_path)?; |
| 30 | + |
| 31 | + // Write header |
| 32 | + writeln!(file, "# RustForge Configuration")?; |
| 33 | + writeln!(file, "# Auto-generated by setup wizard")?; |
| 34 | + writeln!(file, "# Edit manually or re-run setup with: cargo run -p tui -- --setup")?; |
| 35 | + writeln!(file)?; |
| 36 | + |
| 37 | + for (key, value) in &existing { |
| 38 | + writeln!(file, "{}={}", key, value)?; |
| 39 | + } |
| 40 | + |
| 41 | + file.sync_all()?; // Ensure flush to disk |
| 42 | + } |
| 43 | + |
| 44 | + // Atomic rename |
| 45 | + fs::rename(&tmp_path, &path)?; |
| 46 | + |
| 47 | + // Set secure permissions on Unix systems |
| 48 | + #[cfg(unix)] |
| 49 | + { |
| 50 | + use std::os::unix::fs::PermissionsExt; |
| 51 | + let perms = std::fs::Permissions::from_mode(0o600); // owner read/write only |
| 52 | + if let Err(e) = std::fs::set_permissions(&path, perms) { |
| 53 | + eprintln!("Warning: could not set secure .env permissions: {}", e); |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + // Also inject into current process environment |
| 58 | + for (k, v) in keys { |
| 59 | + if !v.trim().is_empty() { |
| 60 | + std::env::set_var(k, v); |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + Ok(()) |
| 65 | +} |
| 66 | + |
| 67 | +fn load_existing(path: &Path) -> BTreeMap<String, String> { |
| 68 | + let mut map = BTreeMap::new(); |
| 69 | + if let Ok(content) = fs::read_to_string(path) { |
| 70 | + for line in content.lines() { |
| 71 | + let line = line.trim(); |
| 72 | + if line.is_empty() || line.starts_with('#') { |
| 73 | + continue; |
| 74 | + } |
| 75 | + if let Some((k, v)) = line.split_once('=') { |
| 76 | + map.insert(k.trim().to_string(), v.trim().to_string()); |
| 77 | + } |
| 78 | + } |
| 79 | + } |
| 80 | + map |
| 81 | +} |
| 82 | + |
| 83 | +#[cfg(test)] |
| 84 | +mod tests { |
| 85 | + use super::*; |
| 86 | + use tempfile::TempDir; |
| 87 | + |
| 88 | + #[test] |
| 89 | + fn test_roundtrip_write_read() { |
| 90 | + let dir = TempDir::new().unwrap(); |
| 91 | + let path = dir.path().join(".env"); |
| 92 | + std::env::set_current_dir(dir.path()).unwrap(); |
| 93 | + |
| 94 | + save_keys(&[ |
| 95 | + ("FINNHUB_API_KEY", "test_key_123"), |
| 96 | + ("ALPACA_API_KEY", "AKID_456"), |
| 97 | + ]).unwrap(); |
| 98 | + |
| 99 | + let loaded = load_existing(&path); |
| 100 | + assert_eq!(loaded.get("FINNHUB_API_KEY").unwrap(), "test_key_123"); |
| 101 | + assert_eq!(loaded.get("ALPACA_API_KEY").unwrap(), "AKID_456"); |
| 102 | + } |
| 103 | +} |
0 commit comments