Completely change the CLI so that we have a server and a client.

This commit is contained in:
Igor Katson 2021-10-22 08:03:04 +01:00
parent b834bb20b3
commit a8efcfdd26
9 changed files with 309 additions and 108 deletions

View file

@ -134,7 +134,7 @@ impl ApiInternal {
) -> anyhow::Result<usize> {
let handle = self
.session
.add_torrent(url, opts)
.add_torrent(&url, opts)
.await
.context("error adding torrent")?
.context("expected session.add_torrent() to return a handle")?;
@ -209,6 +209,13 @@ fn json_or_404<T: Serialize>(idx: usize, v: Option<T>) -> warp::reply::Response
}
}
#[derive(Serialize, Deserialize)]
pub struct TorrentAddQueryParams {
pub overwrite: Option<bool>,
pub output_folder: Option<String>,
pub only_files_regex: Option<String>,
}
impl HttpApi {
pub fn new(session: Arc<Session>) -> Self {
Self {
@ -235,7 +242,8 @@ impl HttpApi {
// This is kind of not secure as it just reads any local file that it has access to,
// or any URL, but whatever, ok for our purposes / thread model.
"POST /torrents/": "Add a torrent here. magnet: or http:// or a local file."
}
},
"server": "rqbit",
});
move || json_response(&api_list)
});
@ -264,11 +272,6 @@ impl HttpApi {
move || json_response(inner.api_torrent_list())
});
#[derive(Deserialize)]
struct TorrentAddQueryParams {
overwrite: Option<bool>,
}
let torrent_add = warp::post()
.and(warp::path("torrents"))
.and(warp::body::bytes())
@ -293,6 +296,8 @@ impl HttpApi {
};
let opts = AddTorrentOptions {
overwrite: params.overwrite.unwrap_or(false),
only_files_regex: params.only_files_regex,
output_folder: params.output_folder,
..Default::default()
};
let idx = inner

View file

@ -0,0 +1,92 @@
use anyhow::Context;
use serde::Deserialize;
use crate::{http_api::TorrentAddQueryParams, session::AddTorrentOptions};
#[derive(Clone)]
pub struct HttpApiClient {
client: reqwest::Client,
base_url: reqwest::Url,
}
async fn check_response(r: reqwest::Response) -> anyhow::Result<reqwest::Response> {
if r.status().is_success() {
return Ok(r);
}
let status = r.status();
let url = r.url().clone();
let body = r.text().await.with_context(|| {
format!(
"cannot read response body for request to {} ({})",
url, status,
)
})?;
anyhow::bail!("{} -> {}: {}", url, status, body)
}
#[derive(Deserialize)]
struct ApiRoot {
server: String,
}
async fn json_response<T: serde::de::DeserializeOwned + std::any::Any>(
url: &reqwest::Url,
response: reqwest::Response,
) -> anyhow::Result<T> {
let response = check_response(response).await?;
let body = response.bytes().await?;
let response: T = serde_json::from_slice(&body).with_context(|| {
format!(
"error deserializing response from {:?} as {:?}",
url,
std::any::type_name::<T>(),
)
})?;
Ok(response)
}
impl HttpApiClient {
pub fn new(url: &str) -> anyhow::Result<Self> {
Ok(Self {
base_url: reqwest::Url::parse(url)?,
client: reqwest::ClientBuilder::new().build()?,
})
}
pub fn base_url(&self) -> &reqwest::Url {
&self.base_url
}
pub async fn validate_rqbit_server(&self) -> anyhow::Result<()> {
let response = self.client.get(self.base_url.clone()).send().await?;
let root: ApiRoot = json_response(&self.base_url, response).await?;
if root.server == "rqbit" {
return Ok(());
}
anyhow::bail!("not an rqbit server at {}", &self.base_url)
}
pub async fn add_torrent(
&self,
torrent: &str,
opts: Option<AddTorrentOptions>,
) -> anyhow::Result<usize> {
let opts = opts.unwrap_or_default();
let params = TorrentAddQueryParams {
overwrite: Some(opts.overwrite),
only_files_regex: opts.only_files_regex,
output_folder: opts.output_folder,
};
let qs = serde_urlencoded::to_string(&params).unwrap();
let url = format!("{}torrents?{}", &self.base_url, qs);
let response = check_response(
self.client
.post(&url)
.body(torrent.to_owned())
.send()
.await?,
)
.await?;
Ok(response.text().await?.parse::<usize>()?)
}
}

View file

@ -2,6 +2,7 @@ pub mod chunk_tracker;
pub mod dht_utils;
pub mod file_ops;
pub mod http_api;
pub mod http_api_client;
pub mod peer_connection;
pub mod peer_handler;
pub mod peer_info_reader;

View file

@ -115,7 +115,7 @@ fn compute_only_files<ByteBuf: AsRef<[u8]>>(
Ok(only_files)
}
#[derive(Default)]
#[derive(Default, Clone)]
pub struct AddTorrentOptions {
pub only_files_regex: Option<String>,
pub overwrite: bool,
@ -177,7 +177,7 @@ impl Session {
}
pub async fn add_torrent(
&self,
url: String,
url: &str,
opts: Option<AddTorrentOptions>,
) -> anyhow::Result<Option<TorrentManagerHandle>> {
// Magnet links are different in that we first need to discover the metadata.
@ -186,7 +186,7 @@ impl Session {
let Magnet {
info_hash,
trackers,
} = Magnet::parse(&url).context("provided path is not a valid magnet URL")?;
} = Magnet::parse(url).context("provided path is not a valid magnet URL")?;
let dht_rx = self
.dht
@ -230,9 +230,9 @@ impl Session {
.await
} else {
let torrent = if url.starts_with("http://") || url.starts_with("https://") {
torrent_from_url(&url).await?
torrent_from_url(url).await?
} else {
torrent_from_file(&url)?
torrent_from_file(url)?
};
let dht_rx = match self.dht.as_ref() {
Some(dht) => Some(dht.get_peers(torrent.info_hash).await?),