use core::ops::Deref; use diesel; use diesel::prelude::*; use regex::Regex; use serde_json; use std::fs; use std::io::Read; use std::path; use toml; use db::DB; use db::ConnectionSource; use db::{ddns_config, misc_settings, mount_points, users}; use ddns::DDNSConfig; use errors::*; use user::*; use vfs::MountPoint; #[derive(Debug, Queryable)] pub struct MiscSettings { id: i32, pub auth_secret: String, pub index_sleep_duration_seconds: i32, pub index_album_art_pattern: String, pub prefix_url: String, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ConfigUser { pub name: String, pub password: String, pub admin: bool, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Config { pub album_art_pattern: Option, pub reindex_every_n_seconds: Option, pub mount_dirs: Option>, pub prefix_url: Option, pub users: Option>, pub ydns: Option, } impl Config { fn clean_paths(&mut self) -> Result<()> { if let Some(ref mut mount_dirs) = self.mount_dirs { for mount_dir in mount_dirs { match clean_path_string(&mount_dir.source).to_str() { Some(p) => mount_dir.source = p.to_owned(), _ => bail!("Bad mount directory path"), } } } Ok(()) } } pub fn parse_json(content: &str) -> Result { let mut config = serde_json::from_str::(content)?; config.clean_paths()?; Ok(config) } pub fn parse_toml_file(path: &path::Path) -> Result { info!("Config file path: {}", path.to_string_lossy()); let mut config_file = fs::File::open(path)?; let mut config_file_content = String::new(); config_file.read_to_string(&mut config_file_content)?; let mut config = toml::de::from_str::(&config_file_content)?; config.clean_paths()?; Ok(config) } pub fn read(db: &T) -> Result where T: ConnectionSource { use self::misc_settings::dsl::*; use self::mount_points::dsl::*; use self::ddns_config::dsl::*; let connection = db.get_connection(); let mut config = Config { album_art_pattern: None, reindex_every_n_seconds: None, mount_dirs: None, prefix_url: None, users: None, ydns: None, }; let (art_pattern, sleep_duration, url) = misc_settings .select((index_album_art_pattern, index_sleep_duration_seconds, prefix_url)) .get_result(connection.deref())?; config.album_art_pattern = Some(art_pattern); config.reindex_every_n_seconds = Some(sleep_duration); config.prefix_url = if url != "" { Some(url) } else { None }; let mount_dirs = mount_points .select((source, name)) .get_results(connection.deref())?; config.mount_dirs = Some(mount_dirs); let found_users: Vec<(String, i32)> = users::table .select((users::columns::name, users::columns::admin)) .get_results(connection.deref())?; config.users = Some(found_users .into_iter() .map(|(n, a)| { ConfigUser { name: n, password: "".to_owned(), admin: a != 0, } }) .collect::<_>()); let ydns = ddns_config .select((host, username, password)) .get_result(connection.deref())?; config.ydns = Some(ydns); Ok(config) } fn reset(db: &T) -> Result<()> where T: ConnectionSource { use self::ddns_config::dsl::*; let connection = db.get_connection(); diesel::delete(mount_points::table) .execute(connection.deref())?; diesel::delete(users::table).execute(connection.deref())?; diesel::update(ddns_config) .set((host.eq(""), username.eq(""), password.eq(""))) .execute(connection.deref())?; Ok(()) } pub fn overwrite(db: &T, new_config: &Config) -> Result<()> where T: ConnectionSource { reset(db)?; amend(db, new_config) } pub fn amend(db: &T, new_config: &Config) -> Result<()> where T: ConnectionSource { let connection = db.get_connection(); if let Some(ref mount_dirs) = new_config.mount_dirs { diesel::delete(mount_points::table) .execute(connection.deref())?; diesel::insert_into(mount_points::table) .values(mount_dirs) .execute(connection.deref())?; } if let Some(ref config_users) = new_config.users { let old_usernames: Vec = users::table .select(users::name) .get_results(connection.deref())?; // Delete users that are not in new list // Delete users that have a new password let delete_usernames: Vec = old_usernames .into_iter() .filter(|old_name| match config_users.iter().find(|u| &u.name == old_name) { None => true, Some(new_user) => !new_user.password.is_empty(), }) .collect::<_>(); diesel::delete(users::table.filter(users::name.eq_any(&delete_usernames))) .execute(connection.deref())?; // Insert users that have a new password let insert_users: Vec<&ConfigUser> = config_users .iter() .filter(|u| !u.password.is_empty()) .collect::<_>(); for ref config_user in insert_users { let new_user = User::new(&config_user.name, &config_user.password, config_user.admin); diesel::insert_into(users::table) .values(&new_user) .execute(connection.deref())?; } // Grant admin rights for ref user in config_users { diesel::update(users::table.filter(users::name.eq(&user.name))) .set(users::admin.eq(user.admin as i32)) .execute(connection.deref())?; } } if let Some(sleep_duration) = new_config.reindex_every_n_seconds { diesel::update(misc_settings::table) .set(misc_settings::index_sleep_duration_seconds.eq(sleep_duration as i32)) .execute(connection.deref())?; } if let Some(ref album_art_pattern) = new_config.album_art_pattern { diesel::update(misc_settings::table) .set(misc_settings::index_album_art_pattern.eq(album_art_pattern)) .execute(connection.deref())?; } if let Some(ref ydns) = new_config.ydns { use self::ddns_config::dsl::*; diesel::update(ddns_config) .set((host.eq(ydns.host.clone()), username.eq(ydns.username.clone()), password.eq(ydns.password.clone()))) .execute(connection.deref())?; } if let Some(ref prefix_url) = new_config.prefix_url { diesel::update(misc_settings::table) .set(misc_settings::prefix_url.eq(prefix_url)) .execute(connection.deref())?; } Ok(()) } fn clean_path_string(path_string: &str) -> path::PathBuf { let separator_regex = Regex::new(r"\\|/").unwrap(); let mut correct_separator = String::new(); correct_separator.push(path::MAIN_SEPARATOR); let path_string = separator_regex.replace_all(path_string, correct_separator.as_str()); path::Path::new(path_string.deref()).iter().collect() } fn _get_test_db(name: &str) -> DB { let mut db_path = path::PathBuf::new(); db_path.push("test"); db_path.push(name); if db_path.exists() { fs::remove_file(&db_path).unwrap(); } let db = DB::new(&db_path).unwrap(); db } #[test] fn test_amend() { let db = _get_test_db("amend.sqlite"); let initial_config = Config { album_art_pattern: Some("file\\.png".into()), reindex_every_n_seconds: Some(123), prefix_url: None, mount_dirs: Some(vec![MountPoint { source: "C:\\Music".into(), name: "root".into(), }]), users: Some(vec![ConfigUser { name: "Teddy🐻".into(), password: "Tasty🍖".into(), admin: false, }]), ydns: None, }; let new_config = Config { album_art_pattern: Some("🖼️\\.jpg".into()), reindex_every_n_seconds: None, prefix_url: Some("polaris".into()), mount_dirs: Some(vec![MountPoint { source: "/home/music".into(), name: "🎵📁".into(), }]), users: Some(vec![ConfigUser { name: "Kermit🐸".into(), password: "🐞🐞".into(), admin: false, }]), ydns: Some(DDNSConfig { host: "🐸🐸🐸.ydns.eu".into(), username: "kfr🐸g".into(), password: "tasty🐞".into(), }), }; let mut expected_config = new_config.clone(); expected_config.reindex_every_n_seconds = initial_config.reindex_every_n_seconds; if let Some(ref mut users) = expected_config.users { users[0].password = "".into(); } amend(&db, &initial_config).unwrap(); amend(&db, &new_config).unwrap(); let db_config = read(&db).unwrap(); assert_eq!(db_config, expected_config); } #[test] fn test_amend_preserve_password_hashes() { use self::users::dsl::*; let db = _get_test_db("amend_preserve_password_hashes.sqlite"); let initial_hash: Vec; let new_hash: Vec; let initial_config = Config { album_art_pattern: None, reindex_every_n_seconds: None, prefix_url: None, mount_dirs: None, users: Some(vec![ConfigUser { name: "Teddy🐻".into(), password: "Tasty🍖".into(), admin: false, }]), ydns: None, }; amend(&db, &initial_config).unwrap(); { let connection = db.get_connection(); initial_hash = users .select(password_hash) .filter(name.eq("Teddy🐻")) .get_result(connection.deref()) .unwrap(); } let new_config = Config { album_art_pattern: None, reindex_every_n_seconds: None, prefix_url: None, mount_dirs: None, users: Some(vec![ConfigUser { name: "Kermit🐸".into(), password: "tasty🐞".into(), admin: false, }, ConfigUser { name: "Teddy🐻".into(), password: "".into(), admin: false, }]), ydns: None, }; amend(&db, &new_config).unwrap(); { let connection = db.get_connection(); new_hash = users .select(password_hash) .filter(name.eq("Teddy🐻")) .get_result(connection.deref()) .unwrap(); } assert_eq!(new_hash, initial_hash); } #[test] fn test_toggle_admin() { use self::users::dsl::*; let db = _get_test_db("amend_toggle_admin.sqlite"); let initial_config = Config { album_art_pattern: None, reindex_every_n_seconds: None, prefix_url: None, mount_dirs: None, users: Some(vec![ConfigUser { name: "Teddy🐻".into(), password: "Tasty🍖".into(), admin: true, }]), ydns: None, }; amend(&db, &initial_config).unwrap(); { let connection = db.get_connection(); let is_admin: i32 = users .select(admin) .get_result(connection.deref()) .unwrap(); assert_eq!(is_admin, 1); } let new_config = Config { album_art_pattern: None, reindex_every_n_seconds: None, prefix_url: None, mount_dirs: None, users: Some(vec![ConfigUser { name: "Teddy🐻".into(), password: "".into(), admin: false, }]), ydns: None, }; amend(&db, &new_config).unwrap(); { let connection = db.get_connection(); let is_admin: i32 = users .select(admin) .get_result(connection.deref()) .unwrap(); assert_eq!(is_admin, 0); } } #[test] fn test_clean_path_string() { let mut correct_path = path::PathBuf::new(); if cfg!(target_os = "windows") { correct_path.push("C:\\"); } else { correct_path.push("/usr"); } correct_path.push("some"); correct_path.push("path"); if cfg!(target_os = "windows") { assert_eq!(correct_path, clean_path_string(r#"C:/some/path"#)); assert_eq!(correct_path, clean_path_string(r#"C:\some\path"#)); assert_eq!(correct_path, clean_path_string(r#"C:\some\path\"#)); assert_eq!(correct_path, clean_path_string(r#"C:\some\path\\\\"#)); assert_eq!(correct_path, clean_path_string(r#"C:\some/path//"#)); } else { assert_eq!(correct_path, clean_path_string(r#"/usr/some/path"#)); assert_eq!(correct_path, clean_path_string(r#"/usr\some\path"#)); assert_eq!(correct_path, clean_path_string(r#"/usr\some\path\"#)); assert_eq!(correct_path, clean_path_string(r#"/usr\some\path\\\\"#)); assert_eq!(correct_path, clean_path_string(r#"/usr\some/path//"#)); } }