From 4f38526dfbd70de7516c3c4c70bcfb7f116d1347 Mon Sep 17 00:00:00 2001 From: "programmer@kl.netlib.re" Date: Thu, 20 Oct 2022 01:28:11 +0200 Subject: [PATCH] Better errors --- src/.error.rs.swp | Bin 12288 -> 0 bytes src/action/.add.rs.swp | Bin 12288 -> 0 bytes src/action/add.rs | 2 +- src/action/hash.rs | 4 +- src/action/magnet.rs | 2 +- src/action/name.rs | 7 +- src/api.rs | 142 ++++++++++++++++++++++++++++++++++------- src/error.rs | 18 +++++- src/utils.rs | 66 +++++++++++-------- 9 files changed, 186 insertions(+), 55 deletions(-) delete mode 100644 src/.error.rs.swp delete mode 100644 src/action/.add.rs.swp diff --git a/src/.error.rs.swp b/src/.error.rs.swp deleted file mode 100644 index 229bff495e01be6add6f794c2046a9080d796567..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2J!=#}7{@1CYE-lk3t>!}JxQ`LHen)x_<|e~5=hh{k+?6<-3{)}Zf0hq;W(`< z@(C=(%EH1@KZcbk*jWh{N;)f_*}XlIND@*;o`wJJ&dl@rn`w^QaPmQOo=;CqF}%(( z_WaYmXzS-VgEuExVL-d-!>%OAZI&Nqg|S^ge^F;`KZRHZ7M}R)q4kZ9e9$`66WSae zg)E2w5jZk|!nVezhuQV1Ym?4*bmR)ZaQ^<0!;k|JAOb{y2oM1xKm>>Y5g-D`mw?TO z*b6NGM8Ad0{pZl0XMac!B0vO)01+SpM1Tko0U|&IhyW2F0z}{#5(r|(zMWz$IgQ2R z|Nr&x{~u==`-=L4`iS~~dX0LC>YyH?{+wa#GinR<7y({TuZQ z^&RyN^$PV2rBMpy^1nsVMFfZd5g-CYfCvx)B0vO)!2dy@$2fPY@0_P#Owa~Vzf4GF zGp{jL8ZAN65@NnG;a!}|i;%ZeZR`!aWtu0viLN)6td^=h+u-A~{Eqvi82!rCdcdjz zeE%*e&@yT+lq;UGu@Ev7q8#=s804-~3SK4Py{g2~6{CVy;lF!8D7*F)!nGimLf~!972hvv-=y8% zs;a~C^_zo8d~UH>)q-VInd&NbXW!lYfqC|ZkFqo^l#Bu^Gqq2$W*R3udX+d7qGD9x zJ}~2Qg~d;UwgSsF1Tk(g$F0o+ZQN@a>Yo;GWvV6HJMvZ&<^CR+u(B(3)ly3)uy0cC z82}!H*#mr`8>iBL5QeqREQn@%`oQs&ZIn^LR_6G9r~kOLsG=(iPL`p Dbr58f diff --git a/src/action/.add.rs.swp b/src/action/.add.rs.swp deleted file mode 100644 index 4ccaed1b5ee3d88a73d55ab8cb7b1fc4bbe33362..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2J#W)M7{{+LAzF$6F<(>EI#L=JSSklvg;FGh5LFQ`QU#G?pOd40>-oZqP+(zZ zWMx4@Oo%VQ#=yqXjfs_R{Q~^YcI1Sm>R9PH>7TQoyL(=KcZ-$lEg2oQmz5E$rSX=R38y>fZkESs;*@$=^%9K{38B?3f%2oM1xKm>>Y5g-CY z;FuH8=@fg58cvjT@Uox!uP>Y5g-CYfCvx)B0vO=ApzHC?DRBa zpCR-AKQC+8pJnVX^b7h9eT3dY9q1{v0$qfDpJD6=^a*+fJ%%1ZYtSWV7Mg*kq5T=g zzCjtZ1+}3I&?#sS@xMY}%3MC67f=jQBLYN#2oM1xKm>>Y5g-CY;204YGLBaKnx}g^ zmQ}Aa^JrSCWURw@Zdf&D?B#;oI&Q#_o608c3R?@2+lz&6@Jxh3Gj(yp*smOiLYtZV zI&Y_PJ(MEWjuWUP>Uc>U$ey)O^!2q??jMf%uzEDFI#CMCIy&hTp{#{GRB5kb4Y>$2 z!Cy18YShN3HoY*3g;lLE*%dyKpbUxVs-%v59BHrIVI;(?xne?Bz7?W$hiN|Q{QJKSzqe}eBwv|>9P-c6FQ@{(I8e91V| z!u5Fq2BvHT_V@4-O+!AVX1SWl7nf>m^a-#(smxZX^(}28T6-oZt3^lFx;83&*LgdRc>Sn$+EJ zW4FO?iteCiFyK6LPvNG!;|#P6Gsh_dRR^jyKB?SFr`XAfq2h+T<5U|sF`V!6gas!b SvquNNRU~7!%5g?Xx7a^|@K#U& diff --git a/src/action/add.rs b/src/action/add.rs index 42f73d5..0f60d46 100644 --- a/src/action/add.rs +++ b/src/action/add.rs @@ -24,7 +24,7 @@ impl ActionExec for AddAction { let magnet = if self.torrent.starts_with("magnet:") { self.torrent.clone() } else { - torrent_to_magnet(&self.torrent) + torrent_to_magnet(&self.torrent)? }; let api = ApiClient::from_config(&config)?; api.add(&magnet, self.paused) diff --git a/src/action/hash.rs b/src/action/hash.rs index 86e5920..d4b95d1 100644 --- a/src/action/hash.rs +++ b/src/action/hash.rs @@ -18,9 +18,9 @@ impl ActionExec for HashAction { fn exec(&self, _config: &Config) -> Result<(), Error> { // TODO errors let hash = if self.torrent.starts_with("magnet:") { - magnet_hash(&self.torrent) + magnet_hash(&self.torrent)? } else { - torrent_hash(&self.torrent) + torrent_hash(&self.torrent)? }; println!("{}", hash); Ok(()) diff --git a/src/action/magnet.rs b/src/action/magnet.rs index 452996d..8867c15 100644 --- a/src/action/magnet.rs +++ b/src/action/magnet.rs @@ -19,7 +19,7 @@ impl ActionExec for MagnetAction { if self.torrent.starts_with("magnet:") { println!("{}", &self.torrent); } else { - println!("{}", torrent_to_magnet(&self.torrent)); + println!("{}", torrent_to_magnet(&self.torrent)?); } Ok(()) } diff --git a/src/action/name.rs b/src/action/name.rs index 3a2e304..874b58d 100644 --- a/src/action/name.rs +++ b/src/action/name.rs @@ -1,9 +1,11 @@ use argh::FromArgs; +use snafu::prelude::*; use crate::Error; use crate::action::ActionExec; use crate::config::Config; use crate::utils::{magnet_name, torrent_name}; +use crate::error::EmptyNameMagnetSnafu as EmptyNameMagnet; #[derive(FromArgs, PartialEq, Debug)] #[argh(subcommand, name = "name")] @@ -17,9 +19,10 @@ pub struct NameAction { impl ActionExec for NameAction { fn exec(&self, _config: &Config) -> Result<(), Error> { let name = if self.torrent.starts_with("magnet:") { - magnet_name(&self.torrent) + magnet_name(&self.torrent)? + .context(EmptyNameMagnet { magnet: &self.torrent })? } else { - torrent_name(&self.torrent) + torrent_name(&self.torrent)? }; println!("{}", name); Ok(()) diff --git a/src/api.rs b/src/api.rs index ee1dba8..0418764 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,40 +1,100 @@ use snafu::ResultExt; use qbittorrent_web_api::Api; +use qbittorrent_web_api::api_impl::Error as QBittorrentError; use tokio::runtime::Builder; use crate::config::Config; use crate::error::{Error, ApiSnafu as IOError, InternalApiSnafu as ApiError}; -pub struct ApiClient { - pub rt: tokio::runtime::Runtime, - pub api: qbittorrent_web_api::api_impl::Authenticated +pub fn blocking_runtime() -> std::io::Result { + Builder::new_current_thread() + .enable_all() + .build() } -impl ApiClient { - pub fn from_config(config: &Config) -> Result { - Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password) - } +#[derive(Debug)] +pub struct UnauthenticatedRawApiClient { + pub rt: tokio::runtime::Runtime, +} - pub fn new(host: &str, login: &str, password: &str) -> Result { - let rt = Builder::new_current_thread() - .enable_all() - .build().context(IOError)?; - // Call the asynchronous connect method using the runtime. - let api = rt.block_on(Api::login(host, &login, &password)).context(ApiError)?; - Ok(ApiClient { - rt, - api, +impl UnauthenticatedRawApiClient { + /// Initialize a blocking runtime for the API client + pub fn new() -> Result { + Ok(UnauthenticatedRawApiClient { + rt: blocking_runtime()?, }) } - pub fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> { + /// Login into a qBittorrent backend and return a proper ApiClient instance + pub fn login(self, host: &str, login: &str, password: &str) -> Result { + let api = self.rt.block_on(Api::login(host, login, password))?; + Ok(RawApiClient { + rt: self.rt, + api, + }) + } +} + +#[derive(Debug)] +pub struct RawApiClient { + rt: tokio::runtime::Runtime, + api: qbittorrent_web_api::api_impl::Authenticated, +} + +impl RawApiClient { + pub fn add(&self, magnet: &str, paused: bool) -> Result { let base_call = self.api.torrent_management(); let call = if paused { base_call.add(magnet).paused("true") } else { base_call.add(magnet) }; - let res = self.rt.block_on(call.send_raw()).context(ApiError)?; + self.rt.block_on(call.send_raw()) + } + + pub fn get(&self, hash: &str) -> Result { + self.rt.block_on(self.api.torrent_management().properties_raw(hash)) + } + + pub fn list(&self) -> Result { + self.rt.block_on(self.api.torrent_management().info().send_raw()) + } + +} + +#[derive(Debug)] +/// ApiClient is a convenience struct around qbittorrent_web_api for use in async programs, using qbt::Error error types. +pub struct ApiClient { + pub raw_api: RawApiClient, +} + +impl ApiClient { + /// Login to a qBittorrent backend and return a handle to the ApiClient + pub fn new(host: &str, login: &str, password: &str) -> Result { + let unauthenticated = UnauthenticatedRawApiClient::new().context(IOError)?; + let authenticated = unauthenticated.login(host, login, password).map_err(|e| { + match e { + QBittorrentError::HttpError(_) => { + Error::FailedToReachAPI { source: e } + }, QBittorrentError::InvalidUsernameOrPassword => { + Error::FailedLogin { user: login.to_string() } + } _ => { + panic!("Cookie error"); + } + } + })?; + Ok(ApiClient { + raw_api: authenticated, + }) + } + + pub fn from_config(config: &Config) -> Result { + Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password) + } + + + pub fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> { + let res = self.raw_api.add(magnet, paused).context(ApiError)?; if res == "Ok." { Ok(()) } else { @@ -43,7 +103,7 @@ impl ApiClient { } pub fn get(&self, hash: &str) -> Result, Error> { - let res = self.rt.block_on(self.api.torrent_management().properties_raw(hash)).context(ApiError)?; + let res = self.raw_api.get(hash).context(ApiError)?; if res == "" { Ok(None) } else { @@ -52,12 +112,50 @@ impl ApiClient { } pub fn list(&self) -> Result { - Ok( - self.rt.block_on(self.api.torrent_management().info().send_raw()).context(ApiError)? - ) + self.raw_api.list().context(ApiError) } } // TODO: typestate builder https://www.greyblake.com/blog/builder-with-typestate-in-rust/ //struct ApiClientBuilder {} + +#[cfg(test)] +mod tests { + use crate::utils::*; + use crate::api::{ApiClient, UnauthenticatedRawApiClient, QBittorrentError}; + use crate::error::Error; + + #[test] + fn raw_wrong_server() { + let port = find_free_port(); + let api = UnauthenticatedRawApiClient::new().expect("IOERROR"); + let login = api.login(&format!("http://localhost:{}", &port), "admin", "adminadmin"); + assert!(login.is_err()); + let err = login.unwrap_err(); + match &err { + QBittorrentError::HttpError(_) => { + return; + }, _ => { + println!("{:?}", err); + panic!("API CHANGE!"); + } + } + } + + #[test] + fn wrong_server() { + let port = find_free_port(); + let api = ApiClient::new(&format!("http://localhost:{}", &port), "admin", "adminadmin"); + assert!(api.is_err()); + let err = api.unwrap_err(); + match &err { + Error::FailedToReachAPI { source: _ } => { + return; + }, _ => { + println!("{:?}", err); + panic!("ERROR CONVERSION PROBLEM"); + } + } + } +} diff --git a/src/error.rs b/src/error.rs index 2bca881..8e45a01 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,8 @@ use snafu::prelude::*; //use snafu::*; +use std::path::PathBuf; + use crate::config::ConfigError; #[derive(Debug, Snafu)] @@ -17,7 +19,21 @@ pub enum Error { InternalApi { source: qbittorrent_web_api::api_impl::Error }, #[snafu(display("Other error:\n{}", message))] Message { message: String }, - //GenericIOError(std::io::Error), + // New and better error messages + #[snafu(display("Failed to reach qBittorrent API:\n{}", source))] + FailedToReachAPI { source: qbittorrent_web_api::api_impl::Error }, + #[snafu(display("Failed to login to qBittorrent API with user: {}", user))] + FailedLogin { user: String }, + #[snafu(display("Invalid torrent: {}", torrent.display()))] + InvalidTorrent { torrent: PathBuf, source: imdl::error::Error }, + #[snafu(display("Invalid magnet: {}", magnet))] + InvalidMagnet { magnet: String, source: imdl::magnet_link_parse_error::MagnetLinkParseError }, + #[snafu(display("Other torrent error inside imdl library with file {}:\n{}", torrent.display(), source))] + OtherTorrent { torrent: PathBuf, source: imdl::error::Error }, + #[snafu(display("The magnet link contains no name: {}", magnet))] + EmptyNameMagnet { magnet: String }, + #[snafu(display("Failed to read file {} because of underlying IO error:\n{}", path.display(), source))] + FailedToReadFile { path: PathBuf, source: std::io::Error }, } impl Error { diff --git a/src/utils.rs b/src/utils.rs index 2d186b9..4579d1a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,5 @@ +use snafu::prelude::*; + use imdl::infohash::Infohash; use imdl::input::Input; use imdl::input_target::InputTarget; @@ -6,12 +8,15 @@ use imdl::metainfo::Metainfo; use imdl::torrent_summary::TorrentSummary; use std::path::Path; +use std::net::TcpListener; + +use crate::error::{Error, InvalidMagnetSnafu as InvalidMagnet, InvalidTorrentSnafu as InvalidTorrent, FailedToReadFileSnafu as FailedToReadFile, OtherTorrentSnafu as OtherTorrentError}; /// Helper method for imdl functions which expect an imdl::Input type -pub fn input_path>(path: T) -> Result { +pub fn input_path>(path: T) -> Result { let path = path.as_ref(); - let absolute = path.canonicalize()?; - let data = std::fs::read(absolute)?; + let absolute = path.canonicalize().context(FailedToReadFile { path: path.to_path_buf() })?; + let data = std::fs::read(absolute).context(FailedToReadFile { path: path.to_path_buf() })?; Ok(Input::new( InputTarget::Path(path.to_path_buf()), data @@ -19,53 +24,62 @@ pub fn input_path>(path: T) -> Result { } /// Extracts the infohash of a magnet link -pub fn magnet_hash>(magnet: T) -> String { - // TODO: error - let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed"); - //Ok(magnet.name.expect("Magnet link has no name!")) - //Ok(String::from_utf8_lossy(&magnet.infohash.inner.bytes).to_string()) +pub fn magnet_hash>(magnet: T) -> Result { + let magnet = magnet.as_ref(); + let magnet = MagnetLink::parse(magnet).context(InvalidMagnet { magnet: magnet.to_string() })?; let bytes = magnet.infohash.inner.bytes.clone(); let mut s = String::new(); for byte in bytes { s.push_str(&format!("{:02x}", byte)); } - s + Ok(s) } /// Extracts the name of a magnet link -pub fn magnet_name>(magnet: T) -> String { - // TODO: error - let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed"); - magnet.name.expect("Magnet link has no name!") +pub fn magnet_name>(magnet: T) -> Result, Error> { + let magnet = magnet.as_ref(); + let magnet = MagnetLink::parse(magnet).context(InvalidMagnet { magnet })?; + Ok(magnet.name) } /// Extracts the infohash of a torrent file -pub fn torrent_hash>(torrent: T) -> String { - // TODO: error - let input = input_path(torrent).unwrap(); - TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().info_hash +pub fn torrent_hash>(torrent: T) -> Result { + let torrent = torrent.as_ref(); + let input = input_path(torrent)?; + let summary = TorrentSummary::from_input(&input).context(InvalidTorrent { torrent })?; + Ok(summary.torrent_summary_data().info_hash) } /// Extracts the name of a torrent file -pub fn torrent_name>(torrent: T) -> String { - let input = input_path(torrent).unwrap(); - TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().name +pub fn torrent_name>(torrent: T) -> Result { + let torrent = torrent.as_ref(); + let input = input_path(torrent)?; + let summary = TorrentSummary::from_input(&input).context(InvalidTorrent { torrent })?; + Ok(summary.torrent_summary_data().name) } /// Turns a torrent file into a magnet link -pub fn torrent_to_magnet>(torrent: T) -> String { - let input = input_path(torrent).expect("Failed to read torrent file"); - let infohash = Infohash::from_input(&input).expect("Failed to parse infohash"); - let metainfo = Metainfo::from_input(&input).expect("Failed to parse meta info"); +pub fn torrent_to_magnet>(torrent: T) -> Result { + let torrent = torrent.as_ref(); + let input = input_path(torrent)?; + let infohash = Infohash::from_input(&input).context(InvalidTorrent { torrent })?; + let metainfo = Metainfo::from_input(&input).context(InvalidTorrent { torrent })?; let mut link = MagnetLink::with_infohash(infohash); link.set_name(&metainfo.info.name); for result in metainfo.trackers() { - link.add_tracker(result.expect("failed tracker in metainfo")); + let result = result.context(OtherTorrentError { torrent })?; + link.add_tracker(result); } - link.to_url().to_string() + Ok(link.to_url().to_string()) } + +// Fallible: only used in tests +pub fn find_free_port() -> u16 { + let bind = TcpListener::bind("127.0.0.1:0").unwrap(); + bind.local_addr().unwrap().port() +}