datui_lib/cloud/
cloud_env.rs1use crate::config::CloudConfig;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::RwLock;
12
13pub const KNOWN: &[&str] = &[
16 "AWS_ACCESS_KEY_ID",
17 "AWS_SECRET_ACCESS_KEY",
18 "AWS_SESSION_TOKEN",
19 "AWS_REGION",
20 "AWS_DEFAULT_REGION",
21 "AWS_PROFILE",
22 "AWS_ENDPOINT_URL",
23 "AWS_ENDPOINT_URL_S3",
24 "AWS_ENDPOINT",
25 "AWS_CONFIG_FILE",
26 "AWS_SHARED_CREDENTIALS_FILE",
27 "GOOGLE_APPLICATION_CREDENTIALS",
28 "GOOGLE_CLOUD_PROJECT",
29 "GCLOUD_PROJECT",
30 "CLOUDSDK_CORE_PROJECT",
31 "DATUI_GCP_PROJECT",
32 "AZURE_STORAGE_CONNECTION_STRING",
33 "AZURE_STORAGE_ACCOUNT_NAME",
34 "AZURE_STORAGE_ACCOUNT_KEY",
35 "AZURE_STORAGE_SAS_TOKEN",
36 "AZURE_TENANT_ID",
37 "AZURE_CLIENT_ID",
38 "AZURE_CLIENT_SECRET",
39 "AZURE_FEDERATED_TOKEN_FILE",
40];
41
42fn store() -> &'static RwLock<HashMap<String, String>> {
43 static VARS: std::sync::OnceLock<RwLock<HashMap<String, String>>> = std::sync::OnceLock::new();
44 VARS.get_or_init(Default::default)
45}
46
47pub fn load(cloud: &CloudConfig, dir: &Path) -> Vec<String> {
50 let named: Vec<&str> = cloud
51 .connections
52 .iter()
53 .flat_map(|s| {
54 [
55 s.access_key_id_env.as_deref(),
56 s.secret_access_key_env.as_deref(),
57 s.session_token_env.as_deref(),
58 s.account_key_env.as_deref(),
59 s.sas_env.as_deref(),
60 s.connection_string_env.as_deref(),
61 ]
62 })
63 .flatten()
64 .collect();
65 let allowed =
66 |key: &str| KNOWN.contains(&key) || key.starts_with("MC_HOST_") || named.contains(&key);
67 let mut vars = HashMap::new();
68 let mut notes = Vec::new();
69 for file in &cloud.env_files {
70 let path = resolve_path(file, dir);
71 match std::fs::read_to_string(&path) {
72 Ok(text) => vars.extend(parse(&text, &allowed)),
73 Err(e) => notes.push(format!("env_files: {}: {e}", path.display())),
74 }
75 }
76 if let Ok(mut store) = store().write() {
77 *store = vars;
78 }
79 notes
80}
81
82fn resolve_path(file: &str, dir: &Path) -> PathBuf {
84 if let Some(rest) = file.strip_prefix("~/").or_else(|| file.strip_prefix("~\\"))
85 && let Some(home) = dirs::home_dir()
86 {
87 return home.join(rest);
88 }
89 let path = PathBuf::from(file);
90 if path.is_absolute() {
91 path
92 } else {
93 dir.join(path)
94 }
95}
96
97pub fn parse(text: &str, allowed: &dyn Fn(&str) -> bool) -> Vec<(String, String)> {
100 text.lines()
101 .filter_map(|line| {
102 let line = line.trim();
103 if line.is_empty() || line.starts_with('#') {
104 return None;
105 }
106 let line = line.strip_prefix("export ").unwrap_or(line);
107 let (key, value) = line.split_once('=')?;
108 let key = key.trim();
109 if !allowed(key) {
110 return None;
111 }
112 let value = value.trim();
113 let value = match value.chars().next() {
114 Some(quote @ ('"' | '\'')) => {
115 let inner = &value[1..];
116 inner.split(quote).next().unwrap_or(inner)
117 }
118 _ => value.split(" #").next().unwrap_or(value).trim(),
120 };
121 Some((key.to_string(), value.to_string()))
122 })
123 .collect()
124}
125
126pub fn var(key: &str) -> Option<String> {
128 std::env::var(key)
129 .ok()
130 .or_else(|| store().read().ok()?.get(key).cloned())
131}
132
133pub fn vars() -> Vec<(String, String)> {
135 let mut all: Vec<(String, String)> = std::env::vars().collect();
136 if let Ok(store) = store().read() {
137 for (key, value) in store.iter() {
138 if std::env::var_os(key).is_none() {
139 all.push((key.clone(), value.clone()));
140 }
141 }
142 }
143 all
144}
145
146#[cfg(test)]
147mod tests;