Moved manager.rs file contents to parent modules
This commit is contained in:
parent
df0de19567
commit
388901cf65
17 changed files with 931 additions and 967 deletions
|
@ -5,12 +5,10 @@ use std::path;
|
||||||
use crate::app::{ddns, settings, user, vfs};
|
use crate::app::{ddns, settings, user, vfs};
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod manager;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
pub use manager::*;
|
|
||||||
|
|
||||||
#[derive(Default, Deserialize)]
|
#[derive(Default, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -29,3 +27,84 @@ impl Config {
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Manager {
|
||||||
|
settings_manager: settings::Manager,
|
||||||
|
user_manager: user::Manager,
|
||||||
|
vfs_manager: vfs::Manager,
|
||||||
|
ddns_manager: ddns::Manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn new(
|
||||||
|
settings_manager: settings::Manager,
|
||||||
|
user_manager: user::Manager,
|
||||||
|
vfs_manager: vfs::Manager,
|
||||||
|
ddns_manager: ddns::Manager,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
settings_manager,
|
||||||
|
user_manager,
|
||||||
|
vfs_manager,
|
||||||
|
ddns_manager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply(&self, config: &Config) -> Result<(), Error> {
|
||||||
|
if let Some(new_settings) = &config.settings {
|
||||||
|
self.settings_manager
|
||||||
|
.amend(new_settings)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mount_dirs) = &config.mount_dirs {
|
||||||
|
self.vfs_manager
|
||||||
|
.set_mount_dirs(mount_dirs)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ddns_config) = &config.ydns {
|
||||||
|
self.ddns_manager
|
||||||
|
.set_config(ddns_config)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref users) = config.users {
|
||||||
|
let old_users: Vec<user::User> =
|
||||||
|
self.user_manager.list().map_err(|_| Error::Unspecified)?;
|
||||||
|
|
||||||
|
// Delete users that are not in new list
|
||||||
|
for old_user in old_users
|
||||||
|
.iter()
|
||||||
|
.filter(|old_user| !users.iter().any(|u| u.name == old_user.name))
|
||||||
|
{
|
||||||
|
self.user_manager
|
||||||
|
.delete(&old_user.name)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new users
|
||||||
|
for new_user in users
|
||||||
|
.iter()
|
||||||
|
.filter(|u| !old_users.iter().any(|old_user| old_user.name == u.name))
|
||||||
|
{
|
||||||
|
self.user_manager
|
||||||
|
.create(new_user)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update users
|
||||||
|
for user in users {
|
||||||
|
self.user_manager
|
||||||
|
.set_password(&user.name, &user.password)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
self.user_manager
|
||||||
|
.set_is_admin(&user.name, user.admin)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
use super::*;
|
|
||||||
use crate::app::{ddns, settings, user, vfs};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Manager {
|
|
||||||
settings_manager: settings::Manager,
|
|
||||||
user_manager: user::Manager,
|
|
||||||
vfs_manager: vfs::Manager,
|
|
||||||
ddns_manager: ddns::Manager,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Manager {
|
|
||||||
pub fn new(
|
|
||||||
settings_manager: settings::Manager,
|
|
||||||
user_manager: user::Manager,
|
|
||||||
vfs_manager: vfs::Manager,
|
|
||||||
ddns_manager: ddns::Manager,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
settings_manager,
|
|
||||||
user_manager,
|
|
||||||
vfs_manager,
|
|
||||||
ddns_manager,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply(&self, config: &Config) -> Result<(), Error> {
|
|
||||||
if let Some(new_settings) = &config.settings {
|
|
||||||
self.settings_manager
|
|
||||||
.amend(new_settings)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(mount_dirs) = &config.mount_dirs {
|
|
||||||
self.vfs_manager
|
|
||||||
.set_mount_dirs(mount_dirs)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ddns_config) = &config.ydns {
|
|
||||||
self.ddns_manager
|
|
||||||
.set_config(ddns_config)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref users) = config.users {
|
|
||||||
let old_users: Vec<user::User> =
|
|
||||||
self.user_manager.list().map_err(|_| Error::Unspecified)?;
|
|
||||||
|
|
||||||
// Delete users that are not in new list
|
|
||||||
for old_user in old_users
|
|
||||||
.iter()
|
|
||||||
.filter(|old_user| !users.iter().any(|u| u.name == old_user.name))
|
|
||||||
{
|
|
||||||
self.user_manager
|
|
||||||
.delete(&old_user.name)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert new users
|
|
||||||
for new_user in users
|
|
||||||
.iter()
|
|
||||||
.filter(|u| !old_users.iter().any(|old_user| old_user.name == u.name))
|
|
||||||
{
|
|
||||||
self.user_manager
|
|
||||||
.create(new_user)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update users
|
|
||||||
for user in users {
|
|
||||||
self.user_manager
|
|
||||||
.set_password(&user.name, &user.password)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
self.user_manager
|
|
||||||
.set_is_admin(&user.name, user.admin)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,88 @@
|
||||||
mod config;
|
use anyhow::bail;
|
||||||
mod manager;
|
use diesel::prelude::*;
|
||||||
|
use log::{error, info};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::thread;
|
||||||
|
use std::time;
|
||||||
|
|
||||||
pub use config::Config;
|
use crate::db::{ddns_config, DB};
|
||||||
pub use manager::Manager;
|
|
||||||
|
const DDNS_UPDATE_URL: &str = "https://ydns.io/api/v1/update/";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)]
|
||||||
|
#[diesel(table_name = ddns_config)]
|
||||||
|
pub struct Config {
|
||||||
|
pub host: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Manager {
|
||||||
|
db: DB,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn new(db: DB) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_my_ip(&self) -> anyhow::Result<()> {
|
||||||
|
let config = self.config()?;
|
||||||
|
if config.host.is_empty() || config.username.is_empty() {
|
||||||
|
info!("Skipping DDNS update because credentials are missing");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.host);
|
||||||
|
let response = ureq::get(full_url.as_str())
|
||||||
|
.auth(&config.username, &config.password)
|
||||||
|
.call();
|
||||||
|
|
||||||
|
if !response.ok() {
|
||||||
|
bail!(
|
||||||
|
"DDNS update query failed with status code: {}",
|
||||||
|
response.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config(&self) -> anyhow::Result<Config> {
|
||||||
|
use crate::db::ddns_config::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
Ok(ddns_config
|
||||||
|
.select((host, username, password))
|
||||||
|
.get_result(&mut connection)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_config(&self, new_config: &Config) -> anyhow::Result<()> {
|
||||||
|
use crate::db::ddns_config::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
diesel::update(ddns_config)
|
||||||
|
.set((
|
||||||
|
host.eq(&new_config.host),
|
||||||
|
username.eq(&new_config.username),
|
||||||
|
password.eq(&new_config.password),
|
||||||
|
))
|
||||||
|
.execute(&mut connection)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_periodic_updates(&self) {
|
||||||
|
let cloned = self.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
cloned.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self) {
|
||||||
|
loop {
|
||||||
|
if let Err(e) = self.update_my_ip() {
|
||||||
|
error!("Dynamic DNS update error: {:?}", e);
|
||||||
|
}
|
||||||
|
thread::sleep(time::Duration::from_secs(60 * 30));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::db::ddns_config;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)]
|
|
||||||
#[diesel(table_name = ddns_config)]
|
|
||||||
pub struct Config {
|
|
||||||
pub host: String,
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
use anyhow::*;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use log::{error, info};
|
|
||||||
use std::thread;
|
|
||||||
use std::time;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::db::DB;
|
|
||||||
|
|
||||||
const DDNS_UPDATE_URL: &str = "https://ydns.io/api/v1/update/";
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Manager {
|
|
||||||
db: DB,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Manager {
|
|
||||||
pub fn new(db: DB) -> Self {
|
|
||||||
Self { db }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_my_ip(&self) -> Result<()> {
|
|
||||||
let config = self.config()?;
|
|
||||||
if config.host.is_empty() || config.username.is_empty() {
|
|
||||||
info!("Skipping DDNS update because credentials are missing");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.host);
|
|
||||||
let response = ureq::get(full_url.as_str())
|
|
||||||
.auth(&config.username, &config.password)
|
|
||||||
.call();
|
|
||||||
|
|
||||||
if !response.ok() {
|
|
||||||
bail!(
|
|
||||||
"DDNS update query failed with status code: {}",
|
|
||||||
response.status()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config(&self) -> Result<Config> {
|
|
||||||
use crate::db::ddns_config::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
Ok(ddns_config
|
|
||||||
.select((host, username, password))
|
|
||||||
.get_result(&mut connection)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_config(&self, new_config: &Config) -> Result<()> {
|
|
||||||
use crate::db::ddns_config::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
diesel::update(ddns_config)
|
|
||||||
.set((
|
|
||||||
host.eq(&new_config.host),
|
|
||||||
username.eq(&new_config.username),
|
|
||||||
password.eq(&new_config.password),
|
|
||||||
))
|
|
||||||
.execute(&mut connection)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn begin_periodic_updates(&self) {
|
|
||||||
let cloned = self.clone();
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
cloned.run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(&self) {
|
|
||||||
loop {
|
|
||||||
if let Err(e) = self.update_my_ip() {
|
|
||||||
error!("Dynamic DNS update error: {:?}", e);
|
|
||||||
}
|
|
||||||
thread::sleep(time::Duration::from_secs(60 * 30));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1,70 @@
|
||||||
mod manager;
|
use anyhow::*;
|
||||||
|
use rustfm_scrobble::{Scrobble, Scrobbler};
|
||||||
|
use std::path::Path;
|
||||||
|
use user::AuthToken;
|
||||||
|
|
||||||
pub use manager::*;
|
use crate::app::{index::Index, user};
|
||||||
|
|
||||||
|
const LASTFM_API_KEY: &str = "02b96c939a2b451c31dfd67add1f696e";
|
||||||
|
const LASTFM_API_SECRET: &str = "0f25a80ceef4b470b5cb97d99d4b3420";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Manager {
|
||||||
|
index: Index,
|
||||||
|
user_manager: user::Manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn new(index: Index, user_manager: user::Manager) -> Self {
|
||||||
|
Self {
|
||||||
|
index,
|
||||||
|
user_manager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_link_token(&self, username: &str) -> Result<AuthToken> {
|
||||||
|
self.user_manager
|
||||||
|
.generate_lastfm_link_token(username)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn link(&self, username: &str, lastfm_token: &str) -> Result<()> {
|
||||||
|
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET);
|
||||||
|
let auth_response = scrobbler.authenticate_with_token(lastfm_token)?;
|
||||||
|
|
||||||
|
self.user_manager
|
||||||
|
.lastfm_link(username, &auth_response.name, &auth_response.key)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlink(&self, username: &str) -> Result<()> {
|
||||||
|
self.user_manager.lastfm_unlink(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scrobble(&self, username: &str, track: &Path) -> Result<()> {
|
||||||
|
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET);
|
||||||
|
let scrobble = self.scrobble_from_path(track)?;
|
||||||
|
let auth_token = self.user_manager.get_lastfm_session_key(username)?;
|
||||||
|
scrobbler.authenticate_with_session_key(&auth_token);
|
||||||
|
scrobbler.scrobble(&scrobble)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn now_playing(&self, username: &str, track: &Path) -> Result<()> {
|
||||||
|
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET);
|
||||||
|
let scrobble = self.scrobble_from_path(track)?;
|
||||||
|
let auth_token = self.user_manager.get_lastfm_session_key(username)?;
|
||||||
|
scrobbler.authenticate_with_session_key(&auth_token);
|
||||||
|
scrobbler.now_playing(&scrobble)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble> {
|
||||||
|
let song = self.index.get_song(track)?;
|
||||||
|
Ok(Scrobble::new(
|
||||||
|
song.artist.as_deref().unwrap_or(""),
|
||||||
|
song.title.as_deref().unwrap_or(""),
|
||||||
|
song.album.as_deref().unwrap_or(""),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
use anyhow::*;
|
|
||||||
use rustfm_scrobble::{Scrobble, Scrobbler};
|
|
||||||
use std::path::Path;
|
|
||||||
use user::AuthToken;
|
|
||||||
|
|
||||||
use crate::app::{index::Index, user};
|
|
||||||
|
|
||||||
const LASTFM_API_KEY: &str = "02b96c939a2b451c31dfd67add1f696e";
|
|
||||||
const LASTFM_API_SECRET: &str = "0f25a80ceef4b470b5cb97d99d4b3420";
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Manager {
|
|
||||||
index: Index,
|
|
||||||
user_manager: user::Manager,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Manager {
|
|
||||||
pub fn new(index: Index, user_manager: user::Manager) -> Self {
|
|
||||||
Self {
|
|
||||||
index,
|
|
||||||
user_manager,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_link_token(&self, username: &str) -> Result<AuthToken> {
|
|
||||||
self.user_manager
|
|
||||||
.generate_lastfm_link_token(username)
|
|
||||||
.map_err(|e| e.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn link(&self, username: &str, lastfm_token: &str) -> Result<()> {
|
|
||||||
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET);
|
|
||||||
let auth_response = scrobbler.authenticate_with_token(lastfm_token)?;
|
|
||||||
|
|
||||||
self.user_manager
|
|
||||||
.lastfm_link(username, &auth_response.name, &auth_response.key)
|
|
||||||
.map_err(|e| e.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unlink(&self, username: &str) -> Result<()> {
|
|
||||||
self.user_manager.lastfm_unlink(username)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scrobble(&self, username: &str, track: &Path) -> Result<()> {
|
|
||||||
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET);
|
|
||||||
let scrobble = self.scrobble_from_path(track)?;
|
|
||||||
let auth_token = self.user_manager.get_lastfm_session_key(username)?;
|
|
||||||
scrobbler.authenticate_with_session_key(&auth_token);
|
|
||||||
scrobbler.scrobble(&scrobble)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn now_playing(&self, username: &str, track: &Path) -> Result<()> {
|
|
||||||
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET);
|
|
||||||
let scrobble = self.scrobble_from_path(track)?;
|
|
||||||
let auth_token = self.user_manager.get_lastfm_session_key(username)?;
|
|
||||||
scrobbler.authenticate_with_session_key(&auth_token);
|
|
||||||
scrobbler.now_playing(&scrobble)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble> {
|
|
||||||
let song = self.index.get_song(track)?;
|
|
||||||
Ok(Scrobble::new(
|
|
||||||
song.artist.as_deref().unwrap_or(""),
|
|
||||||
song.title.as_deref().unwrap_or(""),
|
|
||||||
song.album.as_deref().unwrap_or(""),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,253 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use core::clone::Clone;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::sql_types;
|
||||||
|
use diesel::BelongingToDsl;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::app::index::Song;
|
||||||
|
use crate::app::vfs;
|
||||||
|
use crate::db::{playlist_songs, playlists, users, DB};
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod manager;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
pub use manager::*;
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Manager {
|
||||||
|
db: DB,
|
||||||
|
vfs_manager: vfs::Manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn new(db: DB, vfs_manager: vfs::Manager) -> Self {
|
||||||
|
Self { db, vfs_manager }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_playlists(&self, owner: &str) -> Result<Vec<String>, Error> {
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
|
||||||
|
let user: User = {
|
||||||
|
use self::users::dsl::*;
|
||||||
|
users
|
||||||
|
.filter(name.eq(owner))
|
||||||
|
.select((id,))
|
||||||
|
.first(&mut connection)
|
||||||
|
.optional()
|
||||||
|
.map_err(anyhow::Error::new)?
|
||||||
|
.ok_or(Error::UserNotFound)?
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
use self::playlists::dsl::*;
|
||||||
|
let found_playlists: Vec<String> = Playlist::belonging_to(&user)
|
||||||
|
.select(name)
|
||||||
|
.load(&mut connection)
|
||||||
|
.map_err(anyhow::Error::new)?;
|
||||||
|
Ok(found_playlists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_playlist(
|
||||||
|
&self,
|
||||||
|
playlist_name: &str,
|
||||||
|
owner: &str,
|
||||||
|
content: &[String],
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let new_playlist: NewPlaylist;
|
||||||
|
let playlist: Playlist;
|
||||||
|
let vfs = self.vfs_manager.get_vfs()?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
|
||||||
|
// Find owner
|
||||||
|
let user: User = {
|
||||||
|
use self::users::dsl::*;
|
||||||
|
users
|
||||||
|
.filter(name.eq(owner))
|
||||||
|
.select((id,))
|
||||||
|
.first(&mut connection)
|
||||||
|
.optional()
|
||||||
|
.map_err(anyhow::Error::new)?
|
||||||
|
.ok_or(Error::UserNotFound)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create playlist
|
||||||
|
new_playlist = NewPlaylist {
|
||||||
|
name: playlist_name.into(),
|
||||||
|
owner: user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(playlists::table)
|
||||||
|
.values(&new_playlist)
|
||||||
|
.execute(&mut connection)
|
||||||
|
.map_err(anyhow::Error::new)?;
|
||||||
|
|
||||||
|
playlist = {
|
||||||
|
use self::playlists::dsl::*;
|
||||||
|
playlists
|
||||||
|
.select((id, owner))
|
||||||
|
.filter(name.eq(playlist_name).and(owner.eq(user.id)))
|
||||||
|
.get_result(&mut connection)
|
||||||
|
.map_err(anyhow::Error::new)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_songs: Vec<NewPlaylistSong> = Vec::new();
|
||||||
|
new_songs.reserve(content.len());
|
||||||
|
|
||||||
|
for (i, path) in content.iter().enumerate() {
|
||||||
|
let virtual_path = Path::new(&path);
|
||||||
|
if let Some(real_path) = vfs
|
||||||
|
.virtual_to_real(virtual_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.to_str().map(|s| s.to_owned()))
|
||||||
|
{
|
||||||
|
new_songs.push(NewPlaylistSong {
|
||||||
|
playlist: playlist.id,
|
||||||
|
path: real_path,
|
||||||
|
ordering: i as i32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
connection
|
||||||
|
.transaction::<_, diesel::result::Error, _>(|connection| {
|
||||||
|
// Delete old content (if any)
|
||||||
|
let old_songs = PlaylistSong::belonging_to(&playlist);
|
||||||
|
diesel::delete(old_songs).execute(connection)?;
|
||||||
|
|
||||||
|
// Insert content
|
||||||
|
diesel::insert_into(playlist_songs::table)
|
||||||
|
.values(&new_songs)
|
||||||
|
.execute(&mut *connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.map_err(anyhow::Error::new)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_playlist(&self, playlist_name: &str, owner: &str) -> Result<Vec<Song>, Error> {
|
||||||
|
let vfs = self.vfs_manager.get_vfs()?;
|
||||||
|
let songs: Vec<Song>;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
|
||||||
|
// Find owner
|
||||||
|
let user: User = {
|
||||||
|
use self::users::dsl::*;
|
||||||
|
users
|
||||||
|
.filter(name.eq(owner))
|
||||||
|
.select((id,))
|
||||||
|
.first(&mut connection)
|
||||||
|
.optional()
|
||||||
|
.map_err(anyhow::Error::new)?
|
||||||
|
.ok_or(Error::UserNotFound)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find playlist
|
||||||
|
let playlist: Playlist = {
|
||||||
|
use self::playlists::dsl::*;
|
||||||
|
playlists
|
||||||
|
.select((id, owner))
|
||||||
|
.filter(name.eq(playlist_name).and(owner.eq(user.id)))
|
||||||
|
.get_result(&mut connection)
|
||||||
|
.optional()
|
||||||
|
.map_err(anyhow::Error::new)?
|
||||||
|
.ok_or(Error::PlaylistNotFound)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Select songs. Not using Diesel because we need to LEFT JOIN using a custom column
|
||||||
|
let query = diesel::sql_query(
|
||||||
|
r#"
|
||||||
|
SELECT s.id, s.path, s.parent, s.track_number, s.disc_number, s.title, s.artist, s.album_artist, s.year, s.album, s.artwork, s.duration, s.lyricist, s.composer, s.genre, s.label
|
||||||
|
FROM playlist_songs ps
|
||||||
|
LEFT JOIN songs s ON ps.path = s.path
|
||||||
|
WHERE ps.playlist = ?
|
||||||
|
ORDER BY ps.ordering
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let query = query.bind::<sql_types::Integer, _>(playlist.id);
|
||||||
|
songs = query
|
||||||
|
.get_results(&mut connection)
|
||||||
|
.map_err(anyhow::Error::new)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map real path to virtual paths
|
||||||
|
let virtual_songs = songs
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|s| s.virtualize(&vfs))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(virtual_songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> {
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
|
||||||
|
let user: User = {
|
||||||
|
use self::users::dsl::*;
|
||||||
|
users
|
||||||
|
.filter(name.eq(owner))
|
||||||
|
.select((id,))
|
||||||
|
.first(&mut connection)
|
||||||
|
.optional()
|
||||||
|
.map_err(anyhow::Error::new)?
|
||||||
|
.ok_or(Error::UserNotFound)?
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
use self::playlists::dsl::*;
|
||||||
|
let q = Playlist::belonging_to(&user).filter(name.eq(playlist_name));
|
||||||
|
match diesel::delete(q)
|
||||||
|
.execute(&mut connection)
|
||||||
|
.map_err(anyhow::Error::new)?
|
||||||
|
{
|
||||||
|
0 => Err(Error::PlaylistNotFound),
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations)]
|
||||||
|
#[diesel(belongs_to(User, foreign_key = owner))]
|
||||||
|
struct Playlist {
|
||||||
|
id: i32,
|
||||||
|
owner: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable, Associations)]
|
||||||
|
#[diesel(belongs_to(Playlist, foreign_key = playlist))]
|
||||||
|
struct PlaylistSong {
|
||||||
|
id: i32,
|
||||||
|
playlist: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable)]
|
||||||
|
#[diesel(table_name = playlists)]
|
||||||
|
struct NewPlaylist {
|
||||||
|
name: String,
|
||||||
|
owner: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Insertable)]
|
||||||
|
#[diesel(table_name = playlist_songs)]
|
||||||
|
struct NewPlaylistSong {
|
||||||
|
playlist: i32,
|
||||||
|
path: String,
|
||||||
|
ordering: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Identifiable, Queryable)]
|
||||||
|
struct User {
|
||||||
|
id: i32,
|
||||||
|
}
|
||||||
|
|
|
@ -1,248 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use core::clone::Clone;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use diesel::sql_types;
|
|
||||||
use diesel::BelongingToDsl;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::app::index::Song;
|
|
||||||
use crate::app::vfs;
|
|
||||||
use crate::db::{playlist_songs, playlists, users, DB};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Manager {
|
|
||||||
db: DB,
|
|
||||||
vfs_manager: vfs::Manager,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Manager {
|
|
||||||
pub fn new(db: DB, vfs_manager: vfs::Manager) -> Self {
|
|
||||||
Self { db, vfs_manager }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_playlists(&self, owner: &str) -> Result<Vec<String>, Error> {
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
|
|
||||||
let user: User = {
|
|
||||||
use self::users::dsl::*;
|
|
||||||
users
|
|
||||||
.filter(name.eq(owner))
|
|
||||||
.select((id,))
|
|
||||||
.first(&mut connection)
|
|
||||||
.optional()
|
|
||||||
.map_err(anyhow::Error::new)?
|
|
||||||
.ok_or(Error::UserNotFound)?
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
use self::playlists::dsl::*;
|
|
||||||
let found_playlists: Vec<String> = Playlist::belonging_to(&user)
|
|
||||||
.select(name)
|
|
||||||
.load(&mut connection)
|
|
||||||
.map_err(anyhow::Error::new)?;
|
|
||||||
Ok(found_playlists)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_playlist(
|
|
||||||
&self,
|
|
||||||
playlist_name: &str,
|
|
||||||
owner: &str,
|
|
||||||
content: &[String],
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let new_playlist: NewPlaylist;
|
|
||||||
let playlist: Playlist;
|
|
||||||
let vfs = self.vfs_manager.get_vfs()?;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
|
|
||||||
// Find owner
|
|
||||||
let user: User = {
|
|
||||||
use self::users::dsl::*;
|
|
||||||
users
|
|
||||||
.filter(name.eq(owner))
|
|
||||||
.select((id,))
|
|
||||||
.first(&mut connection)
|
|
||||||
.optional()
|
|
||||||
.map_err(anyhow::Error::new)?
|
|
||||||
.ok_or(Error::UserNotFound)?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create playlist
|
|
||||||
new_playlist = NewPlaylist {
|
|
||||||
name: playlist_name.into(),
|
|
||||||
owner: user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
diesel::insert_into(playlists::table)
|
|
||||||
.values(&new_playlist)
|
|
||||||
.execute(&mut connection)
|
|
||||||
.map_err(anyhow::Error::new)?;
|
|
||||||
|
|
||||||
playlist = {
|
|
||||||
use self::playlists::dsl::*;
|
|
||||||
playlists
|
|
||||||
.select((id, owner))
|
|
||||||
.filter(name.eq(playlist_name).and(owner.eq(user.id)))
|
|
||||||
.get_result(&mut connection)
|
|
||||||
.map_err(anyhow::Error::new)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut new_songs: Vec<NewPlaylistSong> = Vec::new();
|
|
||||||
new_songs.reserve(content.len());
|
|
||||||
|
|
||||||
for (i, path) in content.iter().enumerate() {
|
|
||||||
let virtual_path = Path::new(&path);
|
|
||||||
if let Some(real_path) = vfs
|
|
||||||
.virtual_to_real(virtual_path)
|
|
||||||
.ok()
|
|
||||||
.and_then(|p| p.to_str().map(|s| s.to_owned()))
|
|
||||||
{
|
|
||||||
new_songs.push(NewPlaylistSong {
|
|
||||||
playlist: playlist.id,
|
|
||||||
path: real_path,
|
|
||||||
ordering: i as i32,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
connection
|
|
||||||
.transaction::<_, diesel::result::Error, _>(|connection| {
|
|
||||||
// Delete old content (if any)
|
|
||||||
let old_songs = PlaylistSong::belonging_to(&playlist);
|
|
||||||
diesel::delete(old_songs).execute(connection)?;
|
|
||||||
|
|
||||||
// Insert content
|
|
||||||
diesel::insert_into(playlist_songs::table)
|
|
||||||
.values(&new_songs)
|
|
||||||
.execute(&mut *connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.map_err(anyhow::Error::new)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_playlist(&self, playlist_name: &str, owner: &str) -> Result<Vec<Song>, Error> {
|
|
||||||
let vfs = self.vfs_manager.get_vfs()?;
|
|
||||||
let songs: Vec<Song>;
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
|
|
||||||
// Find owner
|
|
||||||
let user: User = {
|
|
||||||
use self::users::dsl::*;
|
|
||||||
users
|
|
||||||
.filter(name.eq(owner))
|
|
||||||
.select((id,))
|
|
||||||
.first(&mut connection)
|
|
||||||
.optional()
|
|
||||||
.map_err(anyhow::Error::new)?
|
|
||||||
.ok_or(Error::UserNotFound)?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find playlist
|
|
||||||
let playlist: Playlist = {
|
|
||||||
use self::playlists::dsl::*;
|
|
||||||
playlists
|
|
||||||
.select((id, owner))
|
|
||||||
.filter(name.eq(playlist_name).and(owner.eq(user.id)))
|
|
||||||
.get_result(&mut connection)
|
|
||||||
.optional()
|
|
||||||
.map_err(anyhow::Error::new)?
|
|
||||||
.ok_or(Error::PlaylistNotFound)?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Select songs. Not using Diesel because we need to LEFT JOIN using a custom column
|
|
||||||
let query = diesel::sql_query(
|
|
||||||
r#"
|
|
||||||
SELECT s.id, s.path, s.parent, s.track_number, s.disc_number, s.title, s.artist, s.album_artist, s.year, s.album, s.artwork, s.duration, s.lyricist, s.composer, s.genre, s.label
|
|
||||||
FROM playlist_songs ps
|
|
||||||
LEFT JOIN songs s ON ps.path = s.path
|
|
||||||
WHERE ps.playlist = ?
|
|
||||||
ORDER BY ps.ordering
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
let query = query.bind::<sql_types::Integer, _>(playlist.id);
|
|
||||||
songs = query
|
|
||||||
.get_results(&mut connection)
|
|
||||||
.map_err(anyhow::Error::new)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map real path to virtual paths
|
|
||||||
let virtual_songs = songs
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|s| s.virtualize(&vfs))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(virtual_songs)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> {
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
|
|
||||||
let user: User = {
|
|
||||||
use self::users::dsl::*;
|
|
||||||
users
|
|
||||||
.filter(name.eq(owner))
|
|
||||||
.select((id,))
|
|
||||||
.first(&mut connection)
|
|
||||||
.optional()
|
|
||||||
.map_err(anyhow::Error::new)?
|
|
||||||
.ok_or(Error::UserNotFound)?
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
use self::playlists::dsl::*;
|
|
||||||
let q = Playlist::belonging_to(&user).filter(name.eq(playlist_name));
|
|
||||||
match diesel::delete(q)
|
|
||||||
.execute(&mut connection)
|
|
||||||
.map_err(anyhow::Error::new)?
|
|
||||||
{
|
|
||||||
0 => Err(Error::PlaylistNotFound),
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Associations)]
|
|
||||||
#[diesel(belongs_to(User, foreign_key = owner))]
|
|
||||||
struct Playlist {
|
|
||||||
id: i32,
|
|
||||||
owner: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable, Associations)]
|
|
||||||
#[diesel(belongs_to(Playlist, foreign_key = playlist))]
|
|
||||||
struct PlaylistSong {
|
|
||||||
id: i32,
|
|
||||||
playlist: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Insertable)]
|
|
||||||
#[diesel(table_name = playlists)]
|
|
||||||
struct NewPlaylist {
|
|
||||||
name: String,
|
|
||||||
owner: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Insertable)]
|
|
||||||
#[diesel(table_name = playlist_songs)]
|
|
||||||
struct NewPlaylistSong {
|
|
||||||
playlist: i32,
|
|
||||||
path: String,
|
|
||||||
ordering: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Identifiable, Queryable)]
|
|
||||||
struct User {
|
|
||||||
id: i32,
|
|
||||||
}
|
|
|
@ -1,10 +1,14 @@
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::db::{misc_settings, DB};
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod manager;
|
|
||||||
|
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
pub use manager::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct AuthSecret {
|
pub struct AuthSecret {
|
||||||
|
@ -22,3 +26,90 @@ pub struct NewSettings {
|
||||||
pub reindex_every_n_seconds: Option<i32>,
|
pub reindex_every_n_seconds: Option<i32>,
|
||||||
pub album_art_pattern: Option<String>,
|
pub album_art_pattern: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Manager {
|
||||||
|
pub db: DB,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn new(db: DB) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_auth_secret(&self) -> Result<AuthSecret, Error> {
|
||||||
|
use self::misc_settings::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
let secret: Vec<u8> = misc_settings
|
||||||
|
.select(auth_secret)
|
||||||
|
.get_result(&mut connection)
|
||||||
|
.map_err(|e| match e {
|
||||||
|
diesel::result::Error::NotFound => Error::AuthSecretNotFound,
|
||||||
|
_ => Error::Unspecified,
|
||||||
|
})?;
|
||||||
|
secret
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::InvalidAuthSecret)
|
||||||
|
.map(|key| AuthSecret { key })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_index_sleep_duration(&self) -> Result<Duration, Error> {
|
||||||
|
use self::misc_settings::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
misc_settings
|
||||||
|
.select(index_sleep_duration_seconds)
|
||||||
|
.get_result(&mut connection)
|
||||||
|
.map_err(|e| match e {
|
||||||
|
diesel::result::Error::NotFound => Error::IndexSleepDurationNotFound,
|
||||||
|
_ => Error::Unspecified,
|
||||||
|
})
|
||||||
|
.map(|s: i32| Duration::from_secs(s as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_index_album_art_pattern(&self) -> Result<Regex, Error> {
|
||||||
|
use self::misc_settings::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
misc_settings
|
||||||
|
.select(index_album_art_pattern)
|
||||||
|
.get_result(&mut connection)
|
||||||
|
.map_err(|e| match e {
|
||||||
|
diesel::result::Error::NotFound => Error::IndexAlbumArtPatternNotFound,
|
||||||
|
_ => Error::Unspecified,
|
||||||
|
})
|
||||||
|
.and_then(|s: String| {
|
||||||
|
Regex::new(&format!("(?i){}", &s)).map_err(|_| Error::IndexAlbumArtPatternInvalid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&self) -> Result<Settings, Error> {
|
||||||
|
use self::misc_settings::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
|
||||||
|
let settings: Settings = misc_settings
|
||||||
|
.select((index_sleep_duration_seconds, index_album_art_pattern))
|
||||||
|
.get_result(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn amend(&self, new_settings: &NewSettings) -> Result<(), Error> {
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
|
||||||
|
if let Some(sleep_duration) = new_settings.reindex_every_n_seconds {
|
||||||
|
diesel::update(misc_settings::table)
|
||||||
|
.set(misc_settings::index_sleep_duration_seconds.eq(sleep_duration as i32))
|
||||||
|
.execute(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref album_art_pattern) = new_settings.album_art_pattern {
|
||||||
|
diesel::update(misc_settings::table)
|
||||||
|
.set(misc_settings::index_album_art_pattern.eq(album_art_pattern))
|
||||||
|
.execute(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
use diesel::prelude::*;
|
|
||||||
use regex::Regex;
|
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::db::{misc_settings, DB};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Manager {
|
|
||||||
pub db: DB,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Manager {
|
|
||||||
pub fn new(db: DB) -> Self {
|
|
||||||
Self { db }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_auth_secret(&self) -> Result<AuthSecret, Error> {
|
|
||||||
use self::misc_settings::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
let secret: Vec<u8> = misc_settings
|
|
||||||
.select(auth_secret)
|
|
||||||
.get_result(&mut connection)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
diesel::result::Error::NotFound => Error::AuthSecretNotFound,
|
|
||||||
_ => Error::Unspecified,
|
|
||||||
})?;
|
|
||||||
secret
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| Error::InvalidAuthSecret)
|
|
||||||
.map(|key| AuthSecret { key })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_index_sleep_duration(&self) -> Result<Duration, Error> {
|
|
||||||
use self::misc_settings::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
misc_settings
|
|
||||||
.select(index_sleep_duration_seconds)
|
|
||||||
.get_result(&mut connection)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
diesel::result::Error::NotFound => Error::IndexSleepDurationNotFound,
|
|
||||||
_ => Error::Unspecified,
|
|
||||||
})
|
|
||||||
.map(|s: i32| Duration::from_secs(s as u64))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_index_album_art_pattern(&self) -> Result<Regex, Error> {
|
|
||||||
use self::misc_settings::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
misc_settings
|
|
||||||
.select(index_album_art_pattern)
|
|
||||||
.get_result(&mut connection)
|
|
||||||
.map_err(|e| match e {
|
|
||||||
diesel::result::Error::NotFound => Error::IndexAlbumArtPatternNotFound,
|
|
||||||
_ => Error::Unspecified,
|
|
||||||
})
|
|
||||||
.and_then(|s: String| {
|
|
||||||
Regex::new(&format!("(?i){}", &s)).map_err(|_| Error::IndexAlbumArtPatternInvalid)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read(&self) -> Result<Settings, Error> {
|
|
||||||
use self::misc_settings::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
|
|
||||||
let settings: Settings = misc_settings
|
|
||||||
.select((index_sleep_duration_seconds, index_album_art_pattern))
|
|
||||||
.get_result(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
|
|
||||||
Ok(settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn amend(&self, new_settings: &NewSettings) -> Result<(), Error> {
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
|
|
||||||
if let Some(sleep_duration) = new_settings.reindex_every_n_seconds {
|
|
||||||
diesel::update(misc_settings::table)
|
|
||||||
.set(misc_settings::index_sleep_duration_seconds.eq(sleep_duration as i32))
|
|
||||||
.execute(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref album_art_pattern) = new_settings.album_art_pattern {
|
|
||||||
diesel::update(misc_settings::table)
|
|
||||||
.set(misc_settings::index_album_art_pattern.eq(album_art_pattern))
|
|
||||||
.execute(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,68 @@
|
||||||
|
use anyhow::*;
|
||||||
|
use image::ImageOutputFormat;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
mod generate;
|
mod generate;
|
||||||
mod manager;
|
|
||||||
mod options;
|
mod options;
|
||||||
mod read;
|
mod read;
|
||||||
|
|
||||||
pub use generate::*;
|
pub use generate::*;
|
||||||
pub use manager::*;
|
|
||||||
pub use options::*;
|
pub use options::*;
|
||||||
pub use read::*;
|
pub use read::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Manager {
|
||||||
|
thumbnails_dir_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn new(thumbnails_dir_path: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
thumbnails_dir_path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Result<PathBuf> {
|
||||||
|
match self.retrieve_thumbnail(image_path, thumbnailoptions) {
|
||||||
|
Some(path) => Ok(path),
|
||||||
|
None => self.create_thumbnail(image_path, thumbnailoptions),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_thumbnail_path(&self, image_path: &Path, thumbnailoptions: &Options) -> PathBuf {
|
||||||
|
let hash = Manager::hash(image_path, thumbnailoptions);
|
||||||
|
let mut thumbnail_path = self.thumbnails_dir_path.clone();
|
||||||
|
thumbnail_path.push(format!("{}.jpg", hash));
|
||||||
|
thumbnail_path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn retrieve_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Option<PathBuf> {
|
||||||
|
let path = self.get_thumbnail_path(image_path, thumbnailoptions);
|
||||||
|
if path.exists() {
|
||||||
|
Some(path)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Result<PathBuf> {
|
||||||
|
let thumbnail = generate_thumbnail(image_path, thumbnailoptions)?;
|
||||||
|
let quality = 80;
|
||||||
|
|
||||||
|
fs::create_dir_all(&self.thumbnails_dir_path)?;
|
||||||
|
let path = self.get_thumbnail_path(image_path, thumbnailoptions);
|
||||||
|
let mut out_file = File::create(&path)?;
|
||||||
|
thumbnail.write_to(&mut out_file, ImageOutputFormat::Jpeg(quality))?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash(path: &Path, thumbnailoptions: &Options) -> u64 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
path.hash(&mut hasher);
|
||||||
|
thumbnailoptions.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
use anyhow::*;
|
|
||||||
use image::ImageOutputFormat;
|
|
||||||
use std::collections::hash_map::DefaultHasher;
|
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use crate::app::thumbnail::*;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Manager {
|
|
||||||
thumbnails_dir_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Manager {
|
|
||||||
pub fn new(thumbnails_dir_path: PathBuf) -> Self {
|
|
||||||
Self {
|
|
||||||
thumbnails_dir_path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Result<PathBuf> {
|
|
||||||
match self.retrieve_thumbnail(image_path, thumbnailoptions) {
|
|
||||||
Some(path) => Ok(path),
|
|
||||||
None => self.create_thumbnail(image_path, thumbnailoptions),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_thumbnail_path(&self, image_path: &Path, thumbnailoptions: &Options) -> PathBuf {
|
|
||||||
let hash = Manager::hash(image_path, thumbnailoptions);
|
|
||||||
let mut thumbnail_path = self.thumbnails_dir_path.clone();
|
|
||||||
thumbnail_path.push(format!("{}.jpg", hash));
|
|
||||||
thumbnail_path
|
|
||||||
}
|
|
||||||
|
|
||||||
fn retrieve_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Option<PathBuf> {
|
|
||||||
let path = self.get_thumbnail_path(image_path, thumbnailoptions);
|
|
||||||
if path.exists() {
|
|
||||||
Some(path)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Result<PathBuf> {
|
|
||||||
let thumbnail = generate_thumbnail(image_path, thumbnailoptions)?;
|
|
||||||
let quality = 80;
|
|
||||||
|
|
||||||
fs::create_dir_all(&self.thumbnails_dir_path)?;
|
|
||||||
let path = self.get_thumbnail_path(image_path, thumbnailoptions);
|
|
||||||
let mut out_file = File::create(&path)?;
|
|
||||||
thumbnail.write_to(&mut out_file, ImageOutputFormat::Jpeg(quality))?;
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hash(path: &Path, thumbnailoptions: &Options) -> u64 {
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
path.hash(&mut hasher);
|
|
||||||
thumbnailoptions.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
|
||||||
}
|
|
||||||
}
|
|
257
src/app/user.rs
257
src/app/user.rs
|
@ -1,15 +1,20 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use pbkdf2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
|
||||||
|
use pbkdf2::Pbkdf2;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use crate::db::users;
|
use crate::app::settings::AuthSecret;
|
||||||
|
use crate::db::{users, DB};
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod manager;
|
|
||||||
mod preferences;
|
mod preferences;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
pub use manager::*;
|
|
||||||
pub use preferences::*;
|
pub use preferences::*;
|
||||||
|
|
||||||
#[derive(Debug, Insertable, Queryable)]
|
#[derive(Debug, Insertable, Queryable)]
|
||||||
|
@ -47,3 +52,249 @@ pub struct Authorization {
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub scope: AuthorizationScope,
|
pub scope: AuthorizationScope,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Manager {
|
||||||
|
// TODO make this private and move preferences methods in this file
|
||||||
|
pub db: DB,
|
||||||
|
auth_secret: AuthSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn new(db: DB, auth_secret: AuthSecret) -> Self {
|
||||||
|
Self { db, auth_secret }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(&self, new_user: &NewUser) -> Result<(), Error> {
|
||||||
|
if new_user.name.is_empty() {
|
||||||
|
return Err(Error::EmptyUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
let password_hash = hash_password(&new_user.password)?;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
let new_user = User {
|
||||||
|
name: new_user.name.to_owned(),
|
||||||
|
password_hash,
|
||||||
|
admin: new_user.admin as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(users::table)
|
||||||
|
.values(&new_user)
|
||||||
|
.execute(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&self, username: &str) -> Result<(), Error> {
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
diesel::delete(users.filter(name.eq(username)))
|
||||||
|
.execute(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_password(&self, username: &str, password: &str) -> Result<(), Error> {
|
||||||
|
let hash = hash_password(password)?;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
diesel::update(users.filter(name.eq(username)))
|
||||||
|
.set(password_hash.eq(hash))
|
||||||
|
.execute(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_is_admin(&self, username: &str, is_admin: bool) -> Result<(), Error> {
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
diesel::update(users.filter(name.eq(username)))
|
||||||
|
.set(admin.eq(is_admin as i32))
|
||||||
|
.execute(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login(&self, username: &str, password: &str) -> Result<AuthToken, Error> {
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
match users
|
||||||
|
.select(password_hash)
|
||||||
|
.filter(name.eq(username))
|
||||||
|
.get_result(&mut connection)
|
||||||
|
{
|
||||||
|
Err(diesel::result::Error::NotFound) => Err(Error::IncorrectUsername),
|
||||||
|
Ok(hash) => {
|
||||||
|
let hash: String = hash;
|
||||||
|
if verify_password(&hash, password) {
|
||||||
|
let authorization = Authorization {
|
||||||
|
username: username.to_owned(),
|
||||||
|
scope: AuthorizationScope::PolarisAuth,
|
||||||
|
};
|
||||||
|
self.generate_auth_token(&authorization)
|
||||||
|
} else {
|
||||||
|
Err(Error::IncorrectPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err(Error::Unspecified),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn authenticate(
|
||||||
|
&self,
|
||||||
|
auth_token: &AuthToken,
|
||||||
|
scope: AuthorizationScope,
|
||||||
|
) -> Result<Authorization, Error> {
|
||||||
|
let authorization = self.decode_auth_token(auth_token, scope)?;
|
||||||
|
if self.exists(&authorization.username)? {
|
||||||
|
Ok(authorization)
|
||||||
|
} else {
|
||||||
|
Err(Error::IncorrectUsername)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_auth_token(
|
||||||
|
&self,
|
||||||
|
auth_token: &AuthToken,
|
||||||
|
scope: AuthorizationScope,
|
||||||
|
) -> Result<Authorization, Error> {
|
||||||
|
let AuthToken(data) = auth_token;
|
||||||
|
let ttl = match scope {
|
||||||
|
AuthorizationScope::PolarisAuth => 0, // permanent
|
||||||
|
AuthorizationScope::LastFMLink => 10 * 60, // 10 minutes
|
||||||
|
};
|
||||||
|
let authorization = branca::decode(data, &self.auth_secret.key, ttl)
|
||||||
|
.map_err(|_| Error::InvalidAuthToken)?;
|
||||||
|
let authorization: Authorization =
|
||||||
|
serde_json::from_slice(&authorization[..]).map_err(|_| Error::InvalidAuthToken)?;
|
||||||
|
if authorization.scope != scope {
|
||||||
|
return Err(Error::IncorrectAuthorizationScope);
|
||||||
|
}
|
||||||
|
Ok(authorization)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_auth_token(&self, authorization: &Authorization) -> Result<AuthToken, Error> {
|
||||||
|
let serialized_authorization =
|
||||||
|
serde_json::to_string(&authorization).map_err(|_| Error::Unspecified)?;
|
||||||
|
branca::encode(
|
||||||
|
serialized_authorization.as_bytes(),
|
||||||
|
&self.auth_secret.key,
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map_err(|_| Error::Unspecified)?
|
||||||
|
.as_secs() as u32,
|
||||||
|
)
|
||||||
|
.map_err(|_| Error::Unspecified)
|
||||||
|
.map(AuthToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(&self) -> anyhow::Result<i64> {
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
let count = users.count().get_result(&mut connection)?;
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self) -> Result<Vec<User>, Error> {
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
users
|
||||||
|
.select((name, password_hash, admin))
|
||||||
|
.get_results(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(&self, username: &str) -> Result<bool, Error> {
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
let results: Vec<String> = users
|
||||||
|
.select(name)
|
||||||
|
.filter(name.eq(username))
|
||||||
|
.get_results(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
Ok(!results.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_admin(&self, username: &str) -> Result<bool, Error> {
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
let is_admin: i32 = users
|
||||||
|
.filter(name.eq(username))
|
||||||
|
.select(admin)
|
||||||
|
.get_result(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
Ok(is_admin != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lastfm_link(
|
||||||
|
&self,
|
||||||
|
username: &str,
|
||||||
|
lastfm_login: &str,
|
||||||
|
session_key: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
diesel::update(users.filter(name.eq(username)))
|
||||||
|
.set((
|
||||||
|
lastfm_username.eq(lastfm_login),
|
||||||
|
lastfm_session_key.eq(session_key),
|
||||||
|
))
|
||||||
|
.execute(&mut connection)
|
||||||
|
.map_err(|_| Error::Unspecified)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_lastfm_link_token(&self, username: &str) -> Result<AuthToken, Error> {
|
||||||
|
self.generate_auth_token(&Authorization {
|
||||||
|
username: username.to_owned(),
|
||||||
|
scope: AuthorizationScope::LastFMLink,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lastfm_session_key(&self, username: &str) -> anyhow::Result<String> {
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
let token = users
|
||||||
|
.filter(name.eq(username))
|
||||||
|
.select(lastfm_session_key)
|
||||||
|
.get_result(&mut connection)?;
|
||||||
|
match token {
|
||||||
|
Some(t) => Ok(t),
|
||||||
|
_ => Err(anyhow!("Missing LastFM credentials")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_lastfm_linked(&self, username: &str) -> bool {
|
||||||
|
self.get_lastfm_session_key(username).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lastfm_unlink(&self, username: &str) -> anyhow::Result<()> {
|
||||||
|
use crate::db::users::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
let null: Option<String> = None;
|
||||||
|
diesel::update(users.filter(name.eq(username)))
|
||||||
|
.set((lastfm_session_key.eq(&null), lastfm_username.eq(&null)))
|
||||||
|
.execute(&mut connection)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_password(password: &str) -> Result<String, Error> {
|
||||||
|
if password.is_empty() {
|
||||||
|
return Err(Error::EmptyPassword);
|
||||||
|
}
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
match Pbkdf2.hash_password(password.as_bytes(), &salt) {
|
||||||
|
Ok(h) => Ok(h.to_string()),
|
||||||
|
Err(_) => Err(Error::Unspecified),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_password(password_hash: &str, attempted_password: &str) -> bool {
|
||||||
|
match PasswordHash::new(password_hash) {
|
||||||
|
Ok(h) => Pbkdf2
|
||||||
|
.verify_password(attempted_password.as_bytes(), &h)
|
||||||
|
.is_ok(),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,256 +0,0 @@
|
||||||
use anyhow::anyhow;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use pbkdf2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
|
|
||||||
use pbkdf2::Pbkdf2;
|
|
||||||
use rand::rngs::OsRng;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::app::settings::AuthSecret;
|
|
||||||
use crate::db::DB;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Manager {
|
|
||||||
// TODO make this private and move preferences methods in this file
|
|
||||||
pub db: DB,
|
|
||||||
auth_secret: AuthSecret,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Manager {
|
|
||||||
pub fn new(db: DB, auth_secret: AuthSecret) -> Self {
|
|
||||||
Self { db, auth_secret }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(&self, new_user: &NewUser) -> Result<(), Error> {
|
|
||||||
if new_user.name.is_empty() {
|
|
||||||
return Err(Error::EmptyUsername);
|
|
||||||
}
|
|
||||||
|
|
||||||
let password_hash = hash_password(&new_user.password)?;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
let new_user = User {
|
|
||||||
name: new_user.name.to_owned(),
|
|
||||||
password_hash,
|
|
||||||
admin: new_user.admin as i32,
|
|
||||||
};
|
|
||||||
|
|
||||||
diesel::insert_into(users::table)
|
|
||||||
.values(&new_user)
|
|
||||||
.execute(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete(&self, username: &str) -> Result<(), Error> {
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
diesel::delete(users.filter(name.eq(username)))
|
|
||||||
.execute(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_password(&self, username: &str, password: &str) -> Result<(), Error> {
|
|
||||||
let hash = hash_password(password)?;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
diesel::update(users.filter(name.eq(username)))
|
|
||||||
.set(password_hash.eq(hash))
|
|
||||||
.execute(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_is_admin(&self, username: &str, is_admin: bool) -> Result<(), Error> {
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
diesel::update(users.filter(name.eq(username)))
|
|
||||||
.set(admin.eq(is_admin as i32))
|
|
||||||
.execute(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn login(&self, username: &str, password: &str) -> Result<AuthToken, Error> {
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
match users
|
|
||||||
.select(password_hash)
|
|
||||||
.filter(name.eq(username))
|
|
||||||
.get_result(&mut connection)
|
|
||||||
{
|
|
||||||
Err(diesel::result::Error::NotFound) => Err(Error::IncorrectUsername),
|
|
||||||
Ok(hash) => {
|
|
||||||
let hash: String = hash;
|
|
||||||
if verify_password(&hash, password) {
|
|
||||||
let authorization = Authorization {
|
|
||||||
username: username.to_owned(),
|
|
||||||
scope: AuthorizationScope::PolarisAuth,
|
|
||||||
};
|
|
||||||
self.generate_auth_token(&authorization)
|
|
||||||
} else {
|
|
||||||
Err(Error::IncorrectPassword)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => Err(Error::Unspecified),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn authenticate(
|
|
||||||
&self,
|
|
||||||
auth_token: &AuthToken,
|
|
||||||
scope: AuthorizationScope,
|
|
||||||
) -> Result<Authorization, Error> {
|
|
||||||
let authorization = self.decode_auth_token(auth_token, scope)?;
|
|
||||||
if self.exists(&authorization.username)? {
|
|
||||||
Ok(authorization)
|
|
||||||
} else {
|
|
||||||
Err(Error::IncorrectUsername)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode_auth_token(
|
|
||||||
&self,
|
|
||||||
auth_token: &AuthToken,
|
|
||||||
scope: AuthorizationScope,
|
|
||||||
) -> Result<Authorization, Error> {
|
|
||||||
let AuthToken(data) = auth_token;
|
|
||||||
let ttl = match scope {
|
|
||||||
AuthorizationScope::PolarisAuth => 0, // permanent
|
|
||||||
AuthorizationScope::LastFMLink => 10 * 60, // 10 minutes
|
|
||||||
};
|
|
||||||
let authorization = branca::decode(data, &self.auth_secret.key, ttl)
|
|
||||||
.map_err(|_| Error::InvalidAuthToken)?;
|
|
||||||
let authorization: Authorization =
|
|
||||||
serde_json::from_slice(&authorization[..]).map_err(|_| Error::InvalidAuthToken)?;
|
|
||||||
if authorization.scope != scope {
|
|
||||||
return Err(Error::IncorrectAuthorizationScope);
|
|
||||||
}
|
|
||||||
Ok(authorization)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_auth_token(&self, authorization: &Authorization) -> Result<AuthToken, Error> {
|
|
||||||
let serialized_authorization =
|
|
||||||
serde_json::to_string(&authorization).map_err(|_| Error::Unspecified)?;
|
|
||||||
branca::encode(
|
|
||||||
serialized_authorization.as_bytes(),
|
|
||||||
&self.auth_secret.key,
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map_err(|_| Error::Unspecified)?
|
|
||||||
.as_secs() as u32,
|
|
||||||
)
|
|
||||||
.map_err(|_| Error::Unspecified)
|
|
||||||
.map(AuthToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn count(&self) -> anyhow::Result<i64> {
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
let count = users.count().get_result(&mut connection)?;
|
|
||||||
Ok(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list(&self) -> Result<Vec<User>, Error> {
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
users
|
|
||||||
.select((name, password_hash, admin))
|
|
||||||
.get_results(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exists(&self, username: &str) -> Result<bool, Error> {
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
let results: Vec<String> = users
|
|
||||||
.select(name)
|
|
||||||
.filter(name.eq(username))
|
|
||||||
.get_results(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
Ok(!results.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_admin(&self, username: &str) -> Result<bool, Error> {
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
let is_admin: i32 = users
|
|
||||||
.filter(name.eq(username))
|
|
||||||
.select(admin)
|
|
||||||
.get_result(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
Ok(is_admin != 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lastfm_link(
|
|
||||||
&self,
|
|
||||||
username: &str,
|
|
||||||
lastfm_login: &str,
|
|
||||||
session_key: &str,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
diesel::update(users.filter(name.eq(username)))
|
|
||||||
.set((
|
|
||||||
lastfm_username.eq(lastfm_login),
|
|
||||||
lastfm_session_key.eq(session_key),
|
|
||||||
))
|
|
||||||
.execute(&mut connection)
|
|
||||||
.map_err(|_| Error::Unspecified)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_lastfm_link_token(&self, username: &str) -> Result<AuthToken, Error> {
|
|
||||||
self.generate_auth_token(&Authorization {
|
|
||||||
username: username.to_owned(),
|
|
||||||
scope: AuthorizationScope::LastFMLink,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_lastfm_session_key(&self, username: &str) -> anyhow::Result<String> {
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
let token = users
|
|
||||||
.filter(name.eq(username))
|
|
||||||
.select(lastfm_session_key)
|
|
||||||
.get_result(&mut connection)?;
|
|
||||||
match token {
|
|
||||||
Some(t) => Ok(t),
|
|
||||||
_ => Err(anyhow!("Missing LastFM credentials")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_lastfm_linked(&self, username: &str) -> bool {
|
|
||||||
self.get_lastfm_session_key(username).is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lastfm_unlink(&self, username: &str) -> anyhow::Result<()> {
|
|
||||||
use crate::db::users::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
let null: Option<String> = None;
|
|
||||||
diesel::update(users.filter(name.eq(username)))
|
|
||||||
.set((lastfm_session_key.eq(&null), lastfm_username.eq(&null)))
|
|
||||||
.execute(&mut connection)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hash_password(password: &str) -> Result<String, Error> {
|
|
||||||
if password.is_empty() {
|
|
||||||
return Err(Error::EmptyPassword);
|
|
||||||
}
|
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
|
||||||
match Pbkdf2.hash_password(password.as_bytes(), &salt) {
|
|
||||||
Ok(h) => Ok(h.to_string()),
|
|
||||||
Err(_) => Err(Error::Unspecified),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_password(password_hash: &str, attempted_password: &str) -> bool {
|
|
||||||
match PasswordHash::new(password_hash) {
|
|
||||||
Ok(h) => Pbkdf2
|
|
||||||
.verify_password(attempted_password.as_bytes(), &h)
|
|
||||||
.is_ok(),
|
|
||||||
Err(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +1,15 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
|
use diesel::prelude::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::{self, Path, PathBuf};
|
use std::path::{self, Path, PathBuf};
|
||||||
|
|
||||||
use crate::db::mount_points;
|
use crate::db::{mount_points, DB};
|
||||||
|
|
||||||
mod manager;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
pub use manager::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)]
|
||||||
#[diesel(table_name = mount_points)]
|
#[diesel(table_name = mount_points)]
|
||||||
pub struct MountDir {
|
pub struct MountDir {
|
||||||
|
@ -81,3 +79,39 @@ impl VFS {
|
||||||
&self.mounts
|
&self.mounts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Manager {
|
||||||
|
db: DB,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn new(db: DB) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_vfs(&self) -> Result<VFS> {
|
||||||
|
let mount_dirs = self.mount_dirs()?;
|
||||||
|
let mounts = mount_dirs.into_iter().map(|p| p.into()).collect();
|
||||||
|
Ok(VFS::new(mounts))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mount_dirs(&self) -> Result<Vec<MountDir>> {
|
||||||
|
use self::mount_points::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
let mount_dirs: Vec<MountDir> = mount_points
|
||||||
|
.select((source, name))
|
||||||
|
.get_results(&mut connection)?;
|
||||||
|
Ok(mount_dirs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mount_dirs(&self, mount_dirs: &[MountDir]) -> Result<()> {
|
||||||
|
use self::mount_points::dsl::*;
|
||||||
|
let mut connection = self.db.connect()?;
|
||||||
|
diesel::delete(mount_points).execute(&mut connection)?;
|
||||||
|
diesel::insert_into(mount_points)
|
||||||
|
.values(mount_dirs)
|
||||||
|
.execute(&mut *connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
use anyhow::*;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::db::mount_points;
|
|
||||||
use crate::db::DB;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Manager {
|
|
||||||
db: DB,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Manager {
|
|
||||||
pub fn new(db: DB) -> Self {
|
|
||||||
Self { db }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_vfs(&self) -> Result<VFS> {
|
|
||||||
let mount_dirs = self.mount_dirs()?;
|
|
||||||
let mounts = mount_dirs.into_iter().map(|p| p.into()).collect();
|
|
||||||
Ok(VFS::new(mounts))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mount_dirs(&self) -> Result<Vec<MountDir>> {
|
|
||||||
use self::mount_points::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
let mount_dirs: Vec<MountDir> = mount_points
|
|
||||||
.select((source, name))
|
|
||||||
.get_results(&mut connection)?;
|
|
||||||
Ok(mount_dirs)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_mount_dirs(&self, mount_dirs: &[MountDir]) -> Result<()> {
|
|
||||||
use self::mount_points::dsl::*;
|
|
||||||
let mut connection = self.db.connect()?;
|
|
||||||
diesel::delete(mount_points).execute(&mut connection)?;
|
|
||||||
diesel::insert_into(mount_points)
|
|
||||||
.values(mount_dirs)
|
|
||||||
.execute(&mut *connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue