Skip to main content

envvault/cli/commands/
import_cmd.rs

1//! `envvault import` — import secrets from external files.
2//!
3//! Supported formats:
4//! - `.env` files (auto-detected by extension or content)
5//! - JSON files (object with string values)
6
7use std::collections::HashMap;
8use std::fs;
9use std::path::Path;
10
11use crate::cli::env_parser;
12use crate::cli::output;
13use crate::cli::{load_keyfile, prompt_password_for_vault, vault_path, Cli};
14use crate::errors::{EnvVaultError, Result};
15use crate::vault::VaultStore;
16
17/// Execute the `import` command.
18pub fn execute(
19    cli: &Cli,
20    file_path: &str,
21    format: Option<&str>,
22    dry_run: bool,
23    skip_existing: bool,
24) -> Result<()> {
25    let vault = vault_path(cli)?;
26    let source = Path::new(file_path);
27
28    if !source.exists() {
29        return Err(EnvVaultError::CommandFailed(format!(
30            "import file not found: {}",
31            source.display()
32        )));
33    }
34
35    let keyfile = load_keyfile(cli)?;
36    let vault_id = vault.to_string_lossy();
37    let password = prompt_password_for_vault(Some(&vault_id))?;
38    let mut store = VaultStore::open(&vault, password.as_bytes(), keyfile.as_deref())?;
39
40    // Detect format from flag or file extension.
41    let detected_format = match format {
42        Some(f) => f.to_string(),
43        None => detect_format(source),
44    };
45
46    let secrets = match detected_format.as_str() {
47        "env" => env_parser::parse_env_file(source)?,
48        "json" => parse_json_file(source)?,
49        other => {
50            return Err(EnvVaultError::CommandFailed(format!(
51                "unknown import format '{other}' — use 'env' or 'json'"
52            )));
53        }
54    };
55
56    if secrets.is_empty() {
57        output::warning("No secrets found in the import file.");
58        return Ok(());
59    }
60
61    // Import each secret into the vault.
62    let mut count = 0;
63    let mut skipped = 0;
64    for (key, value) in &secrets {
65        if skip_existing && store.contains_key(key) {
66            output::info(&format!("  ~ {key} (skipped, already exists)"));
67            skipped += 1;
68            continue;
69        }
70
71        if dry_run {
72            let label = if store.contains_key(key) {
73                "update"
74            } else {
75                "add"
76            };
77            output::info(&format!("  + {key} (would {label})"));
78        } else {
79            store.set_secret(key, value)?;
80            output::info(&format!("  + {key}"));
81        }
82        count += 1;
83    }
84
85    if dry_run {
86        output::info(&format!(
87            "Dry run: {} secrets would be imported from {}{}",
88            count,
89            source.display(),
90            if skipped > 0 {
91                format!(" ({skipped} skipped)")
92            } else {
93                String::new()
94            }
95        ));
96        return Ok(());
97    }
98
99    store.save()?;
100
101    crate::audit::log_audit(
102        cli,
103        "import",
104        None,
105        Some(&format!("{count} secrets from {}", source.display())),
106    );
107
108    let skip_msg = if skipped > 0 {
109        format!(" ({skipped} skipped)")
110    } else {
111        String::new()
112    };
113    output::success(&format!(
114        "Imported {} secrets from {} into '{}' vault{}",
115        count,
116        source.display(),
117        store.environment(),
118        skip_msg
119    ));
120
121    Ok(())
122}
123
124/// Detect the file format from its extension.
125fn detect_format(path: &Path) -> String {
126    match path.extension().and_then(|e| e.to_str()) {
127        Some("json") => "json".to_string(),
128        _ => "env".to_string(), // Default to .env format.
129    }
130}
131
132/// Parse a JSON file (object with string values) into a key-value map.
133fn parse_json_file(path: &Path) -> Result<HashMap<String, String>> {
134    let content = fs::read_to_string(path)
135        .map_err(|e| EnvVaultError::CommandFailed(format!("failed to read file: {e}")))?;
136
137    let map: HashMap<String, serde_json::Value> = serde_json::from_str(&content)
138        .map_err(|e| EnvVaultError::CommandFailed(format!("invalid JSON: {e}")))?;
139
140    let mut secrets = HashMap::new();
141    for (key, value) in map {
142        let string_value = match value {
143            serde_json::Value::String(s) => s,
144            other => other.to_string(), // Convert non-strings to their JSON repr.
145        };
146        secrets.insert(key, string_value);
147    }
148
149    Ok(secrets)
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use std::io::Write;
156    use tempfile::NamedTempFile;
157
158    #[test]
159    fn parse_env_file_basic() {
160        let mut file = NamedTempFile::new().unwrap();
161        writeln!(file, "KEY=value").unwrap();
162        writeln!(file, "OTHER=123").unwrap();
163
164        let secrets = env_parser::parse_env_file(file.path()).unwrap();
165        assert_eq!(secrets["KEY"], "value");
166        assert_eq!(secrets["OTHER"], "123");
167    }
168
169    #[test]
170    fn parse_env_file_with_export_and_quotes() {
171        let mut file = NamedTempFile::new().unwrap();
172        writeln!(file, "export A=\"hello world\"").unwrap();
173        writeln!(file, "B='single'").unwrap();
174        writeln!(file, "# comment").unwrap();
175
176        let secrets = env_parser::parse_env_file(file.path()).unwrap();
177        assert_eq!(secrets["A"], "hello world");
178        assert_eq!(secrets["B"], "single");
179        assert!(!secrets.contains_key("# comment"));
180    }
181
182    #[test]
183    fn parse_json_file_basic() {
184        let mut file = NamedTempFile::with_suffix(".json").unwrap();
185        write!(file, r#"{{"KEY": "value", "NUM": "42"}}"#).unwrap();
186
187        let secrets = parse_json_file(file.path()).unwrap();
188        assert_eq!(secrets["KEY"], "value");
189        assert_eq!(secrets["NUM"], "42");
190    }
191
192    #[test]
193    fn detect_format_from_extension() {
194        assert_eq!(detect_format(Path::new("secrets.json")), "json");
195        assert_eq!(detect_format(Path::new(".env")), "env");
196        assert_eq!(detect_format(Path::new("secrets.env")), "env");
197        assert_eq!(detect_format(Path::new("noext")), "env");
198    }
199}