use crate::config::CloudConfig;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
pub const KNOWN: &[&str] = &[
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"AWS_REGION",
"AWS_DEFAULT_REGION",
"AWS_PROFILE",
"AWS_ENDPOINT_URL",
"AWS_ENDPOINT_URL_S3",
"AWS_ENDPOINT",
"AWS_CONFIG_FILE",
"AWS_SHARED_CREDENTIALS_FILE",
"GOOGLE_APPLICATION_CREDENTIALS",
"GOOGLE_CLOUD_PROJECT",
"GCLOUD_PROJECT",
"CLOUDSDK_CORE_PROJECT",
"DATUI_GCP_PROJECT",
"AZURE_STORAGE_CONNECTION_STRING",
"AZURE_STORAGE_ACCOUNT_NAME",
"AZURE_STORAGE_ACCOUNT_KEY",
"AZURE_STORAGE_SAS_TOKEN",
"AZURE_TENANT_ID",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"AZURE_FEDERATED_TOKEN_FILE",
];
fn store() -> &'static RwLock<HashMap<String, String>> {
static VARS: std::sync::OnceLock<RwLock<HashMap<String, String>>> = std::sync::OnceLock::new();
VARS.get_or_init(Default::default)
}
pub fn load(cloud: &CloudConfig, dir: &Path) -> Vec<String> {
let named: Vec<&str> = cloud
.connections
.iter()
.flat_map(|s| {
[
s.access_key_id_env.as_deref(),
s.secret_access_key_env.as_deref(),
s.session_token_env.as_deref(),
s.account_key_env.as_deref(),
s.sas_env.as_deref(),
s.connection_string_env.as_deref(),
]
})
.flatten()
.collect();
let allowed =
|key: &str| KNOWN.contains(&key) || key.starts_with("MC_HOST_") || named.contains(&key);
let mut vars = HashMap::new();
let mut notes = Vec::new();
for file in &cloud.env_files {
let path = resolve_path(file, dir);
match std::fs::read_to_string(&path) {
Ok(text) => vars.extend(parse(&text, &allowed)),
Err(e) => notes.push(format!("env_files: {}: {e}", path.display())),
}
}
if let Ok(mut store) = store().write() {
*store = vars;
}
notes
}
fn resolve_path(file: &str, dir: &Path) -> PathBuf {
if let Some(rest) = file.strip_prefix("~/").or_else(|| file.strip_prefix("~\\"))
&& let Some(home) = dirs::home_dir()
{
return home.join(rest);
}
let path = PathBuf::from(file);
if path.is_absolute() {
path
} else {
dir.join(path)
}
}
pub fn parse(text: &str, allowed: &dyn Fn(&str) -> bool) -> Vec<(String, String)> {
text.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return None;
}
let line = line.strip_prefix("export ").unwrap_or(line);
let (key, value) = line.split_once('=')?;
let key = key.trim();
if !allowed(key) {
return None;
}
let value = value.trim();
let value = match value.chars().next() {
Some(quote @ ('"' | '\'')) => {
let inner = &value[1..];
inner.split(quote).next().unwrap_or(inner)
}
_ => value.split(" #").next().unwrap_or(value).trim(),
};
Some((key.to_string(), value.to_string()))
})
.collect()
}
pub fn var(key: &str) -> Option<String> {
std::env::var(key)
.ok()
.or_else(|| store().read().ok()?.get(key).cloned())
}
pub fn vars() -> Vec<(String, String)> {
let mut all: Vec<(String, String)> = std::env::vars().collect();
if let Ok(store) = store().read() {
for (key, value) in store.iter() {
if std::env::var_os(key).is_none() {
all.push((key.clone(), value.clone()));
}
}
}
all
}
#[cfg(test)]
mod tests;