1use anyhow::{Context, Result};
2use reqwest::blocking::{Client, multipart};
3use std::path::Path;
4use crate::config::Credentials;
5use crate::models::*;
6
7const NSG_BASE_URL: &str = "https://nsgr.sdsc.edu:8443/cipresrest/v1";
8
9pub struct NsgClient {
10 client: Client,
11 credentials: Credentials,
12 base_url: String,
13}
14
15impl NsgClient {
16 pub fn new(credentials: Credentials) -> Result<Self> {
17 let client = Client::builder()
18 .timeout(std::time::Duration::from_secs(30))
19 .build()
20 .context("Failed to create HTTP client")?;
21
22 Ok(Self {
23 client,
24 credentials,
25 base_url: NSG_BASE_URL.to_string(),
26 })
27 }
28
29 pub fn new_with_url(credentials: Credentials, base_url: String) -> Result<Self> {
30 let client = Client::builder()
31 .timeout(std::time::Duration::from_secs(30))
32 .build()
33 .context("Failed to create HTTP client")?;
34
35 Ok(Self {
36 client,
37 credentials,
38 base_url,
39 })
40 }
41
42 fn build_request(&self, method: reqwest::Method, path: &str) -> reqwest::blocking::RequestBuilder {
43 let url = format!("{}{}", self.base_url, path);
44 self.client
45 .request(method, &url)
46 .basic_auth(&self.credentials.username, Some(&self.credentials.password))
47 .header("cipres-appkey", &self.credentials.app_key)
48 }
49
50 pub fn test_connection(&self) -> Result<()> {
51 let path = format!("/job/{}", self.credentials.username);
52 let response = self
53 .build_request(reqwest::Method::GET, &path)
54 .send()
55 .context("Failed to connect to NSG API")?;
56
57 if !response.status().is_success() {
58 anyhow::bail!(
59 "Authentication failed: HTTP {} - Check your credentials",
60 response.status()
61 );
62 }
63
64 Ok(())
65 }
66
67 pub fn list_jobs(&self) -> Result<Vec<JobSummary>> {
68 let path = format!("/job/{}", self.credentials.username);
69 let response = self
70 .build_request(reqwest::Method::GET, &path)
71 .send()
72 .context("Failed to fetch job list")?;
73
74 if !response.status().is_success() {
75 anyhow::bail!("Failed to list jobs: HTTP {}", response.status());
76 }
77
78 let body = response.text()?;
79 parse_job_list(&body)
80 }
81
82 pub fn get_job_status(&self, job_url_or_id: &str) -> Result<JobStatus> {
83 let path = if job_url_or_id.starts_with("http") {
84 job_url_or_id
85 .strip_prefix(&self.base_url)
86 .context("Invalid job URL")?
87 .to_string()
88 } else if job_url_or_id.starts_with("/job/") {
89 job_url_or_id.to_string()
90 } else {
91 format!("/job/{}/{}", self.credentials.username, job_url_or_id)
92 };
93
94 let response = self
95 .build_request(reqwest::Method::GET, &path)
96 .send()
97 .context("Failed to fetch job status")?;
98
99 if !response.status().is_success() {
100 anyhow::bail!(
101 "Failed to get job status: HTTP {}\nJob: {}",
102 response.status(),
103 job_url_or_id
104 );
105 }
106
107 let body = response.text()?;
108 parse_job_status(&body)
109 }
110
111 pub fn submit_job(&self, zip_path: &Path, tool: &str) -> Result<JobStatus> {
112 let path = format!("/job/{}", self.credentials.username);
113
114 let file_part = multipart::Part::file(zip_path)
115 .context("Failed to read ZIP file")?
116 .file_name(zip_path.file_name()
117 .and_then(|n| n.to_str())
118 .unwrap_or("job.zip")
119 .to_string());
120
121 let form = multipart::Form::new()
122 .text("tool", tool.to_string())
123 .part("input.infile_", file_part)
124 .text("metadata.statusEmail", "true");
125
126 let response = self
127 .build_request(reqwest::Method::POST, &path)
128 .multipart(form)
129 .timeout(std::time::Duration::from_secs(60))
130 .send()
131 .context("Failed to submit job")?;
132
133 if !response.status().is_success() {
134 let status = response.status();
135 let body = response.text().unwrap_or_default();
136 anyhow::bail!(
137 "Failed to submit job: HTTP {}\nResponse: {}",
138 status,
139 body
140 );
141 }
142
143 let body = response.text()?;
144 parse_job_status(&body)
145 }
146
147 pub fn download_results(&self, job_url_or_id: &str, output_dir: &Path) -> Result<Vec<DownloadedFile>> {
148 let job_status = self.get_job_status(job_url_or_id)?;
149
150 let results_url = job_status.results_uri
151 .context("Job has no results URL - may not be completed yet")?;
152
153 let results_path = results_url
154 .strip_prefix(&self.base_url)
155 .context("Invalid results URL")?;
156
157 let response = self
158 .build_request(reqwest::Method::GET, results_path)
159 .send()
160 .context("Failed to fetch results list")?;
161
162 if !response.status().is_success() {
163 anyhow::bail!("Failed to get results: HTTP {}", response.status());
164 }
165
166 let body = response.text()?;
167 let output_files = parse_output_files(&body)?;
168
169 std::fs::create_dir_all(output_dir)
170 .context("Failed to create output directory")?;
171
172 let mut downloaded = Vec::new();
173
174 for file in output_files {
175 let download_path = file.download_uri
176 .strip_prefix(&self.base_url)
177 .context("Invalid download URL")?;
178
179 let output_path = output_dir.join(&file.filename);
180
181 let mut response = self
182 .build_request(reqwest::Method::GET, download_path)
183 .send()
184 .with_context(|| format!("Failed to download {}", file.filename))?;
185
186 if !response.status().is_success() {
187 anyhow::bail!(
188 "Failed to download {}: HTTP {}",
189 file.filename,
190 response.status()
191 );
192 }
193
194 let mut dest = std::fs::File::create(&output_path)
195 .with_context(|| format!("Failed to create {}", output_path.display()))?;
196
197 response.copy_to(&mut dest)
198 .with_context(|| format!("Failed to write {}", file.filename))?;
199
200 downloaded.push(DownloadedFile {
201 filename: file.filename,
202 path: output_path,
203 size: file.size,
204 });
205 }
206
207 Ok(downloaded)
208 }
209}