This commit is contained in:
Antoine Gersant 2018-10-06 15:46:30 -07:00
parent 42140d3137
commit 0297b351bf
17 changed files with 592 additions and 530 deletions

View file

@ -1,39 +1,38 @@
use diesel::prelude::*; use diesel::prelude::*;
use iron::prelude::*;
use iron::headers::{Authorization, Basic, Range}; use iron::headers::{Authorization, Basic, Range};
use iron::{AroundMiddleware, Handler, status}; use iron::prelude::*;
use iron::{status, AroundMiddleware, Handler};
use mount::Mount; use mount::Mount;
use router::Router;
use params; use params;
use secure_session::middleware::{SessionMiddleware, SessionConfig}; use router::Router;
use secure_session::session::{SessionManager, ChaCha20Poly1305SessionManager}; use secure_session::middleware::{SessionConfig, SessionMiddleware};
use secure_session::session::{ChaCha20Poly1305SessionManager, SessionManager};
use serde_json; use serde_json;
use std::fs; use std::fs;
use std::io; use std::io;
use std::path::*;
use std::ops::Deref; use std::ops::Deref;
use std::sync::{Arc, Mutex}; use std::path::*;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
use typemap; use typemap;
use url::percent_encoding::percent_decode; use url::percent_encoding::percent_decode;
use config; use config;
use db::{ConnectionSource, DB};
use db::misc_settings; use db::misc_settings;
use db::{ConnectionSource, DB};
use errors::*; use errors::*;
use index; use index;
use lastfm; use lastfm;
use playlist; use playlist;
use user;
use serve; use serve;
use thumbnails::*; use thumbnails::*;
use user;
use utils::*; use utils::*;
use vfs::VFSSource; use vfs::VFSSource;
const CURRENT_MAJOR_VERSION: i32 = 2; const CURRENT_MAJOR_VERSION: i32 = 2;
const CURRENT_MINOR_VERSION: i32 = 2; const CURRENT_MINOR_VERSION: i32 = 2;
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
struct Session { struct Session {
username: String, username: String,
@ -46,7 +45,8 @@ impl typemap::Key for SessionKey {
} }
fn get_auth_secret<T>(db: &T) -> Result<String> fn get_auth_secret<T>(db: &T) -> Result<String>
where T: ConnectionSource where
T: ConnectionSource,
{ {
use self::misc_settings::dsl::*; use self::misc_settings::dsl::*;
let connection = db.get_connection(); let connection = db.get_connection();
@ -62,11 +62,11 @@ pub fn get_handler(db: Arc<DB>, index: Arc<Mutex<Sender<index::Command>>>) -> Re
let session_manager = let session_manager =
ChaCha20Poly1305SessionManager::<Session>::from_password(auth_secret.as_bytes()); ChaCha20Poly1305SessionManager::<Session>::from_password(auth_secret.as_bytes());
let session_config = SessionConfig::default(); let session_config = SessionConfig::default();
let session_middleware = let session_middleware = SessionMiddleware::<
SessionMiddleware::<Session, Session,
SessionKey, SessionKey,
ChaCha20Poly1305SessionManager<Session>>::new(session_manager, ChaCha20Poly1305SessionManager<Session>,
session_config); >::new(session_manager, session_config);
api_chain.link_around(session_middleware); api_chain.link_around(session_middleware);
Ok(api_chain) Ok(api_chain)
@ -79,8 +79,9 @@ fn get_endpoints(db: Arc<DB>, index_channel: Arc<Mutex<Sender<index::Command>>>)
api_handler.mount("/version/", self::version); api_handler.mount("/version/", self::version);
{ {
let db = db.clone(); let db = db.clone();
api_handler.mount("/auth/", api_handler.mount("/auth/", move |request: &mut Request| {
move |request: &mut Request| self::auth(request, db.deref())); self::auth(request, db.deref())
});
} }
{ {
let db = db.clone(); let db = db.clone();
@ -94,64 +95,70 @@ fn get_endpoints(db: Arc<DB>, index_channel: Arc<Mutex<Sender<index::Command>>>)
let mut auth_api_mount = Mount::new(); let mut auth_api_mount = Mount::new();
{ {
let db = db.clone(); let db = db.clone();
auth_api_mount.mount("/browse/", auth_api_mount.mount("/browse/", move |request: &mut Request| {
move |request: &mut Request| self::browse(request, db.deref())); self::browse(request, db.deref())
});
} }
{ {
let db = db.clone(); let db = db.clone();
auth_api_mount.mount("/flatten/", auth_api_mount.mount("/flatten/", move |request: &mut Request| {
move |request: &mut Request| self::flatten(request, db.deref())); self::flatten(request, db.deref())
});
} }
{ {
let db = db.clone(); let db = db.clone();
auth_api_mount.mount("/random/", auth_api_mount.mount("/random/", move |request: &mut Request| {
move |request: &mut Request| self::random(request, db.deref())); self::random(request, db.deref())
});
} }
{ {
let db = db.clone(); let db = db.clone();
auth_api_mount.mount("/recent/", auth_api_mount.mount("/recent/", move |request: &mut Request| {
move |request: &mut Request| self::recent(request, db.deref())); self::recent(request, db.deref())
});
} }
{ {
let db = db.clone(); let db = db.clone();
auth_api_mount.mount("/search/", auth_api_mount.mount("/search/", move |request: &mut Request| {
move |request: &mut Request| self::search(request, db.deref())); self::search(request, db.deref())
});
} }
{ {
let db = db.clone(); let db = db.clone();
auth_api_mount.mount("/serve/", auth_api_mount.mount("/serve/", move |request: &mut Request| {
move |request: &mut Request| self::serve(request, db.deref())); self::serve(request, db.deref())
});
} }
{ {
let mut preferences_router = Router::new(); let mut preferences_router = Router::new();
let get_db = db.clone(); let get_db = db.clone();
let put_db = db.clone(); let put_db = db.clone();
preferences_router.get("/", preferences_router.get(
move |request: &mut Request| { "/",
self::get_preferences(request, get_db.deref()) move |request: &mut Request| self::get_preferences(request, get_db.deref()),
}, "get_preferences",
"get_preferences"); );
preferences_router.put("/", preferences_router.put(
move |request: &mut Request| { "/",
self::put_preferences(request, put_db.deref()) move |request: &mut Request| self::put_preferences(request, put_db.deref()),
}, "put_preferences",
"put_preferences"); );
auth_api_mount.mount("/preferences/", preferences_router); auth_api_mount.mount("/preferences/", preferences_router);
} }
{ {
let mut settings_router = Router::new(); let mut settings_router = Router::new();
let get_db = db.clone(); let get_db = db.clone();
let put_db = db.clone(); let put_db = db.clone();
settings_router.get("/", settings_router.get(
move |request: &mut Request| { "/",
self::get_config(request, get_db.deref()) move |request: &mut Request| self::get_config(request, get_db.deref()),
}, "get_config",
"get_config"); );
settings_router.put("/", settings_router.put(
move |request: &mut Request| { "/",
self::put_config(request, put_db.deref()) move |request: &mut Request| self::put_config(request, put_db.deref()),
}, "put_config",
"put_config"); );
let mut settings_api_chain = Chain::new(settings_router); let mut settings_api_chain = Chain::new(settings_router);
let admin_req = AdminRequirement { db: db.clone() }; let admin_req = AdminRequirement { db: db.clone() };
@ -162,9 +169,11 @@ fn get_endpoints(db: Arc<DB>, index_channel: Arc<Mutex<Sender<index::Command>>>)
{ {
let index_channel = index_channel.clone(); let index_channel = index_channel.clone();
let mut reindex_router = Router::new(); let mut reindex_router = Router::new();
reindex_router.post("/", reindex_router.post(
move |_: &mut Request| self::trigger_index(index_channel.deref()), "/",
"trigger_index"); move |_: &mut Request| self::trigger_index(index_channel.deref()),
"trigger_index",
);
let mut reindex_api_chain = Chain::new(reindex_router); let mut reindex_api_chain = Chain::new(reindex_router);
let admin_req = AdminRequirement { db: db.clone() }; let admin_req = AdminRequirement { db: db.clone() };
@ -178,29 +187,29 @@ fn get_endpoints(db: Arc<DB>, index_channel: Arc<Mutex<Sender<index::Command>>>)
let list_db = db.clone(); let list_db = db.clone();
let read_db = db.clone(); let read_db = db.clone();
let delete_db = db.clone(); let delete_db = db.clone();
playlist_router.put("/", playlist_router.put(
move |request: &mut Request| { "/",
self::save_playlist(request, put_db.deref()) move |request: &mut Request| self::save_playlist(request, put_db.deref()),
}, "save_playlist",
"save_playlist"); );
playlist_router.get("/list", playlist_router.get(
move |request: &mut Request| { "/list",
self::list_playlists(request, list_db.deref()) move |request: &mut Request| self::list_playlists(request, list_db.deref()),
}, "list_playlists",
"list_playlists"); );
playlist_router.get("/read/:playlist_name", playlist_router.get(
move |request: &mut Request| { "/read/:playlist_name",
self::read_playlist(request, read_db.deref()) move |request: &mut Request| self::read_playlist(request, read_db.deref()),
}, "read_playlist",
"read_playlist"); );
playlist_router.delete("/:playlist_name", playlist_router.delete(
move |request: &mut Request| { "/:playlist_name",
self::delete_playlist(request, delete_db.deref()) move |request: &mut Request| self::delete_playlist(request, delete_db.deref()),
}, "delete_playlist",
"delete_playlist"); );
auth_api_mount.mount("/playlist/", playlist_router); auth_api_mount.mount("/playlist/", playlist_router);
} }
@ -249,9 +258,9 @@ struct AuthRequirement {
impl AroundMiddleware for AuthRequirement { impl AroundMiddleware for AuthRequirement {
fn around(self, handler: Box<Handler>) -> Box<Handler> { fn around(self, handler: Box<Handler>) -> Box<Handler> {
Box::new(AuthHandler { Box::new(AuthHandler {
db: self.db, db: self.db,
handler: handler, handler: handler,
}) as Box<Handler> }) as Box<Handler>
} }
} }
@ -277,8 +286,9 @@ impl Handler for AuthHandler {
auth_success = auth_success =
user::auth(self.db.deref(), auth.username.as_str(), password.as_str())?; user::auth(self.db.deref(), auth.username.as_str(), password.as_str())?;
if auth_success { if auth_success {
req.extensions req.extensions.insert::<SessionKey>(Session {
.insert::<SessionKey>(Session { username: auth.username.clone() }); username: auth.username.clone(),
});
} }
} }
} }
@ -299,7 +309,6 @@ impl Handler for AuthHandler {
} }
} }
struct AdminRequirement { struct AdminRequirement {
db: Arc<DB>, db: Arc<DB>,
} }
@ -307,9 +316,9 @@ struct AdminRequirement {
impl AroundMiddleware for AdminRequirement { impl AroundMiddleware for AdminRequirement {
fn around(self, handler: Box<Handler>) -> Box<Handler> { fn around(self, handler: Box<Handler>) -> Box<Handler> {
Box::new(AdminHandler { Box::new(AdminHandler {
db: self.db, db: self.db,
handler: handler, handler: handler,
}) as Box<Handler> }) as Box<Handler>
} }
} }
@ -368,7 +377,9 @@ fn initial_setup(_: &mut Request, db: &DB) -> IronResult<Response> {
has_any_users: bool, has_any_users: bool,
}; };
let initial_setup = InitialSetup { has_any_users: user::count(db)? > 0 }; let initial_setup = InitialSetup {
has_any_users: user::count(db)? > 0,
};
match serde_json::to_string(&initial_setup) { match serde_json::to_string(&initial_setup) {
Ok(result_json) => Ok(Response::with((status::Ok, result_json))), Ok(result_json) => Ok(Response::with((status::Ok, result_json))),
@ -395,16 +406,18 @@ fn auth(request: &mut Request, db: &DB) -> IronResult<Response> {
return Err(Error::from(ErrorKind::IncorrectCredentials).into()); return Err(Error::from(ErrorKind::IncorrectCredentials).into());
} }
request request.extensions.insert::<SessionKey>(Session {
.extensions username: username.clone(),
.insert::<SessionKey>(Session { username: username.clone() }); });
#[derive(Serialize)] #[derive(Serialize)]
struct AuthOutput { struct AuthOutput {
admin: bool, admin: bool,
} }
let auth_output = AuthOutput { admin: user::is_admin(db.deref(), &username)? }; let auth_output = AuthOutput {
admin: user::is_admin(db.deref(), &username)?,
};
let result_json = serde_json::to_string(&auth_output); let result_json = serde_json::to_string(&auth_output);
let result_json = match result_json { let result_json = match result_json {
Ok(j) => j, Ok(j) => j,
@ -602,7 +615,6 @@ fn trigger_index(channel: &Mutex<Sender<index::Command>>) -> IronResult<Response
} }
fn save_playlist(request: &mut Request, db: &DB) -> IronResult<Response> { fn save_playlist(request: &mut Request, db: &DB) -> IronResult<Response> {
let username = match request.extensions.get::<SessionKey>() { let username = match request.extensions.get::<SessionKey>() {
Some(s) => s.username.clone(), Some(s) => s.username.clone(),
None => return Err(Error::from(ErrorKind::AuthenticationRequired).into()), None => return Err(Error::from(ErrorKind::AuthenticationRequired).into()),

View file

@ -8,8 +8,8 @@ use std::io::Read;
use std::path; use std::path;
use toml; use toml;
use db::DB;
use db::ConnectionSource; use db::ConnectionSource;
use db::DB;
use db::{ddns_config, misc_settings, mount_points, users}; use db::{ddns_config, misc_settings, mount_points, users};
use ddns::DDNSConfig; use ddns::DDNSConfig;
use errors::*; use errors::*;
@ -76,10 +76,11 @@ pub fn parse_toml_file(path: &path::Path) -> Result<Config> {
} }
pub fn read<T>(db: &T) -> Result<Config> pub fn read<T>(db: &T) -> Result<Config>
where T: ConnectionSource where
T: ConnectionSource,
{ {
use self::misc_settings::dsl::*;
use self::ddns_config::dsl::*; use self::ddns_config::dsl::*;
use self::misc_settings::dsl::*;
let connection = db.get_connection(); let connection = db.get_connection();
@ -93,8 +94,11 @@ pub fn read<T>(db: &T) -> Result<Config>
}; };
let (art_pattern, sleep_duration, url) = misc_settings let (art_pattern, sleep_duration, url) = misc_settings
.select((index_album_art_pattern, index_sleep_duration_seconds, prefix_url)) .select((
.get_result(connection.deref())?; index_album_art_pattern,
index_sleep_duration_seconds,
prefix_url,
)).get_result(connection.deref())?;
config.album_art_pattern = Some(art_pattern); config.album_art_pattern = Some(art_pattern);
config.reindex_every_n_seconds = Some(sleep_duration); config.reindex_every_n_seconds = Some(sleep_duration);
config.prefix_url = if url != "" { Some(url) } else { None }; config.prefix_url = if url != "" { Some(url) } else { None };
@ -111,16 +115,15 @@ pub fn read<T>(db: &T) -> Result<Config>
let found_users: Vec<(String, i32)> = users::table let found_users: Vec<(String, i32)> = users::table
.select((users::columns::name, users::columns::admin)) .select((users::columns::name, users::columns::admin))
.get_results(connection.deref())?; .get_results(connection.deref())?;
config.users = Some(found_users config.users = Some(
.into_iter() found_users
.map(|(name, admin)| { .into_iter()
ConfigUser { .map(|(name, admin)| ConfigUser {
name: name, name: name,
password: "".to_owned(), password: "".to_owned(),
admin: admin != 0, admin: admin != 0,
} }).collect::<_>(),
}) );
.collect::<_>());
let ydns = ddns_config let ydns = ddns_config
.select((host, username, password)) .select((host, username, password))
@ -131,13 +134,13 @@ pub fn read<T>(db: &T) -> Result<Config>
} }
fn reset<T>(db: &T) -> Result<()> fn reset<T>(db: &T) -> Result<()>
where T: ConnectionSource where
T: ConnectionSource,
{ {
use self::ddns_config::dsl::*; use self::ddns_config::dsl::*;
let connection = db.get_connection(); let connection = db.get_connection();
diesel::delete(mount_points::table) diesel::delete(mount_points::table).execute(connection.deref())?;
.execute(connection.deref())?;
diesel::delete(users::table).execute(connection.deref())?; diesel::delete(users::table).execute(connection.deref())?;
diesel::update(ddns_config) diesel::update(ddns_config)
.set((host.eq(""), username.eq(""), password.eq(""))) .set((host.eq(""), username.eq(""), password.eq("")))
@ -147,20 +150,21 @@ fn reset<T>(db: &T) -> Result<()>
} }
pub fn overwrite<T>(db: &T, new_config: &Config) -> Result<()> pub fn overwrite<T>(db: &T, new_config: &Config) -> Result<()>
where T: ConnectionSource where
T: ConnectionSource,
{ {
reset(db)?; reset(db)?;
amend(db, new_config) amend(db, new_config)
} }
pub fn amend<T>(db: &T, new_config: &Config) -> Result<()> pub fn amend<T>(db: &T, new_config: &Config) -> Result<()>
where T: ConnectionSource where
T: ConnectionSource,
{ {
let connection = db.get_connection(); let connection = db.get_connection();
if let Some(ref mount_dirs) = new_config.mount_dirs { if let Some(ref mount_dirs) = new_config.mount_dirs {
diesel::delete(mount_points::table) diesel::delete(mount_points::table).execute(connection.deref())?;
.execute(connection.deref())?;
diesel::insert_into(mount_points::table) diesel::insert_into(mount_points::table)
.values(mount_dirs) .values(mount_dirs)
.execute(connection.deref())?; .execute(connection.deref())?;
@ -175,12 +179,7 @@ pub fn amend<T>(db: &T, new_config: &Config) -> Result<()>
let delete_usernames: Vec<String> = old_usernames let delete_usernames: Vec<String> = old_usernames
.iter() .iter()
.cloned() .cloned()
.filter(|old_name| { .filter(|old_name| config_users.iter().find(|u| &u.name == old_name).is_none())
config_users
.iter()
.find(|u| &u.name == old_name)
.is_none()
})
.collect::<_>(); .collect::<_>();
diesel::delete(users::table.filter(users::name.eq_any(&delete_usernames))) diesel::delete(users::table.filter(users::name.eq_any(&delete_usernames)))
.execute(connection.deref())?; .execute(connection.deref())?;
@ -189,12 +188,11 @@ pub fn amend<T>(db: &T, new_config: &Config) -> Result<()>
let insert_users: Vec<&ConfigUser> = config_users let insert_users: Vec<&ConfigUser> = config_users
.iter() .iter()
.filter(|u| { .filter(|u| {
old_usernames old_usernames
.iter() .iter()
.find(|old_name| *old_name == &u.name) .find(|old_name| *old_name == &u.name)
.is_none() .is_none()
}) }).collect::<_>();
.collect::<_>();
for ref config_user in insert_users { for ref config_user in insert_users {
let new_user = User::new(&config_user.name, &config_user.password); let new_user = User::new(&config_user.name, &config_user.password);
diesel::insert_into(users::table) diesel::insert_into(users::table)
@ -238,10 +236,11 @@ pub fn amend<T>(db: &T, new_config: &Config) -> Result<()>
if let Some(ref ydns) = new_config.ydns { if let Some(ref ydns) = new_config.ydns {
use self::ddns_config::dsl::*; use self::ddns_config::dsl::*;
diesel::update(ddns_config) diesel::update(ddns_config)
.set((host.eq(ydns.host.clone()), .set((
username.eq(ydns.username.clone()), host.eq(ydns.host.clone()),
password.eq(ydns.password.clone()))) username.eq(ydns.username.clone()),
.execute(connection.deref())?; password.eq(ydns.password.clone()),
)).execute(connection.deref())?;
} }
if let Some(ref prefix_url) = new_config.prefix_url { if let Some(ref prefix_url) = new_config.prefix_url {
@ -254,13 +253,15 @@ pub fn amend<T>(db: &T, new_config: &Config) -> Result<()>
} }
pub fn read_preferences<T>(_: &T, _: &str) -> Result<Preferences> pub fn read_preferences<T>(_: &T, _: &str) -> Result<Preferences>
where T: ConnectionSource where
T: ConnectionSource,
{ {
Ok(Preferences {}) Ok(Preferences {})
} }
pub fn write_preferences<T>(_: &T, _: &str, _: &Preferences) -> Result<()> pub fn write_preferences<T>(_: &T, _: &str, _: &Preferences) -> Result<()>
where T: ConnectionSource where
T: ConnectionSource,
{ {
Ok(()) Ok(())
} }
@ -294,14 +295,14 @@ fn test_amend() {
reindex_every_n_seconds: Some(123), reindex_every_n_seconds: Some(123),
prefix_url: None, prefix_url: None,
mount_dirs: Some(vec![MountPoint { mount_dirs: Some(vec![MountPoint {
source: "C:\\Music".into(), source: "C:\\Music".into(),
name: "root".into(), name: "root".into(),
}]), }]),
users: Some(vec![ConfigUser { users: Some(vec![ConfigUser {
name: "Teddy🐻".into(), name: "Teddy🐻".into(),
password: "Tasty🍖".into(), password: "Tasty🍖".into(),
admin: false, admin: false,
}]), }]),
ydns: None, ydns: None,
}; };
@ -310,19 +311,19 @@ fn test_amend() {
reindex_every_n_seconds: None, reindex_every_n_seconds: None,
prefix_url: Some("polaris".into()), prefix_url: Some("polaris".into()),
mount_dirs: Some(vec![MountPoint { mount_dirs: Some(vec![MountPoint {
source: "/home/music".into(), source: "/home/music".into(),
name: "🎵📁".into(), name: "🎵📁".into(),
}]), }]),
users: Some(vec![ConfigUser { users: Some(vec![ConfigUser {
name: "Kermit🐸".into(), name: "Kermit🐸".into(),
password: "🐞🐞".into(), password: "🐞🐞".into(),
admin: false, admin: false,
}]), }]),
ydns: Some(DDNSConfig { ydns: Some(DDNSConfig {
host: "🐸🐸🐸.ydns.eu".into(), host: "🐸🐸🐸.ydns.eu".into(),
username: "kfr🐸g".into(), username: "kfr🐸g".into(),
password: "tasty🐞".into(), password: "tasty🐞".into(),
}), }),
}; };
let mut expected_config = new_config.clone(); let mut expected_config = new_config.clone();
@ -351,10 +352,10 @@ fn test_amend_preserve_password_hashes() {
prefix_url: None, prefix_url: None,
mount_dirs: None, mount_dirs: None,
users: Some(vec![ConfigUser { users: Some(vec![ConfigUser {
name: "Teddy🐻".into(), name: "Teddy🐻".into(),
password: "Tasty🍖".into(), password: "Tasty🍖".into(),
admin: false, admin: false,
}]), }]),
ydns: None, ydns: None,
}; };
amend(&db, &initial_config).unwrap(); amend(&db, &initial_config).unwrap();
@ -373,16 +374,18 @@ fn test_amend_preserve_password_hashes() {
reindex_every_n_seconds: None, reindex_every_n_seconds: None,
prefix_url: None, prefix_url: None,
mount_dirs: None, mount_dirs: None,
users: Some(vec![ConfigUser { users: Some(vec![
name: "Kermit🐸".into(), ConfigUser {
password: "tasty🐞".into(), name: "Kermit🐸".into(),
admin: false, password: "tasty🐞".into(),
}, admin: false,
ConfigUser { },
name: "Teddy🐻".into(), ConfigUser {
password: "".into(), name: "Teddy🐻".into(),
admin: false, password: "".into(),
}]), admin: false,
},
]),
ydns: None, ydns: None,
}; };
amend(&db, &new_config).unwrap(); amend(&db, &new_config).unwrap();
@ -411,20 +414,17 @@ fn test_toggle_admin() {
prefix_url: None, prefix_url: None,
mount_dirs: None, mount_dirs: None,
users: Some(vec![ConfigUser { users: Some(vec![ConfigUser {
name: "Teddy🐻".into(), name: "Teddy🐻".into(),
password: "Tasty🍖".into(), password: "Tasty🍖".into(),
admin: true, admin: true,
}]), }]),
ydns: None, ydns: None,
}; };
amend(&db, &initial_config).unwrap(); amend(&db, &initial_config).unwrap();
{ {
let connection = db.get_connection(); let connection = db.get_connection();
let is_admin: i32 = users let is_admin: i32 = users.select(admin).get_result(connection.deref()).unwrap();
.select(admin)
.get_result(connection.deref())
.unwrap();
assert_eq!(is_admin, 1); assert_eq!(is_admin, 1);
} }
@ -434,27 +434,23 @@ fn test_toggle_admin() {
prefix_url: None, prefix_url: None,
mount_dirs: None, mount_dirs: None,
users: Some(vec![ConfigUser { users: Some(vec![ConfigUser {
name: "Teddy🐻".into(), name: "Teddy🐻".into(),
password: "".into(), password: "".into(),
admin: false, admin: false,
}]), }]),
ydns: None, ydns: None,
}; };
amend(&db, &new_config).unwrap(); amend(&db, &new_config).unwrap();
{ {
let connection = db.get_connection(); let connection = db.get_connection();
let is_admin: i32 = users let is_admin: i32 = users.select(admin).get_result(connection.deref()).unwrap();
.select(admin)
.get_result(connection.deref())
.unwrap();
assert_eq!(is_admin, 0); assert_eq!(is_admin, 0);
} }
} }
#[test] #[test]
fn test_preferences_read_write() { fn test_preferences_read_write() {
let db = _get_test_db("preferences_read_write.sqlite"); let db = _get_test_db("preferences_read_write.sqlite");
let initial_config = Config { let initial_config = Config {
@ -463,10 +459,10 @@ fn test_preferences_read_write() {
prefix_url: None, prefix_url: None,
mount_dirs: None, mount_dirs: None,
users: Some(vec![ConfigUser { users: Some(vec![ConfigUser {
name: "Teddy🐻".into(), name: "Teddy🐻".into(),
password: "Tasty🍖".into(), password: "Tasty🍖".into(),
admin: false, admin: false,
}]), }]),
ydns: None, ydns: None,
}; };
amend(&db, &initial_config).unwrap(); amend(&db, &initial_config).unwrap();

View file

@ -1,7 +1,7 @@
use core::ops::Deref; use core::ops::Deref;
use diesel_migrations;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::sqlite::SqliteConnection; use diesel::sqlite::SqliteConnection;
use diesel_migrations;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::{Arc, Mutex, MutexGuard};
@ -28,9 +28,12 @@ pub struct DB {
impl DB { impl DB {
pub fn new(path: &Path) -> Result<DB> { pub fn new(path: &Path) -> Result<DB> {
info!("Database file path: {}", path.to_string_lossy()); info!("Database file path: {}", path.to_string_lossy());
let connection = let connection = Arc::new(Mutex::new(SqliteConnection::establish(
Arc::new(Mutex::new(SqliteConnection::establish(&path.to_string_lossy())?)); &path.to_string_lossy(),
let db = DB { connection: connection.clone() }; )?));
let db = DB {
connection: connection.clone(),
};
db.init()?; db.init()?;
Ok(db) Ok(db)
} }
@ -49,9 +52,14 @@ impl DB {
let connection = self.connection.lock().unwrap(); let connection = self.connection.lock().unwrap();
let connection = connection.deref(); let connection = connection.deref();
loop { loop {
match diesel_migrations::revert_latest_migration_in_directory(connection, Path::new(DB_MIGRATIONS_PATH)) { match diesel_migrations::revert_latest_migration_in_directory(
connection,
Path::new(DB_MIGRATIONS_PATH),
) {
Ok(_) => (), Ok(_) => (),
Err(diesel_migrations::RunMigrationsError::MigrationError(diesel_migrations::MigrationError::NoMigrationRun)) => break, Err(diesel_migrations::RunMigrationsError::MigrationError(
diesel_migrations::MigrationError::NoMigrationRun,
)) => break,
Err(e) => bail!(e), Err(e) => bail!(e),
} }
} }

View file

@ -6,12 +6,12 @@ use std::io;
use std::thread; use std::thread;
use std::time; use std::time;
use db::{ConnectionSource, DB};
use db::ddns_config; use db::ddns_config;
use db::{ConnectionSource, DB};
use errors; use errors;
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Queryable, Serialize)] #[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Queryable, Serialize)]
#[table_name="ddns_config"] #[table_name = "ddns_config"]
pub struct DDNSConfig { pub struct DDNSConfig {
pub host: String, pub host: String,
pub username: String, pub username: String,
@ -27,8 +27,8 @@ impl DDNSConfigSource for DB {
use self::ddns_config::dsl::*; use self::ddns_config::dsl::*;
let connection = self.get_connection(); let connection = self.get_connection();
Ok(ddns_config Ok(ddns_config
.select((host, username, password)) .select((host, username, password))
.get_result(connection.deref())?) .get_result(connection.deref())?)
} }
} }
@ -60,9 +60,9 @@ impl From<reqwest::Error> for DDNSError {
const DDNS_UPDATE_URL: &'static str = "https://ydns.io/api/v1/update/"; const DDNS_UPDATE_URL: &'static str = "https://ydns.io/api/v1/update/";
fn update_my_ip<T>(config_source: &T) -> Result<(), DDNSError> fn update_my_ip<T>(config_source: &T) -> Result<(), DDNSError>
where T: DDNSConfigSource where
T: DDNSConfigSource,
{ {
let config = config_source.get_ddns_config()?; let config = config_source.get_ddns_config()?;
if config.host.len() == 0 || config.username.len() == 0 { if config.host.len() == 0 || config.username.len() == 0 {
@ -72,14 +72,11 @@ fn update_my_ip<T>(config_source: &T) -> Result<(), DDNSError>
let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.host); let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.host);
let auth_header = Authorization(Basic { let auth_header = Authorization(Basic {
username: config.username.clone(), username: config.username.clone(),
password: Some(config.password.to_owned()), password: Some(config.password.to_owned()),
}); });
let client = reqwest::Client::new()?; let client = reqwest::Client::new()?;
let res = client let res = client.get(full_url.as_str()).header(auth_header).send()?;
.get(full_url.as_str())
.header(auth_header)
.send()?;
if !res.status().is_success() { if !res.status().is_success() {
return Err(DDNSError::UpdateError(*res.status())); return Err(DDNSError::UpdateError(*res.status()));
} }
@ -87,7 +84,8 @@ fn update_my_ip<T>(config_source: &T) -> Result<(), DDNSError>
} }
pub fn run<T>(config_source: &T) pub fn run<T>(config_source: &T)
where T: DDNSConfigSource where
T: DDNSConfigSource,
{ {
loop { loop {
if let Err(e) = update_my_ip(config_source) { if let Err(e) = update_my_ip(config_source) {

View file

@ -2,12 +2,12 @@ use ape;
use core; use core;
use diesel; use diesel;
use diesel_migrations; use diesel_migrations;
use id3;
use getopts; use getopts;
use image;
use hyper; use hyper;
use iron::IronError; use id3;
use image;
use iron::status::Status; use iron::status::Status;
use iron::IronError;
use lewton; use lewton;
use metaflac; use metaflac;
use regex; use regex;
@ -17,46 +17,46 @@ use std;
use toml; use toml;
error_chain! { error_chain! {
foreign_links { foreign_links {
Ape(ape::Error); Ape(ape::Error);
Diesel(diesel::result::Error); Diesel(diesel::result::Error);
DieselConnection(diesel::ConnectionError); DieselConnection(diesel::ConnectionError);
DieselMigration(diesel_migrations::RunMigrationsError); DieselMigration(diesel_migrations::RunMigrationsError);
Encoding(core::str::Utf8Error); Encoding(core::str::Utf8Error);
Flac(metaflac::Error); Flac(metaflac::Error);
GetOpts(getopts::Fail); GetOpts(getopts::Fail);
Hyper(hyper::Error); Hyper(hyper::Error);
Id3(id3::Error); Id3(id3::Error);
Image(image::ImageError); Image(image::ImageError);
Io(std::io::Error); Io(std::io::Error);
Json(serde_json::Error); Json(serde_json::Error);
Time(std::time::SystemTimeError); Time(std::time::SystemTimeError);
Toml(toml::de::Error); Toml(toml::de::Error);
Regex(regex::Error); Regex(regex::Error);
Scrobbler(rustfm_scrobble::ScrobblerError); Scrobbler(rustfm_scrobble::ScrobblerError);
Vorbis(lewton::VorbisError); Vorbis(lewton::VorbisError);
} }
errors { errors {
DaemonError {} DaemonError {}
AuthenticationRequired {} AuthenticationRequired {}
AdminPrivilegeRequired {} AdminPrivilegeRequired {}
MissingConfig {} MissingConfig {}
MissingPreferences {} MissingPreferences {}
MissingUsername {} MissingUsername {}
MissingPassword {} MissingPassword {}
MissingPlaylist {} MissingPlaylist {}
IncorrectCredentials {} IncorrectCredentials {}
CannotServeDirectory {} CannotServeDirectory {}
UnsupportedFileType {} UnsupportedFileType {}
FileNotFound {} FileNotFound {}
MissingIndexVersion {} MissingIndexVersion {}
MissingPlaylistName {} MissingPlaylistName {}
EncodingError {} EncodingError {}
MissingLastFMCredentials {} MissingLastFMCredentials {}
LastFMAuthError {} LastFMAuthError {}
LastFMDeserializationError {} LastFMDeserializationError {}
} }
} }
impl From<Error> for IronError { impl From<Error> for IronError {

View file

@ -9,8 +9,8 @@ use std::fs;
use std::path::Path; use std::path::Path;
#[cfg(test)] #[cfg(test)]
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::*; use std::sync::mpsc::*;
use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time; use std::time;
@ -19,16 +19,18 @@ use config::MiscSettings;
use db; use db;
use db::ConnectionSource; use db::ConnectionSource;
use db::{directories, misc_settings, songs}; use db::{directories, misc_settings, songs};
use vfs::{VFS, VFSSource};
use errors; use errors;
use metadata; use metadata;
use vfs::{VFSSource, VFS};
const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // Insertions in each transaction const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // Insertions in each transaction
const INDEX_BUILDING_CLEAN_BUFFER_SIZE: usize = 500; // Insertions in each transaction const INDEX_BUILDING_CLEAN_BUFFER_SIZE: usize = 500; // Insertions in each transaction
no_arg_sql_function!(random, no_arg_sql_function!(
types::Integer, random,
"Represents the SQL RANDOM() function"); types::Integer,
"Represents the SQL RANDOM() function"
);
pub enum Command { pub enum Command {
REINDEX, REINDEX,
@ -74,7 +76,7 @@ pub enum CollectionFile {
} }
#[derive(Debug, Insertable)] #[derive(Debug, Insertable)]
#[table_name="songs"] #[table_name = "songs"]
struct NewSong { struct NewSong {
path: String, path: String,
parent: String, parent: String,
@ -90,7 +92,7 @@ struct NewSong {
} }
#[derive(Debug, Insertable)] #[derive(Debug, Insertable)]
#[table_name="directories"] #[table_name = "directories"]
struct NewDirectory { struct NewDirectory {
path: String, path: String,
parent: Option<String>, parent: Option<String>,
@ -109,31 +111,31 @@ struct IndexBuilder<'conn> {
} }
impl<'conn> IndexBuilder<'conn> { impl<'conn> IndexBuilder<'conn> {
fn new(connection: &Mutex<SqliteConnection>, fn new(
album_art_pattern: Regex) connection: &Mutex<SqliteConnection>,
-> Result<IndexBuilder, errors::Error> { album_art_pattern: Regex,
) -> Result<IndexBuilder, errors::Error> {
let mut new_songs = Vec::new(); let mut new_songs = Vec::new();
let mut new_directories = Vec::new(); let mut new_directories = Vec::new();
new_songs.reserve_exact(INDEX_BUILDING_INSERT_BUFFER_SIZE); new_songs.reserve_exact(INDEX_BUILDING_INSERT_BUFFER_SIZE);
new_directories.reserve_exact(INDEX_BUILDING_INSERT_BUFFER_SIZE); new_directories.reserve_exact(INDEX_BUILDING_INSERT_BUFFER_SIZE);
Ok(IndexBuilder { Ok(IndexBuilder {
new_songs: new_songs, new_songs: new_songs,
new_directories: new_directories, new_directories: new_directories,
connection: connection, connection: connection,
album_art_pattern: album_art_pattern, album_art_pattern: album_art_pattern,
}) })
} }
fn flush_songs(&mut self) -> Result<(), errors::Error> { fn flush_songs(&mut self) -> Result<(), errors::Error> {
let connection = self.connection.lock().unwrap(); let connection = self.connection.lock().unwrap();
let connection = connection.deref(); let connection = connection.deref();
connection connection.transaction::<_, errors::Error, _>(|| {
.transaction::<_, errors::Error, _>(|| { diesel::insert_into(songs::table)
diesel::insert_into(songs::table) .values(&self.new_songs)
.values(&self.new_songs) .execute(connection)?;
.execute(connection)?; Ok(())
Ok(()) })?;
})?;
self.new_songs.clear(); self.new_songs.clear();
Ok(()) Ok(())
} }
@ -141,13 +143,12 @@ impl<'conn> IndexBuilder<'conn> {
fn flush_directories(&mut self) -> Result<(), errors::Error> { fn flush_directories(&mut self) -> Result<(), errors::Error> {
let connection = self.connection.lock().unwrap(); let connection = self.connection.lock().unwrap();
let connection = connection.deref(); let connection = connection.deref();
connection connection.transaction::<_, errors::Error, _>(|| {
.transaction::<_, errors::Error, _>(|| { diesel::insert_into(directories::table)
diesel::insert_into(directories::table) .values(&self.new_directories)
.values(&self.new_directories) .execute(connection)?;
.execute(connection)?; Ok(())
Ok(()) })?;
})?;
self.new_directories.clear(); self.new_directories.clear();
Ok(()) Ok(())
} }
@ -180,11 +181,11 @@ impl<'conn> IndexBuilder<'conn> {
Ok(None) Ok(None)
} }
fn populate_directory(&mut self, fn populate_directory(
parent: Option<&Path>, &mut self,
path: &Path) parent: Option<&Path>,
-> Result<(), errors::Error> { path: &Path,
) -> Result<(), errors::Error> {
// Find artwork // Find artwork
let artwork = self.get_artwork(path).unwrap_or(None); let artwork = self.get_artwork(path).unwrap_or(None);
@ -228,24 +229,24 @@ impl<'conn> IndexBuilder<'conn> {
if let Some(file_path_string) = file_path.to_str() { if let Some(file_path_string) = file_path.to_str() {
if let Ok(tags) = metadata::read(file_path.as_path()) { if let Ok(tags) = metadata::read(file_path.as_path()) {
if tags.year.is_some() { if tags.year.is_some() {
inconsistent_directory_year |= directory_year.is_some() && inconsistent_directory_year |=
directory_year != tags.year; directory_year.is_some() && directory_year != tags.year;
directory_year = tags.year; directory_year = tags.year;
} }
if tags.album.is_some() { if tags.album.is_some() {
inconsistent_directory_album |= directory_album.is_some() && inconsistent_directory_album |=
directory_album != tags.album; directory_album.is_some() && directory_album != tags.album;
directory_album = tags.album.as_ref().map(|a| a.clone()); directory_album = tags.album.as_ref().map(|a| a.clone());
} }
if tags.album_artist.is_some() { if tags.album_artist.is_some() {
inconsistent_directory_artist |= directory_artist.is_some() && inconsistent_directory_artist |=
directory_artist != tags.album_artist; directory_artist.is_some() && directory_artist != tags.album_artist;
directory_artist = tags.album_artist.as_ref().map(|a| a.clone()); directory_artist = tags.album_artist.as_ref().map(|a| a.clone());
} else if tags.artist.is_some() { } else if tags.artist.is_some() {
inconsistent_directory_artist |= directory_artist.is_some() && inconsistent_directory_artist |=
directory_artist != tags.artist; directory_artist.is_some() && directory_artist != tags.artist;
directory_artist = tags.artist.as_ref().map(|a| a.clone()); directory_artist = tags.artist.as_ref().map(|a| a.clone());
} }
@ -300,7 +301,8 @@ impl<'conn> IndexBuilder<'conn> {
} }
fn clean<T>(db: &T) -> Result<(), errors::Error> fn clean<T>(db: &T) -> Result<(), errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
@ -308,18 +310,15 @@ fn clean<T>(db: &T) -> Result<(), errors::Error>
let all_songs: Vec<String>; let all_songs: Vec<String>;
{ {
let connection = db.get_connection(); let connection = db.get_connection();
all_songs = songs::table all_songs = songs::table.select(songs::path).load(connection.deref())?;
.select(songs::path)
.load(connection.deref())?;
} }
let missing_songs = all_songs let missing_songs = all_songs
.into_iter() .into_iter()
.filter(|ref song_path| { .filter(|ref song_path| {
let path = Path::new(&song_path); let path = Path::new(&song_path);
!path.exists() || vfs.real_to_virtual(path).is_err() !path.exists() || vfs.real_to_virtual(path).is_err()
}) }).collect::<Vec<_>>();
.collect::<Vec<_>>();
{ {
let connection = db.get_connection(); let connection = db.get_connection();
@ -327,7 +326,6 @@ fn clean<T>(db: &T) -> Result<(), errors::Error>
diesel::delete(songs::table.filter(songs::path.eq_any(chunk))) diesel::delete(songs::table.filter(songs::path.eq_any(chunk)))
.execute(connection.deref())?; .execute(connection.deref())?;
} }
} }
} }
@ -343,10 +341,9 @@ fn clean<T>(db: &T) -> Result<(), errors::Error>
let missing_directories = all_directories let missing_directories = all_directories
.into_iter() .into_iter()
.filter(|ref directory_path| { .filter(|ref directory_path| {
let path = Path::new(&directory_path); let path = Path::new(&directory_path);
!path.exists() || vfs.real_to_virtual(path).is_err() !path.exists() || vfs.real_to_virtual(path).is_err()
}) }).collect::<Vec<_>>();
.collect::<Vec<_>>();
{ {
let connection = db.get_connection(); let connection = db.get_connection();
@ -361,7 +358,8 @@ fn clean<T>(db: &T) -> Result<(), errors::Error>
} }
fn populate<T>(db: &T) -> Result<(), errors::Error> fn populate<T>(db: &T) -> Result<(), errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
let mount_points = vfs.get_mount_points(); let mount_points = vfs.get_mount_points();
@ -384,19 +382,23 @@ fn populate<T>(db: &T) -> Result<(), errors::Error>
} }
pub fn update<T>(db: &T) -> Result<(), errors::Error> pub fn update<T>(db: &T) -> Result<(), errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
let start = time::Instant::now(); let start = time::Instant::now();
info!("Beginning library index update"); info!("Beginning library index update");
clean(db)?; clean(db)?;
populate(db)?; populate(db)?;
info!("Library index update took {} seconds", info!(
start.elapsed().as_secs()); "Library index update took {} seconds",
start.elapsed().as_secs()
);
Ok(()) Ok(())
} }
pub fn update_loop<T>(db: &T, command_buffer: Receiver<Command>) pub fn update_loop<T>(db: &T, command_buffer: Receiver<Command>)
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
loop { loop {
// Wait for a command // Wait for a command
@ -425,7 +427,8 @@ pub fn update_loop<T>(db: &T, command_buffer: Receiver<Command>)
} }
pub fn self_trigger<T>(db: &T, command_buffer: Arc<Mutex<Sender<Command>>>) pub fn self_trigger<T>(db: &T, command_buffer: Arc<Mutex<Sender<Command>>>)
where T: ConnectionSource where
T: ConnectionSource,
{ {
loop { loop {
{ {
@ -482,7 +485,8 @@ fn virtualize_directory(vfs: &VFS, mut directory: Directory) -> Option<Directory
} }
pub fn browse<T>(db: &T, virtual_path: &Path) -> Result<Vec<CollectionFile>, errors::Error> pub fn browse<T>(db: &T, virtual_path: &Path) -> Result<Vec<CollectionFile>, errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
let mut output = Vec::new(); let mut output = Vec::new();
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
@ -496,10 +500,11 @@ pub fn browse<T>(db: &T, virtual_path: &Path) -> Result<Vec<CollectionFile>, err
let virtual_directories = real_directories let virtual_directories = real_directories
.into_iter() .into_iter()
.filter_map(|s| virtualize_directory(&vfs, s)); .filter_map(|s| virtualize_directory(&vfs, s));
output.extend(virtual_directories output.extend(
.into_iter() virtual_directories
.map(|d| CollectionFile::Directory(d))); .into_iter()
.map(|d| CollectionFile::Directory(d)),
);
} else { } else {
// Browse sub-directory // Browse sub-directory
let real_path = vfs.virtual_to_real(virtual_path)?; let real_path = vfs.virtual_to_real(virtual_path)?;
@ -528,7 +533,8 @@ pub fn browse<T>(db: &T, virtual_path: &Path) -> Result<Vec<CollectionFile>, err
} }
pub fn flatten<T>(db: &T, virtual_path: &Path) -> Result<Vec<Song>, errors::Error> pub fn flatten<T>(db: &T, virtual_path: &Path) -> Result<Vec<Song>, errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
use self::songs::dsl::*; use self::songs::dsl::*;
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
@ -552,7 +558,8 @@ pub fn flatten<T>(db: &T, virtual_path: &Path) -> Result<Vec<Song>, errors::Erro
} }
pub fn get_random_albums<T>(db: &T, count: i64) -> Result<Vec<Directory>, errors::Error> pub fn get_random_albums<T>(db: &T, count: i64) -> Result<Vec<Directory>, errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
use self::directories::dsl::*; use self::directories::dsl::*;
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
@ -569,7 +576,8 @@ pub fn get_random_albums<T>(db: &T, count: i64) -> Result<Vec<Directory>, errors
} }
pub fn get_recent_albums<T>(db: &T, count: i64) -> Result<Vec<Directory>, errors::Error> pub fn get_recent_albums<T>(db: &T, count: i64) -> Result<Vec<Directory>, errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
use self::directories::dsl::*; use self::directories::dsl::*;
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
@ -586,7 +594,8 @@ pub fn get_recent_albums<T>(db: &T, count: i64) -> Result<Vec<Directory>, errors
} }
pub fn search<T>(db: &T, query: &str) -> Result<Vec<CollectionFile>, errors::Error> pub fn search<T>(db: &T, query: &str) -> Result<Vec<CollectionFile>, errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
let connection = db.get_connection(); let connection = db.get_connection();
@ -612,12 +621,13 @@ pub fn search<T>(db: &T, query: &str) -> Result<Vec<CollectionFile>, errors::Err
{ {
use self::songs::dsl::*; use self::songs::dsl::*;
let real_songs: Vec<Song> = songs let real_songs: Vec<Song> = songs
.filter(path.like(&like_test) .filter(
.or(title.like(&like_test)) path.like(&like_test)
.or(album.like(&like_test)) .or(title.like(&like_test))
.or(artist.like(&like_test)) .or(album.like(&like_test))
.or(album_artist.like(&like_test))) .or(artist.like(&like_test))
.filter(parent.not_like(&like_test)) .or(album_artist.like(&like_test)),
).filter(parent.not_like(&like_test))
.load(connection.deref())?; .load(connection.deref())?;
let virtual_songs = real_songs let virtual_songs = real_songs
@ -631,7 +641,8 @@ pub fn search<T>(db: &T, query: &str) -> Result<Vec<CollectionFile>, errors::Err
} }
pub fn get_song<T>(db: &T, virtual_path: &Path) -> Result<Song, errors::Error> pub fn get_song<T>(db: &T, virtual_path: &Path) -> Result<Song, errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
let vfs = db.get_vfs()?; let vfs = db.get_vfs()?;
let connection = db.get_connection(); let connection = db.get_connection();
@ -695,8 +706,10 @@ fn test_metadata() {
assert_eq!(song.album_artist, None); assert_eq!(song.album_artist, None);
assert_eq!(song.album, Some("Picnic".to_owned())); assert_eq!(song.album, Some("Picnic".to_owned()));
assert_eq!(song.year, Some(2016)); assert_eq!(song.year, Some(2016));
assert_eq!(song.artwork, assert_eq!(
Some(artwork_path.to_string_lossy().into_owned())); song.artwork,
Some(artwork_path.to_string_lossy().into_owned())
);
} }
#[test] #[test]

View file

@ -1,6 +1,6 @@
use md5; use md5;
use reqwest; use reqwest;
use rustfm_scrobble::{Scrobbler, Scrobble}; use rustfm_scrobble::{Scrobble, Scrobbler};
use serde_xml_rs::deserialize; use serde_xml_rs::deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Read; use std::io::Read;
@ -48,16 +48,20 @@ struct AuthResponse {
} }
fn scrobble_from_path<T>(db: &T, track: &Path) -> Result<Scrobble, errors::Error> fn scrobble_from_path<T>(db: &T, track: &Path) -> Result<Scrobble, errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
let song = index::get_song(db, track)?; let song = index::get_song(db, track)?;
Ok(Scrobble::new(song.artist.unwrap_or("".into()), Ok(Scrobble::new(
song.title.unwrap_or("".into()), song.artist.unwrap_or("".into()),
song.album.unwrap_or("".into()))) song.title.unwrap_or("".into()),
song.album.unwrap_or("".into()),
))
} }
pub fn auth<T>(db: &T, username: &str, token: &str) -> Result<(), errors::Error> pub fn auth<T>(db: &T, username: &str, token: &str) -> Result<(), errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert("token".to_string(), token.to_string()); params.insert("token".to_string(), token.to_string());
@ -75,14 +79,15 @@ pub fn auth<T>(db: &T, username: &str, token: &str) -> Result<(), errors::Error>
let auth_response: AuthResponse = match deserialize(body.as_bytes()) { let auth_response: AuthResponse = match deserialize(body.as_bytes()) {
Ok(d) => d, Ok(d) => d,
Err(_) => bail!(errors::ErrorKind::LastFMDeserializationError) Err(_) => bail!(errors::ErrorKind::LastFMDeserializationError),
}; };
user::set_lastfm_session_key(db, username, &auth_response.session.key.body) user::set_lastfm_session_key(db, username, &auth_response.session.key.body)
} }
pub fn scrobble<T>(db: &T, username: &str, track: &Path) -> Result<(), errors::Error> pub fn scrobble<T>(db: &T, username: &str, track: &Path) -> Result<(), errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into()); let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into());
let scrobble = scrobble_from_path(db, track)?; let scrobble = scrobble_from_path(db, track)?;
@ -93,7 +98,8 @@ pub fn scrobble<T>(db: &T, username: &str, track: &Path) -> Result<(), errors::E
} }
pub fn now_playing<T>(db: &T, username: &str, track: &Path) -> Result<(), errors::Error> pub fn now_playing<T>(db: &T, username: &str, track: &Path) -> Result<(), errors::Error>
where T: ConnectionSource + VFSSource where
T: ConnectionSource + VFSSource,
{ {
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into()); let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into());
let scrobble = scrobble_from_path(db, track)?; let scrobble = scrobble_from_path(db, track)?;
@ -103,14 +109,15 @@ pub fn now_playing<T>(db: &T, username: &str, track: &Path) -> Result<(), errors
Ok(()) Ok(())
} }
fn api_request(method: &str, params: &HashMap<String, String>) -> Result<reqwest::Response, reqwest::Error> fn api_request(
{ method: &str,
params: &HashMap<String, String>,
) -> Result<reqwest::Response, reqwest::Error> {
let mut url = LASTFM_API_ROOT.to_string(); let mut url = LASTFM_API_ROOT.to_string();
url.push_str("?"); url.push_str("?");
url.push_str(&format!("method={}&", method)); url.push_str(&format!("method={}&", method));
for (k, v) in params.iter() for (k, v) in params.iter() {
{
url.push_str(&format!("{}={}&", k, v)); url.push_str(&format!("{}={}&", k, v));
} }
let api_signature = get_signature(method, params); let api_signature = get_signature(method, params);
@ -121,21 +128,18 @@ fn api_request(method: &str, params: &HashMap<String, String>) -> Result<reqwest
request.send() request.send()
} }
fn get_signature(method: &str, params: &HashMap<String, String>) -> String fn get_signature(method: &str, params: &HashMap<String, String>) -> String {
{
let mut signature_data = params.clone(); let mut signature_data = params.clone();
signature_data.insert("method".to_string(), method.to_string()); signature_data.insert("method".to_string(), method.to_string());
let mut param_names = Vec::new(); let mut param_names = Vec::new();
for param_name in signature_data.keys() for param_name in signature_data.keys() {
{
param_names.push(param_name); param_names.push(param_name);
} }
param_names.sort(); param_names.sort();
let mut signature = String::new(); let mut signature = String::new();
for param_name in param_names for param_name in param_names {
{
signature.push_str((param_name.to_string() + signature_data[param_name].as_str()).as_str()) signature.push_str((param_name.to_string() + signature_data[param_name].as_str()).as_str())
} }

View file

@ -23,8 +23,8 @@ extern crate mount;
extern crate mp3_duration; extern crate mp3_duration;
extern crate params; extern crate params;
extern crate rand; extern crate rand;
extern crate reqwest;
extern crate regex; extern crate regex;
extern crate reqwest;
extern crate ring; extern crate ring;
extern crate router; extern crate router;
extern crate rustfm_scrobble; extern crate rustfm_scrobble;
@ -50,25 +50,25 @@ extern crate winapi;
#[cfg(unix)] #[cfg(unix)]
extern crate unix_daemonize; extern crate unix_daemonize;
#[cfg(unix)]
use unix_daemonize::{daemonize_redirect, ChdirMode};
#[cfg(unix)] #[cfg(unix)]
use std::fs::File; use std::fs::File;
#[cfg(unix)] #[cfg(unix)]
use std::io::prelude::*; use std::io::prelude::*;
#[cfg(unix)]
use unix_daemonize::{daemonize_redirect, ChdirMode};
use core::ops::Deref; use core::ops::Deref;
use errors::*; use errors::*;
use getopts::Options; use getopts::Options;
use iron::prelude::*; use iron::prelude::*;
use mount::Mount; use mount::Mount;
use staticfile::Static;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::sync::mpsc::channel;
use simplelog::{TermLogger, LogLevelFilter};
#[cfg(unix)] #[cfg(unix)]
use simplelog::SimpleLogger; use simplelog::SimpleLogger;
use simplelog::{LogLevelFilter, TermLogger};
use staticfile::Static;
use std::path::Path;
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex};
mod api; mod api;
mod config; mod config;
@ -79,11 +79,11 @@ mod index;
mod lastfm; mod lastfm;
mod metadata; mod metadata;
mod playlist; mod playlist;
mod serve;
mod thumbnails;
mod ui; mod ui;
mod user; mod user;
mod utils; mod utils;
mod serve;
mod thumbnails;
mod vfs; mod vfs;
static LOG_CONFIG: simplelog::Config = simplelog::Config { static LOG_CONFIG: simplelog::Config = simplelog::Config {
@ -148,7 +148,6 @@ fn init_log(log_level: LogLevelFilter, _: &getopts::Matches) -> Result<()> {
} }
fn run() -> Result<()> { fn run() -> Result<()> {
// Parse CLI options // Parse CLI options
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
let mut options = Options::new(); let mut options = Options::new();
@ -156,15 +155,19 @@ fn run() -> Result<()> {
options.optopt("p", "port", "set polaris to run on a custom port", "PORT"); options.optopt("p", "port", "set polaris to run on a custom port", "PORT");
options.optopt("d", "database", "set the path to index database", "FILE"); options.optopt("d", "database", "set the path to index database", "FILE");
options.optopt("w", "web", "set the path to web client files", "DIRECTORY"); options.optopt("w", "web", "set the path to web client files", "DIRECTORY");
options.optopt("l", options.optopt(
"log", "l",
"set the log level to a value between 0 (off) and 3 (debug)", "log",
"LEVEL"); "set the log level to a value between 0 (off) and 3 (debug)",
"LEVEL",
);
#[cfg(unix)] #[cfg(unix)]
options.optflag("f", options.optflag(
"foreground", "f",
"run polaris in the foreground instead of daemonizing"); "foreground",
"run polaris in the foreground instead of daemonizing",
);
options.optflag("h", "help", "print this help menu"); options.optflag("h", "help", "print this help menu");
@ -214,14 +217,16 @@ fn run() -> Result<()> {
let index_sender = Arc::new(Mutex::new(index_sender)); let index_sender = Arc::new(Mutex::new(index_sender));
let db_ref = db.clone(); let db_ref = db.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let db = db_ref.deref(); let db = db_ref.deref();
index::update_loop(db, index_receiver); index::update_loop(db, index_receiver);
}); });
// Trigger auto-indexing // Trigger auto-indexing
let db_ref = db.clone(); let db_ref = db.clone();
let sender_ref = index_sender.clone(); let sender_ref = index_sender.clone();
std::thread::spawn(move || { index::self_trigger(db_ref.deref(), sender_ref); }); std::thread::spawn(move || {
index::self_trigger(db_ref.deref(), sender_ref);
});
// Mount API // Mount API
let prefix_url = config.prefix_url.unwrap_or("".to_string()); let prefix_url = config.prefix_url.unwrap_or("".to_string());
@ -258,7 +263,9 @@ fn run() -> Result<()> {
// Start DDNS updates // Start DDNS updates
let db_ref = db.clone(); let db_ref = db.clone();
std::thread::spawn(move || { ddns::run(db_ref.deref()); }); std::thread::spawn(move || {
ddns::run(db_ref.deref());
});
// Run UI // Run UI
ui::run(); ui::run();

View file

@ -45,21 +45,22 @@ fn read_id3(path: &Path) -> Result<SongTags> {
let title = tag.title().map(|s| s.to_string()); let title = tag.title().map(|s| s.to_string());
let disc_number = tag.disc(); let disc_number = tag.disc();
let track_number = tag.track(); let track_number = tag.track();
let year = tag.year() let year = tag
.year()
.map(|y| y as i32) .map(|y| y as i32)
.or(tag.date_released().and_then(|d| Some(d.year))) .or(tag.date_released().and_then(|d| Some(d.year)))
.or(tag.date_recorded().and_then(|d| Some(d.year))); .or(tag.date_recorded().and_then(|d| Some(d.year)));
Ok(SongTags { Ok(SongTags {
artist: artist, artist: artist,
album_artist: album_artist, album_artist: album_artist,
album: album, album: album,
title: title, title: title,
duration: duration, duration: duration,
disc_number: disc_number, disc_number: disc_number,
track_number: track_number, track_number: track_number,
year: year, year: year,
}) })
} }
fn read_ape_string(item: &ape::Item) -> Option<String> { fn read_ape_string(item: &ape::Item) -> Option<String> {
@ -100,19 +101,18 @@ fn read_ape(path: &Path) -> Result<SongTags> {
let disc_number = tag.item("Disc").and_then(read_ape_x_of_y); let disc_number = tag.item("Disc").and_then(read_ape_x_of_y);
let track_number = tag.item("Track").and_then(read_ape_x_of_y); let track_number = tag.item("Track").and_then(read_ape_x_of_y);
Ok(SongTags { Ok(SongTags {
artist: artist, artist: artist,
album_artist: album_artist, album_artist: album_artist,
album: album, album: album,
title: title, title: title,
duration: None, duration: None,
disc_number: disc_number, disc_number: disc_number,
track_number: track_number, track_number: track_number,
year: year, year: year,
}) })
} }
fn read_vorbis(path: &Path) -> Result<SongTags> { fn read_vorbis(path: &Path) -> Result<SongTags> {
let file = fs::File::open(path)?; let file = fs::File::open(path)?;
let source = OggStreamReader::new(file)?; let source = OggStreamReader::new(file)?;
@ -159,15 +159,15 @@ fn read_flac(path: &Path) -> Result<SongTags> {
}; };
Ok(SongTags { Ok(SongTags {
artist: vorbis.artist().map(|v| v[0].clone()), artist: vorbis.artist().map(|v| v[0].clone()),
album_artist: vorbis.album_artist().map(|v| v[0].clone()), album_artist: vorbis.album_artist().map(|v| v[0].clone()),
album: vorbis.album().map(|v| v[0].clone()), album: vorbis.album().map(|v| v[0].clone()),
title: vorbis.title().map(|v| v[0].clone()), title: vorbis.title().map(|v| v[0].clone()),
duration: duration, duration: duration,
disc_number: disc_number, disc_number: disc_number,
track_number: vorbis.track(), track_number: vorbis.track(),
year: year, year: year,
}) })
} }
#[test] #[test]
@ -192,6 +192,8 @@ fn test_read_metadata() {
}; };
assert_eq!(read(Path::new("test/sample.mp3")).unwrap(), mp3_sample_tag); assert_eq!(read(Path::new("test/sample.mp3")).unwrap(), mp3_sample_tag);
assert_eq!(read(Path::new("test/sample.ogg")).unwrap(), sample_tags); assert_eq!(read(Path::new("test/sample.ogg")).unwrap(), sample_tags);
assert_eq!(read(Path::new("test/sample.flac")).unwrap(), assert_eq!(
flac_sample_tag); read(Path::new("test/sample.flac")).unwrap(),
flac_sample_tag
);
} }

