nsg_cli/commands/
download.rs

1use 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}