nsg_cli/commands/
download.rs

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