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(cli: &Cli, file_path: &str, format: Option<&str>) -> Result<()> {
19    let vault = vault_path(cli)?;
20    let source = Path::new(file_path);
21
22    if !source.exists() {
23        return Err(EnvVaultError::CommandFailed(format!(
24            "import file not found: {}",
25            source.display()
26        )));
27    }
28
29    let keyfile = load_keyfile(cli)?;
30    let vault_id = vault.to_string_lossy();
31    let password = prompt_password_for_vault(Some(&vault_id))?;
32    let mut store = VaultStore::open(&vault, password.as_bytes(), keyfile.as_deref())?;
33
34    // Detect format from flag or file extension.
35    let detected_format = match format {
36        Some(f) => f.to_string(),
37        None => detect_format(source),
38    };
39
40    let secrets = match detected_format.as_str() {
41        "env" => env_parser::parse_env_file(source)?,
42        "json" => parse_json_file(source)?,
43        other => {
44            return Err(EnvVaultError::CommandFailed(format!(
45                "unknown import format '{other}' — use 'env' or 'json'"
46            )));
47        }
48    };
49
50    if secrets.is_empty() {
51        output::warning("No secrets found in the import file.");
52        return Ok(());
53    }
54
55    // Import each secret into the vault.
56    let mut count = 0;
57    for (key, value) in &secrets {
58        store.set_secret(key, value)?;
59        output::info(&format!("  + {key}"));
60        count += 1;
61    }
62
63    store.save()?;
64
65    crate::audit::log_audit(
66        cli,
67        "import",
68        None,
69        Some(&format!("{count} secrets from {}", source.display())),
70    );
71
72    output::success(&format!(
73        "Imported {} secrets from {} into '{}' vault",
74        count,
75        source.display(),
76        store.environment()
77    ));
78
79    Ok(())
80}
81
82/// Detect the file format from its extension.
83fn detect_format(path: &Path) -> String {
84    match path.extension().and_then(|e| e.to_str()) {
85        Some("json") => "json".to_string(),
86        _ => "env".to_string(), // Default to .env format.
87    }
88}
89
90/// Parse a JSON file (object with string values) into a key-value map.
91fn parse_json_file(path: &Path) -> Result<HashMap<String, String>> {
92    let content = fs::read_to_string(path)
93        .map_err(|e| EnvVaultError::CommandFailed(format!("failed to read file: {e}")))?;
94
95    let map: HashMap<String, serde_json::Value> = serde_json::from_str(&content)
96        .map_err(|e| EnvVaultError::CommandFailed(format!("invalid JSON: {e}")))?;
97
98    let mut secrets = HashMap::new();
99    for (key, value) in map {
100        let string_value = match value {
101            serde_json::Value::String(s) => s,
102            other => other.to_string(), // Convert non-strings to their JSON repr.
103        };
104        secrets.insert(key, string_value);
105    }
106
107    Ok(secrets)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use std::io::Write;
114    use tempfile::NamedTempFile;
115
116    #[test]
117    fn parse_env_file_basic() {
118        let mut file = NamedTempFile::new().unwrap();
119        writeln!(file, "KEY=value").unwrap();
120        writeln!(file, "OTHER=123").unwrap();
121
122        let secrets = env_parser::parse_env_file(file.path()).unwrap();
123        assert_eq!(secrets["KEY"], "value");
124        assert_eq!(secrets["OTHER"], "123");
125    }
126
127    #[test]
128    fn parse_env_file_with_export_and_quotes() {
129        let mut file = NamedTempFile::new().unwrap();
130        writeln!(file, "export A=\"hello world\"").unwrap();
131        writeln!(file, "B='single'").unwrap();
132        writeln!(file, "# comment").unwrap();
133
134        let secrets = env_parser::parse_env_file(file.path()).unwrap();
135        assert_eq!(secrets["A"], "hello world");
136        assert_eq!(secrets["B"], "single");
137        assert!(!secrets.contains_key("# comment"));
138    }
139
140    #[test]
141    fn parse_json_file_basic() {
142        let mut file = NamedTempFile::with_suffix(".json").unwrap();
143        write!(file, r#"{{"KEY": "value", "NUM": "42"}}"#).unwrap();
144
145        let secrets = parse_json_file(file.path()).unwrap();
146        assert_eq!(secrets["KEY"], "value");
147        assert_eq!(secrets["NUM"], "42");
148    }
149
150    #[test]
151    fn detect_format_from_extension() {
152        assert_eq!(detect_format(Path::new("secrets.json")), "json");
153        assert_eq!(detect_format(Path::new(".env")), "env");
154        assert_eq!(detect_format(Path::new("secrets.env")), "env");
155        assert_eq!(detect_format(Path::new("noext")), "env");
156    }
157}