From 54d3f8b000c4f7cac2b3f59ad6e773794e9fd444 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:41:56 -0400 Subject: [PATCH 1/5] refactor: reorganize, use `eyre` for error handling --- Cargo.lock | 176 ++++++++++++++++++++++++-- Cargo.toml | 1 + src/{file_archiver.rs => archiver.rs} | 0 src/args.rs | 2 +- src/directory.rs | 34 +++++ src/{output.rs => emojis.rs} | 0 src/main.rs | 140 +++++++++----------- src/parser.rs | 58 --------- src/requests.rs | 29 ++--- 9 files changed, 274 insertions(+), 166 deletions(-) rename src/{file_archiver.rs => archiver.rs} (100%) create mode 100644 src/directory.rs rename src/{output.rs => emojis.rs} (100%) delete mode 100644 src/parser.rs diff --git a/Cargo.lock b/Cargo.lock index dd35f23..18e980a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -106,6 +115,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.4", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -174,11 +198,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.69" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -239,6 +264,7 @@ version = "1.0.0" dependencies = [ "async-recursion", "clap", + "color-eyre", "env_logger", "log", "proc-macro2", @@ -252,6 +278,33 @@ dependencies = [ "zip", ] +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.2" @@ -361,6 +414,16 @@ dependencies = [ "log", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "flate2" version = "1.0.23" @@ -370,7 +433,7 @@ dependencies = [ "cfg-if", "crc32fast", "libc", - "miniz_oxide", + "miniz_oxide 0.5.1", ] [[package]] @@ -465,6 +528,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "h2" version = "0.3.3" @@ -602,6 +671,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.7.0" @@ -647,9 +722,9 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] @@ -671,9 +746,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "lock_api" @@ -718,6 +793,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.7.13" @@ -786,11 +870,20 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.15.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -837,6 +930,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot" version = "0.11.1" @@ -1079,6 +1178,12 @@ dependencies = [ "winreg", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "ryu" version = "1.0.5" @@ -1198,6 +1303,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1275,6 +1389,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.9" @@ -1382,11 +1506,33 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.18" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ - "lazy_static", + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] @@ -1449,6 +1595,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index fa1a074..c650c4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,4 @@ proc-macro2 = "1.0.46" yansi = "1.0.1" env_logger = "0.11.5" log = "0.4.22" +color-eyre = "0.6.3" diff --git a/src/file_archiver.rs b/src/archiver.rs similarity index 100% rename from src/file_archiver.rs rename to src/archiver.rs diff --git a/src/args.rs b/src/args.rs index 5127251..284cad8 100644 --- a/src/args.rs +++ b/src/args.rs @@ -12,7 +12,7 @@ use clap::{ArgAction, Parser}; {all-args}{after-help} ")] -pub struct CommandArgs { +pub struct Args { /// URLs for GitHub directories or files to download. You can pass a single URL or multiple comma-delimited URLs #[arg( value_delimiter = ',', diff --git a/src/directory.rs b/src/directory.rs new file mode 100644 index 0000000..2cdb242 --- /dev/null +++ b/src/directory.rs @@ -0,0 +1,34 @@ +use url::Url; + +#[derive(Debug)] +pub struct Directory { + pub root: String, + pub branch: String, + pub path: String, + pub username: String, + pub repository: String, + pub clone_path: Option, +} + +impl Directory { + pub fn new(url: Url, clone_path: Option) -> Self { + let mut segments = url.path_segments().expect("URL has path segments"); + + let username = segments.next().expect("URL contains username").to_string(); + let repository = segments + .next() + .expect("URL contains repository") + .to_string(); + let branch = segments.nth(1).unwrap_or("").to_string(); + let path = segments.collect::>().join("/"); + + Directory { + username, + repository: repository.clone(), + branch, + root: repository.clone(), + path, + clone_path, + } + } +} diff --git a/src/output.rs b/src/emojis.rs similarity index 100% rename from src/output.rs rename to src/emojis.rs diff --git a/src/main.rs b/src/main.rs index beabbb1..c609689 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,71 @@ #![warn(clippy::all)] -use std::io::Error; -use std::process; +use color_eyre::eyre::Result; +use directory::Directory; +use url::Url; use yansi::{Condition, Paint}; -use zip::result::ZipError; +pub mod archiver; pub mod args; -pub mod file_archiver; -pub mod output; -pub mod parser; +pub mod directory; +pub mod emojis; pub mod requests; -use crate::args::CommandArgs; -use crate::file_archiver::ZipArchiver; +use crate::archiver::ZipArchiver; +use crate::args::Args; use clap::Parser; +async fn download_and_zip(url: &str, args: &Args) -> Result<()> { + let steps = 3 + if args.zipped { 2 } else { 0 }; + + log::info!( + "{} {} Validating url...", + format!("[1/{steps}]").bold().yellow(), + emojis::LOOKING_GLASS + ); + + let url = Url::parse(url)?; + let data = Directory::new(url, args.path.clone()); + + log::info!( + "{} {} Downloading...", + format!("[2/{steps}]").bold().yellow(), + emojis::TRUCK + ); + + requests::fetch_and_download(&data).await?; + + log::info!( + "{} {} Downloaded successfully.", + format!("[3/{steps}]").bold().yellow(), + emojis::SPARKLES + ); + + if args.zipped { + log::info!( + "{} {} Zipping...", + format!("[4/{steps}]").bold().yellow(), + emojis::PACKAGE + ); + + let dst_zip = format!("{}.zip", &data.root); + let zipper = ZipArchiver::new(&data.root, &dst_zip); + + zipper.run()?; + + log::info!( + "{} {} Zipped successfully.", + format!("[5/{steps}]").bold().yellow(), + emojis::SPARKLES + ); + } + + Ok(()) +} + #[tokio::main] -async fn main() -> Result<(), Error> { - let args = CommandArgs::parse(); +async fn main() -> Result<()> { + color_eyre::install()?; + let args = Args::parse(); env_logger::Builder::from_default_env() .format_timestamp(None) @@ -43,82 +92,19 @@ async fn main() -> Result<(), Error> { yansi::whenever(Condition::cached((USE_COLOR)())); let url_count = args.urls.len(); + for (i, url) in args.urls.iter().enumerate() { log::info!( "{} Cloning {url:?}...", - format!("[{}/{}]", i + 1, url_count + 1).bold().blue() + format!("[{}/{}]", i + 1, url_count).bold().blue() ); - let steps = 3 + (if args.zipped { 2 } else { 0 }); - - log::info!( - "{} {} Validating url...", - format!("[1/{steps}]").bold().yellow(), - output::LOOKING_GLASS - ); - - let path = match parser::parse_url(url) { - Ok(path) => path, - Err(err) => { - log::error!("{}", err.to_string().red()); - process::exit(0); - } - }; - - let data = match parser::parse_path(&path, args.path.clone()) { - Ok(data) => data, - Err(err) => { - log::error!("{}", err.to_string().red()); - process::exit(0); - } - }; - - log::info!( - "{} {} Downloading...", - format!("[2/{steps}]").bold().yellow(), - output::TRUCK - ); - - match requests::fetch_data(&data).await { - Err(err) => { - log::error!("{}", err.to_string().red()); - process::exit(0); - } - Ok(_) => log::info!( - "{} {} Downloaded successfully.", - format!("[3/{steps}]").bold().yellow(), - output::SPARKLES - ), - }; - - if args.zipped { - log::info!( - "{} {} Zipping...", - format!("[4/{steps}]").bold().yellow(), - output::PACKAGE - ); - - let dst_zip = format!("{}.zip", &data.root); - let zipper = ZipArchiver::new(&data.root, &dst_zip); - match zipper.run() { - Ok(_) => log::info!( - "{} {} Zipped successfully.", - format!("[5/{steps}]").bold().yellow(), - output::SPARKLES - ), - Err(ZipError::FileNotFound) => { - log::error!("{}", "Failed to zip files".bold().red()) - } - Err(e) => log::error!("{}", e.to_string().bold().red()), - } - } + download_and_zip(url, &args).await?; } log::info!( "{} Downloaded {:?} director{}.", - format!("[{}/{}]", url_count + 1, url_count + 1) - .bold() - .blue(), + format!("[{}/{}]", url_count, url_count).bold().blue(), &url_count, if url_count == 1 { "y" } else { "ies" }, ); diff --git a/src/parser.rs b/src/parser.rs deleted file mode 100644 index e5990e5..0000000 --- a/src/parser.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::error::Error; -use url::Url; - -#[derive(Debug)] -pub struct Directory { - pub root: String, - pub branch: String, - pub path: String, - pub username: String, - pub repository: String, - pub clone_path: Option, -} - -pub fn parse_url(url: &str) -> Result> { - let parsed_url = match Url::parse(url) { - Ok(url) => url, - Err(err) => return Err(format!("Invalid URL: {}", err.to_string()).into()), - }; - - Ok(parsed_url.path().to_string()) -} - -pub fn parse_path(path: &str, clone_path: Option) -> Result> { - let mut patterns: Vec<&str> = path.split('/').collect(); - if patterns.last().unwrap() == &"" { - patterns.pop(); - } - - if patterns.len() < 3 { - return Err("Error parsing URL".into()); - } - - let data = Directory { - username: patterns[1].to_string(), - repository: patterns[2].to_string(), - branch: if patterns.get(4) == None { - "".to_string() - } else { - patterns[4].to_string() - }, - root: if patterns.last() == None { - patterns[2].to_string() - } else { - (*patterns.last().unwrap()).to_string() - }, - path: if patterns.get(5) == None { - "".to_string() - } else { - patterns[5..] - .iter() - .map(|i| format!("/{}", i)) - .collect::() - }, - clone_path, - }; - - Ok(data) -} diff --git a/src/requests.rs b/src/requests.rs index 8120fb1..7c834d0 100644 --- a/src/requests.rs +++ b/src/requests.rs @@ -1,8 +1,9 @@ -use crate::parser::Directory; +use crate::directory::Directory; use async_recursion::async_recursion; +use color_eyre::eyre::{eyre, Result}; use reqwest::Client; use serde::{Deserialize, Serialize}; -use std::{error::Error, path::Path}; +use std::path::Path; use tokio::io::AsyncWriteExt; use yansi::Paint; @@ -41,7 +42,7 @@ pub struct Links { html: String, } -pub async fn fetch_data(data: &Directory) -> Result<(), Box> { +pub async fn fetch_and_download(data: &Directory) -> Result<()> { let url = if data.path.is_empty() { format!( "https://api.github.com/repos/{}/{}/contents/", @@ -59,7 +60,7 @@ pub async fn fetch_data(data: &Directory) -> Result<(), Box> { Ok(()) } -async fn build_request(url: &str, client: &Client) -> Result> { +async fn build_request(url: &str, client: &Client) -> Result { let res: String = client .get(url) .header("User-Agent", "request") @@ -71,20 +72,16 @@ async fn build_request(url: &str, client: &Client) -> Result { match val { - ApiResponse::Message(msg_object) => return Err(msg_object.message.into()), + ApiResponse::Message(msg_object) => return Err(eyre!(msg_object.message)), _ => (), } Ok(val) } - Err(_) => Err(format!("Error parsing api object, check the provided url").into()), + Err(_) => Err(eyre!("Error parsing api object, check the provided url")), } } -async fn download( - url: &str, - project_root: &str, - clone_path: &Option, -) -> Result<(), Box> { +async fn download(url: &str, project_root: &str, clone_path: &Option) -> Result<()> { let client = Client::new(); let path = Path::new("./"); @@ -146,7 +143,7 @@ async fn download( } #[async_recursion] -async fn get_dir(url: &str, client: &Client, path: &Path) -> Result<(), Box> { +async fn get_dir(url: &str, client: &Client, path: &Path) -> Result<()> { let resp = build_request(url, client).await?; match resp { @@ -174,11 +171,7 @@ async fn get_dir(url: &str, client: &Client, path: &Path) -> Result<(), Box Result<(), Box> { +async fn write_file(obj: ApiObject, root_path: &Path, client: &Client) -> Result<()> { match &obj.download_url { Some(download_url) => { let new_path = root_path.join(&obj.name); @@ -194,6 +187,6 @@ async fn write_file( Ok(()) } - None => return Err(format!("Could not get the download link!").into()), + None => return Err(eyre!("Could not get the download link")), } } From 9c30135ad8da62edba9aa6e1a215551a228f5cf7 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:55:48 -0400 Subject: [PATCH 2/5] fix: parse urls properly and handle errors better --- src/directory.rs | 19 +++++++++++++------ src/main.rs | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/directory.rs b/src/directory.rs index 2cdb242..b641016 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -1,3 +1,4 @@ +use color_eyre::eyre::{OptionExt, Result}; use url::Url; #[derive(Debug)] @@ -11,24 +12,30 @@ pub struct Directory { } impl Directory { - pub fn new(url: Url, clone_path: Option) -> Self { - let mut segments = url.path_segments().expect("URL has path segments"); + pub fn new(url: Url, clone_path: Option) -> Result { + let mut segments = url + .path_segments() + .expect("URL has path segments") + .filter(|s| !s.is_empty()); - let username = segments.next().expect("URL contains username").to_string(); + let username = segments + .next() + .ok_or_eyre("URL must contain username")? + .to_string(); let repository = segments .next() - .expect("URL contains repository") + .ok_or_eyre("URL must contain repository")? .to_string(); let branch = segments.nth(1).unwrap_or("").to_string(); let path = segments.collect::>().join("/"); - Directory { + Ok(Directory { username, repository: repository.clone(), branch, root: repository.clone(), path, clone_path, - } + }) } } diff --git a/src/main.rs b/src/main.rs index c609689..71bc957 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ async fn download_and_zip(url: &str, args: &Args) -> Result<()> { ); let url = Url::parse(url)?; - let data = Directory::new(url, args.path.clone()); + let data = Directory::new(url, args.path.clone())?; log::info!( "{} {} Downloading...", From 34327888234f22721a3e563758ef19b93a871029 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:02:59 -0400 Subject: [PATCH 3/5] refactor: zip archiver craziness --- src/archiver.rs | 137 ++++++++++++++++++++---------------------------- 1 file changed, 58 insertions(+), 79 deletions(-) diff --git a/src/archiver.rs b/src/archiver.rs index 85bae13..f1d1d8b 100644 --- a/src/archiver.rs +++ b/src/archiver.rs @@ -1,109 +1,88 @@ +use color_eyre::eyre::Result; use std::{ - cell::Cell, fs::File, - io::{Read, Seek, Write}, + io::{Read, Write}, iter::Iterator, path::Path, }; -use zip::{result::ZipError, write::FileOptions}; -use walkdir::{DirEntry, WalkDir}; +use walkdir::WalkDir; +use zip::{write::FileOptions, ZipWriter}; -#[derive(Debug)] -pub struct ZipArchiver { - m_src_dir: String, - m_dest_zip_fname: String, - m_dst_zip_exists: Cell, // interior mutability - m_compress_method: zip::CompressionMethod, +pub struct ZipArchiver<'a> { + src: &'a Path, + dest: &'a Path, + options: FileOptions, } -impl ZipArchiver { +impl<'a> ZipArchiver<'a> { #[must_use] - pub fn new(src_dir: &str, dest_zip_fname: &str) -> Self { - Self { - m_src_dir: src_dir.to_string(), - m_dest_zip_fname: dest_zip_fname.to_string(), - m_dst_zip_exists: Cell::new(false), - m_compress_method: zip::CompressionMethod::Deflated, - } - } - - fn zip_dir( - &self, - dir_entries: &mut dyn Iterator, - prefix: &str, - writer: T, - ) -> zip::result::ZipResult<()> - where - T: Write + Seek + Read, - { - let mut zip = if self.m_dst_zip_exists.get() { - zip::ZipWriter::new_append(writer).unwrap() - } else { - zip::ZipWriter::new(writer) - }; - + pub fn new(src: &'a str, dest: &'a str) -> Self { let options = FileOptions::default() - .compression_method(self.m_compress_method) + .compression_method(zip::CompressionMethod::Deflated) .unix_permissions(0o755); - let mut file_buf = Vec::new(); - for curr_entry in dir_entries { - let entry_path = curr_entry.path(); - let entry_name = entry_path - .strip_prefix(Path::new(prefix)) - .unwrap() - .to_str() - .unwrap(); - - // Write file or directory explicitly - // Some unzip tools unzip files with directory paths correctly, some do not! - if entry_path.is_file() { - log::info!("[+] Adding file {:?} as {:?}...", entry_path, entry_name); - zip.start_file(entry_name, options)?; - let mut fh = File::open(entry_path)?; - - fh.read_to_end(&mut file_buf)?; - zip.write_all(&*file_buf)?; - file_buf.clear(); - } else if !entry_name.is_empty() { - // Only if not root! Avoids path spec / warning - // and mapname conversion failed error on unzip - log::info!("[+] Adding dir {:?} as {:?}...", entry_path, entry_name); - zip.add_directory(entry_name, options)?; - } + Self { + src: Path::new(src), + dest: Path::new(dest), + options, } - - zip.finish()?; - Result::Ok(()) } - pub fn run(&self) -> zip::result::ZipResult<()> { - if !Path::new(&self.m_src_dir).is_dir() { - return Err(ZipError::FileNotFound); + fn add_path(&self, zip: &mut ZipWriter, path: &Path) -> Result<()> { + let name = path.strip_prefix(self.src)?.to_str().unwrap(); + + // Write file or directory explicitly + // Some unzip tools unzip files with directory paths correctly, some do not! + if path.is_file() { + log::info!("[+] Adding file {:?} as {:?}...", path, name); + zip.start_file(name, self.options)?; + + let mut file = File::open(path)?; + let mut buff = Vec::new(); + file.read_to_end(&mut buff)?; + zip.write_all(&*buff)?; + buff.clear(); + } else if !name.is_empty() { + // Only if not root! Avoids path spec / warning + // and mapname conversion failed error on unzip + log::info!("[+] Adding dir {:?} as {:?}...", path, name); + zip.add_directory(name, self.options)?; } - let path = Path::new(&self.m_dest_zip_fname); + Ok(()) + } - if path.exists() { - self.m_dst_zip_exists.set(true); + pub fn run(&self) -> Result<()> { + // Individually downloaded files are not placed in the `src` directory, + // and so their paths are unknown and cannot be added to the zip. + if !self.src.is_dir() { + log::warn!("Unable to zip individual files, skipping"); + return Ok(()); } - let file = File::options() + let dest = self.dest; + let dest_exists = dest.exists(); + + let writer = File::options() .read(true) .write(true) .create(true) - .open(&path)?; + .open(&dest)?; + + let mut zip = if dest_exists { + zip::ZipWriter::new_append(writer)? + } else { + zip::ZipWriter::new(writer) + }; - let dir_content = WalkDir::new(&self.m_src_dir).into_iter(); + for entry in WalkDir::new(&self.src).into_iter().filter_map(Result::ok) { + self.add_path(&mut zip, entry.path())?; + } - self.zip_dir( - &mut dir_content.filter_map(Result::ok), - &self.m_src_dir, - file, - )?; + zip.finish()?; - log::info!("[+] Zip archived as {:?}.", &self.m_dest_zip_fname); + log::info!("[+] Zip archived as {:?}.", &self.dest); Ok(()) } From 9ba111ee67299218363252a4598b5d2b39f061fc Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:04:31 -0400 Subject: [PATCH 4/5] refactor: clean up logging, remove extra success steps --- src/main.rs | 50 ++++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/main.rs b/src/main.rs index 71bc957..a22c67d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ #![warn(clippy::all)] use color_eyre::eyre::Result; -use directory::Directory; +use log::Level; +use std::io::Write; use url::Url; -use yansi::{Condition, Paint}; +use yansi::{Color, Condition, Paint, Style}; pub mod archiver; pub mod args; @@ -12,14 +13,17 @@ pub mod requests; use crate::archiver::ZipArchiver; use crate::args::Args; +use crate::directory::Directory; use clap::Parser; +static STEP: Style = Color::Green.bold(); + async fn download_and_zip(url: &str, args: &Args) -> Result<()> { - let steps = 3 + if args.zipped { 2 } else { 0 }; + let steps = if args.zipped { 3 } else { 2 }; log::info!( "{} {} Validating url...", - format!("[1/{steps}]").bold().yellow(), + format!("[1/{steps}]").paint(STEP), emojis::LOOKING_GLASS ); @@ -28,35 +32,22 @@ async fn download_and_zip(url: &str, args: &Args) -> Result<()> { log::info!( "{} {} Downloading...", - format!("[2/{steps}]").bold().yellow(), + format!("[2/{steps}]").paint(STEP), emojis::TRUCK ); requests::fetch_and_download(&data).await?; - log::info!( - "{} {} Downloaded successfully.", - format!("[3/{steps}]").bold().yellow(), - emojis::SPARKLES - ); - if args.zipped { log::info!( "{} {} Zipping...", - format!("[4/{steps}]").bold().yellow(), + format!("[3/{steps}]").paint(STEP), emojis::PACKAGE ); - let dst_zip = format!("{}.zip", &data.root); - let zipper = ZipArchiver::new(&data.root, &dst_zip); - + let dest = format!("{}.zip", &data.root); + let zipper = ZipArchiver::new(&data.root, &dest); zipper.run()?; - - log::info!( - "{} {} Zipped successfully.", - format!("[5/{steps}]").bold().yellow(), - emojis::SPARKLES - ); } Ok(()) @@ -68,9 +59,17 @@ async fn main() -> Result<()> { let args = Args::parse(); env_logger::Builder::from_default_env() - .format_timestamp(None) - .format_target(false) - .format_level(false) + .format(|buf, record| { + let color = match record.level() { + Level::Info => return writeln!(buf, "{}", record.args()), + + Level::Error => Color::Red, + Level::Warn => Color::Yellow, + Level::Debug => Color::Magenta, + Level::Trace => Color::Blue, + }; + writeln!(buf, "[{}] {}", record.level().paint(color), record.args()) + }) .filter_level(if args.quiet { log::LevelFilter::Warn } else { @@ -103,8 +102,7 @@ async fn main() -> Result<()> { } log::info!( - "{} Downloaded {:?} director{}.", - format!("[{}/{}]", url_count, url_count).bold().blue(), + "Downloaded {:?} director{}.", &url_count, if url_count == 1 { "y" } else { "ies" }, ); From 5956dfb1c75dfbfd3340cb8253b1e182725559cd Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:09:23 -0400 Subject: [PATCH 5/5] refactor: args.zipped -> args.zip --- src/args.rs | 4 ++-- src/main.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/args.rs b/src/args.rs index 284cad8..69c56e8 100644 --- a/src/args.rs +++ b/src/args.rs @@ -26,8 +26,8 @@ pub struct Args { pub path: Option, /// Download and zip directories - #[arg(short, long = "zip")] - pub zipped: bool, + #[arg(short, long)] + pub zip: bool, /// Disable verbose logging #[arg(short, long)] diff --git a/src/main.rs b/src/main.rs index a22c67d..419801b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,8 +18,8 @@ use clap::Parser; static STEP: Style = Color::Green.bold(); -async fn download_and_zip(url: &str, args: &Args) -> Result<()> { - let steps = if args.zipped { 3 } else { 2 }; +async fn download_from_url(url: &str, args: &Args) -> Result<()> { + let steps = if args.zip { 3 } else { 2 }; log::info!( "{} {} Validating url...", @@ -38,7 +38,7 @@ async fn download_and_zip(url: &str, args: &Args) -> Result<()> { requests::fetch_and_download(&data).await?; - if args.zipped { + if args.zip { log::info!( "{} {} Zipping...", format!("[3/{steps}]").paint(STEP), @@ -98,7 +98,7 @@ async fn main() -> Result<()> { format!("[{}/{}]", i + 1, url_count).bold().blue() ); - download_and_zip(url, &args).await?; + download_from_url(url, &args).await?; } log::info!(