diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 701b424..def5c49 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -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
diff --git a/Cargo.lock b/Cargo.lock
index 4479f82..c0ef95c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -626,6 +626,7 @@ dependencies = [
"serde",
"serde-hex",
"serde_bytes",
+ "serde_json",
"serde_with",
"sha1",
"snafu",
diff --git a/Cargo.toml b/Cargo.toml
index 1a9a3df..2aa6154 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/src/error.rs b/src/error.rs
index 8c9cca3..e6c2130 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -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 {
diff --git a/src/subcommand/torrent/show.rs b/src/subcommand/torrent/show.rs
index 346180b..e4216e4 100644
--- a/src/subcommand/torrent/show.rs
+++ b/src/subcommand/torrent/show.rs
@@ -9,6 +9,10 @@ const INPUT_POSITIONAL: &str = "";
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,
+ #[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);
+ }
+ }
}
diff --git a/src/torrent_summary.rs b/src/torrent_summary.rs
index 2081e78..43366f7 100644
--- a/src/torrent_summary.rs
+++ b/src/torrent_summary.rs
@@ -6,6 +6,27 @@ pub(crate) struct TorrentSummary {
size: Bytes,
}
+#[derive(Serialize)]
+pub(crate) struct TorrentSummaryJson {
+ name: String,
+ comment: Option,
+ creation_date: Option,
+ created_by: Option,
+ source: Option,
+ info_hash: String,
+ torrent_size: u64,
+ content_size: u64,
+ private: bool,
+ tracker: Option,
+ announce_list: Vec>,
+ update_url: Option,
+ dht_nodes: Vec,
+ piece_size: u64,
+ piece_count: usize,
+ file_count: usize,
+ files: Vec,
+}
+
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::>(),
+ piece_size: self.metainfo.info.piece_length.count(),
+ piece_count: self.metainfo.info.pieces.count(),
+ file_count,
+ files,
+ }
+ }
}