Better errors
This commit is contained in:
parent
cbe7bc51a6
commit
4f38526dfb
Binary file not shown.
Binary file not shown.
|
@ -24,7 +24,7 @@ impl ActionExec for AddAction {
|
||||||
let magnet = if self.torrent.starts_with("magnet:") {
|
let magnet = if self.torrent.starts_with("magnet:") {
|
||||||
self.torrent.clone()
|
self.torrent.clone()
|
||||||
} else {
|
} else {
|
||||||
torrent_to_magnet(&self.torrent)
|
torrent_to_magnet(&self.torrent)?
|
||||||
};
|
};
|
||||||
let api = ApiClient::from_config(&config)?;
|
let api = ApiClient::from_config(&config)?;
|
||||||
api.add(&magnet, self.paused)
|
api.add(&magnet, self.paused)
|
||||||
|
|
|
@ -18,9 +18,9 @@ impl ActionExec for HashAction {
|
||||||
fn exec(&self, _config: &Config) -> Result<(), Error> {
|
fn exec(&self, _config: &Config) -> Result<(), Error> {
|
||||||
// TODO errors
|
// TODO errors
|
||||||
let hash = if self.torrent.starts_with("magnet:") {
|
let hash = if self.torrent.starts_with("magnet:") {
|
||||||
magnet_hash(&self.torrent)
|
magnet_hash(&self.torrent)?
|
||||||
} else {
|
} else {
|
||||||
torrent_hash(&self.torrent)
|
torrent_hash(&self.torrent)?
|
||||||
};
|
};
|
||||||
println!("{}", hash);
|
println!("{}", hash);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -19,7 +19,7 @@ impl ActionExec for MagnetAction {
|
||||||
if self.torrent.starts_with("magnet:") {
|
if self.torrent.starts_with("magnet:") {
|
||||||
println!("{}", &self.torrent);
|
println!("{}", &self.torrent);
|
||||||
} else {
|
} else {
|
||||||
println!("{}", torrent_to_magnet(&self.torrent));
|
println!("{}", torrent_to_magnet(&self.torrent)?);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
use snafu::prelude::*;
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::action::ActionExec;
|
use crate::action::ActionExec;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::utils::{magnet_name, torrent_name};
|
use crate::utils::{magnet_name, torrent_name};
|
||||||
|
use crate::error::EmptyNameMagnetSnafu as EmptyNameMagnet;
|
||||||
|
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
#[argh(subcommand, name = "name")]
|
#[argh(subcommand, name = "name")]
|
||||||
|
@ -17,9 +19,10 @@ pub struct NameAction {
|
||||||
impl ActionExec for NameAction {
|
impl ActionExec for NameAction {
|
||||||
fn exec(&self, _config: &Config) -> Result<(), Error> {
|
fn exec(&self, _config: &Config) -> Result<(), Error> {
|
||||||
let name = if self.torrent.starts_with("magnet:") {
|
let name = if self.torrent.starts_with("magnet:") {
|
||||||
magnet_name(&self.torrent)
|
magnet_name(&self.torrent)?
|
||||||
|
.context(EmptyNameMagnet { magnet: &self.torrent })?
|
||||||
} else {
|
} else {
|
||||||
torrent_name(&self.torrent)
|
torrent_name(&self.torrent)?
|
||||||
};
|
};
|
||||||
println!("{}", name);
|
println!("{}", name);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
146
src/api.rs
146
src/api.rs
|
@ -1,40 +1,100 @@
|
||||||
use snafu::ResultExt;
|
use snafu::ResultExt;
|
||||||
use qbittorrent_web_api::Api;
|
use qbittorrent_web_api::Api;
|
||||||
|
use qbittorrent_web_api::api_impl::Error as QBittorrentError;
|
||||||
use tokio::runtime::Builder;
|
use tokio::runtime::Builder;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::error::{Error, ApiSnafu as IOError, InternalApiSnafu as ApiError};
|
use crate::error::{Error, ApiSnafu as IOError, InternalApiSnafu as ApiError};
|
||||||
|
|
||||||
pub struct ApiClient {
|
pub fn blocking_runtime() -> std::io::Result<tokio::runtime::Runtime> {
|
||||||
pub rt: tokio::runtime::Runtime,
|
Builder::new_current_thread()
|
||||||
pub api: qbittorrent_web_api::api_impl::Authenticated
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiClient {
|
|
||||||
pub fn from_config(config: &Config) -> Result<ApiClient, Error> {
|
|
||||||
Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(host: &str, login: &str, password: &str) -> Result<ApiClient, Error> {
|
|
||||||
let rt = Builder::new_current_thread()
|
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build().context(IOError)?;
|
.build()
|
||||||
// Call the asynchronous connect method using the runtime.
|
}
|
||||||
let api = rt.block_on(Api::login(host, &login, &password)).context(ApiError)?;
|
|
||||||
Ok(ApiClient {
|
#[derive(Debug)]
|
||||||
rt,
|
pub struct UnauthenticatedRawApiClient {
|
||||||
api,
|
pub rt: tokio::runtime::Runtime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnauthenticatedRawApiClient {
|
||||||
|
/// Initialize a blocking runtime for the API client
|
||||||
|
pub fn new() -> Result<UnauthenticatedRawApiClient, std::io::Error> {
|
||||||
|
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<RawApiClient, QBittorrentError> {
|
||||||
|
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<String, QBittorrentError> {
|
||||||
let base_call = self.api.torrent_management();
|
let base_call = self.api.torrent_management();
|
||||||
let call = if paused {
|
let call = if paused {
|
||||||
base_call.add(magnet).paused("true")
|
base_call.add(magnet).paused("true")
|
||||||
} else {
|
} else {
|
||||||
base_call.add(magnet)
|
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<String, QBittorrentError> {
|
||||||
|
self.rt.block_on(self.api.torrent_management().properties_raw(hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self) -> Result<String, QBittorrentError> {
|
||||||
|
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<ApiClient, Error> {
|
||||||
|
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<ApiClient, Error> {
|
||||||
|
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." {
|
if res == "Ok." {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
@ -43,7 +103,7 @@ impl ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, hash: &str) -> Result<Option<String>, Error> {
|
pub fn get(&self, hash: &str) -> Result<Option<String>, 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 == "" {
|
if res == "" {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
|
@ -52,12 +112,50 @@ impl ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(&self) -> Result<String, Error> {
|
pub fn list(&self) -> Result<String, Error> {
|
||||||
Ok(
|
self.raw_api.list().context(ApiError)
|
||||||
self.rt.block_on(self.api.torrent_management().info().send_raw()).context(ApiError)?
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: typestate builder https://www.greyblake.com/blog/builder-with-typestate-in-rust/
|
// TODO: typestate builder https://www.greyblake.com/blog/builder-with-typestate-in-rust/
|
||||||
//struct ApiClientBuilder {}
|
//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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
18
src/error.rs
18
src/error.rs
|
@ -1,6 +1,8 @@
|
||||||
use snafu::prelude::*;
|
use snafu::prelude::*;
|
||||||
//use snafu::*;
|
//use snafu::*;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::config::ConfigError;
|
use crate::config::ConfigError;
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
|
@ -17,7 +19,21 @@ pub enum Error {
|
||||||
InternalApi { source: qbittorrent_web_api::api_impl::Error },
|
InternalApi { source: qbittorrent_web_api::api_impl::Error },
|
||||||
#[snafu(display("Other error:\n{}", message))]
|
#[snafu(display("Other error:\n{}", message))]
|
||||||
Message { message: String },
|
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 {
|
impl Error {
|
||||||
|
|
66
src/utils.rs
66
src/utils.rs
|
@ -1,3 +1,5 @@
|
||||||
|
use snafu::prelude::*;
|
||||||
|
|
||||||
use imdl::infohash::Infohash;
|
use imdl::infohash::Infohash;
|
||||||
use imdl::input::Input;
|
use imdl::input::Input;
|
||||||
use imdl::input_target::InputTarget;
|
use imdl::input_target::InputTarget;
|
||||||
|
@ -6,12 +8,15 @@ use imdl::metainfo::Metainfo;
|
||||||
use imdl::torrent_summary::TorrentSummary;
|
use imdl::torrent_summary::TorrentSummary;
|
||||||
|
|
||||||
use std::path::Path;
|
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
|
/// Helper method for imdl functions which expect an imdl::Input type
|
||||||
pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, std::io::Error> {
|
pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, Error> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let absolute = path.canonicalize()?;
|
let absolute = path.canonicalize().context(FailedToReadFile { path: path.to_path_buf() })?;
|
||||||
let data = std::fs::read(absolute)?;
|
let data = std::fs::read(absolute).context(FailedToReadFile { path: path.to_path_buf() })?;
|
||||||
Ok(Input::new(
|
Ok(Input::new(
|
||||||
InputTarget::Path(path.to_path_buf()),
|
InputTarget::Path(path.to_path_buf()),
|
||||||
data
|
data
|
||||||
|
@ -19,53 +24,62 @@ pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, std::io::Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the infohash of a magnet link
|
/// Extracts the infohash of a magnet link
|
||||||
pub fn magnet_hash<T: AsRef<str>>(magnet: T) -> String {
|
pub fn magnet_hash<T: AsRef<str>>(magnet: T) -> Result<String, Error> {
|
||||||
// TODO: error
|
let magnet = magnet.as_ref();
|
||||||
let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed");
|
let magnet = MagnetLink::parse(magnet).context(InvalidMagnet { magnet: magnet.to_string() })?;
|
||||||
//Ok(magnet.name.expect("Magnet link has no name!"))
|
|
||||||
//Ok(String::from_utf8_lossy(&magnet.infohash.inner.bytes).to_string())
|
|
||||||
let bytes = magnet.infohash.inner.bytes.clone();
|
let bytes = magnet.infohash.inner.bytes.clone();
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
for byte in bytes {
|
for byte in bytes {
|
||||||
s.push_str(&format!("{:02x}", byte));
|
s.push_str(&format!("{:02x}", byte));
|
||||||
}
|
}
|
||||||
s
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the name of a magnet link
|
/// Extracts the name of a magnet link
|
||||||
pub fn magnet_name<T: AsRef<str>>(magnet: T) -> String {
|
pub fn magnet_name<T: AsRef<str>>(magnet: T) -> Result<Option<String>, Error> {
|
||||||
// TODO: error
|
let magnet = magnet.as_ref();
|
||||||
let magnet = MagnetLink::parse(magnet.as_ref()).expect("Parsing magnet failed");
|
let magnet = MagnetLink::parse(magnet).context(InvalidMagnet { magnet })?;
|
||||||
magnet.name.expect("Magnet link has no name!")
|
Ok(magnet.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the infohash of a torrent file
|
/// Extracts the infohash of a torrent file
|
||||||
pub fn torrent_hash<T: AsRef<Path>>(torrent: T) -> String {
|
pub fn torrent_hash<T: AsRef<Path>>(torrent: T) -> Result<String, Error> {
|
||||||
// TODO: error
|
let torrent = torrent.as_ref();
|
||||||
let input = input_path(torrent).unwrap();
|
let input = input_path(torrent)?;
|
||||||
TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().info_hash
|
let summary = TorrentSummary::from_input(&input).context(InvalidTorrent { torrent })?;
|
||||||
|
Ok(summary.torrent_summary_data().info_hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the name of a torrent file
|
/// Extracts the name of a torrent file
|
||||||
pub fn torrent_name<T: AsRef<Path>>(torrent: T) -> String {
|
pub fn torrent_name<T: AsRef<Path>>(torrent: T) -> Result<String, Error> {
|
||||||
let input = input_path(torrent).unwrap();
|
let torrent = torrent.as_ref();
|
||||||
TorrentSummary::from_input(&input).expect("Parsing torrent file failed").torrent_summary_data().name
|
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
|
/// Turns a torrent file into a magnet link
|
||||||
pub fn torrent_to_magnet<T: AsRef<Path>>(torrent: T) -> String {
|
pub fn torrent_to_magnet<T: AsRef<Path>>(torrent: T) -> Result<String, Error> {
|
||||||
let input = input_path(torrent).expect("Failed to read torrent file");
|
let torrent = torrent.as_ref();
|
||||||
let infohash = Infohash::from_input(&input).expect("Failed to parse infohash");
|
let input = input_path(torrent)?;
|
||||||
let metainfo = Metainfo::from_input(&input).expect("Failed to parse meta info");
|
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);
|
let mut link = MagnetLink::with_infohash(infohash);
|
||||||
|
|
||||||
link.set_name(&metainfo.info.name);
|
link.set_name(&metainfo.info.name);
|
||||||
|
|
||||||
for result in metainfo.trackers() {
|
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()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user