Support remote libraries #36
202
Cargo.lock
generated
202
Cargo.lock
generated
@ -43,6 +43,12 @@ version = "1.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cassowary"
|
name = "cassowary"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@ -107,6 +113,26 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.3.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"redox_users",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast"
|
name = "downcast"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -164,6 +190,17 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
|
checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@ -312,11 +349,13 @@ dependencies = [
|
|||||||
"crossterm",
|
"crossterm",
|
||||||
"mockall",
|
"mockall",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"openssh",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"structopt",
|
"structopt",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -341,6 +380,39 @@ version = "1.17.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssh"
|
||||||
|
version = "0.9.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ca6c277973fb549b36dd8980941b5ea3ecebea026f5b1f0060acde74d893c22"
|
||||||
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"openssh-mux-client",
|
||||||
|
"shell-escape",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-pipe",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssh-mux-client"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88eac793af6170bcd6d4f39c3b7ba3f4227cab5680d7189ba30f9d174600b75f"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"sendfd",
|
||||||
|
"serde",
|
||||||
|
"ssh_format",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-io-utility",
|
||||||
|
"typed-builder",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@ -364,6 +436,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "predicates"
|
name = "predicates"
|
||||||
version = "2.1.5"
|
version = "2.1.5"
|
||||||
@ -467,6 +545,17 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"redox_syscall 0.2.16",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.7.3"
|
version = "1.7.3"
|
||||||
@ -510,6 +599,16 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sendfd"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "604b71b8fc267e13bb3023a2c901126c8f349393666a6d98ac1ae5729b701798"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.159"
|
version = "1.0.159"
|
||||||
@ -541,6 +640,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-escape"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
@ -577,6 +682,25 @@ version = "1.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssh_format"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8701239872766d43b8a5f9a560ff7f002b48064fadea87f44a70507069fb482"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@ -657,6 +781,84 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-io-utility"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d672654d175710e52c7c41f6aec77c62b3c0954e2a7ebce9049d1e94ed7c263"
|
||||||
|
dependencies = [
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-pipe"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f213a84bffbd61b8fa0ba8a044b4bbe35d471d0b518867181e82bd5c15542784"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-builder"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
|
@ -7,10 +7,12 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
crossterm = "0.26.1"
|
crossterm = "0.26.1"
|
||||||
|
openssh = { version = "0.9.9", features = ["native-mux"], default-features = false }
|
||||||
serde = { version = "1.0.159", features = ["derive"] }
|
serde = { version = "1.0.159", features = ["derive"] }
|
||||||
serde_json = "1.0.95"
|
serde_json = "1.0.95"
|
||||||
structopt = "0.3.26"
|
structopt = "0.3.26"
|
||||||
ratatui = "0.20.1"
|
ratatui = "0.20.1"
|
||||||
|
tokio = { version = "1.27.0", features = ["rt"] }
|
||||||
uuid = { version = "1.3.0", features = ["serde"] }
|
uuid = { version = "1.3.0", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -5,10 +5,13 @@ use std::{
|
|||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::Command,
|
process::{Command, Output},
|
||||||
str,
|
str,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use openssh::{KnownHosts, Session};
|
||||||
|
use tokio::runtime::{self, Runtime};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use mockall::automock;
|
use mockall::automock;
|
||||||
|
|
||||||
@ -22,6 +25,7 @@ macro_rules! list_format_separator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BEET_DEFAULT: &str = "beet";
|
||||||
const CMD_LIST: &str = "ls";
|
const CMD_LIST: &str = "ls";
|
||||||
const LIST_FORMAT_SEPARATOR: &str = list_format_separator!();
|
const LIST_FORMAT_SEPARATOR: &str = list_format_separator!();
|
||||||
const LIST_FORMAT_ARG: &str = concat!(
|
const LIST_FORMAT_ARG: &str = concat!(
|
||||||
@ -198,6 +202,18 @@ impl<BLE: BeetsLibraryExecutor> LibraryPrivate for BeetsLibrary<BLE> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait BeetsLibraryExecutorPrivate {
|
||||||
|
fn output(output: Output) -> Result<Vec<String>, Error> {
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(Error::Executor(
|
||||||
|
String::from_utf8_lossy(&output.stderr).to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let output = str::from_utf8(&output.stdout)?;
|
||||||
|
Ok(output.split('\n').map(|s| s.to_string()).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Beets library executor that executes beets commands in their own process.
|
/// Beets library executor that executes beets commands in their own process.
|
||||||
pub struct BeetsLibraryProcessExecutor {
|
pub struct BeetsLibraryProcessExecutor {
|
||||||
bin: OsString,
|
bin: OsString,
|
||||||
@ -205,8 +221,13 @@ pub struct BeetsLibraryProcessExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BeetsLibraryProcessExecutor {
|
impl BeetsLibraryProcessExecutor {
|
||||||
|
/// Create a new [`BeetsLibraryProcessExecutor`] that uses the default beets executable.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::bin(BEET_DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new [`BeetsLibraryProcessExecutor`] that uses the provided beets executable.
|
/// Create a new [`BeetsLibraryProcessExecutor`] that uses the provided beets executable.
|
||||||
pub fn new<S: Into<OsString>>(bin: S) -> Self {
|
pub fn bin<S: Into<OsString>>(bin: S) -> Self {
|
||||||
BeetsLibraryProcessExecutor {
|
BeetsLibraryProcessExecutor {
|
||||||
bin: bin.into(),
|
bin: bin.into(),
|
||||||
config: None,
|
config: None,
|
||||||
@ -224,7 +245,7 @@ impl Default for BeetsLibraryProcessExecutor {
|
|||||||
/// Create a new [`BeetsLibraryProcessExecutor`] that uses the system's default beets
|
/// Create a new [`BeetsLibraryProcessExecutor`] that uses the system's default beets
|
||||||
/// executable.
|
/// executable.
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
BeetsLibraryProcessExecutor::new("beet")
|
BeetsLibraryProcessExecutor::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,16 +257,75 @@ impl BeetsLibraryExecutor for BeetsLibraryProcessExecutor {
|
|||||||
cmd.arg(path);
|
cmd.arg(path);
|
||||||
}
|
}
|
||||||
let output = cmd.args(arguments.iter().map(|s| s.as_ref())).output()?;
|
let output = cmd.args(arguments.iter().map(|s| s.as_ref())).output()?;
|
||||||
if !output.status.success() {
|
Self::output(output)
|
||||||
return Err(Error::CmdExec(
|
|
||||||
String::from_utf8_lossy(&output.stderr).to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let output = str::from_utf8(&output.stdout)?;
|
|
||||||
Ok(output.split('\n').map(|s| s.to_string()).collect())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BeetsLibraryExecutorPrivate for BeetsLibraryProcessExecutor {}
|
||||||
|
|
||||||
|
// GRCOV_EXCL_START
|
||||||
|
/// Beets library executor that executes beets commands over SSH.
|
||||||
|
pub struct BeetsLibrarySshExecutor {
|
||||||
|
rt: Runtime,
|
||||||
|
session: Session,
|
||||||
|
bin: String,
|
||||||
|
config: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<openssh::Error> for Error {
|
||||||
|
fn from(err: openssh::Error) -> Error {
|
||||||
|
Error::Executor(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BeetsLibrarySshExecutor {
|
||||||
|
/// Create a new [`BeetsLibrarySshExecutor`] that uses the default beets executable over an SSH
|
||||||
|
/// connection. This call will attempt to establish the connection and will fail if the
|
||||||
|
/// connection fails.
|
||||||
|
pub fn new<H: AsRef<str>>(host: H) -> Result<Self, Error> {
|
||||||
|
Self::bin(host, BEET_DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`BeetsLibrarySshExecutor`] that uses the provided beets executable over an SSH
|
||||||
|
/// connection. This call will attempt to establish the connection and will fail if the
|
||||||
|
/// connection fails.
|
||||||
|
pub fn bin<H: AsRef<str>, S: Into<String>>(host: H, bin: S) -> Result<Self, Error> {
|
||||||
|
let rt = runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let session = rt.block_on(Session::connect_mux(host, KnownHosts::Strict))?;
|
||||||
|
Ok(BeetsLibrarySshExecutor {
|
||||||
|
rt,
|
||||||
|
session,
|
||||||
|
bin: bin.into(),
|
||||||
|
config: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the configuration file passed to the beets executable.
|
||||||
|
pub fn config<P: Into<String>>(mut self, path: Option<P>) -> Self {
|
||||||
|
self.config = path.map(|p| p.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BeetsLibraryExecutor for BeetsLibrarySshExecutor {
|
||||||
|
fn exec<S: AsRef<str> + 'static>(&mut self, arguments: &[S]) -> Result<Vec<String>, Error> {
|
||||||
|
let mut cmd = self.session.command(&self.bin);
|
||||||
|
if let Some(ref path) = self.config {
|
||||||
|
cmd.arg("--config");
|
||||||
|
cmd.arg(path);
|
||||||
|
}
|
||||||
|
cmd.args(arguments.iter().map(|s| s.as_ref()));
|
||||||
|
let output = self.rt.block_on(cmd.output())?;
|
||||||
|
Self::output(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BeetsLibraryExecutorPrivate for BeetsLibrarySshExecutor {}
|
||||||
|
// GRCOV_EXCL_STOP
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use mockall::predicate;
|
use mockall::predicate;
|
||||||
|
@ -60,26 +60,26 @@ impl Query {
|
|||||||
/// Error type for library calls.
|
/// Error type for library calls.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// The underlying library failed to execute a command.
|
/// The library's executor failed.
|
||||||
CmdExec(String),
|
Executor(String),
|
||||||
/// The underlying library returned invalid data.
|
/// The library experienced an I/O error.
|
||||||
Invalid(String),
|
|
||||||
/// The underlying library experienced an I/O error.
|
|
||||||
Io(String),
|
Io(String),
|
||||||
/// The underlying library failed to parse an integer.
|
/// The library received invalid data.
|
||||||
|
Invalid(String),
|
||||||
|
/// The library failed to parse an integer.
|
||||||
ParseInt(String),
|
ParseInt(String),
|
||||||
/// The underlying library failed to parse a UTF-8 string.
|
/// The library failed to parse a UTF-8 string.
|
||||||
Utf8(String),
|
Utf8(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
Self::CmdExec(ref s) => write!(f, "the library failed to execute a command: {s}"),
|
Self::Executor(ref s) => write!(f, "the library's executor failed: {s}"),
|
||||||
Self::Invalid(ref s) => write!(f, "the library received invalid data: {s}"),
|
|
||||||
Self::Io(ref s) => write!(f, "the library experienced an I/O error: {s}"),
|
Self::Io(ref s) => write!(f, "the library experienced an I/O error: {s}"),
|
||||||
Self::ParseInt(ref s) => write!(f, "the library received an invalid integer: {s}"),
|
Self::Invalid(ref s) => write!(f, "the library received invalid data: {s}"),
|
||||||
Self::Utf8(ref s) => write!(f, "the library received invalid UTF-8: {s}"),
|
Self::ParseInt(ref s) => write!(f, "the library failed to parse an integer: {s}"),
|
||||||
|
Self::Utf8(ref s) => write!(f, "the library failed to parse a UTF-8 string: {s}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,21 +133,21 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn errors() {
|
fn errors() {
|
||||||
let cmd_err = Error::CmdExec(String::from("CmdExec"));
|
let exe_err = Error::Executor(String::from("Executor"));
|
||||||
let inv_err = Error::Invalid(String::from("Invalid"));
|
|
||||||
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "Interrupted").into();
|
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "Interrupted").into();
|
||||||
|
let inv_err = Error::Invalid(String::from("Invalid"));
|
||||||
let int_err: Error = "five".parse::<u32>().unwrap_err().into();
|
let int_err: Error = "five".parse::<u32>().unwrap_err().into();
|
||||||
let utf_err: Error = std::str::from_utf8(b"\xe2\x28\xa1").unwrap_err().into();
|
let utf_err: Error = std::str::from_utf8(b"\xe2\x28\xa1").unwrap_err().into();
|
||||||
|
|
||||||
assert!(!cmd_err.to_string().is_empty());
|
assert!(!exe_err.to_string().is_empty());
|
||||||
assert!(!inv_err.to_string().is_empty());
|
|
||||||
assert!(!io_err.to_string().is_empty());
|
assert!(!io_err.to_string().is_empty());
|
||||||
|
assert!(!inv_err.to_string().is_empty());
|
||||||
assert!(!int_err.to_string().is_empty());
|
assert!(!int_err.to_string().is_empty());
|
||||||
assert!(!utf_err.to_string().is_empty());
|
assert!(!utf_err.to_string().is_empty());
|
||||||
|
|
||||||
assert!(!format!("{:?}", cmd_err).is_empty());
|
assert!(!format!("{:?}", exe_err).is_empty());
|
||||||
assert!(!format!("{:?}", inv_err).is_empty());
|
|
||||||
assert!(!format!("{:?}", io_err).is_empty());
|
assert!(!format!("{:?}", io_err).is_empty());
|
||||||
|
assert!(!format!("{:?}", inv_err).is_empty());
|
||||||
assert!(!format!("{:?}", int_err).is_empty());
|
assert!(!format!("{:?}", int_err).is_empty());
|
||||||
assert!(!format!("{:?}", utf_err).is_empty());
|
assert!(!format!("{:?}", utf_err).is_empty());
|
||||||
}
|
}
|
||||||
|
50
src/main.rs
50
src/main.rs
@ -1,7 +1,10 @@
|
|||||||
|
use musichoard::database::Database;
|
||||||
|
use musichoard::library::beets::BeetsLibrarySshExecutor;
|
||||||
|
use musichoard::library::Library;
|
||||||
use ratatui::backend::CrosstermBackend;
|
use ratatui::backend::CrosstermBackend;
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
use std::io;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::{ffi::OsString, io};
|
||||||
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
@ -19,35 +22,22 @@ use tui::{
|
|||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
#[structopt(
|
#[structopt(long = "ssh", name = "beets SSH URI")]
|
||||||
short = "b",
|
beets_ssh_uri: Option<OsString>,
|
||||||
long = "beets",
|
|
||||||
name = "beets config file path",
|
#[structopt(long = "beets", name = "beets config file path")]
|
||||||
parse(from_os_str)
|
beets_config_file_path: Option<OsString>,
|
||||||
)]
|
|
||||||
beets_config_file_path: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[structopt(
|
#[structopt(
|
||||||
short = "d",
|
|
||||||
long = "database",
|
long = "database",
|
||||||
name = "database file path",
|
name = "database file path",
|
||||||
default_value = "database.json",
|
default_value = "database.json"
|
||||||
parse(from_os_str)
|
|
||||||
)]
|
)]
|
||||||
database_file_path: PathBuf,
|
database_file_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn with<LIB: Library, DB: Database>(lib: LIB, db: DB) {
|
||||||
// Create the application.
|
let collection_manager = MhCollectionManager::new(lib, db);
|
||||||
let opt = Opt::from_args();
|
|
||||||
|
|
||||||
let beets = BeetsLibrary::new(
|
|
||||||
BeetsLibraryProcessExecutor::default().config(opt.beets_config_file_path.as_deref()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let database = JsonDatabase::new(JsonDatabaseFileBackend::new(&opt.database_file_path));
|
|
||||||
|
|
||||||
let collection_manager = MhCollectionManager::new(beets, database);
|
|
||||||
|
|
||||||
// Initialize the terminal user interface.
|
// Initialize the terminal user interface.
|
||||||
let backend = CrosstermBackend::new(io::stdout());
|
let backend = CrosstermBackend::new(io::stdout());
|
||||||
@ -65,6 +55,22 @@ fn main() {
|
|||||||
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Create the application.
|
||||||
|
let opt = Opt::from_args();
|
||||||
|
|
||||||
|
if let Some(uri) = opt.beets_ssh_uri {
|
||||||
|
let uri = uri.into_string().expect("invalid SSH URI");
|
||||||
|
let lib_exec = BeetsLibrarySshExecutor::new(uri).expect("failed to initialise beets");
|
||||||
|
let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path);
|
||||||
|
with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec));
|
||||||
|
} else {
|
||||||
|
let lib_exec = BeetsLibraryProcessExecutor::default().config(opt.beets_config_file_path);
|
||||||
|
let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path);
|
||||||
|
with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod testlib;
|
mod testlib;
|
||||||
|
Loading…
Reference in New Issue
Block a user