refactor: move signal handling into downloader module

- Relocate signal handler setup from main to downloader module
- Encapsulate signal handling as internal concern of download operations
- Simplify main function by removing signal-related complexity
- Reduce coupling between application initialization and download control
This commit is contained in:
Martin Wimpress
2025-05-25 11:28:27 +01:00
committed by Martin Wimpress
parent 010d20241d
commit e8db2e1d91
2 changed files with 78 additions and 47 deletions
+71 -4
View File
@@ -18,6 +18,22 @@ const BUFFER_SIZE: usize = 8192;
/// File size threshold for showing hash progress bar (16MB)
const LARGE_FILE_THRESHOLD: u64 = 16 * 1024 * 1024;
/// Sets up signal handling for graceful shutdown on Ctrl+C
///
/// Returns an Arc<AtomicBool> that can be checked to see if the process
/// should stop. When Ctrl+C is pressed, this will be set to false.
fn setup_signal_handler() -> Arc<AtomicBool> {
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
println!("\nReceived Ctrl+C, finishing current operation...");
}).expect("Error setting Ctrl+C handler");
running
}
/// Calculates the MD5 hash of a file
fn calculate_md5(file_path: &str, running: &Arc<AtomicBool>) -> Result<String> {
let file = File::open(file_path)?;
@@ -218,17 +234,22 @@ fn verify_downloaded_file(file_path: &str, expected_md5: Option<&str>, running:
}
/// Download a file from archive.org with resume capability
///
/// This function handles signal setup internally and manages graceful shutdown
/// during download operations.
pub async fn download_file(
client: &Client,
url: &str,
file_path: &str,
expected_md5: Option<&str>,
running: &Arc<AtomicBool>
) -> Result<()> {
// Set up signal handling for this download session
let running = setup_signal_handler();
println!(" ");
println!("📦️ Filename {}", file_path);
if let Some(is_valid) = check_existing_file(file_path, expected_md5, running)? {
if let Some(is_valid) = check_existing_file(file_path, expected_md5, &running)? {
if is_valid {
println!("✅ File already exists and is valid: {}", file_path);
return Ok(());
@@ -243,8 +264,54 @@ pub async fn download_file(
let file_size = file.metadata()?.len();
let is_resuming = file_size > 0;
download_file_content(client, url, file_size, &mut file, running, is_resuming).await?;
verify_downloaded_file(file_path, expected_md5, running)?;
download_file_content(client, url, file_size, &mut file, &running, is_resuming).await?;
verify_downloaded_file(file_path, expected_md5, &running)?;
Ok(())
}
/// Download multiple files with shared signal handling
///
/// This function sets up signal handling once for the entire download session
/// and allows for graceful interruption between files.
pub async fn download_files<I>(
client: &Client,
files: I,
) -> Result<()>
where
I: IntoIterator<Item = (String, String, Option<String>)>, // (url, filename, md5)
{
// Set up signal handling for the entire download session
let running = setup_signal_handler();
for (url, file_path, expected_md5) in files {
// Check if we should stop due to signal
if !running.load(Ordering::SeqCst) {
println!("\nDownload interrupted. Run the command again to resume remaining files.");
break;
}
println!(" ");
println!("📦️ Filename {}", file_path);
if let Some(is_valid) = check_existing_file(&file_path, expected_md5.as_deref(), &running)? {
if is_valid {
println!("✅ File already exists and is valid: {}", file_path);
continue;
} else {
println!("🔄 File exists but is invalid, re-downloading: {}", file_path);
}
}
ensure_parent_directories(&file_path)?;
let mut file = prepare_file_for_download(&file_path)?;
let file_size = file.metadata()?.len();
let is_resuming = file_size > 0;
download_file_content(client, &url, file_size, &mut file, &running, is_resuming).await?;
verify_downloaded_file(&file_path, expected_md5.as_deref(), &running)?;
}
Ok(())
}
+7 -43
View File
@@ -15,8 +15,6 @@ use reqwest::Client;
use serde_xml_rs::from_str;
use clap::Parser;
use std::process;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use ia_get::archive_metadata::XmlFiles;
/// Checks if a URL is accessible by sending a HEAD request
@@ -59,34 +57,14 @@ struct Cli {
url: String,
}
/// Sets up signal handling for graceful shutdown on Ctrl+C
///
/// Returns an Arc<AtomicBool> that can be checked to see if the process
/// should stop. When Ctrl+C is pressed, this will be set to false.
fn setup_signal_handler() -> Arc<AtomicBool> {
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
println!("\nReceived Ctrl+C, finishing current operation...");
}).expect("Error setting Ctrl+C handler");
running
}
/// Main application entry point
///
/// Parses command line arguments, validates the archive.org URL, checks URL accessibility,
/// downloads XML metadata, and iterates through files to download them with resume capability
/// and hash verification.
/// downloads XML metadata, and initiates file downloads with built-in signal handling.
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
// Set up signal handling for graceful shutdown
let running = setup_signal_handler();
// Create a single client instance for all requests
let client = Client::builder()
.user_agent(USER_AGENT)
@@ -150,31 +128,17 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
);
spinner.finish();
// Iterate over the XML files struct and download each file
for file in files.files {
// Check if we should stop due to signal
if !running.load(Ordering::SeqCst) {
println!("\nDownload interrupted. Run the command again to resume remaining files.");
break;
}
// Create a clone of the base URL
// Prepare download data for batch processing
let download_data = files.files.into_iter().map(|file| {
let mut absolute_url = base_url.clone();
// If the URL is relative, join it with the base_url to make it absolute
if let Ok(joined_url) = absolute_url.join(&file.name) {
absolute_url = joined_url;
}
(absolute_url.to_string(), file.name, file.md5)
}).collect::<Vec<_>>();
// Download the file
downloader::download_file( // Updated to use downloader::download_file
&client,
absolute_url.as_str(),
&file.name,
file.md5.as_deref(),
&running
).await?;
}
// Download all files with integrated signal handling
downloader::download_files(&client, download_data).await?;
Ok(())
}