Skip to content

Commit

Permalink
Add a torrent from-link subcommand
Browse files Browse the repository at this point in the history
It is now possible to create a torrent file from a magnet link. The
search is performed with no concurrency, and so will take a while to
complete.

type: added
fixes: casey#255
  • Loading branch information
atomgardner committed Jul 28, 2023
1 parent 5935178 commit 94a9113
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 13 deletions.
4 changes: 4 additions & 0 deletions bin/gen/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ examples:
text: "Intermodal can be used to create `.torrent` files:"
code: "imdl torrent create --input foo"

- command: imdl torrent from-link
text: "Intermodal can be used to create a `.torrent` file from a magnet link:"
code: "imdl torrent from-link magnet:?foo"

- command: imdl torrent show
text: "Print information about existing `.torrent` files:"
code: "imdl torrent show --input foo.torrent"
Expand Down
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub(crate) enum Error {
Filesystem { source: io::Error, path: PathBuf },
#[snafu(display("Error searching for files: {}", source))]
FileSearch { source: ignore::Error },
#[snafu(display("Failed to fetch infodict from accessible peers"))]
FromLinkNoInfo,
#[snafu(display("Invalid glob: {}", source))]
GlobParse { source: globset::Error },
#[snafu(display("Failed to serialize torrent info dictionary: {}", source))]
Expand Down
12 changes: 6 additions & 6 deletions src/magnet_link.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::common::*;

#[derive(Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct MagnetLink {
infohash: Infohash,
name: Option<String>,
peers: Vec<HostPort>,
trackers: Vec<Url>,
indices: BTreeSet<u64>,
pub(crate) infohash: Infohash,
pub(crate) name: Option<String>,
pub(crate) peers: Vec<HostPort>,
pub(crate) trackers: Vec<Url>,
pub(crate) indices: BTreeSet<u64>,
}

impl MagnetLink {
Expand Down
3 changes: 3 additions & 0 deletions src/subcommand/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::common::*;

mod announce;
mod create;
mod from_link;
mod link;
mod piece_length;
mod show;
Expand All @@ -17,6 +18,7 @@ mod verify;
pub(crate) enum Torrent {
Announce(announce::Announce),
Create(create::Create),
FromLink(from_link::FromLink),
Link(link::Link),
#[structopt(alias = "piece-size")]
PieceLength(piece_length::PieceLength),
Expand All @@ -30,6 +32,7 @@ impl Torrent {
match self {
Self::Announce(announce) => announce.run(env),
Self::Create(create) => create.run(env, options),
Self::FromLink(from_link) => from_link.run(env, options),
Self::Link(link) => link.run(env),
Self::PieceLength(piece_length) => piece_length.run(env),
Self::Show(show) => show.run(env),
Expand Down
2 changes: 1 addition & 1 deletion src/subcommand/torrent/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl Announce {
}
};

let client = match tracker::Client::from_url(tracker_url) {
let client = match tracker::Client::from_url(&tracker_url) {
Ok(client) => client,
Err(err) => {
errln!(env, "Couldn't build tracker client. {}", err)?;
Expand Down
182 changes: 182 additions & 0 deletions src/subcommand/torrent/from_link.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
use crate::common::*;

const URI_HELP: &str = "Generate a torrent file from a magnet URI";

const INPUT_FLAG: &str = "input-flag";
const INPUT_POSITIONAL: &str = "<INPUT>";
const INPUT_HELP: &str = "The magnet URI.";

#[derive(StructOpt)]
#[structopt(
help_message(consts::HELP_MESSAGE),
version_message(consts::VERSION_MESSAGE),
about(URI_HELP)
)]
pub(crate) struct FromLink {
#[structopt(
name = INPUT_FLAG,
long = "input",
short = "i",
value_name = "INPUT",
empty_values = false,
help = INPUT_HELP,
)]
input_flag: Option<MagnetLink>,
#[structopt(
name = INPUT_POSITIONAL,
value_name = "INPUT",
empty_values = false,
required_unless = INPUT_FLAG,
conflicts_with = INPUT_FLAG,
help = INPUT_HELP,
)]
input_positional: Option<MagnetLink>,
#[structopt(
long = "output",
short = "o",
value_name = "TARGET",
empty_values(false),
required_if(INPUT_FLAG, "-"),
required_if(INPUT_POSITIONAL, "-"),
help = "Save `.torrent` file to `TARGET`; if omitted, the parameter is set to `./${INFOHASH}.torrent`."
)]
output: Option<PathBuf>,
}

