Compare commits
No commits in common. "be6e2a9540f73d95aef9b65ddb8f7b3a8d6bd319" and "9097a18926f04fee012d2e7174165c13924cc221" have entirely different histories.
be6e2a9540
...
9097a18926
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1 @@
|
|||||||
/target
|
/target
|
||||||
.*
|
|
||||||
|
@ -1,32 +1,22 @@
|
|||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
use tokio::task;
|
||||||
|
use qbittorrent_web_api::Api;
|
||||||
|
|
||||||
use crate::api::ApiClient;
|
|
||||||
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::torrent_to_magnet;
|
|
||||||
|
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
#[argh(subcommand, name = "add")]
|
#[argh(subcommand, name = "add")]
|
||||||
/// add a torrent to qBittorrent (magnet or torrent file)
|
/// add a torrent to qBittorrent (only magnet for the moment)
|
||||||
pub struct AddAction {
|
pub struct AddAction {
|
||||||
#[argh(switch, short = 'p')]
|
|
||||||
/// pause the torrent instead of starting immediately
|
|
||||||
paused: bool,
|
|
||||||
|
|
||||||
#[argh(positional)]
|
#[argh(positional)]
|
||||||
/// the torrent to add
|
/// the magnet link to add
|
||||||
torrent: String,
|
magnet: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionExec for AddAction {
|
impl ActionExec for AddAction {
|
||||||
fn exec(&self, config: &Config) -> Result<(), Error> {
|
fn exec(&self, _config: &Config) -> Result<(), Error> {
|
||||||
let magnet = if self.torrent.starts_with("magnet:") {
|
unimplemented!();
|
||||||
self.torrent.clone()
|
|
||||||
} else {
|
|
||||||
torrent_to_magnet(&self.torrent)?
|
|
||||||
};
|
|
||||||
let api = ApiClient::from_config(&config)?;
|
|
||||||
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,11 +1,9 @@
|
|||||||
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")]
|
||||||
@ -19,10 +17,9 @@ 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(())
|
||||||
|
145
src/api.rs
145
src/api.rs
@ -1,109 +1,34 @@
|
|||||||
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 fn blocking_runtime() -> std::io::Result<tokio::runtime::Runtime> {
|
|
||||||
Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct UnauthenticatedRawApiClient {
|
|
||||||
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()?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 call = if paused {
|
|
||||||
base_call.add(magnet).paused("true")
|
|
||||||
} else {
|
|
||||||
base_call.add(magnet)
|
|
||||||
};
|
|
||||||
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 struct ApiClient {
|
||||||
pub raw_api: RawApiClient,
|
pub rt: tokio::runtime::Runtime,
|
||||||
|
pub api: qbittorrent_web_api::api_impl::Authenticated
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiClient {
|
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> {
|
pub fn from_config(config: &Config) -> Result<ApiClient, Error> {
|
||||||
Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password)
|
Self::new(&config.format_host(), &config.qbittorrent.login, &config.qbittorrent.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new(host: &str, login: &str, password: &str) -> Result<ApiClient, Error> {
|
||||||
pub fn add(&self, magnet: &str, paused: bool) -> Result<(), Error> {
|
let rt = Builder::new_current_thread()
|
||||||
let res = self.raw_api.add(magnet, paused).context(ApiError)?;
|
.enable_all()
|
||||||
if res == "Ok." {
|
.build().context(IOError)?;
|
||||||
Ok(())
|
// Call the asynchronous connect method using the runtime.
|
||||||
} else {
|
let api = rt.block_on(Api::login(host, &login, &password)).context(ApiError)?;
|
||||||
Err(Error::message(res.clone()))
|
Ok(ApiClient {
|
||||||
}
|
rt,
|
||||||
|
api,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, hash: &str) -> Result<Option<String>, Error> {
|
pub fn get(&self, hash: &str) -> Result<Option<String>, Error> {
|
||||||
let res = self.raw_api.get(hash).context(ApiError)?;
|
let res = self.rt.block_on(self.api.torrent_management().properties_raw(hash)).context(ApiError)?;
|
||||||
if res == "" {
|
if res == "" {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
@ -112,50 +37,12 @@ impl ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(&self) -> Result<String, Error> {
|
pub fn list(&self) -> Result<String, Error> {
|
||||||
self.raw_api.list().context(ApiError)
|
Ok(
|
||||||
|
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,16 +1,10 @@
|
|||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
|
||||||
use std::path::{PathBuf};
|
|
||||||
|
|
||||||
use crate::action::Action;
|
use crate::action::Action;
|
||||||
|
|
||||||
#[derive(FromArgs, Debug)]
|
#[derive(FromArgs, Debug)]
|
||||||
/// interact with your qBittorrent instance from the command line
|
/// interact with your qBittorrent instance from the command line
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[argh(option, short = 'c')]
|
|
||||||
/// path to config file (defaults to ~/.config/qbt/.qbt.toml)
|
|
||||||
pub config: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[argh(subcommand)]
|
#[argh(subcommand)]
|
||||||
pub command: Action,
|
pub command: Action,
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,6 @@ use xdg::{BaseDirectories, BaseDirectoriesError};
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::cli::Cli;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub qbittorrent: ClientConfig,
|
pub qbittorrent: ClientConfig,
|
||||||
@ -48,15 +46,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_default_path() -> Result<Config, ConfigError> {
|
pub fn from_default_path() -> Result<Config, ConfigError> {
|
||||||
Self::from_path(&Self::default_path()?)
|
Ok(Self::from_path(&Self::default_path()?)?)
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_cli(cli: &Cli) -> Result<Config, ConfigError> {
|
|
||||||
if let Some(cfg_path) = &cli.config {
|
|
||||||
Self::from_path(cfg_path)
|
|
||||||
} else {
|
|
||||||
Self::from_default_path()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_host(&self) -> String {
|
pub fn format_host(&self) -> String {
|
||||||
|
36
src/error.rs
36
src/error.rs
@ -1,45 +1,19 @@
|
|||||||
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)]
|
||||||
#[snafu(visibility(pub))]
|
#[snafu(visibility(pub))]
|
||||||
/// The possible error cases
|
/// The possible error cases
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[snafu(display("{}\nConfiguration file reading or parsing error (see above)", source))]
|
#[snafu(display("Configuration file reading or parsing error"))]
|
||||||
Config { source: ConfigError },
|
Config { source: ConfigError },
|
||||||
#[snafu(display("{}\nTorrent/magnet reading or parsing error", source))]
|
#[snafu(display("Torrent/magnet reading or parsing error"))]
|
||||||
Imdl { source: imdl::error::Error },
|
Imdl { source: imdl::error::Error },
|
||||||
#[snafu(display("{}\nqBittorrent API communication error", source))]
|
#[snafu(display("qBittorrent API communication error"))]
|
||||||
Api { source: std::io::Error },
|
Api { source: std::io::Error },
|
||||||
#[snafu(display("{}\nInternal qBittorrent API error", source))]
|
#[snafu(display("Internal qBittorrent API error"))]
|
||||||
InternalApi { source: qbittorrent_web_api::api_impl::Error },
|
InternalApi { source: qbittorrent_web_api::api_impl::Error },
|
||||||
#[snafu(display("Other error:\n{}", message))]
|
//GenericIOError(std::io::Error),
|
||||||
Message { message: String },
|
|
||||||
// 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 {
|
|
||||||
pub fn message(message: impl Into<String>) -> Error {
|
|
||||||
Self::Message {
|
|
||||||
message: message.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
13
src/main.rs
13
src/main.rs
@ -10,20 +10,11 @@ mod error;
|
|||||||
use crate::error::{Error, ConfigSnafu as ConfigError};
|
use crate::error::{Error, ConfigSnafu as ConfigError};
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
fn fallible_main() -> Result<(), Error> {
|
fn main() -> Result<(), Error> {
|
||||||
|
let config = Config::from_default_path().context(ConfigError)?;
|
||||||
let cli = Cli::from_args();
|
let cli = Cli::from_args();
|
||||||
let config = Config::from_cli(&cli).context(ConfigError)?;
|
|
||||||
// TODO: Make ActionExec type return Option<String>? where None means no data was found and so we abort with a proper exit code
|
// TODO: Make ActionExec type return Option<String>? where None means no data was found and so we abort with a proper exit code
|
||||||
cli.command.exec(&config)?;
|
cli.command.exec(&config)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let res = fallible_main();
|
|
||||||
if res.is_err() {
|
|
||||||
let err = res.unwrap_err();
|
|
||||||
eprintln!("{}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
63
src/utils.rs
63
src/utils.rs
@ -1,5 +1,3 @@
|
|||||||
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;
|
||||||
@ -8,15 +6,12 @@ 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, Error> {
|
pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, std::io::Error> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let absolute = path.canonicalize().context(FailedToReadFile { path: path.to_path_buf() })?;
|
let absolute = path.canonicalize()?;
|
||||||
let data = std::fs::read(absolute).context(FailedToReadFile { path: path.to_path_buf() })?;
|
let data = std::fs::read(absolute)?;
|
||||||
Ok(Input::new(
|
Ok(Input::new(
|
||||||
InputTarget::Path(path.to_path_buf()),
|
InputTarget::Path(path.to_path_buf()),
|
||||||
data
|
data
|
||||||
@ -24,62 +19,50 @@ pub fn input_path<T: AsRef<Path>>(path: T) -> Result<Input, Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the infohash of a magnet link
|
/// Extracts the infohash of a magnet link
|
||||||
pub fn magnet_hash<T: AsRef<str>>(magnet: T) -> Result<String, Error> {
|
pub fn magnet_hash<T: AsRef<str>>(magnet: T) -> String {
|
||||||
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));
|
||||||
}
|
}
|
||||||
Ok(s)
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts the name of a magnet link
|
/// Extracts the name of a magnet link
|
||||||
pub fn magnet_name<T: AsRef<str>>(magnet: T) -> Result<Option<String>, Error> {
|
pub fn magnet_name<T: AsRef<str>>(magnet: T) -> String {
|
||||||
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) -> Result<String, Error> {
|
pub fn torrent_hash<T: AsRef<Path>>(torrent: T) -> String {
|
||||||
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) -> Result<String, Error> {
|
pub fn torrent_name<T: AsRef<Path>>(torrent: T) -> String {
|
||||||
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().name
|
||||||
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) -> Result<String, Error> {
|
pub fn torrent_to_magnet<T: AsRef<Path>>(torrent: T) -> String {
|
||||||
let torrent = torrent.as_ref();
|
let input = input_path(torrent).expect("Failed to read torrent file");
|
||||||
let input = input_path(torrent)?;
|
let infohash = Infohash::from_input(&input).expect("Failed to parse infohash");
|
||||||
let infohash = Infohash::from_input(&input).context(InvalidTorrent { torrent })?;
|
let metainfo = Metainfo::from_input(&input).expect("Failed to parse meta info");
|
||||||
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() {
|
||||||
let result = result.context(OtherTorrentError { torrent })?;
|
link.add_tracker(result.expect("failed tracker in metainfo"));
|
||||||
link.add_tracker(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(link.to_url().to_string())
|
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()
|
|
||||||
}
|
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
[qbittorrent]
|
|
||||||
host = "127.0.0.1" # qbittorrent webui-api hostname/ip
|
|
||||||
port = $FREEPORT # qbittorrent webui-api port
|
|
||||||
login = "admin" # qbittorrent webui-api user
|
|
||||||
password = "adminadmin" # qbittorrent webui-api password
|
|
@ -1,85 +0,0 @@
|
|||||||
#! /usr/bin/env bash
|
|
||||||
|
|
||||||
# qbittorrent-nox.sh start|stop DIR PORT
|
|
||||||
# DIR will contain the qbittorrent-nox profile, as well as "pid" and "qBottorrent.log"
|
|
||||||
# PORT configures port for qbittorrent-nox web API
|
|
||||||
|
|
||||||
# Wait for qBittorrent WEB API to come online at address $1
|
|
||||||
# waitforqBittorrentStart "http://localhost:1312"
|
|
||||||
function waitforqBittorrentStart() {
|
|
||||||
#set +x
|
|
||||||
TIMESTAMP=$(date +%s)
|
|
||||||
END=$((TIMESTAMP+10))
|
|
||||||
ERR=0
|
|
||||||
while true; do
|
|
||||||
NEWTIMESTAMP=$(date +%s)
|
|
||||||
if [ $NEWTIMESTAMP -gt $END ]; then
|
|
||||||
ERR=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if curl --silent "$1" 2>&1 > /dev/null; then
|
|
||||||
break
|
|
||||||
else
|
|
||||||
sleep 0.1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return $ERR
|
|
||||||
}
|
|
||||||
|
|
||||||
# Wait for qBittorrent to be done cleanly exiting
|
|
||||||
# Necessary because otherwise it will leave temporary files behind!
|
|
||||||
# waitforQbittorrentStop 1234
|
|
||||||
function waitforqBittorrentStop() {
|
|
||||||
TIMESTAMP=$(date +%s)
|
|
||||||
END=$((TIMESTAMP+15))
|
|
||||||
ERR=0
|
|
||||||
while true; do
|
|
||||||
NEWTIMESTAMP=$(date +%s)
|
|
||||||
if [ $NEWTIMESTAMP -gt $END ]; then
|
|
||||||
ERR=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if ! ps x | grep -P "^\s+$PID\s+" 2>&1 > /dev/null; then
|
|
||||||
# process died successfully
|
|
||||||
break
|
|
||||||
else
|
|
||||||
sleep 0.1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return $ERR
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
echo "y" | qbittorrent-nox --profile="$1" --webui-port="$2" 2>&1 > "$1"/qBittorrent.log &
|
|
||||||
echo $! > "$1"/pid
|
|
||||||
if waitforqBittorrentStart "http://localhost:$2"; then
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
PID="$(cat "$1"/pid)"
|
|
||||||
kill $PID
|
|
||||||
if ! waitforqBittorrentStop $PID; then
|
|
||||||
echo "qBittorrent does not quit. Using SIGKILL"
|
|
||||||
kill -9 $PID
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
"start")
|
|
||||||
QBTNOX_DIR="$2"
|
|
||||||
QBTNOX_PORT="$3"
|
|
||||||
start "$QBTNOX_DIR" "$QBTNOX_PORT"
|
|
||||||
STATUS=$?
|
|
||||||
;;
|
|
||||||
"stop")
|
|
||||||
QBTNOX_DIR="$2"
|
|
||||||
stop "$QBTNOX_DIR"
|
|
||||||
STATUS=$?
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
exit $STATUS
|
|
@ -1,44 +0,0 @@
|
|||||||
#! /usr/bin/bash
|
|
||||||
|
|
||||||
DEPENDENCIES=("qbittorrent-nox" "bats" "curl" "grep" "cat" "sed")
|
|
||||||
|
|
||||||
if [[ "${QBTNOX_DIR:-plz}" = "plz" ]]; then
|
|
||||||
export QBTNOX_DIR="$(mktemp -d)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ORIGDIR="$(pwd)"
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ../utils.sh
|
|
||||||
|
|
||||||
SUBCOMMAND="${1:-test}"
|
|
||||||
if [[ "$SUBCOMMAND" = "test" ]] && [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
|
|
||||||
depsCheck
|
|
||||||
fi
|
|
||||||
|
|
||||||
export FREEPORT="$(findFreePort)"
|
|
||||||
API="http://localhost:$FREEPORT"
|
|
||||||
|
|
||||||
# Subcommand is test and checks succeeded
|
|
||||||
subcommand "$SUBCOMMAND"
|
|
||||||
STATUS=$?
|
|
||||||
if [ $STATUS -eq 0 ]; then
|
|
||||||
if [[ "$SUBCOMMAND" = "test" ]]; then
|
|
||||||
cat ../default.toml | envsubst > "$QBTNOX_DIR"/config.toml
|
|
||||||
export CFGFILE="$QBTNOX_DIR"/config.toml
|
|
||||||
if ../qbittorrent-nox.sh start "$QBTNOX_DIR" "$FREEPORT"; then
|
|
||||||
runTests "$API"
|
|
||||||
STATUS=$?
|
|
||||||
../qbittorrent-nox.sh stop "$QBTNOX_DIR"
|
|
||||||
else
|
|
||||||
echo "qBittorrent did not start"
|
|
||||||
STATUS=1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
|
|
||||||
rm -R "$QBTNOX_DIR"
|
|
||||||
fi
|
|
||||||
cd "$ORIGDIR"
|
|
||||||
exit $STATUS
|
|
@ -1,56 +0,0 @@
|
|||||||
#! /usr/bin/env bash
|
|
||||||
|
|
||||||
DEPENDENCIES=("nix" "nix-shell")
|
|
||||||
# TODO: find grep package name for --pure environment
|
|
||||||
NIX_DEPENDENCIES=("bats" "curl" "qbittorrent-nox")
|
|
||||||
|
|
||||||
if [[ "${QBTNOX_DIR:-plz}" = "plz" ]]; then
|
|
||||||
export QBTNOX_DIR="$(mktemp -d)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ORIGDIR="$(pwd)"
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
source ../utils.sh
|
|
||||||
|
|
||||||
SUBCOMMAND="${1:-test}"
|
|
||||||
|
|
||||||
# Ensure nix shell with bats and curl... if we are running tests
|
|
||||||
if [[ "$SUBCOMMAND" = "test" ]] && [[ "${IN_NIX_SHELL:-plz}" = "plz" ]]; then
|
|
||||||
# Did test.sh perform depsCheck already?
|
|
||||||
if [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
|
|
||||||
depsCheck
|
|
||||||
fi
|
|
||||||
# Are we in nix-shell already?
|
|
||||||
# TODO: redo --pure when grep dep is found
|
|
||||||
IN_NIX_SHELL=1 nix-shell -p ${NIX_DEPENDENCIES[@]} --run "\"$ORIGDIR\"/\"$0\" "$@""
|
|
||||||
exit $?
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
export FREEPORT="$(findFreePort)"
|
|
||||||
API="http://localhost:$FREEPORT"
|
|
||||||
|
|
||||||
# Subcommand is test and checks succeeded
|
|
||||||
subcommand "$SUBCOMMAND"
|
|
||||||
STATUS=$?
|
|
||||||
if [ $STATUS -eq 0 ]; then
|
|
||||||
if [[ "$SUBCOMMAND" = "test" ]]; then
|
|
||||||
cat ../default.toml | envsubst > "$QBTNOX_DIR"/config.toml
|
|
||||||
export CFGFILE="$QBTNOX_DIR"/config.toml
|
|
||||||
if ../qbittorrent-nox.sh start "$QBTNOX_DIR" "$FREEPORT"; then
|
|
||||||
runTests "$API"
|
|
||||||
STATUS=$?
|
|
||||||
../qbittorrent-nox.sh stop "$QBTNOX_DIR"
|
|
||||||
else
|
|
||||||
echo "qBittorrent did not start"
|
|
||||||
STATUS=1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${TESTRUNNER:-plz}" = "plz" ]]; then
|
|
||||||
rm -R "$QBTNOX_DIR"
|
|
||||||
fi
|
|
||||||
cd "$ORIGDIR"
|
|
||||||
exit $STATUS
|
|
@ -1,94 +0,0 @@
|
|||||||
#! /usr/bin/env bash
|
|
||||||
|
|
||||||
# LET TEST RUNNERS KNOW WE ALREADY PERFORM CHECKS
|
|
||||||
export TESTRUNNER="YEAH"
|
|
||||||
|
|
||||||
export QBTNOX_DIR="$(mktemp -d)"
|
|
||||||
|
|
||||||
ORIGDIR="$(pwd)"
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
help() {
|
|
||||||
echo "test.sh RUNNER"
|
|
||||||
echo " Run the test suite. Requires bats and qBittorrent as dependencies. Dependencies are found by one of the following runners:"
|
|
||||||
echo " - local: the local system's \$PATH"
|
|
||||||
echo " - nix: nixos.org package repositories (requires working nix setup)"
|
|
||||||
echo " - docker: Debian stable base image + APT packages (requires working docker setup/daemon)"
|
|
||||||
echo " (default) if no explicit runner is requested, they are tried in order."
|
|
||||||
}
|
|
||||||
|
|
||||||
# FIND THE QBT BINARY
|
|
||||||
cd ..
|
|
||||||
BIN_NAME="$(grep -Po '^name = (.*)' Cargo.toml | sed 's/[" ]//g' | sed 's/name=//')"
|
|
||||||
if [ -f target/release/$BIN_NAME ]; then
|
|
||||||
cargo build --release
|
|
||||||
export BIN_PATH="$(realpath target/release/"$BIN_NAME")"
|
|
||||||
else
|
|
||||||
cargo build
|
|
||||||
export BIN_PATH="$(realpath target/debug/"$BIN_NAME")"
|
|
||||||
fi
|
|
||||||
echo "Using build $BIN_PATH"
|
|
||||||
|
|
||||||
cd tests
|
|
||||||
SUCCESS=0
|
|
||||||
RUNNER="${1:-default}"
|
|
||||||
shift
|
|
||||||
|
|
||||||
case "$RUNNER" in
|
|
||||||
"--help"|"-h"|"help")
|
|
||||||
help
|
|
||||||
;;
|
|
||||||
# "docker")
|
|
||||||
# echo "Running tests under: docker ("$QBTNOX_DIR")"
|
|
||||||
# if ./runners/docker.sh deps-check; then
|
|
||||||
# ./runners/docker.sh test
|
|
||||||
# SUCCESS=$?
|
|
||||||
# else
|
|
||||||
# SUCCESS=1
|
|
||||||
# fi
|
|
||||||
# ;;
|
|
||||||
"nix")
|
|
||||||
echo "Running tests under: nix ("$QBTNOX_DIR")"
|
|
||||||
if ./runners/nix.sh deps-check; then
|
|
||||||
./runners/nix.sh test
|
|
||||||
SUCCESS=$?
|
|
||||||
else
|
|
||||||
SUCCESS=1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
"local")
|
|
||||||
echo "Running tests under: local ("$QBTNOX_DIR")"
|
|
||||||
if ./runners/.local.sh deps-check; then
|
|
||||||
./runners/local.sh test
|
|
||||||
SUCCESS=$?
|
|
||||||
else
|
|
||||||
SUCCESS=1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Autodiscovery for runner"
|
|
||||||
if ./runners/local.sh deps-check; then
|
|
||||||
echo "Running tests under: local ("$QBTNOX_DIR")"
|
|
||||||
./runners/local.sh test
|
|
||||||
SUCCESS=$?
|
|
||||||
elif ./runners/nix.sh deps-check; then
|
|
||||||
echo "Running tests under: nix ("$QBTNOX_DIR")"
|
|
||||||
./runners/nix.sh test
|
|
||||||
SUCCESS=$?
|
|
||||||
# elif ./runners/docker.sh deps-check; then
|
|
||||||
# echo "Running tests under: docker"
|
|
||||||
# ./runners/docker.sh test
|
|
||||||
# SUCCESS=$?
|
|
||||||
else
|
|
||||||
echo "Failed to find a test runner! Please setup qbittorrent-nox and bats on your system, or alternatively setup the nix package manager or the docker container manager to install them dynamically."
|
|
||||||
SUCCESS=1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -d "$QBTNOX_DIR" ]; then
|
|
||||||
rm -R "$QBTNOX_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "$ORIGDIR"
|
|
||||||
exit $SUCCESS
|
|
@ -1,51 +0,0 @@
|
|||||||
#! /usr/bin/env bats
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
bats_require_minimum_version 1.5.0
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "wrong server" {
|
|
||||||
cat "$CFGFILE" | sed "s/$FREEPORT/22/" > "$CFGFILE".wrongserver.toml
|
|
||||||
cat "$CFGFILE".wrongserver.toml
|
|
||||||
run -1 "$BIN_PATH" -c "$CFGFILE".wrongserver.toml list
|
|
||||||
echo "$output"
|
|
||||||
echo "$output" | grep "request error: error sending request"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "wrong login" {
|
|
||||||
cat "$CFGFILE" | sed 's/admin/user/g' > "$CFGFILE".wronguser.toml
|
|
||||||
cat "$CFGFILE".wronguser.toml
|
|
||||||
run -1 "$BIN_PATH" -c "$CFGFILE".wronguser.toml list
|
|
||||||
echo "$output"
|
|
||||||
echo "$output" | grep "Failed to login.*user"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "empty list" {
|
|
||||||
run "$BIN_PATH" -c "$CFGFILE" list
|
|
||||||
echo "$output"
|
|
||||||
[[ "$output" = "[]" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "add ISO" {
|
|
||||||
run "$BIN_PATH" -c "$CFGFILE" add --paused "magnet:?xt=urn:btih:a982743fdb1115a0e501cabb75cca85c828f9445&dn=tails-amd64-4.29-img&tr=udp%3a%2f%2ftracker.torrent.eu.org%3a451&tr=udp%3a%2f%2ftracker.coppersurfer.tk%3a6969"
|
|
||||||
echo "$output"
|
|
||||||
[[ "$output" = "" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "add broken magnet" {
|
|
||||||
run -1 "$BIN_PATH" -c "$CFGFILE" add --paused "magnet:?qsdjksqdjsqdsqdsqldkqs"
|
|
||||||
echo "$output"
|
|
||||||
echo "$output" | grep "Other error:"
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "get info about ISO" {
|
|
||||||
run "$BIN_PATH" -c "$CFGFILE" get "a982743fdb1115a0e501cabb75cca85c828f9445"
|
|
||||||
echo "$output"
|
|
||||||
echo "$output" | grep 'a982743fdb1115a0e501cabb75cca85c828f9445'
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "non-empty list" {
|
|
||||||
run "$BIN_PATH" -c "$CFGFILE" list
|
|
||||||
echo "$output"
|
|
||||||
echo "$output" | grep "a982743fdb1115a0e501cabb75cca85c828f9445"
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
#! /usr/bin/bash
|
|
||||||
|
|
||||||
#echo "-- " "$0" "$@" "(utils)"
|
|
||||||
|
|
||||||
function help() {
|
|
||||||
echo "$(basename "$0") SUBCOMMAND"
|
|
||||||
echo " test: run the test suite using this runner"
|
|
||||||
echo " deps-check: check for this runner's dependencies"
|
|
||||||
}
|
|
||||||
|
|
||||||
function depsCheck() {
|
|
||||||
# Explicit arguments, or $DEPENDENCIES
|
|
||||||
echo "-- Dependency checks for "$(basename "$0")""
|
|
||||||
if [ ! $# -eq 0 ]; then
|
|
||||||
DEPS=("$@")
|
|
||||||
else
|
|
||||||
DEPS=("${DEPENDENCIES[@]}")
|
|
||||||
fi
|
|
||||||
MISSING=()
|
|
||||||
for i in "${DEPS[@]}"; do
|
|
||||||
if ! command -v "$i" >/dev/null 2>&1; then
|
|
||||||
MISSING+=("$i")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ ${#MISSING[@]} -gt 0 ]; then
|
|
||||||
echo "ERROR: missing dependencies: "${MISSING[@]}""
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function subcommand() {
|
|
||||||
SUBCOMMAND="${1:-test}"
|
|
||||||
#echo " Subcommand "$1" evaluated to "$SUBCOMMAND""
|
|
||||||
case "$SUBCOMMAND" in
|
|
||||||
"help"|"--help"|"-h")
|
|
||||||
help
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
"deps-check")
|
|
||||||
depsCheck
|
|
||||||
return $?
|
|
||||||
;;
|
|
||||||
"test")
|
|
||||||
echo "Running tests from "$QBTNOX_DIR""
|
|
||||||
return $?
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "$(basename "$0") ERROR: wrong subcommand "$SUBCOMMAND""
|
|
||||||
return 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# $1 exit code
|
|
||||||
# $2 reason
|
|
||||||
# $3 orig dir to return to
|
|
||||||
function abortIfError() {
|
|
||||||
if [ ! $1 -eq 0 ]; then
|
|
||||||
cd "$ORIGDIR"
|
|
||||||
echo "$2"
|
|
||||||
#set +x
|
|
||||||
if [ $# -eq 3 ]; then
|
|
||||||
cd "$3"
|
|
||||||
fi
|
|
||||||
exit $1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# finds a free TCP port
|
|
||||||
# https://stackoverflow.com/questions/28989069/how-to-find-a-free-tcp-port/45539101#45539101
|
|
||||||
function findFreePort() {
|
|
||||||
BASE_PORT=16998
|
|
||||||
INCREMENT=1
|
|
||||||
|
|
||||||
port=$BASE_PORT
|
|
||||||
isfree=$(netstat -taln | grep $port)
|
|
||||||
|
|
||||||
while [[ -n "$isfree" ]]; do
|
|
||||||
port=$[port+INCREMENT]
|
|
||||||
isfree=$(netstat -taln | grep $port)
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "$port"
|
|
||||||
}
|
|
||||||
|
|
||||||
# runTests APIADDR
|
|
||||||
function runTests {
|
|
||||||
echo "Running tests at "$1""
|
|
||||||
if bats ../units/; then
|
|
||||||
echo "OK"
|
|
||||||
else
|
|
||||||
echo "FAIL"
|
|
||||||
fi
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user