datui_lib/cloud/
s3_tools.rs1use crate::cloud::cloud_browse::Environment;
9use std::path::PathBuf;
10
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub struct ToolServer {
14 pub name: String,
16 pub endpoint: Option<String>,
18 pub access_key_id: String,
19 pub secret_access_key: String,
20 pub session_token: Option<String>,
21 pub region: Option<String>,
22 pub virtual_hosted: Option<bool>,
25 pub origin: String,
27}
28
29const MC_PLACEHOLDER_KEYS: [&str; 2] = ["YOUR-ACCESS-KEY-HERE", "YOUR-SECRET-KEY-HERE"];
31
32pub fn mc_config_paths(env: &Environment<'_>) -> Vec<PathBuf> {
36 if let Some(dir) = (env.var)("MC_CONFIG_DIR").filter(|v| !v.trim().is_empty()) {
37 return vec![PathBuf::from(dir.trim()).join("config.json")];
38 }
39 let Some(home) = &env.home else {
40 return Vec::new();
41 };
42 ["mc", "mcli"]
43 .iter()
44 .map(|name| {
45 let directory = if env.windows {
46 name.to_string()
47 } else {
48 format!(".{name}")
49 };
50 home.join(directory).join("config.json")
51 })
52 .collect()
53}
54
55pub fn parse_mc_config(text: &str) -> Vec<ToolServer> {
58 let Ok(value) = serde_json::from_str::<serde_json::Value>(text) else {
59 return Vec::new();
60 };
61 let Some(aliases) = value.get("aliases").and_then(|a| a.as_object()) else {
62 return Vec::new();
63 };
64 let mut servers: Vec<ToolServer> = aliases
65 .iter()
66 .filter(|(name, _)| name.as_str() != "play")
67 .filter_map(|(name, alias)| {
68 let field = |key: &str| {
69 alias
70 .get(key)
71 .and_then(|v| v.as_str())
72 .map(str::to_string)
73 .filter(|v| !v.is_empty())
74 };
75 let url = field("url")?;
76 let access_key_id = field("accessKey")?;
77 let secret_access_key = field("secretKey")?;
78 if MC_PLACEHOLDER_KEYS.contains(&access_key_id.as_str()) {
79 return None;
80 }
81 Some(ToolServer {
82 name: name.clone(),
83 endpoint: server_endpoint(&url),
84 access_key_id,
85 secret_access_key,
86 session_token: field("sessionToken"),
87 region: None,
88 virtual_hosted: match field("path").as_deref() {
89 Some("on") => Some(false),
90 Some("off") => Some(true),
91 _ => None,
92 },
93 origin: "mc alias".to_string(),
94 })
95 })
96 .collect();
97 servers.sort_by(|a, b| a.name.cmp(&b.name));
98 servers
99}
100
101pub fn parse_mc_host(alias: &str, value: &str) -> Option<ToolServer> {
104 let value = value.trim();
105 let (scheme, rest) = value.split_once("://")?;
106 if !matches!(scheme, "http" | "https") {
107 return None;
108 }
109 let (credentials, host) = rest.rsplit_once('@')?;
110 let host = host.trim_end_matches('/');
111 let mut parts = credentials.splitn(3, ':');
112 let access_key_id = parts.next()?.to_string();
113 let second = parts.next()?;
114 let (secret_access_key, session_token) = match parts.next() {
115 Some(token) => (second.to_string(), Some(token.to_string())),
116 None => (second.to_string(), None),
117 };
118 if access_key_id.is_empty() || secret_access_key.is_empty() || host.is_empty() {
119 return None;
120 }
121 Some(ToolServer {
122 name: alias.to_string(),
123 endpoint: server_endpoint(&format!("{scheme}://{host}")),
124 access_key_id,
125 secret_access_key,
126 session_token: session_token.filter(|t| !t.is_empty()),
127 region: None,
128 virtual_hosted: None,
129 origin: format!("MC_HOST_{alias}"),
130 })
131}
132
133pub fn mc_hosts(vars: &[(String, String)]) -> Vec<ToolServer> {
135 let mut servers: Vec<ToolServer> = vars
136 .iter()
137 .filter_map(|(key, value)| {
138 let alias = key.strip_prefix("MC_HOST_")?;
139 (!alias.is_empty()).then(|| parse_mc_host(alias, value))?
140 })
141 .collect();
142 servers.sort_by(|a, b| a.name.cmp(&b.name));
143 servers
144}
145
146pub fn s3cfg_path(env: &Environment<'_>) -> Option<PathBuf> {
149 if let Some(path) = (env.var)("S3CMD_CONFIG").filter(|v| !v.trim().is_empty()) {
150 return Some(PathBuf::from(path.trim()));
151 }
152 if env.windows {
153 return (env.var)("APPDATA").map(|dir| PathBuf::from(dir).join("s3cmd.ini"));
154 }
155 env.home.as_ref().map(|home| home.join(".s3cfg"))
156}
157
158pub fn parse_s3cfg(text: &str) -> Option<ToolServer> {
160 let mut in_default = false;
161 let mut values = std::collections::HashMap::new();
162 for line in text.lines() {
163 let line = line.trim();
164 if line.starts_with('[') {
165 in_default = line == "[default]";
166 continue;
167 }
168 if !in_default || line.starts_with('#') || line.starts_with(';') {
169 continue;
170 }
171 if let Some((key, value)) = line.split_once('=') {
172 values.insert(key.trim().to_string(), value.trim().to_string());
173 }
174 }
175 let get = |key: &str| values.get(key).filter(|v| !v.is_empty()).cloned();
176 let access_key_id = get("access_key")?;
177 let secret_access_key = get("secret_key")?;
178 let host_base = get("host_base").unwrap_or_else(|| "s3.amazonaws.com".to_string());
179 let https = get("use_https").is_none_or(|v| v.eq_ignore_ascii_case("true"));
180 let aws = host_base.eq_ignore_ascii_case("s3.amazonaws.com");
181 let region = get("bucket_location").map(|location| {
182 if location.eq_ignore_ascii_case("us") {
184 "us-east-1".to_string()
185 } else {
186 location
187 }
188 });
189 Some(ToolServer {
190 name: "s3cmd".to_string(),
191 endpoint: (!aws).then(|| format!("{}://{host_base}", if https { "https" } else { "http" })),
192 access_key_id,
193 secret_access_key,
194 session_token: get("access_token"),
195 region,
196 virtual_hosted: get("host_bucket").map(|template| template.contains("%(bucket)s")),
198 origin: "s3cmd".to_string(),
199 })
200}
201
202fn server_endpoint(url: &str) -> Option<String> {
204 let url = url.trim().trim_end_matches('/');
205 let host = url.split_once("://").map(|(_, h)| h).unwrap_or(url);
206 if host.eq_ignore_ascii_case("s3.amazonaws.com") {
207 return None;
208 }
209 Some(url.to_string())
210}
211
212#[cfg(test)]
213mod tests;