impl FromLink {
pub(crate) fn run(self, env: &mut Env, _options: &Options) -> Result<()> {
let link = xor_args(
"input_flag",
&self.input_flag,
"input_positional",
&self.input_positional,
)?;

let infohash = link.infohash;
let mut info: Option<Info> = None;

'trackers_loop: for announce_url in &link.trackers {
errln!(env, "trying `{}`", announce_url)?;

let client = match tracker::Client::from_url(announce_url) {
Err(err) => {
errln!(env, "Couldn't connect to tracker: {}", err)?;
continue;
}
Ok(client) => client,
};

let peer_list = match client.announce_exchange(link.infohash) {
Ok(peer_list) => peer_list,
Err(err) => {
errln!(env, "Couldn't connect to tracker: {}", err)?;
continue;
}
};

for peer in peer_list {
let mut peer = match peer::Client::connect(&peer, link.infohash) {
Ok(peer) => peer,
Err(err) => {
errln!(env, "Couldn't connect to peer: {}", err)?;
continue;
}
};

match peer.fetch_info_dict() {
Ok(infodict) => {
errln!(env, "Received and verified info")?;
info.replace(infodict);
break 'trackers_loop;
}
Err(err) => {
errln!(env, "Couldn't fetch info: {}", err)?;
continue;
}
};
}
}

let metainfo = match info {
Some(info) => Metainfo {
announce: None,
announce_list: Some(vec![link.trackers.iter().map(Url::to_string).collect()]),
nodes: None,
comment: None,
created_by: None,
creation_date: None,
encoding: None,
info,
},
None => return Err(Error::FromLinkNoInfo),
};

let path = self.output.unwrap_or_else(|| {
let mut path = PathBuf::new();
path.push(infohash.to_string());
path.set_extension("torrent");
path
});

fs::File::create(&path)
.context(error::Filesystem { path: path.clone() })
.and_then(|mut f| {
f.write_all(&bendy::serde::ser::to_bytes(&metainfo)?)
.context(error::Filesystem { path: path.clone() })
})
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn input_required() {
test_env! {
args: [
"torrent",
"from-magnet",
],
tree: {
},
matches: Err(Error::Clap { .. }),
};
}

#[test]
fn test_no_info() {
let tracker_url = "udp://1.2.3.4:1337";
let metainfo = Metainfo {
announce: None,
announce_list: Some(vec![vec![tracker_url.into()]]),
nodes: None,
comment: None,
created_by: None,
creation_date: None,
encoding: None,
info: Info {
private: None,
piece_length: Bytes(16 * 1024),
source: None,
name: "testing".into(),
pieces: PieceList::from_pieces(["test", "data"]),
mode: Mode::Single {
length: Bytes(2 * 16 * 1024),
md5sum: None,
},
update_url: None,
},
};
let link = MagnetLink::from_metainfo_lossy(&metainfo).unwrap();
let mut env = test_env! {
args: [
"torrent",
"from-link",
link.to_url().as_str(),
],
tree: {},
};
assert_matches!(env.run(), Err(Error::FromLinkNoInfo));
}
}
18 changes: 12 additions & 6 deletions src/tracker/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,17 @@ impl Client {
Ok(sock)
}

pub fn from_url(tracker_url: Url) -> Result<Self> {
pub fn from_url(tracker_url: &Url) -> Result<Self> {
if tracker_url.scheme() != "udp" {
return Err(Error::TrackerUdpOnly { tracker_url });
return Err(Error::TrackerUdpOnly {
tracker_url: tracker_url.clone(),
});
}
Self::connect(HostPort::try_from(&tracker_url).context(error::TrackerHostPort { tracker_url })?)
Self::connect(
HostPort::try_from(tracker_url).context(error::TrackerHostPort {
tracker_url: tracker_url.clone(),
})?,
)
}

fn connect_exchange(&mut self) -> Result<()> {
Expand Down Expand Up @@ -232,7 +238,7 @@ mod tests {
fn client_from_url_no_port() {
let tracker_url = Url::parse("udp://intermodal.io/announce").unwrap();
assert_matches!(
Client::from_url(tracker_url),
Client::from_url(&tracker_url),
Err(Error::TrackerHostPort { .. })
);
}
Expand All @@ -241,7 +247,7 @@ mod tests {
fn client_from_url_no_host() {
let tracker_url = Url::parse("udp://magnet:?announce=no_host").unwrap();
assert_matches!(
Client::from_url(tracker_url),
Client::from_url(&tracker_url),
Err(Error::TrackerHostPort { .. })
);
}
Expand All @@ -250,7 +256,7 @@ mod tests {
fn client_from_url_not_udp() {
let tracker_url = Url::parse("https://intermodal.io:100/announce").unwrap();
assert_matches!(
Client::from_url(tracker_url),
Client::from_url(&tracker_url),
Err(Error::TrackerUdpOnly { .. })
);
}
Expand Down

0 comments on commit 94a9113

Please sign in to comment.