use std::{fs::File, io::Write, path::PathBuf, process::exit, time::SystemTime}; use reqwest::StatusCode; use sha2::{Digest, Sha512}; use crate::{ check_command::{check_item, CheckResult}, cli::{colors::{GREEN, RED, RESET}, DownloadArgs, DownloadExistingTreatment}, lockfile::{load_project_printing, LockedFile3}, }; pub fn download_command(args: DownloadArgs) { let rt = tokio::runtime::Builder::new_current_thread() .enable_io() .enable_time() .build() .expect("Failed to build tokio runtime"); rt.block_on(download_command_async(args)); } async fn download_command_async(args: DownloadArgs) { let res = load_project_printing(); for record in res.locked { handle_locked_file(record, args).await; } } async fn handle_locked_file(record: LockedFile3, args: DownloadArgs) { let check_res = check_item(&record); match check_res { CheckResult::Ok(_) => { println!("{GREEN}OK{RESET}:\t {}", &record.path); } CheckResult::Invalid(invalid_hash) => match args.existing_file_behavior { DownloadExistingTreatment::ValidateReplace => { println!( "Downloading and replacing invalid file:\n\t{}", &record.path ); let _ = download_file(args, &record).await.unwrap(); println!("Replaced invalid file:\n\t{}", &record.path); } DownloadExistingTreatment::ValidateFail => { println!("Existing file has an invalid hash:\n\t{}\n\texpected: {}\n\tfound: {invalid_hash} ;)", record.path, record.expected_hash); } DownloadExistingTreatment::ValidateReport => todo!(), DownloadExistingTreatment::Ignore => todo!(), }, CheckResult::NotAFile => { println!("{RED}ERROR{RESET}:\tPath {} exists but is a directory.\nThis is unsupported at the moment", record.path); } CheckResult::Absent => { println!("Downloading absent file:\n\t{}", &record.path); match download_file(args, &record).await { Ok(_) => { println!("Downloaded absent file:\n\t{}", &record.path); } Err(download_error) => handle_download_error(download_error, &record), } } } } fn handle_download_error(download_error: DownloadError, record: &LockedFile3) { match download_error { DownloadError::ErrorResponse(sc) => { println!( "Received an error HTTP status code ({sc}) upon downloading file: \n\t{}", record.path ); exit(5); } DownloadError::HashMismatch { calculated, expected, } => { println!( "Downloaded file has a different hash:\n\tfile:{} \n\texpected: {}\n\treceived: {}", record.path, expected, calculated ); exit(5); } DownloadError::IoError(err) => { println!( "An I/O error occurred while attempting to download a file: {}\n{err}", record.path ); } } } async fn download_file(args: DownloadArgs, record: &LockedFile3) -> Result { let mut res = reqwest::get(&record.url).await.unwrap(); if res.status() != StatusCode::OK { return Err(DownloadError::ErrorResponse(res.status())); } let mut hasher = Sha512::new(); let mut file = if args.direct_write { // TODO: Handle not-existent paths File::create(&record.path).unwrap() } else { todo!() }; while let Some(chunk) = res.chunk().await.unwrap() { hasher.write_all(&chunk).unwrap(); file.write_all(&chunk).unwrap(); } let hash = hasher.finalize(); let hash = format!("{hash:x}"); if hash != record.expected_hash { return Err(DownloadError::HashMismatch { calculated: hash, expected: record.expected_hash.clone(), }); } if let Err(err) = file.flush() { return Err(DownloadError::IoError(err)); } Ok(hash) } /// TODO: not in use yet fn get_temp_dir() -> PathBuf { let particle = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap_or_default() .as_millis(); let particle = format!("{}-{particle}", env!("CARGO_BIN_NAME")); let mut path = std::env::temp_dir(); path.push(particle); path } #[derive(Debug)] enum DownloadError { ErrorResponse(StatusCode), HashMismatch { calculated: String, expected: String, }, IoError(std::io::Error), }