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::*;
|
||||
|
||||
#[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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
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)] {
|
||||
&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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user