From 43d87c06b6b5a3b1c6b21f088a56da1892279700 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 5 Feb 2020 23:57:35 -0800 Subject: [PATCH] Display torrent file tree Display the contents of torrents as tree of files when showing torrents with `imdl torrent show` and after `imdl torrent create`. The formatting and structure of the code was lifted entirely from torf. type: added --- src/file_path.rs | 20 +++++- src/main.rs | 1 + src/opt/torrent/create.rs | 12 +++- src/opt/torrent/show.rs | 2 + src/table.rs | 132 ++++++++++++++++++++++++++++++++++++++ src/torrent_summary.rs | 25 ++++++-- 6 files changed, 181 insertions(+), 11 deletions(-) diff --git a/src/file_path.rs b/src/file_path.rs index 25d50c6..1be1878 100644 --- a/src/file_path.rs +++ b/src/file_path.rs @@ -1,7 +1,7 @@ use crate::common::*; #[serde(transparent)] -#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Ord, PartialOrd, Eq)] pub(crate) struct FilePath { components: Vec, } @@ -39,7 +39,11 @@ impl FilePath { } pub(crate) fn name(&self) -> &str { - &self.components[0] + &self.components[self.components.len() - 1] + } + + pub(crate) fn components(&self) -> &[String] { + &self.components } pub(crate) fn absolute(&self, root: &Path) -> PathBuf { @@ -61,3 +65,15 @@ impl FilePath { FilePath { components } } } + +impl Display for FilePath { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + for (i, component) in self.components.iter().enumerate() { + if i > 0 { + write!(f, "/")?; + } + write!(f, "{}", component)?; + } + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index af855b9..fde9e31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ clippy::large_enum_variant, clippy::missing_docs_in_private_items, clippy::needless_pass_by_value, + clippy::non_ascii_literal, clippy::option_map_unwrap_or_else, clippy::option_unwrap_used, clippy::result_expect_used, diff --git a/src/opt/torrent/create.rs b/src/opt/torrent/create.rs index c84349f..fd4773c 100644 --- a/src/opt/torrent/create.rs +++ b/src/opt/torrent/create.rs @@ -994,7 +994,9 @@ mod tests { fs::write(dir.join("h"), "hij").unwrap(); env.run().unwrap(); let have = env.out(); - let want = " Name foo + let want = format!( + " Name foo + Created By {} Info Hash d3432a4b9d18baa413095a70f1e417021ceaca5b Torrent Size 237 bytes Content Size 9 bytes @@ -1003,7 +1005,13 @@ Content Size 9 bytes Piece Size 16 KiB Piece Count 1 File Count 3 -"; + Files foo + ├─a + ├─h + └─x +", + consts::CREATED_BY_DEFAULT + ); assert_eq!(have, want); } diff --git a/src/opt/torrent/show.rs b/src/opt/torrent/show.rs index 0cde0a6..779a560 100644 --- a/src/opt/torrent/show.rs +++ b/src/opt/torrent/show.rs @@ -72,6 +72,7 @@ mod tests { let want = " Name foo Comment comment Created 1970-01-01 00:00:01 UTC + Created By created by Source source Info Hash b7595205a46491b3e8686e10b28efe7144d066cc Torrent Size 252 bytes @@ -104,6 +105,7 @@ Content Size 20 bytes Name\tfoo Comment\tcomment Created\t1970-01-01 00:00:01 UTC +Created By\tcreated by Source\tsource Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc Torrent Size\t252 diff --git a/src/table.rs b/src/table.rs index 6cdf14f..dbd871b 100644 --- a/src/table.rs +++ b/src/table.rs @@ -38,6 +38,17 @@ impl Table { )); } + pub(crate) fn directory(&mut self, name: &'static str, root: &str, mut files: Vec) { + files.sort(); + self.rows.push(( + name, + Value::Directory { + root: root.to_owned(), + files, + }, + )); + } + fn rows(&self) -> &[(&'static str, Value)] { &self.rows } @@ -68,6 +79,38 @@ impl Table { )?; match value { + Value::Directory { root, files } => { + let mut tree = Tree::new(&root); + for file in files { + tree.insert(file.components()); + } + let lines = tree.lines(); + + for (i, (last, name)) in lines.iter().enumerate() { + if i == 0 { + write!(out, " ")?; + } else { + write!(out, "{:indent$} ", "", indent = name_width)?; + } + + if !last.is_empty() { + for last in &last[..last.len() - 1] { + if *last { + write!(out, " ")?; + } else { + write!(out, "│ ")?; + } + } + if last[last.len() - 1] { + write!(out, "└─")?; + } else { + write!(out, "├─")?; + } + } + + writeln!(out, "{}", name)?; + } + } Value::Scalar(scalar) => writeln!(out, " {}", scalar)?, Value::Size(bytes) => writeln!(out, " {}", bytes)?, Value::Tiers(tiers) => { @@ -108,6 +151,15 @@ impl Table { for (name, value) in self.rows() { write!(out, "{}\t", name)?; match value { + Value::Directory { root, files } => { + for (i, file) in files.iter().enumerate() { + if i > 0 { + write!(out, "\t")?; + } + write!(out, "{}/{}", root, file)?; + } + writeln!(out)?; + } Value::Scalar(scalar) => writeln!(out, "{}", scalar)?, Value::Size(Bytes(value)) => writeln!(out, "{}", value)?, Value::Tiers(tiers) => { @@ -130,6 +182,61 @@ enum Value { Scalar(String), Tiers(Vec<(String, Vec)>), Size(Bytes), + Directory { root: String, files: Vec }, +} + +struct Tree<'name> { + name: &'name str, + children: Vec>, +} + +impl<'name> Tree<'name> { + fn new(name: &'name str) -> Tree<'name> { + Self { + name, + children: Vec::new(), + } + } + + fn insert(&mut self, file: &'name [String]) { + if file.is_empty() { + return; + } + + let head = &file[0]; + + for child in &mut self.children { + if child.name == head { + child.insert(&file[1..]); + return; + } + } + + let mut child = Self::new(head); + child.insert(&file[1..]); + + self.children.push(child); + } + + fn lines(&self) -> Vec<(Vec, &'name str)> { + let mut lines = Vec::new(); + let mut last = Vec::new(); + self.lines_inner(&mut last, &mut lines); + lines + } + + fn lines_inner(&self, last: &mut Vec, lines: &mut Vec<(Vec, &'name str)>) { + lines.push((last.clone(), self.name)); + last.push(false); + for (i, child) in self.children.iter().enumerate() { + if i == self.children.len() - 1 { + last.pop(); + last.push(true); + } + child.lines_inner(last, lines); + } + last.pop(); + } } #[cfg(test)] @@ -172,6 +279,31 @@ mod tests { ); } + #[test] + fn directory() { + let mut table = Table::new(); + table.directory( + "Files", + "Foo", + vec![ + FilePath::from_components(&["a", "b"]), + FilePath::from_components(&["a", "c"]), + FilePath::from_components(&["d"]), + ], + ); + tab_delimited(&table, "Files\tFoo/a/b\tFoo/a/c\tFoo/d\n"); + human_readable( + &table, + "\ +Files Foo + ├─a + │ ├─b + │ └─c + └─d +", + ); + } + #[test] fn single_row() { let mut table = Table::new(); diff --git a/src/torrent_summary.rs b/src/torrent_summary.rs index 5dbe1d1..4ef331d 100644 --- a/src/torrent_summary.rs +++ b/src/torrent_summary.rs @@ -82,6 +82,10 @@ impl TorrentSummary { ); } + if let Some(created_by) = &self.metainfo.created_by { + table.row("Created By", created_by); + } + if let Some(source) = &self.metainfo.info.source { table.row("Source", source); } @@ -125,13 +129,20 @@ impl TorrentSummary { table.row("Piece Count", self.metainfo.info.pieces.len() / 20); - table.row( - "File Count", - match &self.metainfo.info.mode { - Mode::Single { .. } => 1, - Mode::Multiple { files } => files.len(), - }, - ); + match &self.metainfo.info.mode { + Mode::Single { .. } => table.row("File Count", 1), + Mode::Multiple { files } => { + table.row("File Count", files.len()); + table.directory( + "Files", + &self.metainfo.info.name, + files + .iter() + .map(|file_info| file_info.path.clone()) + .collect(), + ); + } + }; table }