mirror of
https://github.com/ApfelTeeSaft/ia-get.git
synced 2026-08-26 19:23:36 +00:00
feat: add graceful signal handling for downloads
- Implement Ctrl+C signal handler with Arc<AtomicBool> for thread-safe state - Add signal interruption checks in download loops and MD5 calculation - Preserve download progress when interrupted, allowing seamless resume - Provide clear user feedback on interruption with resume instructions - Ensure progress bars are properly cleaned up on signal interruption This enables users to gracefully cancel long-running downloads while maintaining the ability to resume from where they left off by running the same command again.
This commit is contained in:
committed by
Martin Wimpress
parent
564e6f7584
commit
191a85cd39
Generated
+23
@@ -207,6 +207,16 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
@@ -468,6 +478,7 @@ name = "ia-get"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"ctrlc",
|
||||
"futures",
|
||||
"indicatif",
|
||||
"md5",
|
||||
@@ -706,6 +717,18 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
|
||||
@@ -23,6 +23,7 @@ serde-xml-rs = "0.8.0"
|
||||
thiserror = "1.0"
|
||||
url = "2.4.1"
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
ctrlc = "3.4"
|
||||
|
||||
[profile.release]
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
|
||||
+65
-3
@@ -17,6 +17,8 @@ use std::fs::{self, File};
|
||||
use std::io::{BufReader, Read, Seek, Write};
|
||||
use std::process;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use md5;
|
||||
|
||||
/// Root structure for parsing the XML files list from archive.org
|
||||
@@ -109,11 +111,12 @@ fn get_xml_url(original_url: &str) -> String {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `file_path` - Path to the file to hash
|
||||
/// * `running` - Signal handler to check for interruption
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(String)` - The MD5 hash as a lowercase hexadecimal string
|
||||
/// * `Err(IaGetError)` - If the file cannot be read
|
||||
fn calculate_md5(file_path: &str) -> Result<String> {
|
||||
fn calculate_md5(file_path: &str, running: &Arc<AtomicBool>) -> Result<String> {
|
||||
let file = File::open(file_path)?;
|
||||
let file_size = file.metadata()?.len();
|
||||
let is_large_file = file_size > 16 * 1024 * 1024; // 16 MB threshold
|
||||
@@ -145,6 +148,17 @@ fn calculate_md5(file_path: &str) -> Result<String> {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for signal interruption during hash calculation
|
||||
if !running.load(Ordering::SeqCst) {
|
||||
if let Some(ref progress_bar) = pb {
|
||||
progress_bar.finish_and_clear();
|
||||
}
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Interrupted,
|
||||
"Hash calculation interrupted by signal",
|
||||
).into());
|
||||
}
|
||||
|
||||
context.consume(&buffer[..bytes_read]);
|
||||
|
||||
// Update progress if we're showing it
|
||||
@@ -177,6 +191,22 @@ 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,
|
||||
@@ -186,6 +216,9 @@ struct Cli {
|
||||
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();
|
||||
|
||||
let client = Client::builder()
|
||||
.user_agent("ia-get")
|
||||
.build()?;
|
||||
@@ -237,6 +270,12 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Iterate over the XML files struct and print every field
|
||||
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
|
||||
let mut absolute_url = base_url.clone();
|
||||
|
||||
@@ -255,7 +294,17 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
if Path::new(&file.name).exists() {
|
||||
println!("├╼ Hash Check 🧮");
|
||||
// Calculate the MD5 hash of the local file
|
||||
let local_md5 = calculate_md5(&file.name).expect("╰╼ Failed to calculate MD5 hash");
|
||||
let local_md5 = match calculate_md5(&file.name, &running) {
|
||||
Ok(hash) => hash,
|
||||
Err(e) => {
|
||||
// Check if this is an interruption by looking at the error message
|
||||
if e.to_string().contains("interrupted by signal") {
|
||||
println!("\nDownload interrupted. Run the command again to resume remaining files.");
|
||||
return Ok(());
|
||||
}
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
let expected_md5 = file.md5.as_ref().unwrap();
|
||||
if &local_md5 != expected_md5 {
|
||||
download_action = "╰╼ Resuming ";
|
||||
@@ -309,6 +358,13 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
// Download the remaining chunks and update the progress bar
|
||||
let mut total_bytes: u64 = file_size;
|
||||
while let Some(chunk) = response.chunk().await? {
|
||||
// Check for signal interruption during download
|
||||
if !running.load(Ordering::SeqCst) {
|
||||
pb.finish_and_clear();
|
||||
println!("\nDownload interrupted during file transfer. Progress saved, resume with the same command.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
download.write_all(&chunk)?;
|
||||
total_bytes += chunk.len() as u64;
|
||||
pb.set_position(total_bytes);
|
||||
@@ -322,7 +378,13 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
println!("├╼ Hash Check 🧮");
|
||||
// Calculate the MD5 hash of the local file
|
||||
let local_md5 = calculate_md5(&file.name).expect("╰╼ Failed to calculate MD5 hash");
|
||||
let local_md5 = match calculate_md5(&file.name, &running) {
|
||||
Ok(hash) => hash,
|
||||
Err(e) => {
|
||||
println!("╰╼ Failed to calculate MD5 hash: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match &file.md5 {
|
||||
Some(expected_md5) => {
|
||||
|
||||
Reference in New Issue
Block a user