| 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 UploadCommand { |
| 14 |
/// File path to upload |
| 15 |
file: PathBuf, |
| 16 |
|
| 17 |
/// Custom file name (defaults to original filename) |
| 18 |
#[arg(short, long)] |
| 19 |
name: Option<String>, |
| 20 |
|
| 21 |
/// Show progress bar |
| 22 |
#[arg(long, default_value = "true")] |
| 23 |
progress: bool, |
| 24 |
|
| 25 |
/// Verify upload after completion |
| 26 |
#[arg(long)] |
| 27 |
verify: bool, |
| 28 |
} |
| 29 |
|
| 30 |
#[async_trait::async_trait] |
| 31 |
impl Command for UploadCommand { |
| 32 |
async fn execute(&self, config: &Config) -> Result<()> { |
| 33 |
info!("Uploading file: {:?}", self.file); |
| 34 |
|
| 35 |
// Validate file exists and is readable |
| 36 |
if !self.file.exists() { |
| 37 |
anyhow::bail!("File does not exist: {:?}", self.file); |
| 38 |
} |
| 39 |
|
| 40 |
if !self.file.is_file() { |
| 41 |
anyhow::bail!("Path is not a file: {:?}", self.file); |
| 42 |
} |
| 43 |
|
| 44 |
let metadata = tokio::fs::metadata(&self.file).await |
| 45 |
.with_context(|| format!("Failed to read file metadata: {:?}", self.file))?; |
| 46 |
|
| 47 |
let file_size = metadata.len(); |
| 48 |
let display_name = self.name.as_deref() |
| 49 |
.or_else(|| self.file.file_name().and_then(|n| n.to_str())) |
| 50 |
.unwrap_or("unknown"); |
| 51 |
|
| 52 |
println!("Uploading: {}", display_name); |
| 53 |
println!(" Size: {}", format_size(file_size, BINARY)); |
| 54 |
println!(" Path: {:?}", self.file); |
| 55 |
|
| 56 |
// Create progress bar |
| 57 |
let progress = if self.progress { |
| 58 |
let pb = ProgressBar::new(file_size); |
| 59 |
pb.set_style( |
| 60 |
ProgressStyle::default_bar() |
| 61 |
.template("[{elapsed_precise}] {bar:40.cyan/blue} {percent}% {bytes}/{total_bytes} ETA: {eta}") |
| 62 |
.unwrap() |
| 63 |
.progress_chars("##-") |
| 64 |
); |
| 65 |
Some(pb) |
| 66 |
} else { |
| 67 |
None |
| 68 |
}; |
| 69 |
|
| 70 |
let client = ZephyrClient::new(config); |
| 71 |
|
| 72 |
// Upload the file |
| 73 |
let upload_result = client.upload_file(&self.file).await |
| 74 |
.context("Failed to upload file")?; |
| 75 |
|
| 76 |
if let Some(pb) = progress { |
| 77 |
pb.finish_with_message("Upload complete"); |
| 78 |
} |
| 79 |
|
| 80 |
println!("✓ File uploaded successfully!"); |
| 81 |
println!(" File hash: {}", upload_result.file_hash); |
| 82 |
println!(" Chunks: {}", upload_result.chunks_uploaded); |
| 83 |
|
| 84 |
// Verify upload if requested |
| 85 |
if self.verify { |
| 86 |
println!("\nVerifying upload..."); |
| 87 |
let files = client.list_files().await |
| 88 |
.context("Failed to list files for verification")?; |
| 89 |
|
| 90 |
if files.iter().any(|f| f.hash == upload_result.file_hash) { |
| 91 |
println!("✓ Upload verified successfully"); |
| 92 |
} else { |
| 93 |
println!("⚠ Warning: Could not verify upload in file list"); |
| 94 |
} |
| 95 |
} |
| 96 |
|
| 97 |
println!("\nTo download this file later, use:"); |
| 98 |
println!(" zephyrfs download {}", upload_result.file_hash); |
| 99 |
|
| 100 |
Ok(()) |
| 101 |
} |
| 102 |
} |