nsg_cli/
client.rs

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}