diff --git a/Cargo.lock b/Cargo.lock index 0e5380f..190e586 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,6 +338,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lena" version = "0.1.0" +dependencies = [ + "transmission_rs", +] [[package]] name = "libc" diff --git a/crates/lena/Cargo.toml b/crates/lena/Cargo.toml index a3559d5..52ed97c 100644 --- a/crates/lena/Cargo.toml +++ b/crates/lena/Cargo.toml @@ -6,3 +6,5 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + +transmission_rs = { path = "../transmission-rs" } diff --git a/crates/lena/diesel.toml b/crates/lena/diesel.toml new file mode 100644 index 0000000..92267c8 --- /dev/null +++ b/crates/lena/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" diff --git a/crates/lena/migrations/.gitkeep b/crates/lena/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/crates/transmission-rs/src/client.rs b/crates/transmission-rs/src/client.rs index 7eccca2..1348b0a 100644 --- a/crates/transmission-rs/src/client.rs +++ b/crates/transmission-rs/src/client.rs @@ -16,13 +16,12 @@ mod list; use list::FieldList; use list::TorrentList; pub mod torrent; -use torrent::Torrent; -use torrent::TorrentFile; use torrent::AddTorrent; use torrent::AddTorrentResponse; use torrent::GetTorrentFilesRequest; use torrent::GetTorrentFilesResponse; -//use torrent::AddedTorrent; +use torrent::Torrent; +use torrent::TorrentFile; /// Interface into the major functions of Transmission /// including adding, and removing torrents. @@ -31,149 +30,191 @@ use torrent::GetTorrentFilesResponse; /// ``` #[derive(Clone)] pub struct Client { - host: String, - port: u16, - tls: bool, - auth: Option<(String, String)>, + host: String, + port: u16, + tls: bool, + auth: Option<(String, String)>, - base_url: String, - session_id: Option, + base_url: String, + session_id: Option, - http: reqwest::Client, + http: reqwest::Client, } impl Client { - // TODO: Take Option<(&str, &str)> for auth - pub fn new(host: impl ToString, port: u16, tls: bool, auth: Option<(String, String)>) -> Result { - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert(reqwest::header::USER_AGENT, "transmission-rs/0.1".try_into()?); - let this = Self{ - host: host.to_string(), - port: port, - tls: tls, - auth: auth, - base_url: { - let protocol = match tls { - true => "https", - false => "http" - }; - format!("{}://{}:{}", protocol, host.to_string(), port) - }, - session_id: None, - http: reqwest::Client::builder() - .gzip(true) - .default_headers(headers) - .build()? - }; - Ok(this) - } + pub fn new( + host: impl ToString, + port: u16, + tls: bool, + auth: Option<(T, U)>, + ) -> Result + where + T: Into, + U: Into, + { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::USER_AGENT, + "transmission-rs/0.1".try_into()?, + ); + let auth = match auth { + Some((u, p)) => Some((u.into(), p.into())), + None => None, + }; + let this = Self { + host: host.to_string(), + port, + tls, + auth, + base_url: { + let protocol = match tls { + true => "https", + false => "http", + }; + format!("{}://{}:{}", protocol, host.to_string(), port) + }, + session_id: None, + http: reqwest::Client::builder() + .gzip(true) + .default_headers(headers) + .build()?, + }; + Ok(this) + } - fn build_url(&self, path: impl AsRef) -> String { - format!("{}{}", self.base_url, path.as_ref()).to_string() - } + fn build_url(&self, path: impl AsRef) -> String { + format!("{}{}", self.base_url, path.as_ref()).to_string() + } - pub async fn authenticate(&mut self) -> Result<(), Error> { - self.session_id = None; - let response = self.post("/transmission/rpc/") - .header("Content-Type", "application/x-www-form-urlencoded") - .send() - .await?; - self.session_id = match response.headers().get("X-Transmission-Session-Id") { - Some(v) => Some(v.to_str()?.to_string()), - None => None // TODO: Return an error - }; - Ok(()) - } + pub async fn authenticate(&mut self) -> Result<(), Error> { + self.session_id = None; + let response = self + .post("/transmission/rpc/") + .header("Content-Type", "application/x-www-form-urlencoded") + .send() + .await?; + self.session_id = match response.headers().get("X-Transmission-Session-Id") { + Some(v) => Some(v.to_str()?.to_string()), + None => None, // TODO: Return an error + }; + Ok(()) + } - pub fn post(&self, path: impl AsRef) -> reqwest::RequestBuilder { - let url = self.build_url(path); - //println!("{}", url); - let mut request = self.http.post(&url); - request = match &self.auth { - Some(auth) => request.basic_auth(&auth.0, Some(&auth.1)), - None => request - }; - request = match &self.session_id { - Some(token) => request.header("X-Transmission-Session-Id", token), - None => request - }; - request - } + pub fn post(&self, path: impl AsRef) -> reqwest::RequestBuilder { + let url = self.build_url(path); + //println!("{}", url); + let mut request = self.http.post(&url); + request = match &self.auth { + Some(auth) => request.basic_auth(&auth.0, Some(&auth.1)), + None => request, + }; + request = match &self.session_id { + Some(token) => request.header("X-Transmission-Session-Id", token), + None => request, + }; + request + } - pub async fn rpc(&mut self, method: impl ToString, tag: u8, body: impl Serialize) -> Result { - let request = Request::new(method, tag, body); - let mut error = None; - for i in 0..5 { - sleep(Duration::from_secs(i)).await; - let result = self.post("/transmission/rpc/") - // Content-Type doesn't actually appear to be necessary, and is - // technically a lie, since there's no key/value being passed, - // just some JSON, but the official client sends this, so we - // will too. - .header("Content-Type", "application/x-www-form-urlencoded") - .body(serde_json::to_string(&request)?) - .send().await? - .error_for_status(); - match result { - Ok(r) => return Ok(r), - Err(e) => match e.status() { - Some(reqwest::StatusCode::CONFLICT) => { - self.authenticate().await?; - error = Some(e); - continue; - }, - _ => return Err(e.into()) - } - }; - } - match error { - Some(e) => Err(e.into()), - // Should be unreachable - None => Err(Error::HTTPUnknown) - } - } + pub async fn rpc( + &mut self, + method: impl ToString, + tag: u8, + body: impl Serialize, + ) -> Result { + let request = Request::new(method, tag, body); + let mut error = None; + for i in 0..5 { + sleep(Duration::from_secs(i)).await; + let result = self + .post("/transmission/rpc/") + // Content-Type doesn't actually appear to be necessary, and is + // technically a lie, since there's no key/value being passed, + // just some JSON, but the official client sends this, so we + // will too. + .header("Content-Type", "application/x-www-form-urlencoded") + .body(serde_json::to_string(&request)?) + .send() + .await? + .error_for_status(); + match result { + Ok(r) => return Ok(r), + Err(e) => match e.status() { + Some(reqwest::StatusCode::CONFLICT) => { + self.authenticate().await?; + error = Some(e); + continue; + } + _ => return Err(e.into()), + }, + }; + } + match error { + Some(e) => Err(e.into()), + // Should be unreachable + None => Err(Error::HTTPUnknown), + } + } - pub async fn list(&mut self) -> Result, Error> { - let field_list = FieldList::from_vec(vec![ - "error", - "errorString", - "eta", - "id", - "isFinished", - "leftUntilDone", - "name", - "peersGettingFromUs", - "peersSendingToUs", - "rateDownload", - "rateUpload", - "sizeWhenDone", - "status", - "uploadRatio" - ]); - let response: Response = self.rpc("torrent-get", 4, field_list).await?.error_for_status()?.json().await?; - Ok(response.arguments.torrents) - } + pub async fn list(&mut self) -> Result, Error> { + let field_list = FieldList::from_vec(vec![ + "error", + "errorString", + "eta", + "id", + "isFinished", + "leftUntilDone", + "name", + "peersGettingFromUs", + "peersSendingToUs", + "rateDownload", + "rateUpload", + "sizeWhenDone", + "status", + "uploadRatio", + ]); + let response: Response = self + .rpc("torrent-get", 4, field_list) + .await? + .error_for_status()? + .json() + .await?; + Ok(response.arguments.torrents) + } - // TODO: Borrow key from value - pub async fn list_by_name(&mut self) -> Result, Error> { - Ok(self.list().await?.into_iter().map(|v| (v.name.clone(), v)).collect::>()) - } + // TODO: Borrow key from value + pub async fn list_by_name(&mut self) -> Result, Error> { + Ok(self + .list() + .await? + .into_iter() + .map(|v| (v.name.clone(), v)) + .collect::>()) + } - pub async fn add_torrent_from_link(&mut self, url: impl ToString) -> Result { - let response: Response = self.rpc("torrent-add", 8, AddTorrent{filename: url.to_string()}).await?.error_for_status()?.json().await?; - Ok(response.arguments) - } + pub async fn add_torrent(&mut self, data: AddTorrent) -> Result { + let response: Response = self + .rpc("torrent-add", 8, data) + .await? + .error_for_status()? + .json() + .await?; + Ok(response.arguments) + } - pub async fn get_files_by_id(&mut self, id: u16) -> Result, Error> { - let response: Response = self.rpc("torrent-get", 3, GetTorrentFilesRequest::new(id)).await?.error_for_status()?.json().await?; - if(response.arguments.torrents.len() == 0) { - // TODO: Maybe make the Ok result an Option? Or maybe return a 404 error? - return Ok(vec![]); - } else if(response.arguments.torrents.len() > 1) { - return Err(Error::WeirdGetFilesResponse); - } - // TODO: Figure out how to move files out of this structure, since we're discarding the rest anyway, instead of cloning - Ok(response.arguments.torrents[0].files.clone()) - } + pub async fn get_files_by_id(&mut self, id: u16) -> Result, Error> { + let response: Response = self + .rpc("torrent-get", 3, GetTorrentFilesRequest::new(id)) + .await? + .error_for_status()? + .json() + .await?; + if (response.arguments.torrents.len() == 0) { + // TODO: Maybe make the Ok result an Option? Or maybe return a 404 error? + return Ok(vec![]); + } else if (response.arguments.torrents.len() > 1) { + return Err(Error::WeirdGetFilesResponse); + } + // TODO: Figure out how to move files out of this structure, since we're discarding the rest anyway, instead of cloning + Ok(response.arguments.torrents[0].files.clone()) + } } - diff --git a/crates/transmission-rs/src/client/torrent.rs b/crates/transmission-rs/src/client/torrent.rs index a49f33e..c0b13b6 100644 --- a/crates/transmission-rs/src/client/torrent.rs +++ b/crates/transmission-rs/src/client/torrent.rs @@ -6,79 +6,80 @@ use serde::Serialize; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Torrent { - pub id: u16, - pub error: u8, - pub error_string: String, - pub eta: i64, // TODO: Option with -1 as None - pub is_finished: bool, - pub left_until_done: u64, // TODO: u32? - pub name: String, - pub peers_getting_from_us: u16, - pub peers_sending_to_us: u16, - pub rate_download: u32, - pub rate_upload: u32, - pub size_when_done: u64, - pub status: u8, - pub upload_ratio: f32 + pub id: u16, + pub error: u8, + pub error_string: String, + pub eta: i64, // TODO: Option with -1 as None + pub is_finished: bool, + pub left_until_done: u64, // TODO: u32? + pub name: String, + pub peers_getting_from_us: u16, + pub peers_sending_to_us: u16, + pub rate_download: u32, + pub rate_upload: u32, + pub size_when_done: u64, + pub status: u8, + pub upload_ratio: f32, } #[derive(Debug, Serialize)] pub struct AddTorrent { - pub filename: String + pub filename: String, + #[serde(rename = "download-dir")] + pub download_dir: String, } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AddedTorrent { - pub id: u16, - pub name: String, - pub hash_string: String + pub id: u16, + pub name: String, + pub hash_string: String, } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub enum AddTorrentResponse { - TorrentAdded(AddedTorrent), - TorrentDuplicate(AddedTorrent) + TorrentAdded(AddedTorrent), + TorrentDuplicate(AddedTorrent), } #[derive(Debug, Serialize)] #[serde(rename_all = "kebab-case")] pub struct GetTorrentFilesRequest { - fields: Vec, - ids: u16 + fields: Vec, + ids: u16, } impl GetTorrentFilesRequest { - pub fn new(id: u16) -> Self { - Self{ - fields: vec!["files".to_string()], - ids: id - } - } + pub fn new(id: u16) -> Self { + Self { + fields: vec!["files".to_string()], + ids: id, + } + } } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TorrentFile { - pub name: PathBuf, - pub bytes_completed: u64, - pub length: u64 + pub name: PathBuf, + pub bytes_completed: u64, + pub length: u64, } impl TorrentFile { - pub fn take_name(self) -> PathBuf { - self.name - } + pub fn take_name(self) -> PathBuf { + self.name + } } #[derive(Debug, Deserialize)] pub struct TorrentFileWrapper { - pub files: Vec + pub files: Vec, } #[derive(Debug, Deserialize)] pub struct GetTorrentFilesResponse { - pub torrents: Vec + pub torrents: Vec, } - diff --git a/shell.nix b/shell.nix index 5859c41..fcbfb86 100644 --- a/shell.nix +++ b/shell.nix @@ -3,9 +3,10 @@ pkgs.mkShell { buildInputs = with pkgs; [ rustc cargo rustfmt rls rust-analyzer - pkg-config sqlite openssl + pkg-config sqlite openssl diesel-cli # keep this line if you use bash pkgs.bashInteractive ]; + DATABASE_URL = "./var/lena.db"; }