envvault/cli/commands/
import_cmd.rs1use 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
17pub 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 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 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
124fn 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(), }
130}
131
132fn 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(), };
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}