Add imdl torrent show
The `imdl torrent show` command displays information about on-disk torrent files. The formatting of the command's output is copied from torf, an excellent command-line torrent creator, editor, and viewer. type: added
This commit is contained in:
parent
6df45e0244
commit
99a069a021
109
Cargo.lock
generated
109
Cargo.lock
generated
|
@ -2,9 +2,9 @@
|
|||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f56c476256dc249def911d6f7580b5fc7e875895b5d7ee88f5d602208035744"
|
||||
checksum = "743ad5a418686aad3b87fd14c43badd828cf26e214a00f92a384291cf22e1811"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -38,6 +38,12 @@ dependencies = [
|
|||
"winapi 0.3.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
|
@ -59,6 +65,17 @@ version = "0.1.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.33.0"
|
||||
|
@ -75,6 +92,22 @@ dependencies = [
|
|||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "difference"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
||||
|
||||
[[package]]
|
||||
name = "doc-comment"
|
||||
version = "0.3.1"
|
||||
|
@ -155,9 +188,11 @@ version = "0.0.1"
|
|||
dependencies = [
|
||||
"ansi_term 0.12.1",
|
||||
"atty",
|
||||
"chrono",
|
||||
"env_logger",
|
||||
"libc",
|
||||
"md5",
|
||||
"pretty_assertions",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_bencode",
|
||||
|
@ -167,6 +202,7 @@ dependencies = [
|
|||
"static_assertions",
|
||||
"structopt",
|
||||
"tempfile",
|
||||
"unicode-width",
|
||||
"url",
|
||||
"walkdir",
|
||||
]
|
||||
|
@ -220,6 +256,34 @@ version = "2.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "output_vt100"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
|
||||
dependencies = [
|
||||
"winapi 0.3.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
|
@ -233,10 +297,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "0.4.6"
|
||||
name = "pretty_assertions"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a427176b1223957a219780f6bd014fad03f59543ff7feb3854a6fb8e9626ac2b"
|
||||
checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
|
||||
dependencies = [
|
||||
"ansi_term 0.11.0",
|
||||
"ctor",
|
||||
"difference",
|
||||
"output_vt100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875077759af22fa20b610ad4471d8155b321c89c3f2785526c9839b099be4e0a"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
|
@ -247,9 +323,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "0.4.6"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3e4daa0eae3f30db348ecfa5f942281173d441588ebdac871d730acbfc7f16"
|
||||
checksum = "c5717d9fa2664351a01ed73ba5ef6df09c01a521cb42cb65a061432a826f3c7a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -471,9 +547,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
|||
|
||||
[[package]]
|
||||
name = "structopt"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df136b42d76b1fbea72e2ab3057343977b04b4a2e00836c3c7c0673829572713"
|
||||
checksum = "a1bcbed7d48956fcbb5d80c6b95aedb553513de0a1b451ea92679d999c010e98"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"lazy_static",
|
||||
|
@ -482,9 +558,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "structopt-derive"
|
||||
version = "0.4.1"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd50a87d2f7b8958055f3e73a963d78feaccca3836767a9069844e34b5b03c0a"
|
||||
checksum = "095064aa1f5b94d14e635d0a5684cf140c43ae40a0fd990708d38f5d669e5f64"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
|
@ -568,6 +644,17 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"winapi 0.3.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.4"
|
||||
|
|
|
@ -15,9 +15,11 @@ default-run = "imdl"
|
|||
[dependencies]
|
||||
ansi_term = "0.12"
|
||||
atty = "0.2"
|
||||
chrono = "0.4.1"
|
||||
env_logger = "0.7"
|
||||
libc = "0.2"
|
||||
md5 = "0.7"
|
||||
pretty_assertions = "0.6"
|
||||
regex = "1"
|
||||
serde_bencode = "0.2"
|
||||
serde_bytes = "0.11"
|
||||
|
@ -25,6 +27,7 @@ sha1 = "0.6"
|
|||
snafu = "0.6"
|
||||
static_assertions = "1"
|
||||
tempfile = "3"
|
||||
unicode-width = "0.1"
|
||||
url = "2"
|
||||
walkdir = "2.1"
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
- [References](#references)
|
||||
- [Alternatives & Prior Art](#alternatives--prior-art)
|
||||
- [BitTorrent](#bittorrent)
|
||||
- [Acknowledgments](#acknowledgments)
|
||||
|
||||
## General
|
||||
|
||||
|
@ -161,3 +162,9 @@ at any time.
|
|||
| https://wiki.theory.org/index.php/Main_Page | Wiki with lots of information about all aspects of the BitTorrent protocol and implementations. |
|
||||
| https://archive.org/details/2014_torrent_archive_organized) | Massive 158 GiB archive containing 5.5 million torrents, assembled in 2014. |
|
||||
| https://github.com/internetarchive/dweb-transport | Github repository hosting The Internet Archive's distributed web and BitTorrent-related software. |
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
The formatting of `imdl torrent show` is entirely copied from
|
||||
[torf](https://github.com/rndusr/torf-cli), an excellent command-line torrent
|
||||
creator, editor, and viewer.
|
||||
|
|
2
bin/lint
2
bin/lint
|
@ -2,4 +2,4 @@
|
|||
|
||||
set -euxo pipefail
|
||||
|
||||
! grep --color -REn 'FIXME|TODO|XXX' src
|
||||
! grep --color -REni 'FIXME|TODO|XXX' src
|
||||
|
|
|
@ -15,15 +15,13 @@ impl<'buffer> Value<'buffer> {
|
|||
Parser::parse(buffer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
pub(crate) fn encode(&self) -> Vec<u8> {
|
||||
let mut buffer = Vec::new();
|
||||
self.encode_into(&mut buffer);
|
||||
buffer
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn encode_into(&self, buffer: &mut Vec<u8>) {
|
||||
pub(crate) fn encode_into(&self, buffer: &mut Vec<u8>) {
|
||||
match self {
|
||||
Self::Int(value) => {
|
||||
buffer.push(b'i');
|
||||
|
@ -49,7 +47,6 @@ impl<'buffer> Value<'buffer> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn encode_str(buffer: &mut Vec<u8>, contents: &[u8]) {
|
||||
buffer.extend_from_slice(contents.len().to_string().as_bytes());
|
||||
buffer.push(b':');
|
||||
|
|
|
@ -13,6 +13,10 @@ const YI: u128 = ZI << 10;
|
|||
pub(crate) struct Bytes(pub(crate) u128);
|
||||
|
||||
impl Bytes {
|
||||
pub(crate) fn from(bytes: impl Into<u128>) -> Bytes {
|
||||
Bytes(bytes.into())
|
||||
}
|
||||
|
||||
pub(crate) fn is_power_of_two(self) -> bool {
|
||||
self.0 == 0 || self.0 & (self.0 - 1) == 0
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ pub(crate) use std::{
|
|||
};
|
||||
|
||||
// dependencies
|
||||
pub(crate) use chrono::{TimeZone, Utc};
|
||||
pub(crate) use libc::EXIT_FAILURE;
|
||||
pub(crate) use regex::{Regex, RegexSet};
|
||||
pub(crate) use serde::{Deserialize, Serialize};
|
||||
|
@ -29,6 +30,7 @@ pub(crate) use structopt::{
|
|||
clap::{AppSettings, ArgSettings},
|
||||
StructOpt,
|
||||
};
|
||||
pub(crate) use unicode_width::UnicodeWidthStr;
|
||||
pub(crate) use url::Url;
|
||||
pub(crate) use walkdir::WalkDir;
|
||||
|
||||
|
@ -45,13 +47,10 @@ pub(crate) use crate::{
|
|||
pub(crate) use crate::{
|
||||
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info,
|
||||
lint::Lint, metainfo::Metainfo, mode::Mode, opt::Opt, platform::Platform, style::Style,
|
||||
subcommand::Subcommand, torrent::Torrent, use_color::UseColor,
|
||||
subcommand::Subcommand, table::Table, torrent::Torrent, torrent_summary::TorrentSummary,
|
||||
use_color::UseColor,
|
||||
};
|
||||
|
||||
// test modules
|
||||
#[cfg(test)]
|
||||
pub(crate) use crate::testing;
|
||||
|
||||
// test stdlib types
|
||||
#[cfg(test)]
|
||||
pub(crate) use std::{
|
||||
|
@ -63,6 +62,10 @@ pub(crate) use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
// test modules
|
||||
#[cfg(test)]
|
||||
pub(crate) use crate::testing;
|
||||
|
||||
// test structs and enums
|
||||
#[cfg(test)]
|
||||
pub(crate) use crate::{capture::Capture, test_env::TestEnv};
|
||||
|
|
17
src/error.rs
17
src/error.rs
|
@ -9,8 +9,13 @@ pub(crate) enum Error {
|
|||
AnnounceEmpty,
|
||||
#[snafu(display("Failed to parse announce URL: {}", source))]
|
||||
AnnounceUrlParse { source: url::ParseError },
|
||||
#[snafu(display("Failed to decode bencode: {}", source))]
|
||||
BencodeDecode { source: serde_bencode::Error },
|
||||
#[snafu(display("Failed to deserialize torrent metainfo from `{}`: {}", path.display(), source))]
|
||||
MetainfoLoad {
|
||||
source: serde_bencode::Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
#[snafu(display("Failed to serialize torrent metainfo: {}", source))]
|
||||
MetainfoSerialize { source: serde_bencode::Error },
|
||||
#[snafu(display("Failed to parse byte count `{}`: {}", text, source))]
|
||||
ByteParse {
|
||||
text: String,
|
||||
|
@ -44,8 +49,6 @@ pub(crate) enum Error {
|
|||
PieceLengthSmall,
|
||||
#[snafu(display("Piece length cannot be zero"))]
|
||||
PieceLengthZero,
|
||||
#[snafu(display("Serialization failed: {}", source))]
|
||||
Serialize { source: serde_bencode::Error },
|
||||
#[snafu(display("Failed to write to standard error: {}", source))]
|
||||
Stderr { source: io::Error },
|
||||
#[snafu(display("Failed to write to standard output: {}", source))]
|
||||
|
@ -77,12 +80,6 @@ impl From<clap::Error> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<serde_bencode::Error> for Error {
|
||||
fn from(source: serde_bencode::Error) -> Self {
|
||||
Self::Serialize { source }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemTimeError> for Error {
|
||||
fn from(source: SystemTimeError) -> Self {
|
||||
Self::SystemTime { source }
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::common::*;
|
|||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Info {
|
||||
pub private: u8,
|
||||
pub private: Option<u8>,
|
||||
#[serde(rename = "piece length")]
|
||||
pub piece_length: u32,
|
||||
pub name: String,
|
||||
|
|
|
@ -7,11 +7,15 @@
|
|||
clippy::implicit_return,
|
||||
clippy::indexing_slicing,
|
||||
clippy::integer_arithmetic,
|
||||
clippy::integer_division,
|
||||
clippy::large_enum_variant,
|
||||
clippy::missing_docs_in_private_items,
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::option_map_unwrap_or_else,
|
||||
clippy::option_unwrap_used,
|
||||
clippy::result_expect_used,
|
||||
clippy::result_unwrap_used,
|
||||
clippy::shadow_reuse,
|
||||
clippy::unreachable,
|
||||
clippy::wildcard_enum_match_arm
|
||||
)]
|
||||
|
@ -62,7 +66,9 @@ mod platform_interface;
|
|||
mod reckoner;
|
||||
mod style;
|
||||
mod subcommand;
|
||||
mod table;
|
||||
mod torrent;
|
||||
mod torrent_summary;
|
||||
mod use_color;
|
||||
|
||||
fn main() {
|
||||
|
|
|
@ -3,13 +3,33 @@ use crate::common::*;
|
|||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Metainfo {
|
||||
pub announce: String,
|
||||
#[serde(rename = "announce list")]
|
||||
#[serde(rename = "announce-list")]
|
||||
pub announce_list: Option<Vec<Vec<String>>>,
|
||||
pub comment: Option<String>,
|
||||
#[serde(rename = "created by")]
|
||||
pub created_by: Option<String>,
|
||||
#[serde(rename = "creation date")]
|
||||
pub creation_date: Option<u64>,
|
||||
pub encoding: String,
|
||||
pub encoding: Option<String>,
|
||||
pub info: Info,
|
||||
}
|
||||
|
||||
impl Metainfo {
|
||||
pub(crate) fn _load(path: impl AsRef<Path>) -> Result<Metainfo, Error> {
|
||||
let path = path.as_ref();
|
||||
let bytes = fs::read(path).context(error::Filesystem { path })?;
|
||||
Self::deserialize(path, &bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn dump(&self, path: impl AsRef<Path>) -> Result<(), Error> {
|
||||
let path = path.as_ref();
|
||||
let bytes = serde_bencode::ser::to_bytes(&self).context(error::MetainfoSerialize)?;
|
||||
fs::write(path, &bytes).context(error::Filesystem { path })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize(path: impl AsRef<Path>, bytes: &[u8]) -> Result<Metainfo, Error> {
|
||||
let path = path.as_ref();
|
||||
serde_bencode::de::from_bytes(&bytes).context(error::MetainfoLoad { path })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,3 +6,12 @@ pub enum Mode {
|
|||
Single { length: u64, md5sum: Option<String> },
|
||||
Multiple { files: Vec<FileInfo> },
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub(crate) fn total_size(&self) -> Bytes {
|
||||
match self {
|
||||
Self::Single { length, .. } => Bytes::from(*length),
|
||||
Self::Multiple { files } => Bytes::from(files.iter().map(|file| file.length).sum::<u64>()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
172
src/table.rs
Normal file
172
src/table.rs
Normal file
|
@ -0,0 +1,172 @@
|
|||
use crate::common::*;
|
||||
|
||||
pub(crate) struct Table {
|
||||
rows: Vec<(&'static str, Value)>,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { rows: Vec::new() }
|
||||
}
|
||||
|
||||
pub(crate) fn row(&mut self, name: &'static str, value: impl ToString) {
|
||||
self.rows.push((name, Value::Scalar(value.to_string())));
|
||||
}
|
||||
|
||||
pub(crate) fn tiers(
|
||||
&mut self,
|
||||
name: &'static str,
|
||||
tiers: impl IntoIterator<Item = (impl ToString, impl IntoIterator<Item = impl ToString>)>,
|
||||
) {
|
||||
self.rows.push((
|
||||
name,
|
||||
Value::Tiers(
|
||||
tiers
|
||||
.into_iter()
|
||||
.map(|(name, values)| {
|
||||
(
|
||||
name.to_string(),
|
||||
values.into_iter().map(|value| value.to_string()).collect(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
fn rows(&self) -> &[(&'static str, Value)] {
|
||||
&self.rows
|
||||
}
|
||||
|
||||
pub(crate) fn name_width(&self) -> usize {
|
||||
self
|
||||
.rows()
|
||||
.iter()
|
||||
.map(|row| UnicodeWidthStr::width(row.0))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub(crate) fn write_human_readable(&self, out: &mut dyn Write) -> io::Result<()> {
|
||||
fn padding(out: &mut dyn Write, n: usize) -> io::Result<()> {
|
||||
write!(out, "{:width$}", "", width = n)
|
||||
}
|
||||
|
||||
let name_width = self.name_width();
|
||||
|
||||
for (name, value) in self.rows() {
|
||||
write!(out, "{:>width$}", name, width = name_width)?;
|
||||
|
||||
match value {
|
||||
Value::Scalar(value) => writeln!(out, " {}", value)?,
|
||||
Value::Tiers(tiers) => {
|
||||
let tier_name_width = tiers
|
||||
.iter()
|
||||
.map(|(name, _values)| UnicodeWidthStr::width(name.as_str()))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
for (i, (name, values)) in tiers.iter().enumerate() {
|
||||
if i > 0 {
|
||||
padding(out, name_width)?;
|
||||
}
|
||||
|
||||
write!(
|
||||
out,
|
||||
" {:width$}",
|
||||
format!("{}:", name).as_str(),
|
||||
width = tier_name_width + 1
|
||||
)?;
|
||||
|
||||
for (i, value) in values.iter().enumerate() {
|
||||
if i > 0 {
|
||||
padding(out, name_width + 2 + tier_name_width + 1)?;
|
||||
}
|
||||
|
||||
writeln!(out, " {}", value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
enum Value {
|
||||
Scalar(String),
|
||||
Tiers(Vec<(String, Vec<String>)>),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn human_readable(table: Table, want: &str) {
|
||||
let mut cursor = Cursor::new(Vec::new());
|
||||
table.write_human_readable(&mut cursor).unwrap();
|
||||
let have = String::from_utf8(cursor.into_inner()).unwrap();
|
||||
if have != want {
|
||||
panic!("have != want:\nHAVE:\n{}\nWANT:\n{}", have, want);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_row() {
|
||||
let mut table = Table::new();
|
||||
table.row("Foo", "bar");
|
||||
human_readable(table, "Foo bar\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_rows() {
|
||||
let mut table = Table::new();
|
||||
table.row("Foo", "bar");
|
||||
table.row("X", "y");
|
||||
human_readable(table, "Foo bar\n X y\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiers_aligned() {
|
||||
let mut table = Table::new();
|
||||
table.tiers("Foo", vec![("Bar", &["a", "b"]), ("Baz", &["x", "y"])]);
|
||||
human_readable(
|
||||
table,
|
||||
"\
|
||||
Foo Bar: a
|
||||
b
|
||||
Baz: x
|
||||
y
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiers_unaligned() {
|
||||
let mut table = Table::new();
|
||||
table.tiers(
|
||||
"First",
|
||||
vec![("Some", &["the", "thing"]), ("Other", &["about", "that"])],
|
||||
);
|
||||
table.tiers(
|
||||
"Second",
|
||||
vec![
|
||||
("Row", &["the", "thing"]),
|
||||
("More Stuff", &["about", "that"]),
|
||||
],
|
||||
);
|
||||
human_readable(
|
||||
table,
|
||||
" First Some: the
|
||||
thing
|
||||
Other: about
|
||||
that
|
||||
Second Row: the
|
||||
thing
|
||||
More Stuff: about
|
||||
that
|
||||
",
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use crate::common::*;
|
||||
|
||||
mod create;
|
||||
mod show;
|
||||
mod stats;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
|
@ -12,6 +13,7 @@ mod stats;
|
|||
pub(crate) enum Torrent {
|
||||
Create(torrent::create::Create),
|
||||
Stats(torrent::stats::Stats),
|
||||
Show(torrent::show::Show),
|
||||
}
|
||||
|
||||
impl Torrent {
|
||||
|
@ -19,6 +21,7 @@ impl Torrent {
|
|||
match self {
|
||||
Self::Create(create) => create.run(env),
|
||||
Self::Stats(stats) => stats.run(env, unstable),
|
||||
Self::Show(show) => show.run(env),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -197,7 +197,7 @@ impl Create {
|
|||
input.parent().unwrap().join(torrent_name)
|
||||
});
|
||||
|
||||
let private = if self.private { 1 } else { 0 };
|
||||
let private = if self.private { Some(1) } else { None };
|
||||
|
||||
let creation_date = if self.no_creation_date {
|
||||
None
|
||||
|
@ -227,7 +227,7 @@ impl Create {
|
|||
|
||||
let metainfo = Metainfo {
|
||||
comment: self.comment,
|
||||
encoding: consts::ENCODING_UTF8.to_string(),
|
||||
encoding: Some(consts::ENCODING_UTF8.to_string()),
|
||||
announce: self.announce.to_string(),
|
||||
announce_list: if announce_list.is_empty() {
|
||||
None
|
||||
|
@ -239,9 +239,7 @@ impl Create {
|
|||
info,
|
||||
};
|
||||
|
||||
let bytes = serde_bencode::ser::to_bytes(&metainfo)?;
|
||||
|
||||
fs::write(&output, bytes).context(error::Filesystem { path: &output })?;
|
||||
metainfo.dump(&output)?;
|
||||
|
||||
if self.open {
|
||||
Platform::open(&output)?;
|
||||
|
@ -255,8 +253,6 @@ impl Create {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_env::TestEnv;
|
||||
|
||||
fn environment(args: &[&str]) -> TestEnv {
|
||||
testing::env(["torrent", "create"].iter().chain(args).cloned())
|
||||
}
|
||||
|
@ -292,7 +288,7 @@ mod tests {
|
|||
let torrent = env.resolve("foo.torrent");
|
||||
let bytes = fs::read(torrent).unwrap();
|
||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
||||
assert_eq!(metainfo.info.private, 0);
|
||||
assert_eq!(metainfo.info.private, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -303,7 +299,7 @@ mod tests {
|
|||
let torrent = env.resolve("foo.torrent");
|
||||
let bytes = fs::read(torrent).unwrap();
|
||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
||||
assert_eq!(metainfo.info.private, 1);
|
||||
assert_eq!(metainfo.info.private, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -573,7 +569,7 @@ mod tests {
|
|||
let torrent = env.resolve("foo.torrent");
|
||||
let bytes = fs::read(torrent).unwrap();
|
||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
|
||||
assert_eq!(metainfo.encoding, "UTF-8");
|
||||
assert_eq!(metainfo.encoding, Some("UTF-8".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
90
src/torrent/show.rs
Normal file
90
src/torrent/show.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use crate::common::*;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(
|
||||
help_message(consts::HELP_MESSAGE),
|
||||
version_message(consts::VERSION_MESSAGE),
|
||||
about("Display information about a `.torrent` file.")
|
||||
)]
|
||||
pub(crate) struct Show {
|
||||
#[structopt(
|
||||
name = "TORRENT",
|
||||
long = "input",
|
||||
help = "Show information about `TORRENT`."
|
||||
)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
impl Show {
|
||||
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
|
||||
let summary = TorrentSummary::load(&env.resolve(self.input))?;
|
||||
|
||||
let table = summary.table();
|
||||
|
||||
table
|
||||
.write_human_readable(&mut env.out)
|
||||
.context(error::Stdout)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut env = testing::env(
|
||||
["torrent", "show", "--input", "foo.torrent"]
|
||||
.iter()
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
let metainfo = Metainfo {
|
||||
announce: "announce".into(),
|
||||
announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]),
|
||||
comment: Some("comment".into()),
|
||||
created_by: Some("created by".into()),
|
||||
creation_date: Some(1),
|
||||
encoding: Some("UTF-8".into()),
|
||||
info: Info {
|
||||
private: Some(1),
|
||||
piece_length: 16 * 1024,
|
||||
name: "foo".into(),
|
||||
pieces: vec![
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
|
||||
],
|
||||
mode: Mode::Single {
|
||||
length: 20,
|
||||
md5sum: None,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let path = env.resolve("foo.torrent");
|
||||
|
||||
metainfo.dump(path).unwrap();
|
||||
|
||||
env.run().unwrap();
|
||||
|
||||
let have = env.out();
|
||||
let want = " Name foo
|
||||
Comment comment
|
||||
Created 1970-01-01 00:00:01 UTC
|
||||
Info Hash bd68a8a5ab377e37e8cdbfd37b670408c59a009f
|
||||
Torrent Size 236 bytes
|
||||
Content Size 20 bytes
|
||||
Private yes
|
||||
Trackers Main: announce
|
||||
Tier 1: announce
|
||||
b
|
||||
Tier 2: c
|
||||
Piece Size 16 KiB
|
||||
Piece Count 1
|
||||
File Count 1
|
||||
";
|
||||
|
||||
assert_eq!(have, want);
|
||||
}
|
||||
}
|
104
src/torrent_summary.rs
Normal file
104
src/torrent_summary.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
use crate::common::*;
|
||||
|
||||
pub(crate) struct TorrentSummary {
|
||||
metainfo: Metainfo,
|
||||
infohash: sha1::Digest,
|
||||
size: Bytes,
|
||||
}
|
||||
|
||||
impl TorrentSummary {
|
||||
pub(crate) fn load(path: &Path) -> Result<Self, Error> {
|
||||
let bytes = fs::read(path).context(error::Filesystem { path })?;
|
||||
|
||||
let metainfo = Metainfo::deserialize(path, &bytes)?;
|
||||
|
||||
let value = bencode::Value::decode(&bytes).unwrap();
|
||||
|
||||
let infohash = if let bencode::Value::Dict(items) = value {
|
||||
let info = items
|
||||
.iter()
|
||||
.find(|(key, _value)| key == b"info")
|
||||
.unwrap()
|
||||
.1
|
||||
.encode();
|
||||
Sha1::from(info).digest()
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let metadata = path.metadata().context(error::Filesystem { path })?;
|
||||
|
||||
Ok(Self {
|
||||
size: Bytes(metadata.len().into()),
|
||||
infohash,
|
||||
metainfo,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn table(&self) -> Table {
|
||||
let mut table = Table::new();
|
||||
|
||||
table.row("Name", &self.metainfo.info.name);
|
||||
|
||||
if let Some(comment) = &self.metainfo.comment {
|
||||
table.row("Comment", comment);
|
||||
}
|
||||
|
||||
if let Some(creation_date) = self.metainfo.creation_date {
|
||||
#[allow(clippy::as_conversions)]
|
||||
table.row(
|
||||
"Created",
|
||||
Utc.timestamp(
|
||||
creation_date
|
||||
.min(i64::max_value() as u64)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
table.row("Info Hash", self.infohash);
|
||||
|
||||
table.row("Torrent Size", self.size);
|
||||
|
||||
table.row("Content Size", self.metainfo.info.mode.total_size());
|
||||
|
||||
table.row(
|
||||
"Private",
|
||||
if self.metainfo.info.private.unwrap_or(0) == 1 {
|
||||
"yes"
|
||||
} else {
|
||||
"no"
|
||||
},
|
||||
);
|
||||
|
||||
match &self.metainfo.announce_list {
|
||||
Some(tiers) => {
|
||||
let mut value = Vec::new();
|
||||
value.push(("Main".to_owned(), vec![self.metainfo.announce.clone()]));
|
||||
|
||||
for (i, tier) in tiers.iter().enumerate() {
|
||||
value.push((format!("Tier {}", i + 1), tier.clone()));
|
||||
}
|
||||
|
||||
table.tiers("Trackers", value);
|
||||
}
|
||||
None => table.row("Tracker", &self.metainfo.announce),
|
||||
}
|
||||
|
||||
table.row("Piece Size", Bytes::from(self.metainfo.info.piece_length));
|
||||
|
||||
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(),
|
||||
},
|
||||
);
|
||||
|
||||
table
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user