Optionally print torrent details as JSON

Adds `--json` flag to `imdl torrent show` to print torrent details as
JSON.

type: added
This commit is contained in:
Celeo 2020-09-18 19:57:46 -07:00 committed by Casey Rodarmor
parent 70c1f4e57c
commit 0c17c3da49
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
6 changed files with 183 additions and 4 deletions

View File

@ -10,6 +10,12 @@ on:
branches:
- master
env:
# Increment to invalidate github actions caches if they become corrupt.
# Errors of the form "can't find crate for `snafu_derive` which `snafu` depends on"
# can usually be fixed by incrementing this value.
CACHE_KEY_PREFIX: 2
jobs:
all:
name: All
@ -45,23 +51,31 @@ jobs:
with:
fetch-depth: 0
# An issue with BSD Tar causes sporadic failures on macOS.
# c.f https://github.com/actions/cache/issues/403
- name: Install GNU Tar
if: matrix.build == 'macos'
run: |
brew install gnu-tar
echo "::add-path::/usr/local/opt/gnu-tar/libexec/gnubin"
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: v1-${{ runner.os }}-cargo-registry
key: ${{ env.CACHE_KEY_PREFIX }}-${{ runner.os }}-cargo-registry
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: v1-${{ runner.os }}-cargo-index
key: ${{ env.CACHE_KEY_PREFIX }}-${{ runner.os }}-cargo-index
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: v1-${{ runner.os }}-cargo-build-target
key: ${{ env.CACHE_KEY_PREFIX }}-${{ runner.os }}-cargo-build-target
- name: Install Stable
uses: actions-rs/toolchain@v1

1
Cargo.lock generated
View File

@ -626,6 +626,7 @@ dependencies = [
"serde",
"serde-hex",
"serde_bytes",
"serde_json",
"serde_with",
"sha1",
"snafu",

View File

@ -34,6 +34,7 @@ pretty_env_logger = "0.4.0"
regex = "1.0.0"
serde-hex = "0.1.0"
serde_bytes = "0.11.0"
serde_json = "1.0.57"
serde_with = "1.4.0"
sha1 = "0.6.0"
snafu = "0.6.0"

View File

@ -136,6 +136,8 @@ pub(crate) enum Error {
Unstable { feature: &'static str },
#[snafu(display("Torrent verification failed."))]
Verify,
#[snafu(display("Failed to serialize JSON: {}", source))]
JsonSerialize { source: serde_json::Error },
}
impl Error {

View File

@ -9,6 +9,10 @@ const INPUT_POSITIONAL: &str = "<INPUT>";
const INPUT_VALUE: &str = "INPUT";
const JSON_OUTPUT: &str = "json";
const JSON_HELP: &str = "Output data as JSON instead of the default format.";
#[derive(StructOpt)]
#[structopt(
help_message(consts::HELP_MESSAGE),
@ -36,6 +40,13 @@ pub(crate) struct Show {
help = INPUT_HELP,
)]
input_positional: Option<InputTarget>,
#[structopt(
name = JSON_OUTPUT,
long = "json",
short = "j",
help = JSON_HELP,
)]
json: bool,
}
impl Show {
@ -49,7 +60,11 @@ impl Show {
let input = env.read(target)?;
let summary = TorrentSummary::from_input(&input)?;
summary.write(env)?;
if self.json {
summary.write_json(env)?;
} else {
summary.write(env)?;
}
Ok(())
}
}
@ -559,4 +574,63 @@ files\tNAME
Ok(())
}
#[test]
fn output_json() {
{
let metainfo = Metainfo::test_value_single();
let mut want = r#"{"name":"NAME","comment":"COMMENT","creation_date":1,
"created_by":"CREATED BY","source":"SOURCE","info_hash":"5d6f53772b4c20536fcce0c4c364d764a6efa39c",
"torrent_size":509,"content_size":32768,"private":true,"tracker":
"udp://announce.example:1337","announce_list":[["http://a.example:4567",
"https://b.example:77"],["udp://c.example:88"]],"update_url":"https://update.example/",
"dht_nodes":["node.example:12","1.1.1.1:16","[2001:db8:85a3::8a2e:370]:7334"],
"piece_size":16384,"piece_count":2,"file_count":1,"files":["NAME"]}"#
.replace('\n', "");
want.push('\n');
let mut env = TestEnvBuilder::new()
.arg_slice(&[
"imdl",
"torrent",
"show",
"--input",
"foo.torrent",
"--json",
])
.out_is_term()
.build();
let path = env.resolve("foo.torrent").unwrap();
metainfo.dump(path).unwrap();
env.assert_ok();
let have = env.out();
assert_eq!(have, want);
}
{
let metainfo = Metainfo::test_value_single_unset();
let mut want = r#"{"name":"NAME","comment":null,"creation_date":null,
"created_by":null,"source":null,"info_hash":"a9105b0ff5f7cefeee5599ed7831749be21cc04e",
"torrent_size":85,"content_size":5,"private":false,"tracker":null,"announce_list":[],
"update_url":null,"dht_nodes":[],"piece_size":1024,"piece_count":1,"file_count":1,
"files":["NAME"]}"#
.replace('\n', "");
want.push('\n');
let mut env = TestEnvBuilder::new()
.arg_slice(&[
"imdl",
"torrent",
"show",
"--input",
"foo.torrent",
"--json",
])
.out_is_term()
.build();
let path = env.resolve("foo.torrent").unwrap();
metainfo.dump(path).unwrap();
env.assert_ok();
let have = env.out();
assert_eq!(have, want);
}
}
}

