Skip to content

Commit 7ddf2db

Browse files
feat: Implement add_torrent (#107)
* feat: Add `Engine::add_torrent` for creating and appending torrent instances to the engine * docs: Docs for add_torrent * docs: More docs for add_torrent + comments * refactor: Added more verbose error handling * docs: Add `Engine::add_torrent` usage examples Co-authored-by: kurealnum <oscar.gaske.cs@gmail.com> * docs: Fix minor doc issues Co-authored-by: kurealnum <oscar.gaske.cs@gmail.com> --------- Co-authored-by: kurealnum <oscar.gaske.cs@gmail.com>
1 parent de852f1 commit 7ddf2db

File tree

2 files changed

+96
-3
lines changed

2 files changed

+96
-3
lines changed

crates/libtortillas/src/engine/mod.rs

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
actor_request_response,
1616
errors::EngineError,
1717
hashes::InfoHash,
18-
metainfo::MetaInfo,
18+
metainfo::{MetaInfo, TorrentFile},
1919
peer::{Peer, PeerId},
2020
protocol::{
2121
messages::PeerMessages,
@@ -41,8 +41,8 @@ actor_request_response!(
4141
);
4242

4343
/// The "top level" struct for torrenting. Handles all
44-
/// [Torrent](crate::torrent::Torrent) actors. Note that the engine itself also
45-
/// implements the [Actor](kameo::Actor) trait, and consequently behaves like an
44+
/// [Torrent] actors. Note that the engine itself also
45+
/// implements the [Actor] trait, and consequently behaves like an
4646
/// actor.
4747
pub struct Engine {
4848
/// Listener to wait for incoming TCP connections from peers
@@ -67,6 +67,91 @@ pub struct Engine {
6767
actor_ref: ActorRef<Engine>,
6868
}
6969

70+
impl Engine {
71+
/// Starts the torrenting process for a given torrent. This function
72+
/// automatically contacts trackers and connects to peers. The spawned
73+
/// [Torrent Actor](Torrent) will be controlled by the [Engine].
74+
///
75+
/// This function accepts the following as input:
76+
/// - A remote URL to a torrent file over HTTP/HTTPS
77+
/// - The path, either absolute or relative, to a local torrent file
78+
/// - A magnet URI
79+
///
80+
/// If the inputted value is a remote url to a torrent file, this function
81+
/// requests the bytes and deserializes them into a [TorrentFile]. If
82+
/// it isn't, we assume that it is either a magnet URI or a path to a
83+
/// torrent file, and pass the string to [MetaInfo::new].
84+
///
85+
///
86+
/// # Examples
87+
///
88+
/// With a remote torrent file
89+
/// ```no_run
90+
/// use libtortillas::Engine;
91+
///
92+
/// #[tokio::main]
93+
/// async fn main() {
94+
/// let mut engine = Engine::new(todo!());
95+
/// let torrent_link = "https://example.com/example.torrent";
96+
/// let torrent_key = engine
97+
/// .add_torrent(torrent_link)
98+
/// .await
99+
/// .expect("Failed to add torrent");
100+
///
101+
/// println!("Started Torrenting: {}", torrent_key);
102+
/// }
103+
/// ```
104+
///
105+
/// With a magnet URI
106+
/// ```no_run
107+
/// use libtortillas::Engine;
108+
///
109+
/// #[tokio::main]
110+
/// async fn main() {
111+
/// let mut engine = Engine::new(todo!());
112+
/// let magnet_uri = "magnet:?xt=?????";
113+
/// let torrent_key = engine
114+
/// .add_torrent(magnet_uri)
115+
/// .await
116+
/// .expect("Failed to add torrent");
117+
///
118+
/// println!("Started Torrenting: {}", torrent_key);
119+
/// }
120+
/// ```
121+
pub async fn add_torrent(&self, metainfo: &str) -> Result<InfoHash, EngineError> {
122+
// File paths should either start with "/" or "./", and magnet URIs start
123+
// with "magnet:", so a check like this should be entirely appropriate.
124+
let metainfo = if metainfo.starts_with("http") {
125+
let torrent_file_bytes = reqwest::get(metainfo)
126+
.await
127+
.map_err(EngineError::MetaInfoFetchError)?
128+
.bytes()
129+
.await
130+
.map_err(EngineError::MetaInfoFetchError)?;
131+
let torrent_file = TorrentFile::parse(&torrent_file_bytes);
132+
torrent_file.map_err(|_| {
133+
error!(remote_url = metainfo);
134+
EngineError::MetaInfoDeserializeError
135+
})?
136+
} else {
137+
MetaInfo::new(metainfo.into()).await.map_err(|_| {
138+
error!(magnet_uri_or_file = metainfo);
139+
EngineError::MetaInfoDeserializeError
140+
})?
141+
};
142+
143+
let info_hash = metainfo.info_hash().map_err(EngineError::Other)?;
144+
145+
self
146+
.actor_ref
147+
.tell(EngineMessage::Torrent(Box::new(metainfo)))
148+
.await
149+
.expect("Failed to add torrent");
150+
151+
Ok(info_hash)
152+
}
153+
}
154+
70155
impl Actor for Engine {
71156
/// TCP socket address for incoming peers, uTP socket address for incoming
72157
/// peers, UDP socket address for UDP trackers.

crates/libtortillas/src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ pub enum EngineError {
4040
#[error("Network setup failed: {0}")]
4141
NetworkSetupFailed(String),
4242

43+
/// Failed to fetch [crate::metainfo::MetaInfo]
44+
#[error(transparent)]
45+
MetaInfoFetchError(#[from] reqwest::Error),
46+
47+
/// Failed to deserialize [MetaInfo](crate::metainfo::MetaInfo)
48+
#[error("Failed to deserialize meta info")]
49+
MetaInfoDeserializeError,
50+
4351
/// Any other engine-level error wrapped in [`anyhow::Error`]
4452
#[error(transparent)]
4553
Other(#[from] anyhow::Error),

0 commit comments

Comments
 (0)