Add progress messages to imdl torrent create

- Add messages showing overall progress
- Add file search spinner

type: added
This commit is contained in:
RJ Rybarczyk 2020-03-11 19:04:22 -04:00 committed by Casey Rodarmor
parent 2415d88d92
commit c6cd78f565
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
7 changed files with 163 additions and 20 deletions

62
Cargo.lock generated
View File

@ -148,6 +148,34 @@ dependencies = [
"vec_map", "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]] [[package]]
name = "ctor" name = "ctor"
version = "0.1.12" version = "0.1.12"
@ -170,6 +198,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97" checksum = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.7.1" version = "0.7.1"
@ -288,6 +322,7 @@ dependencies = [
"bendy", "bendy",
"chrono", "chrono",
"globset", "globset",
"indicatif",
"libc", "libc",
"md5", "md5",
"pretty_assertions", "pretty_assertions",
@ -309,6 +344,18 @@ dependencies = [
"walkdir", "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]] [[package]]
name = "kernel32-sys" name = "kernel32-sys"
version = "0.2.2" version = "0.2.2"
@ -389,6 +436,12 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "number_prefix"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
[[package]] [[package]]
name = "output_vt100" name = "output_vt100"
version = "0.1.2" version = "0.1.2"
@ -798,6 +851,15 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "termios"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b620c5ea021d75a735c943269bb07d30c9b77d6ac6b236bc8b5c496ef05625"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.11.0"

View File

@ -17,6 +17,7 @@ ansi_term = "0.12.0"
atty = "0.2.0" atty = "0.2.0"
chrono = "0.4.1" chrono = "0.4.1"
globset = "0.4.0" globset = "0.4.0"
indicatif = "0.14.0"
libc = "0.2.0" libc = "0.2.0"
md5 = "0.7.0" md5 = "0.7.0"
pretty_assertions = "0.6.0" pretty_assertions = "0.6.0"

View File

@ -24,6 +24,7 @@ pub(crate) use std::{
pub(crate) use bendy::{decoding::FromBencode, encoding::ToBencode, value::Value}; pub(crate) use bendy::{decoding::FromBencode, encoding::ToBencode, value::Value};
pub(crate) use chrono::{TimeZone, Utc}; pub(crate) use chrono::{TimeZone, Utc};
pub(crate) use globset::{Glob, GlobMatcher}; pub(crate) use globset::{Glob, GlobMatcher};
pub(crate) use indicatif::{ProgressBar, ProgressStyle};
pub(crate) use libc::EXIT_FAILURE; pub(crate) use libc::EXIT_FAILURE;
pub(crate) use regex::{Regex, RegexSet}; pub(crate) use regex::{Regex, RegexSet};
pub(crate) use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; pub(crate) use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};

View File

@ -7,8 +7,8 @@ pub(crate) struct Status {
} }
impl Status { impl Status {
pub(crate) fn new(pieces: bool, files: Vec<FileStatus>) -> Status { pub(crate) fn new(pieces: bool, files: Vec<FileStatus>) -> Self {
Status { pieces, files } Self { pieces, files }
} }
pub(crate) fn pieces(&self) -> bool { pub(crate) fn pieces(&self) -> bool {

View File

@ -184,11 +184,31 @@ impl Create {
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> { pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
let input = env.resolve(&self.input); 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::<Vec<String>>();
tier
.iter()
.map(|announce| announce.parse())
.collect::<Result<Vec<Url>, 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) let files = Walker::new(&input)
.include_junk(self.include_junk) .include_junk(self.include_junk)
.include_hidden(self.include_hidden) .include_hidden(self.include_hidden)
.follow_symlinks(self.follow_symlinks) .follow_symlinks(self.follow_symlinks)
.globs(&self.globs)? .globs(&self.globs)?
.spinner(spinner)
.files()?; .files()?;
let piece_length = self let piece_length = self
@ -212,19 +232,6 @@ impl Create {
return Err(Error::PieceLengthSmall); 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::<Vec<String>>();
tier
.iter()
.map(|announce| announce.parse())
.collect::<Result<Vec<Url>, url::ParseError>>()
.context(error::AnnounceUrlParse)?;
announce_list.push(tier);
}
let filename = input.file_name().ok_or_else(|| Error::FilenameExtract { let filename = input.file_name().ok_or_else(|| Error::FilenameExtract {
path: input.clone(), path: input.clone(),
})?; })?;
@ -268,12 +275,16 @@ impl Create {
Some(String::from(consts::CREATED_BY_DEFAULT)) Some(String::from(consts::CREATED_BY_DEFAULT))
}; };
errln!(env, "[2/3] \u{1F9EE} Hashing pieces…");
let (mode, pieces) = Hasher::hash( let (mode, pieces) = Hasher::hash(
&files, &files,
self.md5sum, self.md5sum,
piece_length.as_piece_length()?.into_usize(), piece_length.as_piece_length()?.into_usize(),
)?; )?;
errln!(env, "[3/3] \u{1F4BE} Writing metainfo to {}…", output);
let info = Info { let info = Info {
source: self.source, source: self.source,
piece_length, piece_length,
@ -345,6 +356,8 @@ impl Create {
} }
} }
errln!(env, "\u{2728}\u{2728} Done! \u{2728}\u{2728}");
Ok(()) 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);
}
} }

View File

@ -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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -38,4 +47,19 @@ mod tests {
fn stdio() { fn stdio() {
assert_eq!(Target::from(OsStr::new("-")), Target::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);
}
} }

View File

@ -14,28 +14,30 @@ pub(crate) struct Walker {
include_junk: bool, include_junk: bool,
patterns: Vec<Pattern>, patterns: Vec<Pattern>,
root: PathBuf, root: PathBuf,
spinner: Option<ProgressBar>,
} }
impl Walker { impl Walker {
pub(crate) fn new(root: &Path) -> Walker { pub(crate) fn new(root: &Path) -> Self {
Walker { Self {
follow_symlinks: false, follow_symlinks: false,
include_hidden: false, include_hidden: false,
include_junk: false, include_junk: false,
patterns: Vec::new(), patterns: Vec::new(),
root: root.to_owned(), root: root.to_owned(),
spinner: None,
} }
} }
pub(crate) fn include_junk(self, include_junk: bool) -> Self { pub(crate) fn include_junk(self, include_junk: bool) -> Self {
Walker { Self {
include_junk, include_junk,
..self ..self
} }
} }
pub(crate) fn include_hidden(self, include_hidden: bool) -> Self { pub(crate) fn include_hidden(self, include_hidden: bool) -> Self {
Walker { Self {
include_hidden, include_hidden,
..self ..self
} }
@ -55,12 +57,19 @@ impl Walker {
} }
pub(crate) fn follow_symlinks(self, follow_symlinks: bool) -> Self { pub(crate) fn follow_symlinks(self, follow_symlinks: bool) -> Self {
Walker { Self {
follow_symlinks, follow_symlinks,
..self ..self
} }
} }
pub(crate) fn spinner(self, spinner: ProgressBar) -> Self {
Self {
spinner: Some(spinner),
..self
}
}
pub(crate) fn files(self) -> Result<Files, Error> { pub(crate) fn files(self) -> Result<Files, Error> {
if !self.follow_symlinks if !self.follow_symlinks
&& self && self
@ -85,6 +94,12 @@ impl Walker {
let filter = |entry: &walkdir::DirEntry| { let filter = |entry: &walkdir::DirEntry| {
let path = entry.path(); 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(); let file_name = entry.file_name();
if !self.include_hidden && file_name.to_string_lossy().starts_with('.') { if !self.include_hidden && file_name.to_string_lossy().starts_with('.') {