From c6cd78f56594c2c101e678f03e84007cc4b352a6 Mon Sep 17 00:00:00 2001 From: RJ Rybarczyk Date: Wed, 11 Mar 2020 19:04:22 -0400 Subject: [PATCH] Add progress messages to `imdl torrent create` - Add messages showing overall progress - Add file search spinner type: added --- Cargo.lock | 62 ++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/common.rs | 1 + src/status.rs | 4 +- src/subcommand/torrent/create.rs | 66 +++++++++++++++++++++++++------- src/target.rs | 24 ++++++++++++ src/walker.rs | 25 +++++++++--- 7 files changed, 163 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c8619c..28af9fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,34 @@ dependencies = [ "vec_map", ] +[[package]] +name = "clicolors-control" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90082ee5dcdd64dc4e9e0d37fbf3ee325419e39c0092191e0393df65518f741e" +dependencies = [ + "atty", + "lazy_static", + "libc", + "winapi 0.3.8", +] + +[[package]] +name = "console" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e0f3986890b3acbc782009e2629dfe2baa430ac091519ce3be26164a2ae6c0" +dependencies = [ + "clicolors-control", + "encode_unicode", + "lazy_static", + "libc", + "regex", + "termios", + "unicode-width", + "winapi 0.3.8", +] + [[package]] name = "ctor" version = "0.1.12" @@ -170,6 +198,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "env_logger" version = "0.7.1" @@ -288,6 +322,7 @@ dependencies = [ "bendy", "chrono", "globset", + "indicatif", "libc", "md5", "pretty_assertions", @@ -309,6 +344,18 @@ dependencies = [ "walkdir", ] +[[package]] +name = "indicatif" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a68371cf417889c9d7f98235b7102ea7c54fc59bcbd22f3dea785be9d27e40" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -389,6 +436,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "number_prefix" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" + [[package]] name = "output_vt100" version = "0.1.2" @@ -798,6 +851,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termios" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b620c5ea021d75a735c943269bb07d30c9b77d6ac6b236bc8b5c496ef05625" +dependencies = [ + "libc", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index a0bb611..4bf02ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ ansi_term = "0.12.0" atty = "0.2.0" chrono = "0.4.1" globset = "0.4.0" +indicatif = "0.14.0" libc = "0.2.0" md5 = "0.7.0" pretty_assertions = "0.6.0" diff --git a/src/common.rs b/src/common.rs index 24d4fb4..686a3c3 100644 --- a/src/common.rs +++ b/src/common.rs @@ -24,6 +24,7 @@ pub(crate) use std::{ pub(crate) use bendy::{decoding::FromBencode, encoding::ToBencode, value::Value}; pub(crate) use chrono::{TimeZone, Utc}; pub(crate) use globset::{Glob, GlobMatcher}; +pub(crate) use indicatif::{ProgressBar, ProgressStyle}; pub(crate) use libc::EXIT_FAILURE; pub(crate) use regex::{Regex, RegexSet}; pub(crate) use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; diff --git a/src/status.rs b/src/status.rs index ebae10d..fd995fa 100644 --- a/src/status.rs +++ b/src/status.rs @@ -7,8 +7,8 @@ pub(crate) struct Status { } impl Status { - pub(crate) fn new(pieces: bool, files: Vec) -> Status { - Status { pieces, files } + pub(crate) fn new(pieces: bool, files: Vec) -> Self { + Self { pieces, files } } pub(crate) fn pieces(&self) -> bool { diff --git a/src/subcommand/torrent/create.rs b/src/subcommand/torrent/create.rs index 689ce13..e407744 100644 --- a/src/subcommand/torrent/create.rs +++ b/src/subcommand/torrent/create.rs @@ -184,11 +184,31 @@ impl Create { pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> { let input = env.resolve(&self.input); + let mut announce_list = Vec::new(); + for tier in &self.announce_tiers { + let tier = tier.split(',').map(str::to_string).collect::>(); + + tier + .iter() + .map(|announce| announce.parse()) + .collect::, url::ParseError>>() + .context(error::AnnounceUrlParse)?; + + announce_list.push(tier); + } + + errln!(env, "[1/3] \u{1F9FF} Searching for files…"); + + let style = ProgressStyle::default_spinner().template("{spinner} {msg}…"); + + let spinner = ProgressBar::new_spinner().with_style(style); + let files = Walker::new(&input) .include_junk(self.include_junk) .include_hidden(self.include_hidden) .follow_symlinks(self.follow_symlinks) .globs(&self.globs)? + .spinner(spinner) .files()?; let piece_length = self @@ -212,19 +232,6 @@ impl Create { return Err(Error::PieceLengthSmall); } - let mut announce_list = Vec::new(); - for tier in &self.announce_tiers { - let tier = tier.split(',').map(str::to_string).collect::>(); - - tier - .iter() - .map(|announce| announce.parse()) - .collect::, url::ParseError>>() - .context(error::AnnounceUrlParse)?; - - announce_list.push(tier); - } - let filename = input.file_name().ok_or_else(|| Error::FilenameExtract { path: input.clone(), })?; @@ -268,12 +275,16 @@ impl Create { Some(String::from(consts::CREATED_BY_DEFAULT)) }; + errln!(env, "[2/3] \u{1F9EE} Hashing pieces…"); + let (mode, pieces) = Hasher::hash( &files, self.md5sum, piece_length.as_piece_length()?.into_usize(), )?; + errln!(env, "[3/3] \u{1F4BE} Writing metainfo to {}…", output); + let info = Info { source: self.source, piece_length, @@ -345,6 +356,8 @@ impl Create { } } + errln!(env, "\u{2728}\u{2728} Done! \u{2728}\u{2728}"); + Ok(()) } } @@ -2118,4 +2131,31 @@ Content Size 9 bytes ]), ); } + + #[test] + fn create_progress_messages() { + let mut env = TestEnvBuilder::new() + .arg_slice(&[ + "imdl", + "torrent", + "create", + "--input", + "foo", + "--announce", + "http://bar", + ]) + .build(); + + fs::write(env.resolve("foo"), "").unwrap(); + + let want = format!( + "[1/3] \u{1F9FF} Searching for files…\n[2/3] \u{1F9EE} Hashing pieces…\n[3/3] \u{1F4BE} \ + Writing metainfo to `{}`…\n\u{2728}\u{2728} Done! \u{2728}\u{2728}\n", + env.resolve("foo.torrent").display() + ); + + env.run().unwrap(); + + assert_eq!(env.err(), want); + } } diff --git a/src/target.rs b/src/target.rs index f887de0..fe0e2f4 100644 --- a/src/target.rs +++ b/src/target.rs @@ -25,6 +25,15 @@ impl From<&OsStr> for Target { } } +impl Display for Target { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Stdio => write!(f, "standard I/O"), + Self::File(path) => write!(f, "`{}`", path.display()), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -38,4 +47,19 @@ mod tests { fn stdio() { assert_eq!(Target::from(OsStr::new("-")), Target::Stdio); } + + #[test] + fn display_file() { + let path = PathBuf::from("./path"); + let have = Target::File(path).to_string(); + let want = "`./path`"; + assert_eq!(have, want); + } + + #[test] + fn display_stdio() { + let have = Target::Stdio.to_string(); + let want = "standard I/O"; + assert_eq!(have, want); + } } diff --git a/src/walker.rs b/src/walker.rs index ff192bb..5ac42e6 100644 --- a/src/walker.rs +++ b/src/walker.rs @@ -14,28 +14,30 @@ pub(crate) struct Walker { include_junk: bool, patterns: Vec, root: PathBuf, + spinner: Option, } impl Walker { - pub(crate) fn new(root: &Path) -> Walker { - Walker { + pub(crate) fn new(root: &Path) -> Self { + Self { follow_symlinks: false, include_hidden: false, include_junk: false, patterns: Vec::new(), root: root.to_owned(), + spinner: None, } } pub(crate) fn include_junk(self, include_junk: bool) -> Self { - Walker { + Self { include_junk, ..self } } pub(crate) fn include_hidden(self, include_hidden: bool) -> Self { - Walker { + Self { include_hidden, ..self } @@ -55,12 +57,19 @@ impl Walker { } pub(crate) fn follow_symlinks(self, follow_symlinks: bool) -> Self { - Walker { + Self { follow_symlinks, ..self } } + pub(crate) fn spinner(self, spinner: ProgressBar) -> Self { + Self { + spinner: Some(spinner), + ..self + } + } + pub(crate) fn files(self) -> Result { if !self.follow_symlinks && self @@ -85,6 +94,12 @@ impl Walker { let filter = |entry: &walkdir::DirEntry| { let path = entry.path(); + if let Some(s) = &self.spinner { + let display_path = path.strip_prefix(&self.root).unwrap_or(&path); + s.set_message(&display_path.display().to_string()); + s.tick(); + } + let file_name = entry.file_name(); if !self.include_hidden && file_name.to_string_lossy().starts_with('.') {