View File

@ -6,6 +6,27 @@ pub(crate) struct TorrentSummary {
size: Bytes,
}
#[derive(Serialize)]
pub(crate) struct TorrentSummaryJson {
name: String,
comment: Option<String>,
creation_date: Option<u64>,
created_by: Option<String>,
source: Option<String>,
info_hash: String,
torrent_size: u64,
content_size: u64,
private: bool,
tracker: Option<String>,
announce_list: Vec<Vec<String>>,
update_url: Option<String>,
dht_nodes: Vec<String>,
piece_size: u64,
piece_count: usize,
file_count: usize,
files: Vec<String>,
}
impl TorrentSummary {
fn new(metainfo: Metainfo, infohash: Infohash, size: Bytes) -> Self {
Self {
@ -146,4 +167,70 @@ impl TorrentSummary {
table
}
pub(crate) fn write_json(&self, env: &mut Env) -> Result<()> {
let data = self.torrent_summary_data();
let json = serde_json::to_string(&data).context(error::JsonSerialize)?;
outln!(env, "{}", json)?;
Ok(())
}
fn torrent_summary_data(&self) -> TorrentSummaryJson {
let (file_count, files) = match &self.metainfo.info.mode {
Mode::Single { .. } => (1, vec![self.metainfo.info.name.to_string()]),
Mode::Multiple { files } => (
files.len(),
files
.iter()
.map(|file_info| {
format!(
"{}",
file_info
.path
.absolute(Path::new(&self.metainfo.info.name))
.as_path()
.display()
)
})
.collect(),
),
};
TorrentSummaryJson {
name: self.metainfo.info.name.to_string(),
comment: self.metainfo.comment.clone(),
creation_date: self.metainfo.creation_date,
created_by: self.metainfo.created_by.clone(),
source: self.metainfo.info.source.clone(),
info_hash: self.infohash.to_string(),
torrent_size: self.size.count(),
content_size: self.metainfo.content_size().count(),
private: self.metainfo.info.private.unwrap_or_default(),
tracker: self.metainfo.announce.clone(),
announce_list: self
.metainfo
.announce_list
.as_ref()
.map(Clone::clone)
.unwrap_or_default(),
update_url: self
.metainfo
.info
.update_url
.as_ref()
.map(ToString::to_string),
dht_nodes: self
.metainfo
.nodes
.as_ref()
.unwrap_or(&Vec::new())
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>(),
piece_size: self.metainfo.info.piece_length.count(),
piece_count: self.metainfo.info.pieces.count(),
file_count,
files,
}
}
}