| 1 |
use clap::Args; |
| 2 |
use anyhow::{Context, Result}; |
| 3 |
use indicatif::{ProgressBar, ProgressStyle}; |
| 4 |
use std::path::PathBuf; |
| 5 |
use tracing::info; |
| 6 |
use humansize::{format_size, BINARY}; |
| 7 |
|
| 8 |
use crate::config::Config; |
| 9 |
use crate::client::ZephyrClient; |
| 10 |
use super::Command; |
| 11 |
|
| 12 |
#[derive(Debug, Args)] |
| 13 |
pub struct DownloadCommand { |
| 14 |
/// File hash to download |
| 15 |
file_hash: String, |
| 16 |
|
| 17 |
/// Output file path (defaults to original filename) |
| 18 |
#[arg(short, long)] |
| 19 |
output: Option<PathBuf>, |
| 20 |
|
| 21 |
/// Overwrite existing file |
| 22 |
#[arg(long)] |
| 23 |
force: bool, |
| 24 |
|
| 25 |
/// Show progress bar |
| 26 |
#[arg(long, default_value = "true")] |
| 27 |
progress: bool, |
| 28 |
|
| 29 |
/// Verify download integrity |
| 30 |
#[arg(long, default_value = "true")] |
| 31 |
verify: bool, |
| 32 |
} |
| 33 |
|
| 34 |
#[async_trait::async_trait] |
| 35 |
impl Command for DownloadCommand { |
| 36 |
async fn execute(&self, config: &Config) -> Result<()> { |
| 37 |
info!("Downloading file: {}", self.file_hash); |
| 38 |
|
| 39 |
let client = ZephyrClient::new(config); |
| 40 |
|
| 41 |
// Get file info first |
| 42 |
let files = client.list_files().await |
| 43 |
.context("Failed to list files")?; |
| 44 |
|
| 45 |
let file_info = files.iter() |
| 46 |
.find(|f| f.hash == self.file_hash) |
| 47 |
.with_context(|| format!("File not found: {}", self.file_hash))?; |
| 48 |
|
| 49 |
// Determine output path |
| 50 |
let output_path = match &self.output { |
| 51 |
Some(path) => path.clone(), |
| 52 |
None => PathBuf::from(&file_info.name), |
| 53 |
}; |
| 54 |
|
| 55 |
// Check if file exists and handle overwrite |
| 56 |
if output_path.exists() && !self.force { |
| 57 |
anyhow::bail!( |
| 58 |
"Output file already exists: {:?}. Use --force to overwrite.", |
| 59 |
output_path |
| 60 |
); |
| 61 |
} |
| 62 |
|
| 63 |
println!("Downloading: {}", file_info.name); |
| 64 |
println!(" Hash: {}", file_info.hash); |
| 65 |
println!(" Size: {}", format_size(file_info.size, BINARY)); |
| 66 |
println!(" Chunks: {}", file_info.chunks); |
| 67 |
println!(" Output: {:?}", output_path); |
| 68 |
|
| 69 |
// Create progress bar |
| 70 |
let progress = if self.progress { |
| 71 |
let pb = ProgressBar::new(file_info.size); |
| 72 |
pb.set_style( |
| 73 |
ProgressStyle::default_bar() |
| 74 |
.template("[{elapsed_precise}] {bar:40.cyan/blue} {percent}% {bytes}/{total_bytes} ETA: {eta}") |
| 75 |
.unwrap() |
| 76 |
.progress_chars("##-") |
| 77 |
); |
| 78 |
Some(pb) |
| 79 |
} else { |
| 80 |
None |
| 81 |
}; |
| 82 |
|
| 83 |
// Download the file |
| 84 |
client.download_file(&self.file_hash, &output_path).await |
| 85 |
.context("Failed to download file")?; |
| 86 |
|
| 87 |
if let Some(pb) = progress { |
| 88 |
pb.finish_with_message("Download complete"); |
| 89 |
} |
| 90 |
|
| 91 |
// Verify download if requested |
| 92 |
if self.verify { |
| 93 |
println!("Verifying download integrity..."); |
| 94 |
|
| 95 |
let downloaded_metadata = tokio::fs::metadata(&output_path).await |
| 96 |
.context("Failed to read downloaded file metadata")?; |
| 97 |
|
| 98 |
if downloaded_metadata.len() != file_info.size { |
| 99 |
anyhow::bail!( |
| 100 |
"File size mismatch: expected {}, got {}", |
| 101 |
file_info.size, |
| 102 |
downloaded_metadata.len() |
| 103 |
); |
| 104 |
} |
| 105 |
|
| 106 |
// TODO: Verify file hash |
| 107 |
println!("✓ Download verified successfully"); |
| 108 |
} |
| 109 |
|
| 110 |
println!("✓ File downloaded successfully!"); |
| 111 |
println!(" Location: {:?}", output_path); |
| 112 |
println!(" Size: {}", format_size(file_info.size, BINARY)); |
| 113 |
|
| 114 |
Ok(()) |
| 115 |
} |
| 116 |
} |