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