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:
parent
a574368ffc
commit
43d87c06b6
|
@ -1,7 +1,7 @@
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
|
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Ord, PartialOrd, Eq)]
|
||||||
pub(crate) struct FilePath {
|
pub(crate) struct FilePath {
|
||||||
components: Vec<String>,
|
components: Vec<String>,
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,11 @@ impl FilePath {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn name(&self) -> &str {
|
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 {
|
pub(crate) fn absolute(&self, root: &Path) -> PathBuf {
|
||||||
|
@ -61,3 +65,15 @@ impl FilePath {
|
||||||
FilePath { components }
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
clippy::large_enum_variant,
|
clippy::large_enum_variant,
|
||||||
clippy::missing_docs_in_private_items,
|
clippy::missing_docs_in_private_items,
|
||||||
clippy::needless_pass_by_value,
|
clippy::needless_pass_by_value,
|
||||||
|
clippy::non_ascii_literal,
|
||||||
clippy::option_map_unwrap_or_else,
|
clippy::option_map_unwrap_or_else,
|
||||||
clippy::option_unwrap_used,
|
clippy::option_unwrap_used,
|
||||||
clippy::result_expect_used,
|
clippy::result_expect_used,
|
||||||
|
|
|
@ -994,7 +994,9 @@ mod tests {
|
||||||
fs::write(dir.join("h"), "hij").unwrap();
|
fs::write(dir.join("h"), "hij").unwrap();
|
||||||
env.run().unwrap();
|
env.run().unwrap();
|
||||||
let have = env.out();
|
let have = env.out();
|
||||||
let want = " Name foo
|
let want = format!(
|
||||||
|
" Name foo
|
||||||
|
Created By {}
|
||||||
Info Hash d3432a4b9d18baa413095a70f1e417021ceaca5b
|
Info Hash d3432a4b9d18baa413095a70f1e417021ceaca5b
|
||||||
Torrent Size 237 bytes
|
Torrent Size 237 bytes
|
||||||
Content Size 9 bytes
|
Content Size 9 bytes
|
||||||
|
@ -1003,7 +1005,13 @@ Content Size 9 bytes
|
||||||
Piece Size 16 KiB
|
Piece Size 16 KiB
|
||||||
Piece Count 1
|
Piece Count 1
|
||||||
File Count 3
|
File Count 3
|
||||||
";
|
Files foo
|
||||||
|
├─a
|
||||||
|
├─h
|
||||||
|
└─x
|
||||||
|
",
|
||||||
|
consts::CREATED_BY_DEFAULT
|
||||||
|
);
|
||||||
assert_eq!(have, want);
|
assert_eq!(have, want);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ mod tests {
|
||||||
let want = " Name foo
|
let want = " Name foo
|
||||||
Comment comment
|
Comment comment
|
||||||
Created 1970-01-01 00:00:01 UTC
|
Created 1970-01-01 00:00:01 UTC
|
||||||
|
Created By created by
|
||||||
Source source
|
Source source
|
||||||
Info Hash b7595205a46491b3e8686e10b28efe7144d066cc
|
Info Hash b7595205a46491b3e8686e10b28efe7144d066cc
|
||||||
Torrent Size 252 bytes
|
Torrent Size 252 bytes
|
||||||
|
@ -104,6 +105,7 @@ Content Size 20 bytes
|
||||||
Name\tfoo
|
Name\tfoo
|
||||||
Comment\tcomment
|
Comment\tcomment
|
||||||
Created\t1970-01-01 00:00:01 UTC
|
Created\t1970-01-01 00:00:01 UTC
|
||||||
|
Created By\tcreated by
|
||||||
Source\tsource
|
Source\tsource
|
||||||
Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc
|
Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc
|
||||||
Torrent Size\t252
|
Torrent Size\t252
|
||||||
|
|
132
src/table.rs
132
src/table.rs
|
@ -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)] {
|
fn rows(&self) -> &[(&'static str, Value)] {
|
||||||
&self.rows
|
&self.rows
|
||||||
}
|
}
|
||||||
|
@ -68,6 +79,38 @@ impl Table {
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
match value {
|
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::Scalar(scalar) => writeln!(out, " {}", scalar)?,
|
||||||
Value::Size(bytes) => writeln!(out, " {}", bytes)?,
|
Value::Size(bytes) => writeln!(out, " {}", bytes)?,
|
||||||
Value::Tiers(tiers) => {
|
Value::Tiers(tiers) => {
|
||||||
|
@ -108,6 +151,15 @@ impl Table {
|
||||||
for (name, value) in self.rows() {
|
for (name, value) in self.rows() {
|
||||||
write!(out, "{}\t", name)?;
|
write!(out, "{}\t", name)?;
|
||||||
match value {
|
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::Scalar(scalar) => writeln!(out, "{}", scalar)?,
|
||||||
Value::Size(Bytes(value)) => writeln!(out, "{}", value)?,
|
Value::Size(Bytes(value)) => writeln!(out, "{}", value)?,
|
||||||
Value::Tiers(tiers) => {
|
Value::Tiers(tiers) => {
|
||||||
|
@ -130,6 +182,61 @@ enum Value {
|
||||||
Scalar(String),
|
Scalar(String),
|
||||||
Tiers(Vec<(String, Vec<String>)>),
|
Tiers(Vec<(String, Vec<String>)>),
|
||||||
Size(Bytes),
|
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)]
|
#[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]
|
#[test]
|
||||||
fn single_row() {
|
fn single_row() {
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
|
|
|
@ -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 {
|
if let Some(source) = &self.metainfo.info.source {
|
||||||
table.row("Source", source);
|
table.row("Source", source);
|
||||||
}
|
}
|
||||||
|
@ -125,13 +129,20 @@ impl TorrentSummary {
|
||||||
|
|
||||||
table.row("Piece Count", self.metainfo.info.pieces.len() / 20);
|
table.row("Piece Count", self.metainfo.info.pieces.len() / 20);
|
||||||
|
|
||||||
table.row(
|
|
||||||
"File Count",
|
|
||||||
match &self.metainfo.info.mode {
|
match &self.metainfo.info.mode {
|
||||||
Mode::Single { .. } => 1,
|
Mode::Single { .. } => table.row("File Count", 1),
|
||||||
Mode::Multiple { files } => files.len(),
|
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
|
table
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user