diff --git a/Cargo.lock b/Cargo.lock index b620164..f31a0bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + [[package]] name = "autocfg" version = "1.1.0" @@ -1009,6 +1015,7 @@ dependencies = [ name = "quicksync" version = "0.1.0" dependencies = [ + "anyhow", "chrono", "clap", "duration-string", diff --git a/Cargo.toml b/Cargo.toml index eef2595..a1101be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.79" chrono = "0.4.31" clap = { version="4.4.14", features=["derive"] } duration-string = "0.3.0" diff --git a/src/checksum.rs b/src/checksum.rs index 360d6a9..f1d8b1c 100644 --- a/src/checksum.rs +++ b/src/checksum.rs @@ -1,42 +1,38 @@ -use reqwest::blocking::Client; +use anyhow::Result; +use reqwest::blocking::{Client, Response}; use std::{ - fs::File, - io::{self, BufReader, BufRead, Error}, - path::Path, + fs::File, + io::{BufRead, BufReader}, + path::Path, }; use url::Url; use crate::utils::strip_trailing_newline; -pub fn download_checksum(url: &Url) -> Result { - let mut u = url.clone(); - u.path_segments_mut() - .expect("Wrong URL") - .pop() - .push("state.sql.md5"); - let md5_url = u.to_string(); - - let client = Client::new(); - let response = client - .get(md5_url) - .send() - .map_err(|e| Error::new(std::io::ErrorKind::Other, e.to_string()))?; - - if response.status().is_success() { - let md5 = response - .text() - .map_err(|e| Error::new(std::io::ErrorKind::Other, e.to_string()))?; - let stripped = strip_trailing_newline(&md5); - Ok(stripped.to_string()) - } else { - Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Cannot download MD5 checksum", - )) - } +pub fn download_checksum(url: &Url) -> Result { + let mut u = url.clone(); + u.path_segments_mut() + .expect("Wrong URL") + .pop() + .push("state.sql.md5"); + let md5_url = u.to_string(); + + let client = Client::new(); + let response: Response = client.get(md5_url).send()?; + + if response.status().is_success() { + let md5 = response.text()?; + let stripped = strip_trailing_newline(&md5); + Ok(stripped.to_string()) + } else { + anyhow::bail!( + "Cannot download MD5 checksum: status code is {:?}", + response.status() + ); + } } -pub fn calculate_checksum(file_path: &Path) -> io::Result { +pub fn calculate_checksum(file_path: &Path) -> Result { let file = File::open(file_path)?; let mut reader = BufReader::with_capacity(16 * 1024 * 1024, file); let mut hasher = md5::Context::new(); @@ -55,15 +51,10 @@ pub fn calculate_checksum(file_path: &Path) -> io::Result { Ok(format!("{:x}", hash)) } -pub fn verify( - redirect_file_path: &Path, - unpacked_file_path: &Path, -) -> Result { - let archive_url_str = String::from_utf8(std::fs::read(redirect_file_path)?) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; +pub fn verify(redirect_file_path: &Path, unpacked_file_path: &Path) -> Result { + let archive_url_str = String::from_utf8(std::fs::read(redirect_file_path)?)?; - let archive_url = Url::parse(&archive_url_str) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; + let archive_url = Url::parse(&archive_url_str)?; let md5_expected = download_checksum(&archive_url)?; let md5_actual = calculate_checksum(unpacked_file_path)?; diff --git a/src/download.rs b/src/download.rs index f26d8da..e21626e 100644 --- a/src/download.rs +++ b/src/download.rs @@ -1,98 +1,101 @@ +use anyhow::Result; use reqwest::blocking::Client; use std::collections::VecDeque; -use std::error::Error; use std::fs::{self, OpenOptions}; -use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::io::{Read, Seek, SeekFrom, Write}; use std::path::Path; use std::time::Instant; -pub fn download_file(url: &str, file_path: &Path, redirect_path: &Path) -> Result<(), Box> { - if let Some(dir) = file_path.parent() { - fs::create_dir_all(dir)?; - } +pub fn download_file(url: &str, file_path: &Path, redirect_path: &Path) -> Result<()> { + if let Some(dir) = file_path.parent() { + fs::create_dir_all(dir)?; + } - let mut file = OpenOptions::new() - .create(true) - .read(true) - .write(true) - .open(file_path)?; + let mut file = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .open(file_path)?; - let file_size = file.metadata()?.len(); + let file_size = file.metadata()?.len(); - let client = Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build()?; - let mut response = client - .get(url) - .header("Range", format!("bytes={}-", file_size)) - .send()?; + let client = Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build()?; + let mut response = client + .get(url) + .header("Range", format!("bytes={}-", file_size)) + .send()?; - let final_url = response.url().clone(); + let final_url = response.url().clone(); - fs::write(redirect_path, final_url.as_str())?; + fs::write(redirect_path, final_url.as_str())?; - if !response.status().is_success() { - let err_message = format!("Cannot download: {:?}", response.status()); - fs::remove_file(redirect_path)?; - fs::remove_file(file_path)?; - return Err(Box::new(io::Error::new(io::ErrorKind::NotFound, err_message))); - } + if !response.status().is_success() { + fs::remove_file(redirect_path)?; + fs::remove_file(file_path)?; + + anyhow::bail!( + "Failed to download: Response status code is {:?}", + response.status() + ); + } - let total_size = response - .headers() - .get(reqwest::header::CONTENT_LENGTH) - .and_then(|ct_len| ct_len.to_str().ok()) - .and_then(|ct_len| ct_len.parse::().ok()) - .unwrap_or(0) - + file_size; - - file.seek(SeekFrom::End(0))?; - let mut downloaded: u64 = file_size; - let mut last_reported_progress: i64 = -1; - let start = Instant::now(); - let mut measurements = VecDeque::with_capacity(10); - - let mut buffer = [0; 16 * 1024]; - while let Ok(bytes_read) = response.read(&mut buffer) { - if bytes_read == 0 { - break; - } - file.write_all(&buffer[..bytes_read])?; - downloaded += bytes_read as u64; - - let elapsed = start.elapsed().as_secs_f64(); - let speed = if elapsed > 0.0 { - downloaded as f64 / elapsed - } else { - 0.0 - }; - measurements.push_back(speed); - if measurements.len() > 10 { - measurements.pop_front(); - } - let avg_speed = measurements.iter().sum::() / measurements.len() as f64; - let eta = if avg_speed > 0.0 { - (total_size as f64 - downloaded as f64) / avg_speed - } else { - 0.0 - }; - - let progress = (downloaded as f64 / total_size as f64 * 100.0).round() as i64; - if progress > last_reported_progress { - println!( - "Downloading... {:.2}% ({:.2} MB/{:.2} MB) ETA: {:.0} sec", - progress, - downloaded as f64 / 1_024_000.00, - total_size as f64 / 1_024_000.00, - eta - ); - last_reported_progress = progress; - } + let total_size = response + .headers() + .get(reqwest::header::CONTENT_LENGTH) + .and_then(|ct_len| ct_len.to_str().ok()) + .and_then(|ct_len| ct_len.parse::().ok()) + .unwrap_or(0) + + file_size; + + file.seek(SeekFrom::End(0))?; + let mut downloaded: u64 = file_size; + let mut last_reported_progress: i64 = -1; + let start = Instant::now(); + let mut measurements = VecDeque::with_capacity(10); + + let mut buffer = [0; 16 * 1024]; + while let Ok(bytes_read) = response.read(&mut buffer) { + if bytes_read == 0 { + break; } + file.write_all(&buffer[..bytes_read])?; + downloaded += bytes_read as u64; + + let elapsed = start.elapsed().as_secs_f64(); + let speed = if elapsed > 0.0 { + downloaded as f64 / elapsed + } else { + 0.0 + }; + measurements.push_back(speed); + if measurements.len() > 10 { + measurements.pop_front(); + } + let avg_speed = measurements.iter().sum::() / measurements.len() as f64; + let eta = if avg_speed > 0.0 { + (total_size as f64 - downloaded as f64) / avg_speed + } else { + 0.0 + }; + + let progress = (downloaded as f64 / total_size as f64 * 100.0).round() as i64; + if progress > last_reported_progress { + println!( + "Downloading... {:.2}% ({:.2} MB/{:.2} MB) ETA: {:.0} sec", + progress, + downloaded as f64 / 1_024_000.00, + total_size as f64 / 1_024_000.00, + eta + ); + last_reported_progress = progress; + } + } - println!("Download finished"); + println!("Download finished"); - Ok(()) + Ok(()) } pub fn download_with_retries( @@ -100,23 +103,23 @@ pub fn download_with_retries( file_path: &Path, redirect_path: &Path, max_retries: u32, -) -> Result<(), Box> { +) -> Result<()> { let mut attempts = 0; loop { - match download_file(url, file_path, redirect_path) { - Ok(()) => return Ok(()), - Err(e) if attempts < max_retries => { - eprintln!( - "Download error: {}. Attempt {} / {}", - e, - attempts + 1, - max_retries - ); - attempts += 1; - std::thread::sleep(std::time::Duration::from_secs(5)); - } - Err(e) => return Err(e), + match download_file(url, file_path, redirect_path) { + Ok(()) => return Ok(()), + Err(e) if attempts < max_retries => { + eprintln!( + "Download error: {}. Attempt {} / {}", + e, + attempts + 1, + max_retries + ); + attempts += 1; + std::thread::sleep(std::time::Duration::from_secs(5)); } + Err(e) => return Err(e), + } } } diff --git a/src/go_spacemesh.rs b/src/go_spacemesh.rs index 703706f..f2bc38f 100644 --- a/src/go_spacemesh.rs +++ b/src/go_spacemesh.rs @@ -1,9 +1,9 @@ -use std::error::Error; +use anyhow::Result; use std::process::Command; use crate::utils::trim_version; -pub fn get_version(path: &str) -> Result> { +pub fn get_version(path: &str) -> Result { let output = Command::new(path).arg("version").output()?; let version = String::from_utf8(output.stdout)?; diff --git a/src/main.rs b/src/main.rs index 85496ae..2b57e89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ use chrono::Duration; use clap::{Parser, Subcommand}; -use std::error::Error; use std::path::PathBuf; use std::process; use url::Url; @@ -73,7 +72,7 @@ fn go_spacemesh_default_path() -> &'static str { "./go-spacemesh" } } -fn main() -> Result<(), Box> { +fn main() -> anyhow::Result<()> { let cli = Cli::parse(); match cli.command { @@ -157,12 +156,14 @@ fn main() -> Result<(), Box> { Ok(_) => { println!("Archive unpacked successfully"); } - Err(e) if e.raw_os_error() == Some(28) => { - println!("Cannot unpack archive: not enough disk space"); - std::fs::remove_file(&unpacked_file_path)?; - process::exit(2); - } Err(e) => { + if let Some(io_err) = e.downcast_ref::() { + if io_err.raw_os_error() == Some(28) { + println!("Cannot unpack archive: not enough disk space"); + std::fs::remove_file(&unpacked_file_path)?; + process::exit(2); + } + } println!("Cannot unpack archive: {}", e); std::fs::remove_file(&unpacked_file_path)?; std::fs::remove_file(&archive_file_path)?; diff --git a/src/sql.rs b/src/sql.rs index 0ae5813..2072889 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1,8 +1,9 @@ +use anyhow::{Context, Result}; use rusqlite::{params, Connection}; -use std::{error::Error, path::PathBuf}; +use std::path::PathBuf; -pub fn get_last_layer_from_db(db_path: &PathBuf) -> Result> { - let conn = Connection::open(db_path)?; +pub fn get_last_layer_from_db(db_path: &PathBuf) -> Result { + let conn = Connection::open(db_path).context("Failed to connect to db")?; let mut stmt = conn.prepare("SELECT * FROM layers ORDER BY id DESC LIMIT 1")?; let mut layer_iter = stmt.query_map(params![], |row| row.get::<_, i32>(0))?; diff --git a/src/utils.rs b/src/utils.rs index 350bbc1..d456b36 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ +use anyhow::Result; use chrono::{DateTime, Duration, Utc}; -use std::{env, error::Error, path::PathBuf}; +use std::{env, path::PathBuf}; use url::{ParseError, Url}; pub fn strip_trailing_newline(input: &str) -> &str { @@ -9,12 +10,12 @@ pub fn strip_trailing_newline(input: &str) -> &str { pub fn calculate_latest_layer( genesis_time: DateTime, layer_duration: Duration, -) -> Result> { +) -> Result { let delta = Utc::now() - genesis_time; Ok(delta.num_milliseconds() / layer_duration.num_milliseconds()) } -pub fn resolve_path(relative_path: &PathBuf) -> Result> { +pub fn resolve_path(relative_path: &PathBuf) -> Result { let current_dir = env::current_dir()?; let resolved_path = current_dir.join(relative_path); Ok(resolved_path) @@ -33,12 +34,9 @@ pub fn build_url(base: &Url, path: &str) -> Result { Ok(url) } -pub fn backup_file(original_path: &PathBuf) -> Result { +pub fn backup_file(original_path: &PathBuf) -> Result { if !original_path.exists() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "No file to make a backup", - )); + anyhow::bail!("No file to make a backup"); } let mut backup_path = original_path.with_extension("sql.bak"); diff --git a/src/zip.rs b/src/zip.rs index 7d4d29c..c8eb5b5 100644 --- a/src/zip.rs +++ b/src/zip.rs @@ -1,9 +1,10 @@ +use anyhow::Result; use std::fs::File; use std::io::{Error, Read, Write}; use std::path::Path; use zip::ZipArchive; -pub fn unpack(archive_path: &Path, output_path: &Path) -> Result<(), Error> { +pub fn unpack(archive_path: &Path, output_path: &Path) -> Result<()> { let file = File::open(archive_path)?; let mut zip = ZipArchive::new(file)?; @@ -42,15 +43,12 @@ pub fn unpack(archive_path: &Path, output_path: &Path) -> Result<(), Error> { println!("Unzipping... {}%", progress); } } - Err(e) => return Err(e), + Err(e) => anyhow::bail!(e), } } if last_reported_progress < 100 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Archive was not fully unpacked", - )); + anyhow::bail!("Archive was not fully unpacked"); } Ok(())