mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 19:57:25 +01:00
Revert merging of server and client repository
This commit is contained in:
parent
2b9cff885b
commit
8c3c439409
147 changed files with 53 additions and 2113 deletions
18
src/backend/client/ensembles.rs
Normal file
18
src/backend/client/ensembles.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use super::Backend;
|
||||
use crate::database::Ensemble;
|
||||
use anyhow::Result;
|
||||
|
||||
impl Backend {
|
||||
/// Get all available ensembles from the server.
|
||||
pub async fn get_ensembles(&self) -> Result<Vec<Ensemble>> {
|
||||
let body = self.get("ensembles").await?;
|
||||
let ensembles: Vec<Ensemble> = serde_json::from_str(&body)?;
|
||||
Ok(ensembles)
|
||||
}
|
||||
|
||||
/// Post a new ensemble to the server.
|
||||
pub async fn post_ensemble(&self, data: &Ensemble) -> Result<()> {
|
||||
self.post("ensembles", serde_json::to_string(data)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
18
src/backend/client/instruments.rs
Normal file
18
src/backend/client/instruments.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use super::Backend;
|
||||
use crate::database::Instrument;
|
||||
use anyhow::Result;
|
||||
|
||||
impl Backend {
|
||||
/// Get all available instruments from the server.
|
||||
pub async fn get_instruments(&self) -> Result<Vec<Instrument>> {
|
||||
let body = self.get("instruments").await?;
|
||||
let instruments: Vec<Instrument> = serde_json::from_str(&body)?;
|
||||
Ok(instruments)
|
||||
}
|
||||
|
||||
/// Post a new instrument to the server.
|
||||
pub async fn post_instrument(&self, data: &Instrument) -> Result<()> {
|
||||
self.post("instruments", serde_json::to_string(data)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
170
src/backend/client/mod.rs
Normal file
170
src/backend/client/mod.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
use super::secure;
|
||||
use super::Backend;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use gio::prelude::*;
|
||||
use isahc::http::StatusCode;
|
||||
use isahc::prelude::*;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod ensembles;
|
||||
pub use ensembles::*;
|
||||
|
||||
pub mod instruments;
|
||||
pub use instruments::*;
|
||||
|
||||
pub mod persons;
|
||||
pub use persons::*;
|
||||
|
||||
pub mod recordings;
|
||||
pub use recordings::*;
|
||||
|
||||
pub mod works;
|
||||
pub use works::*;
|
||||
|
||||
/// Credentials used for login.
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoginData {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// Initialize the client.
|
||||
pub(super) fn init_client(&self) -> Result<()> {
|
||||
if let Some(data) = secure::load_login_data()? {
|
||||
self.login_data.replace(Some(data));
|
||||
}
|
||||
|
||||
if let Some(url) = self.settings.get_string("server-url") {
|
||||
if !url.is_empty() {
|
||||
self.server_url.replace(Some(url.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the URL of the Musicus server to connect to.
|
||||
pub fn set_server_url(&self, url: &str) -> Result<()> {
|
||||
self.settings.set_string("server-url", url)?;
|
||||
self.server_url.replace(Some(url.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the currently used login token.
|
||||
pub fn get_token(&self) -> Option<String> {
|
||||
self.token.borrow().clone()
|
||||
}
|
||||
|
||||
/// Set the login token to use. This will be done automatically by the login method.
|
||||
pub fn set_token(&self, token: &str) {
|
||||
self.token.replace(Some(token.to_string()));
|
||||
}
|
||||
|
||||
/// Get the currently set server URL.
|
||||
pub fn get_server_url(&self) -> Option<String> {
|
||||
self.server_url.borrow().clone()
|
||||
}
|
||||
|
||||
/// Get the currently stored login credentials.
|
||||
pub fn get_login_data(&self) -> Option<LoginData> {
|
||||
self.login_data.borrow().clone()
|
||||
}
|
||||
|
||||
/// Set the user credentials to use.
|
||||
pub async fn set_login_data(&self, data: LoginData) -> Result<()> {
|
||||
secure::store_login_data(data.clone()).await?;
|
||||
self.login_data.replace(Some(data));
|
||||
self.token.replace(None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try to login a user with the provided credentials and return, wether the login suceeded.
|
||||
pub async fn login(&self) -> Result<bool> {
|
||||
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
|
||||
let data = self.get_login_data().ok_or(anyhow!("No login data set!"))?;
|
||||
|
||||
let request = Request::post(format!("{}/login", server_url))
|
||||
.timeout(Duration::from_secs(10))
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_json::to_string(&data)?)?;
|
||||
|
||||
let mut response = isahc::send_async(request).await?;
|
||||
|
||||
let success = match response.status() {
|
||||
StatusCode::OK => {
|
||||
let token = response.text_async().await?;
|
||||
self.set_token(&token);
|
||||
true
|
||||
}
|
||||
StatusCode::UNAUTHORIZED => false,
|
||||
_ => bail!("Unexpected response status!"),
|
||||
};
|
||||
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
/// Make an unauthenticated get request to the server.
|
||||
async fn get(&self, url: &str) -> Result<String> {
|
||||
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
|
||||
|
||||
let mut response = Request::get(format!("{}/{}", server_url, url))
|
||||
.timeout(Duration::from_secs(10))
|
||||
.body(())?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
let body = response.text_async().await?;
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Make an authenticated post request to the server.
|
||||
async fn post(&self, url: &str, body: String) -> Result<String> {
|
||||
let body = match self.get_token() {
|
||||
Some(_) => {
|
||||
let mut response = self.post_priv(url, body.clone()).await?;
|
||||
|
||||
// Try one more time (maybe the token was expired)
|
||||
if response.status() == StatusCode::UNAUTHORIZED {
|
||||
if self.login().await? {
|
||||
response = self.post_priv(url, body).await?;
|
||||
} else {
|
||||
bail!("Login failed!");
|
||||
}
|
||||
}
|
||||
|
||||
response.text_async().await?
|
||||
}
|
||||
None => {
|
||||
let mut response = if self.login().await? {
|
||||
self.post_priv(url, body).await?
|
||||
} else {
|
||||
bail!("Login failed!");
|
||||
};
|
||||
|
||||
response.text_async().await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Post something to the server assuming there is a valid login token.
|
||||
async fn post_priv(&self, url: &str, body: String) -> Result<Response<Body>> {
|
||||
let server_url = self.get_server_url().ok_or(anyhow!("No server URL set!"))?;
|
||||
let token = self.get_token().ok_or(anyhow!("No login token!"))?;
|
||||
|
||||
let response = Request::post(format!("{}/{}", server_url, url))
|
||||
.timeout(Duration::from_secs(10))
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
18
src/backend/client/persons.rs
Normal file
18
src/backend/client/persons.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use super::Backend;
|
||||
use crate::database::Person;
|
||||
use anyhow::Result;
|
||||
|
||||
impl Backend {
|
||||
/// Get all available persons from the server.
|
||||
pub async fn get_persons(&self) -> Result<Vec<Person>> {
|
||||
let body = self.get("persons").await?;
|
||||
let persons: Vec<Person> = serde_json::from_str(&body)?;
|
||||
Ok(persons)
|
||||
}
|
||||
|
||||
/// Post a new person to the server.
|
||||
pub async fn post_person(&self, data: &Person) -> Result<()> {
|
||||
self.post("persons", serde_json::to_string(data)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
18
src/backend/client/recordings.rs
Normal file
18
src/backend/client/recordings.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use super::Backend;
|
||||
use crate::database::Recording;
|
||||
use anyhow::Result;
|
||||
|
||||
impl Backend {
|
||||
/// Get all available recordings from the server.
|
||||
pub async fn get_recordings_for_work(&self, work_id: &str) -> Result<Vec<Recording>> {
|
||||
let body = self.get(&format!("works/{}/recordings", work_id)).await?;
|
||||
let recordings: Vec<Recording> = serde_json::from_str(&body)?;
|
||||
Ok(recordings)
|
||||
}
|
||||
|
||||
/// Post a new recording to the server.
|
||||
pub async fn post_recording(&self, data: &Recording) -> Result<()> {
|
||||
self.post("recordings", serde_json::to_string(data)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
18
src/backend/client/works.rs
Normal file
18
src/backend/client/works.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use super::Backend;
|
||||
use crate::database::Work;
|
||||
use anyhow::Result;
|
||||
|
||||
impl Backend {
|
||||
/// Get all available works from the server.
|
||||
pub async fn get_works(&self, composer_id: &str) -> Result<Vec<Work>> {
|
||||
let body = self.get(&format!("persons/{}/works", composer_id)).await?;
|
||||
let works: Vec<Work> = serde_json::from_str(&body)?;
|
||||
Ok(works)
|
||||
}
|
||||
|
||||
/// Post a new work to the server.
|
||||
pub async fn post_work(&self, data: &Work) -> Result<()> {
|
||||
self.post("works", serde_json::to_string(data)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
83
src/backend/library.rs
Normal file
83
src/backend/library.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use super::{Backend, BackendState};
|
||||
use crate::database::DbThread;
|
||||
use crate::player::Player;
|
||||
use anyhow::Result;
|
||||
use gio::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
impl Backend {
|
||||
/// Initialize the music library if it is set in the settings.
|
||||
pub(super) async fn init_library(&self) -> Result<()> {
|
||||
if let Some(path) = self.settings.get_string("music-library-path") {
|
||||
if !path.is_empty() {
|
||||
self.set_music_library_path_priv(PathBuf::from(path.to_string()))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the path to the music library folder and start a database thread in the background.
|
||||
pub async fn set_music_library_path(&self, path: PathBuf) -> Result<()> {
|
||||
self.settings
|
||||
.set_string("music-library-path", path.to_str().unwrap())?;
|
||||
self.set_music_library_path_priv(path).await
|
||||
}
|
||||
|
||||
/// Set the path to the music library folder and start a database thread in the background.
|
||||
pub async fn set_music_library_path_priv(&self, path: PathBuf) -> Result<()> {
|
||||
self.set_state(BackendState::Loading);
|
||||
|
||||
if let Some(db) = &*self.database.borrow() {
|
||||
db.stop().await?;
|
||||
}
|
||||
|
||||
self.music_library_path.replace(Some(path.clone()));
|
||||
|
||||
let mut db_path = path.clone();
|
||||
db_path.push("musicus.db");
|
||||
|
||||
let database = DbThread::new(db_path.to_str().unwrap().to_string()).await?;
|
||||
self.database.replace(Some(Rc::new(database)));
|
||||
|
||||
let player = Player::new(path);
|
||||
self.player.replace(Some(player));
|
||||
|
||||
self.set_state(BackendState::Ready);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the currently set music library path.
|
||||
pub fn get_music_library_path(&self) -> Option<PathBuf> {
|
||||
self.music_library_path.borrow().clone()
|
||||
}
|
||||
|
||||
/// Get an interface to the current music library database.
|
||||
pub fn get_database(&self) -> Option<Rc<DbThread>> {
|
||||
self.database.borrow().clone()
|
||||
}
|
||||
|
||||
/// Get an interface to the database and panic if there is none.
|
||||
pub fn db(&self) -> Rc<DbThread> {
|
||||
self.get_database().unwrap()
|
||||
}
|
||||
|
||||
/// Get an interface to the playback service.
|
||||
pub fn get_player(&self) -> Option<Rc<Player>> {
|
||||
self.player.borrow().clone()
|
||||
}
|
||||
|
||||
/// Notify the frontend that the library was changed.
|
||||
pub fn library_changed(&self) {
|
||||
self.set_state(BackendState::Loading);
|
||||
self.set_state(BackendState::Ready);
|
||||
}
|
||||
|
||||
/// Get an interface to the player and panic if there is none.
|
||||
pub fn pl(&self) -> Rc<Player> {
|
||||
self.get_player().unwrap()
|
||||
}
|
||||
}
|
||||
76
src/backend/mod.rs
Normal file
76
src/backend/mod.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
use crate::database::DbThread;
|
||||
use crate::player::Player;
|
||||
use anyhow::Result;
|
||||
use futures_channel::mpsc;
|
||||
use std::cell::RefCell;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub mod client;
|
||||
pub use client::*;
|
||||
|
||||
pub mod library;
|
||||
pub use library::*;
|
||||
|
||||
mod secure;
|
||||
|
||||
/// General states the application can be in.
|
||||
pub enum BackendState {
|
||||
/// The backend is not set up yet. This means that no backend methods except for setting the
|
||||
/// music library path should be called. The user interface should adapt and only present this
|
||||
/// option.
|
||||
NoMusicLibrary,
|
||||
|
||||
/// The backend is loading the music library. No methods should be called. The user interface
|
||||
/// should represent that state by prohibiting all interaction.
|
||||
Loading,
|
||||
|
||||
/// The backend is ready and all methods may be called.
|
||||
Ready,
|
||||
}
|
||||
|
||||
/// A collection of all backend state and functionality.
|
||||
pub struct Backend {
|
||||
pub state_stream: RefCell<mpsc::Receiver<BackendState>>,
|
||||
state_sender: RefCell<mpsc::Sender<BackendState>>,
|
||||
settings: gio::Settings,
|
||||
music_library_path: RefCell<Option<PathBuf>>,
|
||||
database: RefCell<Option<Rc<DbThread>>>,
|
||||
player: RefCell<Option<Rc<Player>>>,
|
||||
server_url: RefCell<Option<String>>,
|
||||
login_data: RefCell<Option<LoginData>>,
|
||||
token: RefCell<Option<String>>,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// Create a new backend initerface. The user interface should subscribe to the state stream
|
||||
/// and call init() afterwards.
|
||||
pub fn new() -> Self {
|
||||
let (state_sender, state_stream) = mpsc::channel(1024);
|
||||
|
||||
Backend {
|
||||
state_stream: RefCell::new(state_stream),
|
||||
state_sender: RefCell::new(state_sender),
|
||||
settings: gio::Settings::new("de.johrpan.musicus"),
|
||||
music_library_path: RefCell::new(None),
|
||||
database: RefCell::new(None),
|
||||
player: RefCell::new(None),
|
||||
server_url: RefCell::new(None),
|
||||
login_data: RefCell::new(None),
|
||||
token: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the backend updating the state accordingly.
|
||||
pub async fn init(self: Rc<Backend>) -> Result<()> {
|
||||
self.init_library().await?;
|
||||
self.init_client()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the current state and notify the user interface.
|
||||
fn set_state(&self, state: BackendState) {
|
||||
self.state_sender.borrow_mut().try_send(state).unwrap();
|
||||
}
|
||||
}
|
||||
108
src/backend/secure.rs
Normal file
108
src/backend/secure.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use super::LoginData;
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures_channel::oneshot;
|
||||
use secret_service::{Collection, EncryptionType, SecretService};
|
||||
|
||||
/// Savely store the user's current login credentials.
|
||||
pub async fn store_login_data(data: LoginData) -> Result<()> {
|
||||
let (sender, receiver) = oneshot::channel::<Result<()>>();
|
||||
std::thread::spawn(move || sender.send(store_login_data_priv(data)));
|
||||
receiver.await?
|
||||
}
|
||||
|
||||
/// Savely store the user's current login credentials.
|
||||
fn store_login_data_priv(data: LoginData) -> Result<()> {
|
||||
let ss = get_ss()?;
|
||||
let collection = get_collection(&ss)?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
delete_secrets(&collection, key)?;
|
||||
|
||||
collection
|
||||
.create_item(
|
||||
key,
|
||||
vec![("username", &data.username)],
|
||||
data.password.as_bytes(),
|
||||
true,
|
||||
"text/plain",
|
||||
)
|
||||
.or(Err(anyhow!(
|
||||
"Failed to save login data using SecretService!"
|
||||
)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the login credentials from secret storage.
|
||||
pub fn load_login_data() -> Result<Option<LoginData>> {
|
||||
let ss = get_ss()?;
|
||||
let collection = get_collection(&ss)?;
|
||||
|
||||
let items = collection.get_all_items().or(Err(anyhow!(
|
||||
"Failed to get items from SecretService collection!"
|
||||
)))?;
|
||||
|
||||
let key = "musicus-login-data";
|
||||
let item = items
|
||||
.iter()
|
||||
.find(|item| item.get_label().unwrap_or_default() == key);
|
||||
|
||||
Ok(match item {
|
||||
Some(item) => {
|
||||
let attrs = item.get_attributes().or(Err(anyhow!(
|
||||
"Failed to get attributes for ScretService item!"
|
||||
)))?;
|
||||
|
||||
let username = attrs
|
||||
.iter()
|
||||
.find(|attr| attr.0 == "username")
|
||||
.ok_or(anyhow!("No username in login data!"))?
|
||||
.1
|
||||
.clone();
|
||||
|
||||
let password = std::str::from_utf8(
|
||||
&item
|
||||
.get_secret()
|
||||
.or(Err(anyhow!("Failed to get secret from SecretService!")))?,
|
||||
)?
|
||||
.to_string();
|
||||
|
||||
Some(LoginData { username, password })
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete all stored secrets for the provided key.
|
||||
fn delete_secrets(collection: &Collection, key: &str) -> Result<()> {
|
||||
let items = collection.get_all_items().or(Err(anyhow!(
|
||||
"Failed to get items from SecretService collection!"
|
||||
)))?;
|
||||
|
||||
for item in items {
|
||||
if item.get_label().unwrap_or_default() == key {
|
||||
item.delete()
|
||||
.or(Err(anyhow!("Failed to delete SecretService item!")))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the SecretService interface.
|
||||
fn get_ss() -> Result<SecretService> {
|
||||
SecretService::new(EncryptionType::Dh).or(Err(anyhow!("Failed to get SecretService!")))
|
||||
}
|
||||
|
||||
/// Get the default SecretService collection and unlock it.
|
||||
fn get_collection(ss: &SecretService) -> Result<Collection> {
|
||||
let collection = ss
|
||||
.get_default_collection()
|
||||
.or(Err(anyhow!("Failed to get SecretService connection!")))?;
|
||||
|
||||
collection
|
||||
.unlock()
|
||||
.or(Err(anyhow!("Failed to unclock SecretService collection!")))?;
|
||||
|
||||
Ok(collection)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue