nsg_cli/commands/
download.rs1use crate::client::NsgClient;
2use crate::config::Credentials;
3use anyhow::Result;
4use clap::Args;
5use colored::Colorize;
6use indicatif::{ProgressBar, ProgressStyle};
7use std::path::PathBuf;
8
9#[derive(Debug, Args)]
10pub struct DownloadCommand {
11 #[arg(help = "Job URL or Job ID")]
12 job: String,
13
14 #[arg(
15 short,
16 long,
17 default_value = "./nsg_results",
18 help = "Output directory"
19 )]
20 output: PathBuf,
21}
22
23impl DownloadCommand {
24 pub fn execute(self) -> Result<()> {
25 let credentials = Credentials::load()?;
26 let client = NsgClient::new(credentials)?;
27
28 println!("{}", "NSG Results Downloader".bold().cyan());
29 println!("{}", "=".repeat(80).cyan());
30 println!();
31 println!("{} Checking job status...", "→".cyan());
32 println!(" Job: {}", self.job.bold());
33 println!();
34
35 let status = client.get_job_status(&self.job)?;
36
37 println!("Job ID: {}", status.job_id.cyan());
38 println!("Stage: {}", status.job_stage.bold());
39
40 if status.job_stage != "COMPLETED" {
41 println!();
42 println!("{} Job is not completed yet", "⚠".yellow().bold());
43 println!(" Current stage: {}", status.job_stage.bold());
44 println!();
45 println!("Results may not be available. Continue anyway? [y/N] ");
46
47 let mut input = String::new();
48 std::io::stdin().read_line(&mut input)?;
49 if !input.trim().eq_ignore_ascii_case("y") {
50 println!("Cancelled.");
51 return Ok(());
52 }
53 }
54
55 println!();
56 println!(
57 "{} Output directory: {}",
58 "→".cyan(),
59 self.output.display().to_string().bold()
60 );
61 println!();
62
63 if self.output.exists() && std::fs::read_dir(&self.output)?.next().is_some() {
64 println!("{} Directory already exists and is not empty", "⚠".yellow());
65 println!(" Files may be overwritten. Continue? [y/N] ");
66
67 let mut input = String::new();
68 std::io::stdin().read_line(&mut input)?;
69 if !input.trim().eq_ignore_ascii_case("y") {
70 println!("Cancelled.");
71 return Ok(());
72 }
73 }
74
75 println!("{} Downloading output files...", "→".yellow().bold());
76 println!();
77
78 let pb = ProgressBar::new(0);
79 pb.set_style(
80 ProgressStyle::default_bar()
81 .template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")
82 .unwrap()
83 .progress_chars("#>-"),
84 );
85
86 let mut current_file = String::new();
87
88 let downloaded = client.download_results(
89 &self.job,
90 &self.output,
91 |filename, downloaded_bytes, total_bytes| {
92 if current_file != filename {
93 current_file = filename.to_string();
94 pb.set_length(total_bytes);
95 pb.set_position(0);
96 pb.set_message(format!("Downloading: {}", filename));
97 }
98 pb.set_position(downloaded_bytes);
99 },
100 )?;
101
102 pb.finish_and_clear();
103
104 if downloaded.is_empty() {
105 println!("{} No output files found", "⚠".yellow());
106 println!();
107 println!("This could mean:");
108 println!(" 1. Job hasn't produced output files yet");
109 println!(" 2. Job failed without creating outputs");
110 println!(" 3. Check stderr.txt and stdout.txt for details");
111 return Ok(());
112 }
113
114 println!(
115 "{} Downloaded {} file(s):",
116 "✓".green().bold(),
117 downloaded.len()
118 );
119 println!();
120
121 let mut total_size = 0u64;
122 for file in &downloaded {
123 total_size += file.size;
124 println!(
125 " {} {} ({})",
126 "✓".green(),
127 file.filename.cyan(),
128 format_size(file.size)
129 );
130 }
131
132 println!();
133 println!("{}", "=".repeat(80).green());
134 println!("{} Download complete!", "✓".green().bold());
135 println!("{}", "=".repeat(80).green());
136 println!();
137 println!("Location: {}", self.output.display().to_string().cyan());
138 println!("Files: {}", downloaded.len());
139 println!("Total size: {}", format_size(total_size));
140 println!();
141
142 if downloaded.iter().any(|f| f.filename == "dda_results.json") {
143 println!("{} DDA results found!", "✓".green());
144 println!();
145 println!("View results:");
146 let path = self.output.join("dda_results.json");
147 println!(" cat {} | jq .", path.display());
148 }
149
150 if downloaded.iter().any(|f| f.filename == "stderr.txt") {
151 println!();
152 println!("{} stderr.txt exists - check for errors:", "⚠".yellow());
153 let path = self.output.join("stderr.txt");
154 println!(" cat {}", path.display());
155 }
156
157 if downloaded.iter().any(|f| f.filename == "stdout.txt") {
158 println!();
159 println!("stdout.txt exists:");
160 let path = self.output.join("stdout.txt");
161 println!(" cat {}", path.display());
162 }
163
164 println!();
165
166 Ok(())
167 }
168}
169
170fn format_size(bytes: u64) -> String {
171 const KB: u64 = 1024;
172 const MB: u64 = KB * 1024;
173 const GB: u64 = MB * 1024;
174
175 if bytes >= GB {
176 format!("{:.2} GB", bytes as f64 / GB as f64)
177 } else if bytes >= MB {
178 format!("{:.2} MB", bytes as f64 / MB as f64)
179 } else if bytes >= KB {
180 format!("{:.2} KB", bytes as f64 / KB as f64)
181 } else {
182 format!("{} B", bytes)
183 }
184}