1use crate::engine::log_action;
4use crate::profiles::{Profile, Profiles, load_profiles};
5
6pub struct ProfileList {
8 pub profiles: Vec<String>,
9}
10
11pub struct ProfileTestResult {
13 pub exists: bool,
14 pub testable: bool,
15 pub connected: bool,
16}
17
18pub fn list_profiles() -> Result<(ProfileList, String), String> {
20 match load_profiles() {
21 Ok(profiles) => {
22 let profile_names: Vec<String> = profiles.keys().cloned().collect();
23 let message = log_action("profiles_listed", None, None, None, None);
24 Ok((
25 ProfileList {
26 profiles: profile_names,
27 },
28 message,
29 ))
30 }
31 Err(_) => Err("Failed to load profiles".to_string()),
32 }
33}
34
35pub async fn test_profile(profile_name: &str) -> (ProfileTestResult, String) {
37 let profiles = match load_profiles() {
38 Ok(p) => p,
39 Err(_) => {
40 let message = log_action(
41 "profile_tested",
42 Some("exists=false, testable=false, connected=false"),
43 None,
44 None,
45 Some(profile_name),
46 );
47 return (
48 ProfileTestResult {
49 exists: false,
50 testable: false,
51 connected: false,
52 },
53 message,
54 );
55 }
56 };
57
58 if let Some(_profile) = profiles.get(profile_name) {
59 let connected = test_profile_internal(profile_name, &profiles).await;
60 let details = format!("exists=true, testable=true, connected={}", connected);
61 let message = log_action(
62 "profile_tested",
63 Some(&details),
64 None,
65 None,
66 Some(profile_name),
67 );
68 (
69 ProfileTestResult {
70 exists: true,
71 testable: true,
72 connected,
73 },
74 message,
75 )
76 } else {
77 let message = log_action(
78 "profile_tested",
79 Some("exists=false, testable=false, connected=false"),
80 None,
81 None,
82 Some(profile_name),
83 );
84 (
85 ProfileTestResult {
86 exists: false,
87 testable: false,
88 connected: false,
89 },
90 message,
91 )
92 }
93}
94
95pub async fn test_profile_internal(profile_name: &str, profiles: &Profiles) -> bool {
97 if let Some(profile) = profiles.get(profile_name) {
98 match profile.provider.as_str() {
99 "s3" => test_s3_profile_internal(profile).await,
100 "local" => true, "azure" => test_azure_profile_internal(profile).await,
102 "gcs" => test_gcs_profile_internal(profile).await,
103 "sftp" => false, _ => false,
105 }
106 } else {
107 false
108 }
109}
110
111async fn test_s3_profile_internal(profile: &Profile) -> bool {
112 use aws_sdk_s3::config::Credentials;
113
114 let region = profile
115 .region
116 .clone()
117 .unwrap_or_else(|| "us-east-1".to_string());
118 let mut cfg_loader = aws_config::defaults(aws_config::BehaviorVersion::latest())
119 .region(aws_config::Region::new(region));
120
121 if let Some(endpoint) = &profile.endpoint {
122 cfg_loader = cfg_loader.endpoint_url(endpoint);
123 }
124
125 let base = cfg_loader.load().await;
126 let mut s3b = aws_sdk_s3::config::Builder::from(&base);
127
128 if profile.path_style.unwrap_or(false) {
129 s3b = s3b.force_path_style(true);
130 }
131
132 if let (Some(access_key), Some(secret_key)) = (&profile.access_key, &profile.secret_key) {
134 if !access_key.is_empty() && !secret_key.is_empty() {
135 let creds = Credentials::new(
136 access_key.clone(),
137 secret_key.clone(),
138 None,
139 None,
140 "profile",
141 );
142 s3b = s3b.credentials_provider(creds);
143 }
144 }
145
146 let client = aws_sdk_s3::Client::from_conf(s3b.build());
147 client.list_buckets().send().await.is_ok()
148}
149
150async fn test_azure_profile_internal(profile: &Profile) -> bool {
151 if let Some(connection_string) = &profile.connection_string {
152 !connection_string.is_empty()
153 } else {
154 false
155 }
156}
157
158async fn test_gcs_profile_internal(profile: &Profile) -> bool {
159 if let Some(service_account_json) = &profile.service_account_json {
160 return test_gcs_service_account(service_account_json).await;
161 }
162 false
163}
164
165fn parse_gcs_service_account(
166 service_account_json: &str,
167) -> Result<(String, String, String), Box<dyn std::error::Error>> {
168 use serde_json::Value;
169
170 let json: Value = serde_json::from_str(service_account_json)?;
171
172 let project_id = json["project_id"]
173 .as_str()
174 .ok_or("Missing project_id in service account JSON")?
175 .to_string();
176
177 let client_email = json["client_email"]
178 .as_str()
179 .ok_or("Missing client_email in service account JSON")?
180 .to_string();
181
182 let private_key = json["private_key"]
183 .as_str()
184 .ok_or("Missing private_key in service account JSON")?
185 .to_string();
186
187 Ok((project_id, client_email, private_key))
188}
189
190async fn test_gcs_service_account(service_account_json: &str) -> bool {
191 println!(
192 "Debug: GCS service account JSON length: {}",
193 service_account_json.len()
194 );
195 use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
201 use serde_json::json;
202
203 let (project_id, client_email, private_key) =
204 match parse_gcs_service_account(service_account_json) {
205 Ok((pid, email, key)) => (pid, email, key),
206 Err(e) => {
207 println!("Debug: GCS service account parsing failed: {}", e);
208 return false;
209 }
210 };
211
212 let now = chrono::Utc::now().timestamp();
214 let claims = json!({
215 "iss": client_email,
216 "scope": "https://www.googleapis.com/auth/cloud-platform",
217 "aud": "https://oauth2.googleapis.com/token",
218 "exp": now + 3600, "iat": now
220 });
221
222 let header = Header::new(Algorithm::RS256);
224 let encoding_key = match EncodingKey::from_rsa_pem(private_key.as_bytes()) {
225 Ok(key) => key,
226 Err(e) => {
227 println!("Debug: GCS private key parsing failed: {}", e);
228 return false;
229 }
230 };
231
232 let jwt_token = match encode(&header, &claims, &encoding_key) {
233 Ok(token) => token,
234 Err(e) => {
235 println!("Debug: GCS JWT generation failed: {}", e);
236 return false;
237 }
238 };
239
240 println!("Debug: GCS JWT generated successfully");
241
242 let client = reqwest::Client::new();
244
245 let token_response = match client
246 .post("https://oauth2.googleapis.com/token")
247 .header("Content-Type", "application/x-www-form-urlencoded")
248 .form(&[
249 ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
250 ("assertion", &jwt_token),
251 ])
252 .send()
253 .await
254 {
255 Ok(response) => response,
256 Err(e) => {
257 println!("Debug: GCS token exchange request failed: {}", e);
258 return false;
259 }
260 };
261
262 if !token_response.status().is_success() {
263 println!(
264 "Debug: GCS token exchange failed: {}",
265 token_response.status()
266 );
267 return false;
268 }
269
270 let access_token = match token_response.json::<serde_json::Value>().await {
271 Ok(json) => match json["access_token"].as_str() {
272 Some(token) => token.to_string(),
273 None => {
274 println!("Debug: GCS access_token not found in response");
275 return false;
276 }
277 },
278 Err(e) => {
279 println!("Debug: GCS token response parsing failed: {}", e);
280 return false;
281 }
282 };
283
284 let bucket_list_url = format!(
286 "https://storage.googleapis.com/storage/v1/b?project={}",
287 project_id
288 );
289
290 match client
291 .get(&bucket_list_url)
292 .header("Authorization", format!("Bearer {}", access_token))
293 .send()
294 .await
295 {
296 Ok(response) => {
297 println!("Debug: GCS bucket list response: {}", response.status());
298 response.status().is_success()
299 }
300 Err(e) => {
301 println!("Debug: GCS bucket list failed: {}", e);
302 false
303 }
304 }
305}