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
This commit is contained in:
Casey Rodarmor 2020-02-05 23:57:35 -08:00
parent a574368ffc
commit 43d87c06b6
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
6 changed files with 181 additions and 11 deletions

View File

@ -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<String>,
}
@ -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(())
}
}

View File

@ -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,

View File

@ -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);
}

View File

@ -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

View File

@ -38,6 +38,17 @@ impl Table {
));
}
pub(crate) fn directory(&mut self, name: &'static str, root: &str, mut files: Vec<FilePath>) {
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<String>)>),
Size(Bytes),
Directory { root: String, files: Vec<FilePath> },
}
struct Tree<'name> {
name: &'name str,
children: Vec<Tree<'name>>,
}
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<bool>, &'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<bool>, lines: &mut Vec<(Vec<bool>, &'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();

View File

@ -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(),
},
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
}