diff --git a/crates/librqbit/examples/ubuntu.rs b/crates/librqbit/examples/ubuntu.rs index 6813043..c4932b0 100644 --- a/crates/librqbit/examples/ubuntu.rs +++ b/crates/librqbit/examples/ubuntu.rs @@ -6,7 +6,7 @@ use std::time::Duration; use anyhow::Context; -use librqbit::session::{AddTorrentOptions, AddTorrentResponse, Session}; +use librqbit::session::{AddTorrent, AddTorrentOptions, AddTorrentResponse, Session}; use tracing::info; // This is ubuntu-21.04-live-server-amd64.iso.torrent @@ -30,7 +30,7 @@ async fn main() -> Result<(), anyhow::Error> { // Add the torrent to the session let handle = match session .add_torrent( - MAGNET_LINK, + AddTorrent::from_url(MAGNET_LINK), Some(AddTorrentOptions { // Set this to true to allow writing on top of existing files. // If the file is partially downloaded, librqbit will only download the diff --git a/crates/librqbit/src/http_api.rs b/crates/librqbit/src/http_api.rs index 81ad43e..43077fe 100644 --- a/crates/librqbit/src/http_api.rs +++ b/crates/librqbit/src/http_api.rs @@ -84,8 +84,8 @@ impl HttpApi { ) -> Result { let opts = params.into_add_torrent_options(); let add = match String::from_utf8(data.to_vec()) { - Ok(s) => AddTorrent::from(s), - Err(e) => AddTorrent::from(e.into_bytes()), + Ok(s) => AddTorrent::Url(s.into()), + Err(e) => AddTorrent::TorrentFileBytes(e.into_bytes().into()), }; state.api_add_torrent(add, Some(opts)).await.map(axum::Json) } diff --git a/crates/librqbit/src/http_api_client.rs b/crates/librqbit/src/http_api_client.rs index 033354d..f1cd8dd 100644 --- a/crates/librqbit/src/http_api_client.rs +++ b/crates/librqbit/src/http_api_client.rs @@ -1,7 +1,10 @@ use anyhow::Context; use serde::Deserialize; -use crate::{http_api::TorrentAddQueryParams, session::AddTorrentOptions}; +use crate::{ + http_api::TorrentAddQueryParams, + session::{AddTorrent, AddTorrentOptions}, +}; #[derive(Clone)] pub struct HttpApiClient { @@ -77,7 +80,7 @@ impl HttpApiClient { pub async fn add_torrent( &self, - torrent: &str, + torrent: AddTorrent<'_>, opts: Option, ) -> anyhow::Result { let opts = opts.unwrap_or_default(); @@ -94,7 +97,7 @@ impl HttpApiClient { let response = check_response( self.client .post(&url) - .body(torrent.to_owned()) + .body(torrent.into_bytes()) .send() .await?, ) diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 5a6c88e..644cb81 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, fs::File, io::Read, net::SocketAddr, path::PathBuf, time::Duration}; +use std::{borrow::Cow, io::Read, net::SocketAddr, path::PathBuf, time::Duration}; use anyhow::{bail, Context}; use buffers::ByteString; @@ -20,6 +20,8 @@ use crate::{ torrent_manager::{TorrentManagerBuilder, TorrentManagerHandle}, }; +pub const SUPPORTED_SCHEMES: [&str; 3] = ["http:", "https:", "magnet:"]; + #[derive(Clone)] pub enum ManagedTorrentState { Initializing, @@ -83,21 +85,6 @@ async fn torrent_from_url(url: &str) -> anyhow::Result { torrent_from_bytes(&b).context("error decoding torrent") } -fn torrent_from_file(filename: &str) -> anyhow::Result { - let mut buf = Vec::new(); - if filename == "-" { - std::io::stdin() - .read_to_end(&mut buf) - .context("error reading stdin")?; - } else { - File::open(filename) - .with_context(|| format!("error opening {filename}"))? - .read_to_end(&mut buf) - .with_context(|| format!("error reading {filename}"))?; - } - torrent_from_bytes(&buf).context("error decoding torrent") -} - fn compute_only_files>( torrent: &TorrentMetaV1Info, filename_re: &str, @@ -142,26 +129,55 @@ pub enum AddTorrentResponse { Added(TorrentManagerHandle), } +pub fn read_local_file_including_stdin(filename: &str) -> anyhow::Result> { + let mut buf = Vec::new(); + if filename == "-" { + std::io::stdin() + .read_to_end(&mut buf) + .context("error reading stdin")?; + } else { + std::fs::File::open(filename) + .context("error opening")? + .read_to_end(&mut buf) + .context("error reading")?; + } + Ok(buf) +} + pub enum AddTorrent<'a> { Url(Cow<'a, str>), - TorrentFileBytes(Vec), + TorrentFileBytes(Cow<'a, [u8]>), } -impl<'a> From<&'a str> for AddTorrent<'a> { - fn from(s: &'a str) -> Self { - Self::Url(Cow::Borrowed(s)) +impl<'a> AddTorrent<'a> { + // Don't call this from HTTP API. + pub fn from_cli_argument(path: &'a str) -> anyhow::Result { + if SUPPORTED_SCHEMES.iter().any(|s| path.starts_with(s)) { + return Ok(Self::Url(Cow::Borrowed(path))); + } + Self::from_local_filename(path) } -} -impl<'a> From for AddTorrent<'a> { - fn from(s: String) -> Self { - Self::Url(Cow::Owned(s)) + pub fn from_url(url: impl Into>) -> Self { + Self::Url(url.into()) } -} -impl<'a> From> for AddTorrent<'a> { - fn from(b: Vec) -> Self { - Self::TorrentFileBytes(b) + pub fn from_bytes(bytes: impl Into>) -> Self { + Self::TorrentFileBytes(bytes.into()) + } + + // Don't call this from HTTP API. + pub fn from_local_filename(filename: &str) -> anyhow::Result { + let file = read_local_file_including_stdin(filename) + .with_context(|| format!("error reading local file {filename:?}"))?; + Ok(Self::TorrentFileBytes(Cow::Owned(file))) + } + + pub fn into_bytes(self) -> Vec { + match self { + Self::Url(s) => s.into_owned().into_bytes(), + Self::TorrentFileBytes(b) => b.into_owned(), + } } } @@ -270,7 +286,12 @@ impl Session { { torrent_from_url(&url).await? } - AddTorrent::Url(filename) => torrent_from_file(&filename)?, + AddTorrent::Url(url) => { + bail!( + "unsupported URL {:?}. Supporting magnet:, http:, and https", + url + ) + } AddTorrent::TorrentFileBytes(bytes) => { torrent_from_bytes(&bytes).context("error decoding torrent")? } diff --git a/crates/rqbit/src/main.rs b/crates/rqbit/src/main.rs index d0745f3..33fd911 100644 --- a/crates/rqbit/src/main.rs +++ b/crates/rqbit/src/main.rs @@ -7,8 +7,8 @@ use librqbit::{ http_api_client, peer_connection::PeerConnectionOptions, session::{ - AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, ManagedTorrentState, Session, - SessionOptions, + AddTorrent, AddTorrentOptions, AddTorrentResponse, ListOnlyResponse, ManagedTorrentState, + Session, SessionOptions, }, spawn_utils::{spawn, BlockingSpawner}, torrent_state::timeit, @@ -329,7 +329,10 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> if connect_to_existing { for torrent_url in &download_opts.torrent_path { match client - .add_torrent(torrent_url, Some(torrent_opts.clone())) + .add_torrent( + AddTorrent::from_cli_argument(torrent_url)?, + Some(torrent_opts.clone()), + ) .await { Ok(ApiAddTorrentResponse { id, details }) => { @@ -382,7 +385,10 @@ async fn async_main(opts: Opts, spawner: BlockingSpawner) -> anyhow::Result<()> for path in &download_opts.torrent_path { let handle = match session - .add_torrent(path.as_str(), Some(torrent_opts.clone())) + .add_torrent( + AddTorrent::from_cli_argument(path)?, + Some(torrent_opts.clone()), + ) .await { Ok(v) => match v {