View file

@ -5,6 +5,7 @@ use diesel::prelude::*;
use diesel::BelongingToDsl; use diesel::BelongingToDsl;
use diesel::types::*; use diesel::types::*;
use std::path::Path; use std::path::Path;
#[cfg(test)] #[cfg(test)]
use db; use db;
use db::ConnectionSource; use db::ConnectionSource;

View file

@ -1,19 +1,19 @@
use std::cmp; use iron::headers::{
use std::fs::{self, File}; AcceptRanges, ByteRangeSpec, ContentLength, ContentRange, ContentRangeSpec, Range, RangeUnit,
use std::io::{self, Read, Seek, SeekFrom, Write}; };
use std::path::Path;
use iron::headers::{AcceptRanges, ByteRangeSpec, ContentLength, ContentRange, ContentRangeSpec,
Range, RangeUnit};
use iron::modifier::Modifier; use iron::modifier::Modifier;
use iron::modifiers::Header; use iron::modifiers::Header;
use iron::prelude::*; use iron::prelude::*;
use iron::response::WriteBody; use iron::response::WriteBody;
use iron::status::{self, Status}; use iron::status::{self, Status};
use std::cmp;
use std::fs::{self, File};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
use errors::{Error, ErrorKind}; use errors::{Error, ErrorKind};
pub fn deliver(path: &Path, range_header: Option<&Range>) -> IronResult<Response> { pub fn deliver(path: &Path, range_header: Option<&Range>) -> IronResult<Response> {
match fs::metadata(path) { match fs::metadata(path) {
Ok(meta) => meta, Ok(meta) => meta,
Err(e) => { Err(e) => {
@ -31,18 +31,20 @@ pub fn deliver(path: &Path, range_header: Option<&Range>) -> IronResult<Response
match range_header { match range_header {
None => Ok(Response::with((status::Ok, path, accept_range_header))), None => Ok(Response::with((status::Ok, path, accept_range_header))),
Some(range) => { Some(range) => match range {
match range { Range::Bytes(vec_range) => {
Range::Bytes(vec_range) => { if let Ok(partial_file) = PartialFile::from_path(path, vec_range) {
if let Ok(partial_file) = PartialFile::from_path(path, vec_range) { Ok(Response::with((
Ok(Response::with((status::Ok, partial_file, accept_range_header))) status::Ok,
} else { partial_file,
Err(Error::from(ErrorKind::FileNotFound).into()) accept_range_header,
} )))
} else {
Err(Error::from(ErrorKind::FileNotFound).into())
} }
_ => Ok(Response::with(status::RangeNotSatisfiable)),
} }
} _ => Ok(Response::with(status::RangeNotSatisfiable)),
},
} }
} }
@ -78,7 +80,8 @@ impl From<Vec<ByteRangeSpec>> for PartialFileRange {
impl PartialFile { impl PartialFile {
pub fn new<Range>(file: File, range: Range) -> PartialFile pub fn new<Range>(file: File, range: Range) -> PartialFile
where Range: Into<PartialFileRange> where
Range: Into<PartialFileRange>,
{ {
let range = range.into(); let range = range.into();
PartialFile { PartialFile {
@ -88,14 +91,14 @@ impl PartialFile {
} }
pub fn from_path<P: AsRef<Path>, Range>(path: P, range: Range) -> Result<PartialFile, io::Error> pub fn from_path<P: AsRef<Path>, Range>(path: P, range: Range) -> Result<PartialFile, io::Error>
where Range: Into<PartialFileRange> where
Range: Into<PartialFileRange>,
{ {
let file = File::open(path.as_ref())?; let file = File::open(path.as_ref())?;
Ok(Self::new(file, range)) Ok(Self::new(file, range))
} }
} }
impl Modifier<Response> for PartialFile { impl Modifier<Response> for PartialFile {
fn modify(self, res: &mut Response) { fn modify(self, res: &mut Response) {
use self::PartialFileRange::*; use self::PartialFileRange::*;
@ -124,13 +127,12 @@ impl Modifier<Response> for PartialFile {
} }
} }
(_, None) => None, (_, None) => None,
}; };
if let Some(range) = range { if let Some(range) = range {
let content_range = ContentRange(ContentRangeSpec::Bytes { let content_range = ContentRange(ContentRangeSpec::Bytes {
range: Some(range), range: Some(range),
instance_length: file_length, instance_length: file_length,
}); });
let content_len = range.1 - range.0 + 1; let content_len = range.1 - range.0 + 1;
res.headers.set(ContentLength(content_len)); res.headers.set(ContentLength(content_len));
res.headers.set(content_range); res.headers.set(content_range);
@ -143,11 +145,10 @@ impl Modifier<Response> for PartialFile {
res.body = Some(Box::new(partial_content)); res.body = Some(Box::new(partial_content));
} else { } else {
if let Some(file_length) = file_length { if let Some(file_length) = file_length {
res.headers res.headers.set(ContentRange(ContentRangeSpec::Bytes {
.set(ContentRange(ContentRangeSpec::Bytes { range: None,
range: None, instance_length: Some(file_length),
instance_length: Some(file_length), }));
}));
}; };
res.status = Some(Status::RangeNotSatisfiable); res.status = Some(Status::RangeNotSatisfiable);
} }

View file

@ -1,8 +1,8 @@
use image; use image;
use image::FilterType;
use image::GenericImage; use image::GenericImage;
use image::ImageBuffer; use image::ImageBuffer;
use image::ImageFormat; use image::ImageFormat;
use image::FilterType;
use std::cmp; use std::cmp;
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::fs::{DirBuilder, File}; use std::fs::{DirBuilder, File};
@ -23,7 +23,6 @@ fn hash(path: &Path, dimension: u32) -> u64 {
} }
pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result<PathBuf> { pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result<PathBuf> {
let mut out_path = utils::get_data_root()?; let mut out_path = utils::get_data_root()?;
out_path.push(THUMBNAILS_PATH); out_path.push(THUMBNAILS_PATH);
@ -46,9 +45,11 @@ pub fn get_thumbnail(real_path: &Path, max_dimension: u32) -> Result<PathBuf> {
source_image.resize(out_dimension, out_dimension, FilterType::Lanczos3); source_image.resize(out_dimension, out_dimension, FilterType::Lanczos3);
let (scaled_width, scaled_height) = scaled_image.dimensions(); let (scaled_width, scaled_height) = scaled_image.dimensions();
let mut final_image = ImageBuffer::new(out_dimension, out_dimension); let mut final_image = ImageBuffer::new(out_dimension, out_dimension);
final_image.copy_from(&scaled_image, final_image.copy_from(
(out_dimension - scaled_width) / 2, &scaled_image,
(out_dimension - scaled_height) / 2); (out_dimension - scaled_width) / 2,
(out_dimension - scaled_height) / 2,
);
final_image.save(&out_path)?; final_image.save(&out_path)?;
} else { } else {
let mut out_file = File::create(&out_path)?; let mut out_file = File::create(&out_path)?;

View file

@ -1,5 +1,5 @@
use std::time;
use std::thread; use std::thread;
use std::time;
pub fn run() { pub fn run() {
info!("Starting up UI (headless)"); info!("Starting up UI (headless)");

View file

@ -3,9 +3,9 @@ use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt; use std::os::windows::ffi::OsStrExt;
use uuid; use uuid;
use winapi; use winapi;
use winapi::um::{shellapi, winuser}; use winapi::shared::minwindef::{DWORD, LOWORD, LPARAM, LRESULT, UINT, WPARAM};
use winapi::shared::minwindef::{DWORD, LOWORD, UINT, WPARAM, LPARAM, LRESULT};
use winapi::shared::windef::HWND; use winapi::shared::windef::HWND;
use winapi::um::{shellapi, winuser};
const IDI_POLARIS_TRAY: isize = 0x102; const IDI_POLARIS_TRAY: isize = 0x102;
const UID_NOTIFICATION_ICON: u32 = 0; const UID_NOTIFICATION_ICON: u32 = 0;
@ -33,12 +33,15 @@ impl ToWin for uuid::Uuid {
fn to_win(&self) -> Self::Out { fn to_win(&self) -> Self::Out {
let bytes = self.as_bytes(); let bytes = self.as_bytes();
let end = [bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], let end = [
bytes[15]]; bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
];
winapi::shared::guiddef::GUID { winapi::shared::guiddef::GUID {
Data1: ((bytes[0] as u32) << 24 | (bytes[1] as u32) << 16 | (bytes[2] as u32) << 8 | Data1: ((bytes[0] as u32) << 24
(bytes[3] as u32)), | (bytes[1] as u32) << 16
| (bytes[2] as u32) << 8
| (bytes[3] as u32)),
Data2: ((bytes[4] as u16) << 8 | (bytes[5] as u16)), Data2: ((bytes[4] as u16) << 8 | (bytes[5] as u16)),
Data3: ((bytes[6] as u16) << 8 | (bytes[7] as u16)), Data3: ((bytes[6] as u16) << 8 | (bytes[7] as u16)),
Data4: end, Data4: end,
@ -55,7 +58,6 @@ impl Constructible for shellapi::NOTIFYICONDATAW {
type Out = shellapi::NOTIFYICONDATAW; type Out = shellapi::NOTIFYICONDATAW;
fn new() -> Self::Out { fn new() -> Self::Out {
let mut version_union: shellapi::NOTIFYICONDATAW_u = unsafe { std::mem::zeroed() }; let mut version_union: shellapi::NOTIFYICONDATAW_u = unsafe { std::mem::zeroed() };
unsafe { unsafe {
let version = version_union.uVersion_mut(); let version = version_union.uVersion_mut();
@ -83,7 +85,6 @@ impl Constructible for shellapi::NOTIFYICONDATAW {
} }
fn create_window() -> Option<HWND> { fn create_window() -> Option<HWND> {
let class_name = "Polaris-class".to_win(); let class_name = "Polaris-class".to_win();
let window_name = "Polaris-window".to_win(); let window_name = "Polaris-window".to_win();
@ -107,18 +108,20 @@ fn create_window() -> Option<HWND> {
return None; return None;
} }
let window_handle = winuser::CreateWindowExW(0, let window_handle = winuser::CreateWindowExW(
atom as winapi::shared::ntdef::LPCWSTR, 0,
window_name.as_ptr(), atom as winapi::shared::ntdef::LPCWSTR,
winuser::WS_DISABLED, window_name.as_ptr(),
0, winuser::WS_DISABLED,
0, 0,
0, 0,
0, 0,
winuser::GetDesktopWindow(), 0,
std::ptr::null_mut(), winuser::GetDesktopWindow(),
std::ptr::null_mut(), std::ptr::null_mut(),
std::ptr::null_mut()); std::ptr::null_mut(),
std::ptr::null_mut(),
);
if window_handle.is_null() { if window_handle.is_null() {
return None; return None;
@ -129,7 +132,6 @@ fn create_window() -> Option<HWND> {
} }
fn add_notification_icon(window: HWND) { fn add_notification_icon(window: HWND) {
let mut tooltip = [0 as winapi::um::winnt::WCHAR; 128]; let mut tooltip = [0 as winapi::um::winnt::WCHAR; 128];
for (&x, p) in "Polaris".to_win().iter().zip(tooltip.iter_mut()) { for (&x, p) in "Polaris".to_win().iter().zip(tooltip.iter_mut()) {
*p = x; *p = x;
@ -173,24 +175,28 @@ fn open_notification_context_menu(window: HWND) {
if context_menu.is_null() { if context_menu.is_null() {
return; return;
} }
winuser::InsertMenuW(context_menu, winuser::InsertMenuW(
0, context_menu,
winuser::MF_STRING, 0,
MESSAGE_NOTIFICATION_ICON_QUIT as usize, winuser::MF_STRING,
quit_string.as_ptr()); MESSAGE_NOTIFICATION_ICON_QUIT as usize,
quit_string.as_ptr(),
);
let mut cursor_position = winapi::shared::windef::POINT { x: 0, y: 0 }; let mut cursor_position = winapi::shared::windef::POINT { x: 0, y: 0 };
winuser::GetCursorPos(&mut cursor_position); winuser::GetCursorPos(&mut cursor_position);
winuser::SetForegroundWindow(window); winuser::SetForegroundWindow(window);
let flags = winuser::TPM_RIGHTALIGN | winuser::TPM_BOTTOMALIGN | winuser::TPM_RIGHTBUTTON; let flags = winuser::TPM_RIGHTALIGN | winuser::TPM_BOTTOMALIGN | winuser::TPM_RIGHTBUTTON;
winuser::TrackPopupMenu(context_menu, winuser::TrackPopupMenu(
flags, context_menu,
cursor_position.x, flags,
cursor_position.y, cursor_position.x,
0, cursor_position.y,
window, 0,
std::ptr::null_mut()); window,
std::ptr::null_mut(),
);
winuser::PostMessageW(window, 0, 0, 0); winuser::PostMessageW(window, 0, 0, 0);
info!("Closing notification context menu"); info!("Closing notification context menu");
@ -224,8 +230,10 @@ pub fn run() {
unsafe { unsafe {
status = winuser::GetMessageW(&mut message, std::ptr::null_mut(), 0, 0); status = winuser::GetMessageW(&mut message, std::ptr::null_mut(), 0, 0);
if status == -1 { if status == -1 {
panic!("GetMessageW error: {}", panic!(
winapi::um::errhandlingapi::GetLastError()); "GetMessageW error: {}",
winapi::um::errhandlingapi::GetLastError()
);
} }
if status == 0 { if status == 0 {
break; break;
@ -236,34 +244,30 @@ pub fn run() {
} }
} }
pub unsafe extern "system" fn window_proc(window: HWND, pub unsafe extern "system" fn window_proc(
msg: UINT, window: HWND,
w_param: WPARAM, msg: UINT,
l_param: LPARAM) w_param: WPARAM,
-> LRESULT { l_param: LPARAM,
) -> LRESULT {
match msg { match msg {
winuser::WM_CREATE => { winuser::WM_CREATE => {
add_notification_icon(window); add_notification_icon(window);
} }
MESSAGE_NOTIFICATION_ICON => { MESSAGE_NOTIFICATION_ICON => match LOWORD(l_param as DWORD) as u32 {
match LOWORD(l_param as DWORD) as u32 { winuser::WM_RBUTTONUP => {
winuser::WM_RBUTTONUP => { open_notification_context_menu(window);
open_notification_context_menu(window);
}
_ => (),
} }
} _ => (),
},
winuser::WM_COMMAND => { winuser::WM_COMMAND => match LOWORD(w_param as DWORD) as u32 {
match LOWORD(w_param as DWORD) as u32 { MESSAGE_NOTIFICATION_ICON_QUIT => {
MESSAGE_NOTIFICATION_ICON_QUIT => { quit(window);
quit(window);
}
_ => (),
} }
} _ => (),
},
winuser::WM_DESTROY => { winuser::WM_DESTROY => {
remove_notification_icon(window); remove_notification_icon(window);

View file

@ -4,12 +4,12 @@ use diesel::prelude::*;
use rand; use rand;
use ring::{digest, pbkdf2}; use ring::{digest, pbkdf2};
use db::ConnectionSource;
use db::users; use db::users;
use db::ConnectionSource;
use errors::*; use errors::*;
#[derive(Debug, Insertable, Queryable)] #[derive(Debug, Insertable, Queryable)]
#[table_name="users"] #[table_name = "users"]
pub struct User { pub struct User {
pub name: String, pub name: String,
pub password_salt: Vec<u8>, pub password_salt: Vec<u8>,
@ -37,35 +37,41 @@ impl User {
pub fn hash_password(salt: &Vec<u8>, password: &str) -> Vec<u8> { pub fn hash_password(salt: &Vec<u8>, password: &str) -> Vec<u8> {
let mut hash: PasswordHash = [0; CREDENTIAL_LEN]; let mut hash: PasswordHash = [0; CREDENTIAL_LEN];
pbkdf2::derive(DIGEST_ALG, pbkdf2::derive(
HASH_ITERATIONS, DIGEST_ALG,
salt, HASH_ITERATIONS,
password.as_bytes(), salt,
&mut hash); password.as_bytes(),
&mut hash,
);
hash.to_vec() hash.to_vec()
} }
fn verify_password(password_hash: &Vec<u8>, fn verify_password(
password_salt: &Vec<u8>, password_hash: &Vec<u8>,
attempted_password: &str) password_salt: &Vec<u8>,
-> bool { attempted_password: &str,
pbkdf2::verify(DIGEST_ALG, ) -> bool {
HASH_ITERATIONS, pbkdf2::verify(
password_salt, DIGEST_ALG,
attempted_password.as_bytes(), HASH_ITERATIONS,
password_hash) password_salt,
.is_ok() attempted_password.as_bytes(),
password_hash,
).is_ok()
} }
pub fn auth<T>(db: &T, username: &str, password: &str) -> Result<bool> pub fn auth<T>(db: &T, username: &str, password: &str) -> Result<bool>
where T: ConnectionSource where
T: ConnectionSource,
{ {
use db::users::dsl::*; use db::users::dsl::*;
let connection = db.get_connection(); let connection = db.get_connection();
match users match users
.select((password_hash, password_salt)) .select((password_hash, password_salt))
.filter(name.eq(username)) .filter(name.eq(username))
.get_result(connection.deref()) { .get_result(connection.deref())
{
Err(diesel::result::Error::NotFound) => Ok(false), Err(diesel::result::Error::NotFound) => Ok(false),
Ok((hash, salt)) => Ok(verify_password(&hash, &salt, password)), Ok((hash, salt)) => Ok(verify_password(&hash, &salt, password)),
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
@ -73,7 +79,8 @@ pub fn auth<T>(db: &T, username: &str, password: &str) -> Result<bool>
} }
pub fn count<T>(db: &T) -> Result<i64> pub fn count<T>(db: &T) -> Result<i64>
where T: ConnectionSource where
T: ConnectionSource,
{ {
use db::users::dsl::*; use db::users::dsl::*;
let connection = db.get_connection(); let connection = db.get_connection();
@ -82,7 +89,8 @@ pub fn count<T>(db: &T) -> Result<i64>
} }
pub fn is_admin<T>(db: &T, username: &str) -> Result<bool> pub fn is_admin<T>(db: &T, username: &str) -> Result<bool>
where T: ConnectionSource where
T: ConnectionSource,
{ {
use db::users::dsl::*; use db::users::dsl::*;
let connection = db.get_connection(); let connection = db.get_connection();
@ -94,7 +102,8 @@ pub fn is_admin<T>(db: &T, username: &str) -> Result<bool>
} }
pub fn set_lastfm_session_key<T>(db: &T, username: &str, token: &str) -> Result<()> pub fn set_lastfm_session_key<T>(db: &T, username: &str, token: &str) -> Result<()>
where T: ConnectionSource where
T: ConnectionSource,
{ {
use db::users::dsl::*; use db::users::dsl::*;
let connection = db.get_connection(); let connection = db.get_connection();
@ -105,7 +114,8 @@ pub fn set_lastfm_session_key<T>(db: &T, username: &str, token: &str) -> Result<
} }
pub fn get_lastfm_session_key<T>(db: &T, username: &str) -> Result<String> pub fn get_lastfm_session_key<T>(db: &T, username: &str) -> Result<String>
where T: ConnectionSource where
T: ConnectionSource,
{ {
use db::users::dsl::*; use db::users::dsl::*;
let connection = db.get_connection(); let connection = db.get_connection();

View file

@ -1,6 +1,6 @@
use app_dirs::{AppDataType, AppInfo, app_root}; use app_dirs::{app_root, AppDataType, AppInfo};
use std::path::{Path, PathBuf};
use std::fs; use std::fs;
use std::path::{Path, PathBuf};
use errors::*; use errors::*;
@ -18,8 +18,7 @@ const APP_INFO: AppInfo = AppInfo {
pub fn get_data_root() -> Result<PathBuf> { pub fn get_data_root() -> Result<PathBuf> {
if let Ok(root) = app_root(AppDataType::UserData, &APP_INFO) { if let Ok(root) = app_root(AppDataType::UserData, &APP_INFO) {
fs::create_dir_all(&root) fs::create_dir_all(&root).chain_err(|| format!("opening user data: {}", root.display()))?;
.chain_err(|| format!("opening user data: {}", root.display()))?;
return Ok(root); return Ok(root);
} }
bail!("Could not retrieve data directory root"); bail!("Could not retrieve data directory root");
@ -55,10 +54,14 @@ pub fn get_audio_format(path: &Path) -> Option<AudioFormat> {
#[test] #[test]
fn test_get_audio_format() { fn test_get_audio_format() {
assert_eq!(get_audio_format(Path::new("animals/🐷/my🐖file.jpg")), assert_eq!(
None); get_audio_format(Path::new("animals/🐷/my🐖file.jpg")),
assert_eq!(get_audio_format(Path::new("animals/🐷/my🐖file.flac")), None
Some(AudioFormat::FLAC)); );
assert_eq!(
get_audio_format(Path::new("animals/🐷/my🐖file.flac")),
Some(AudioFormat::FLAC)
);
} }
pub fn is_song(path: &Path) -> bool { pub fn is_song(path: &Path) -> bool {

View file

@ -1,11 +1,11 @@
use core::ops::Deref; use core::ops::Deref;
use diesel::prelude::*; use diesel::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf;
use std::path::Path; use std::path::Path;
use std::path::PathBuf;
use db::{ConnectionSource, DB};
use db::mount_points; use db::mount_points;
use db::{ConnectionSource, DB};
use errors::*; use errors::*;
pub trait VFSSource { pub trait VFSSource {
@ -28,7 +28,7 @@ impl VFSSource for DB {
} }
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Queryable, Serialize)] #[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Queryable, Serialize)]
#[table_name="mount_points"] #[table_name = "mount_points"]
pub struct MountPoint { pub struct MountPoint {
pub source: String, pub source: String,
pub name: String, pub name: String,
@ -40,7 +40,9 @@ pub struct VFS {
impl VFS { impl VFS {
pub fn new() -> VFS { pub fn new() -> VFS {
VFS { mount_points: HashMap::new() } VFS {
mount_points: HashMap::new(),
}
} }
pub fn mount(&mut self, real_path: &Path, name: &str) -> Result<()> { pub fn mount(&mut self, real_path: &Path, name: &str) -> Result<()> {
@ -55,10 +57,10 @@ impl VFS {
Ok(p) => { Ok(p) => {
let mount_path = Path::new(&name); let mount_path = Path::new(&name);
return if p.components().count() == 0 { return if p.components().count() == 0 {
Ok(mount_path.to_path_buf()) Ok(mount_path.to_path_buf())
} else { } else {
Ok(mount_path.join(p)) Ok(mount_path.join(p))
}; };
} }
Err(_) => (), Err(_) => (),
} }
@ -72,10 +74,10 @@ impl VFS {
match virtual_path.strip_prefix(mount_path) { match virtual_path.strip_prefix(mount_path) {
Ok(p) => { Ok(p) => {
return if p.components().count() == 0 { return if p.components().count() == 0 {
Ok(target.clone()) Ok(target.clone())
} else { } else {
Ok(target.join(p)) Ok(target.join(p))
}; };
} }
Err(_) => (), Err(_) => (),
} }