Diesel -> SQLx
This commit is contained in:
parent
138eacc9fc
commit
12a9f2ec3c
76 changed files with 2482 additions and 2084 deletions
.env.gitignoreCargo.lockCargo.tomldiesel.toml
docs
migrations
201706250006_init
201706250228_directories_date_added
201706272129_users_table
201706272304_misc_settings_table
201706272313_ddns_config_table
201706272327_mount_points_table
201707091522_playlists_tables
20170929203228_add_prefix_url
20171015224223_add_song_duration
20180303211100_add_last_fm_credentials
2019-08-08-042731_blob_auth_secret
2019-09-28-231910_pbkdf2_simple
2020-01-08-231420_add_theme
2020-11-25-174000_remove_prefix_url
2021-05-01-011426_add_lyricist
src
app.rs
update_db_schema.batapp
db.rsdb
main.rspaths.rsservice
1
.env
Normal file
1
.env
Normal file
|
@ -0,0 +1 @@
|
|||
DATABASE_URL=sqlite:./src/db/schema.sqlite
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -9,6 +9,8 @@ TestConfig.toml
|
|||
|
||||
# Runtime artifacts
|
||||
*.sqlite
|
||||
**/*.sqlite-shm
|
||||
**/*.sqlite-wal
|
||||
polaris.log
|
||||
polaris.pid
|
||||
/thumbnails
|
||||
|
|
917
Cargo.lock
generated
917
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
19
Cargo.toml
19
Cargo.toml
|
@ -6,8 +6,6 @@ edition = "2021"
|
|||
build = "build.rs"
|
||||
|
||||
[features]
|
||||
default = ["bundle-sqlite"]
|
||||
bundle-sqlite = ["libsqlite3-sys"]
|
||||
ui = ["native-windows-gui", "native-windows-derive"]
|
||||
|
||||
[dependencies]
|
||||
|
@ -18,16 +16,11 @@ ape = "0.5"
|
|||
base64 = "0.21"
|
||||
branca = "0.10.1"
|
||||
crossbeam-channel = "0.5"
|
||||
diesel_migrations = { version = "2.0", features = ["sqlite"] }
|
||||
futures-util = { version = "0.3" }
|
||||
getopts = "0.2.21"
|
||||
http = "0.2.8"
|
||||
id3 = "1.7.0"
|
||||
lewton = "0.10.2"
|
||||
libsqlite3-sys = { version = "0.26", features = [
|
||||
"bundled",
|
||||
"bundled-windows",
|
||||
], optional = true }
|
||||
log = "0.4.17"
|
||||
metaflac = "0.2.5"
|
||||
mp3-duration = "0.1.10"
|
||||
|
@ -44,19 +37,16 @@ serde = { version = "1.0.147", features = ["derive"] }
|
|||
serde_derive = "1.0.147"
|
||||
serde_json = "1.0.87"
|
||||
simplelog = "0.12.0"
|
||||
sqlx = { version = "0.7.4", features = ["migrate", "runtime-tokio", "sqlite"] }
|
||||
thiserror = "1.0.37"
|
||||
tokio = { version = "1.38", features = ["macros", "rt-multi-thread"] }
|
||||
toml = "0.7"
|
||||
ureq = "2.7"
|
||||
url = "2.3"
|
||||
|
||||
[dependencies.diesel]
|
||||
version = "2.0.2"
|
||||
default_features = false
|
||||
features = ["libsqlite3-sys", "r2d2", "sqlite"]
|
||||
|
||||
[dependencies.image]
|
||||
version = "0.24.4"
|
||||
default_features = false
|
||||
default-features = false
|
||||
features = ["bmp", "gif", "jpeg", "png"]
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
@ -81,3 +71,6 @@ winres = "0.1"
|
|||
actix-test = "0.1.0"
|
||||
headers = "0.3"
|
||||
fs_extra = "1.2.0"
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[print_schema]
|
||||
file = "src/db/schema.rs"
|
|
@ -8,8 +8,3 @@
|
|||
- Input a user-facing version name (eg: **0.13.0**)
|
||||
- Click the **Run workflow** button
|
||||
- After CI completes, move the release from Draft to Published
|
||||
|
||||
## How to change the database schema
|
||||
|
||||
- Add a new folder under `migrations` following the existing pattern
|
||||
- Run `update_db_schema.bat`
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
DROP TABLE directories;
|
||||
DROP TABLE songs;
|
|
@ -1,25 +0,0 @@
|
|||
CREATE TABLE directories (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
parent TEXT,
|
||||
artist TEXT,
|
||||
year INTEGER,
|
||||
album TEXT,
|
||||
artwork TEXT,
|
||||
UNIQUE(path) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
CREATE TABLE songs (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
parent TEXT NOT NULL,
|
||||
track_number INTEGER,
|
||||
disc_number INTEGER,
|
||||
title TEXT,
|
||||
artist TEXT,
|
||||
album_artist TEXT,
|
||||
year INTEGER,
|
||||
album TEXT,
|
||||
artwork TEXT,
|
||||
UNIQUE(path) ON CONFLICT REPLACE
|
||||
);
|
|
@ -1,15 +0,0 @@
|
|||
CREATE TEMPORARY TABLE directories_backup(id, path, parent, artist, year, album, artwork);
|
||||
INSERT INTO directories_backup SELECT id, path, parent, artist, year, album, artwork FROM directories;
|
||||
DROP TABLE directories;
|
||||
CREATE TABLE directories (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
parent TEXT,
|
||||
artist TEXT,
|
||||
year INTEGER,
|
||||
album TEXT,
|
||||
artwork TEXT,
|
||||
UNIQUE(path) ON CONFLICT REPLACE
|
||||
);
|
||||
INSERT INTO directories SELECT * FROM directories_backup;
|
||||
DROP TABLE directories_backup;
|
|
@ -1 +0,0 @@
|
|||
ALTER TABLE directories ADD COLUMN date_added INTEGER DEFAULT 0 NOT NULL;
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE users;
|
|
@ -1,8 +0,0 @@
|
|||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
password_salt BLOB NOT NULL,
|
||||
password_hash BLOB NOT NULL,
|
||||
admin INTEGER NOT NULL,
|
||||
UNIQUE(name)
|
||||
);
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE misc_settings;
|
|
@ -1,7 +0,0 @@
|
|||
CREATE TABLE misc_settings (
|
||||
id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
|
||||
auth_secret TEXT NOT NULL,
|
||||
index_sleep_duration_seconds INTEGER NOT NULL,
|
||||
index_album_art_pattern TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO misc_settings (id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern) VALUES (0, hex(randomblob(64)), 1800, "Folder.(jpeg|jpg|png)");
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE ddns_config;
|
|
@ -1,8 +0,0 @@
|
|||
CREATE TABLE ddns_config (
|
||||
id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
|
||||
host TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO ddns_config (id, host, username, password) VALUES (0, "", "", "");
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE mount_points;
|
|
@ -1,6 +0,0 @@
|
|||
CREATE TABLE mount_points (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
UNIQUE(name)
|
||||
);
|
|
@ -1,2 +0,0 @@
|
|||
DROP TABLE playlists;
|
||||
DROP TABLE playlist_songs;
|
|
@ -1,16 +0,0 @@
|
|||
CREATE TABLE playlists (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
owner INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
FOREIGN KEY(owner) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE(owner, name) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
CREATE TABLE playlist_songs (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
playlist INTEGER NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
ordering INTEGER NOT NULL,
|
||||
FOREIGN KEY(playlist) REFERENCES playlists(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
UNIQUE(playlist, ordering) ON CONFLICT REPLACE
|
||||
);
|
|
@ -1,11 +0,0 @@
|
|||
CREATE TEMPORARY TABLE misc_settings_backup(id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern);
|
||||
INSERT INTO misc_settings_backup SELECT id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern FROM misc_settings;
|
||||
DROP TABLE misc_settings;
|
||||
CREATE TABLE misc_settings (
|
||||
id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
|
||||
auth_secret TEXT NOT NULL,
|
||||
index_sleep_duration_seconds INTEGER NOT NULL,
|
||||
index_album_art_pattern TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO misc_settings SELECT * FROM misc_settings_backup;
|
||||
DROP TABLE misc_settings_backup;
|
|
@ -1 +0,0 @@
|
|||
ALTER TABLE misc_settings ADD COLUMN prefix_url TEXT NOT NULL DEFAULT "";
|
|
@ -1,19 +0,0 @@
|
|||
CREATE TEMPORARY TABLE songs_backup(id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork);
|
||||
INSERT INTO songs_backup SELECT id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork FROM songs;
|
||||
DROP TABLE songs;
|
||||
CREATE TABLE songs (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
parent TEXT NOT NULL,
|
||||
track_number INTEGER,
|
||||
disc_number INTEGER,
|
||||
title TEXT,
|
||||
artist TEXT,
|
||||
album_artist TEXT,
|
||||
year INTEGER,
|
||||
album TEXT,
|
||||
artwork TEXT,
|
||||
UNIQUE(path) ON CONFLICT REPLACE
|
||||
);
|
||||
INSERT INTO songs SELECT * FROM songs_backup;
|
||||
DROP TABLE songs_backup;
|
|
@ -1 +0,0 @@
|
|||
ALTER TABLE songs ADD COLUMN duration INTEGER;
|
|
@ -1,13 +0,0 @@
|
|||
CREATE TEMPORARY TABLE users_backup(id, name, password_salt, password_hash, admin);
|
||||
INSERT INTO users_backup SELECT id, name, password_salt, password_hash, admin FROM users;
|
||||
DROP TABLE users;
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
password_salt BLOB NOT NULL,
|
||||
password_hash BLOB NOT NULL,
|
||||
admin INTEGER NOT NULL,
|
||||
UNIQUE(name)
|
||||
);
|
||||
INSERT INTO users SELECT * FROM users_backup;
|
||||
DROP TABLE users_backup;
|
|
@ -1,2 +0,0 @@
|
|||
ALTER TABLE users ADD COLUMN lastfm_username TEXT;
|
||||
ALTER TABLE users ADD COLUMN lastfm_session_key TEXT;
|
|
@ -1,15 +0,0 @@
|
|||
CREATE TEMPORARY TABLE misc_settings_backup(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url);
|
||||
INSERT INTO misc_settings_backup
|
||||
SELECT id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url
|
||||
FROM misc_settings;
|
||||
DROP TABLE misc_settings;
|
||||
CREATE TABLE misc_settings (
|
||||
id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
|
||||
auth_secret BLOB NOT NULL DEFAULT (hex(randomblob(32))),
|
||||
index_sleep_duration_seconds INTEGER NOT NULL,
|
||||
index_album_art_pattern TEXT NOT NULL,
|
||||
prefix_url TEXT NOT NULL DEFAULT ""
|
||||
);
|
||||
INSERT INTO misc_settings(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url)
|
||||
SELECT * FROM misc_settings_backup;
|
||||
DROP TABLE misc_settings_backup;
|
|
@ -1,15 +0,0 @@
|
|||
CREATE TEMPORARY TABLE misc_settings_backup(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url);
|
||||
INSERT INTO misc_settings_backup
|
||||
SELECT id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url
|
||||
FROM misc_settings;
|
||||
DROP TABLE misc_settings;
|
||||
CREATE TABLE misc_settings (
|
||||
id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
|
||||
auth_secret BLOB NOT NULL DEFAULT (randomblob(32)),
|
||||
index_sleep_duration_seconds INTEGER NOT NULL,
|
||||
index_album_art_pattern TEXT NOT NULL,
|
||||
prefix_url TEXT NOT NULL DEFAULT ""
|
||||
);
|
||||
INSERT INTO misc_settings(id, index_sleep_duration_seconds, index_album_art_pattern, prefix_url)
|
||||
SELECT * FROM misc_settings_backup;
|
||||
DROP TABLE misc_settings_backup;
|
|
@ -1,11 +0,0 @@
|
|||
DROP TABLE users;
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
password_salt BLOB NOT NULL,
|
||||
password_hash BLOB NOT NULL,
|
||||
admin INTEGER NOT NULL,
|
||||
lastfm_username TEXT,
|
||||
lastfm_session_key TEXT,
|
||||
UNIQUE(name)
|
||||
);
|
|
@ -1,10 +0,0 @@
|
|||
DROP TABLE users;
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
admin INTEGER NOT NULL,
|
||||
lastfm_username TEXT,
|
||||
lastfm_session_key TEXT,
|
||||
UNIQUE(name)
|
||||
);
|
|
@ -1,14 +0,0 @@
|
|||
CREATE TEMPORARY TABLE users_backup(id, name, password_hash, admin, lastfm_username, lastfm_session_key);
|
||||
INSERT INTO users_backup SELECT id, name, password_hash, admin, lastfm_username, lastfm_session_key FROM users;
|
||||
DROP TABLE users;
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
admin INTEGER NOT NULL,
|
||||
lastfm_username TEXT,
|
||||
lastfm_session_key TEXT,
|
||||
UNIQUE(name)
|
||||
);
|
||||
INSERT INTO users SELECT * FROM users_backup;
|
||||
DROP TABLE users_backup;
|
|
@ -1,2 +0,0 @@
|
|||
ALTER TABLE users ADD COLUMN web_theme_base TEXT;
|
||||
ALTER TABLE users ADD COLUMN web_theme_accent TEXT;
|
|
@ -1 +0,0 @@
|
|||
ALTER TABLE misc_settings ADD COLUMN prefix_url TEXT NOT NULL DEFAULT "";
|
|
@ -1,11 +0,0 @@
|
|||
CREATE TEMPORARY TABLE misc_settings_backup(id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern);
|
||||
INSERT INTO misc_settings_backup SELECT id, auth_secret, index_sleep_duration_seconds, index_album_art_pattern FROM misc_settings;
|
||||
DROP TABLE misc_settings;
|
||||
CREATE TABLE misc_settings (
|
||||
id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
|
||||
auth_secret BLOB NOT NULL DEFAULT (randomblob(32)),
|
||||
index_sleep_duration_seconds INTEGER NOT NULL,
|
||||
index_album_art_pattern TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO misc_settings SELECT * FROM misc_settings_backup;
|
||||
DROP TABLE misc_settings_backup;
|
|
@ -1,20 +0,0 @@
|
|||
CREATE TEMPORARY TABLE songs_backup(id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration);
|
||||
INSERT INTO songs_backup SELECT id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration FROM songs;
|
||||
DROP TABLE songs;
|
||||
CREATE TABLE songs (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
parent TEXT NOT NULL,
|
||||
track_number INTEGER,
|
||||
disc_number INTEGER,
|
||||
title TEXT,
|
||||
artist TEXT,
|
||||
album_artist TEXT,
|
||||
year INTEGER,
|
||||
album TEXT,
|
||||
artwork TEXT,
|
||||
duration INTEGER,
|
||||
UNIQUE(path) ON CONFLICT REPLACE
|
||||
);
|
||||
INSERT INTO songs SELECT * FROM songs_backup;
|
||||
DROP TABLE songs_backup;
|
|
@ -1,4 +0,0 @@
|
|||
ALTER TABLE songs ADD COLUMN lyricist TEXT;
|
||||
ALTER TABLE songs ADD COLUMN composer TEXT;
|
||||
ALTER TABLE songs ADD COLUMN genre TEXT;
|
||||
ALTER TABLE songs ADD COLUMN label TEXT;
|
14
src/app.rs
14
src/app.rs
|
@ -32,10 +32,8 @@ pub enum Error {
|
|||
#[derive(Clone)]
|
||||
pub struct App {
|
||||
pub port: u16,
|
||||
pub auth_secret: settings::AuthSecret,
|
||||
pub web_dir_path: PathBuf,
|
||||
pub swagger_dir_path: PathBuf,
|
||||
pub db: DB,
|
||||
pub index: index::Index,
|
||||
pub config_manager: config::Manager,
|
||||
pub ddns_manager: ddns::Manager,
|
||||
|
@ -48,8 +46,8 @@ pub struct App {
|
|||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(port: u16, paths: Paths) -> Result<Self, Error> {
|
||||
let db = DB::new(&paths.db_file_path)?;
|
||||
pub async fn new(port: u16, paths: Paths) -> Result<Self, Error> {
|
||||
let db = DB::new(&paths.db_file_path).await?;
|
||||
fs::create_dir_all(&paths.web_dir_path)
|
||||
.map_err(|e| Error::Io(paths.web_dir_path.clone(), e))?;
|
||||
fs::create_dir_all(&paths.swagger_dir_path)
|
||||
|
@ -61,7 +59,7 @@ impl App {
|
|||
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret()?;
|
||||
let auth_secret = settings_manager.get_auth_secret().await?;
|
||||
let ddns_manager = ddns::Manager::new(db.clone());
|
||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||
let index = index::Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone());
|
||||
|
@ -77,14 +75,11 @@ impl App {
|
|||
|
||||
if let Some(config_path) = paths.config_file_path {
|
||||
let config = config::Config::from_path(&config_path)?;
|
||||
config_manager.apply(&config)?;
|
||||
config_manager.apply(&config).await?;
|
||||
}
|
||||
|
||||
let auth_secret = settings_manager.get_auth_secret()?;
|
||||
|
||||
Ok(Self {
|
||||
port,
|
||||
auth_secret,
|
||||
web_dir_path: paths.web_dir_path,
|
||||
swagger_dir_path: paths.swagger_dir_path,
|
||||
index,
|
||||
|
@ -96,7 +91,6 @@ impl App {
|
|||
thumbnail_manager,
|
||||
user_manager,
|
||||
vfs_manager,
|
||||
db,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,28 +64,28 @@ impl Manager {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn apply(&self, config: &Config) -> Result<(), Error> {
|
||||
pub async fn apply(&self, config: &Config) -> Result<(), Error> {
|
||||
if let Some(new_settings) = &config.settings {
|
||||
self.settings_manager.amend(new_settings)?;
|
||||
self.settings_manager.amend(new_settings).await?;
|
||||
}
|
||||
|
||||
if let Some(mount_dirs) = &config.mount_dirs {
|
||||
self.vfs_manager.set_mount_dirs(mount_dirs)?;
|
||||
self.vfs_manager.set_mount_dirs(mount_dirs).await?;
|
||||
}
|
||||
|
||||
if let Some(ddns_config) = &config.ydns {
|
||||
self.ddns_manager.set_config(ddns_config)?;
|
||||
self.ddns_manager.set_config(ddns_config).await?;
|
||||
}
|
||||
|
||||
if let Some(ref users) = config.users {
|
||||
let old_users: Vec<user::User> = self.user_manager.list()?;
|
||||
let old_users: Vec<user::User> = self.user_manager.list().await?;
|
||||
|
||||
// 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)?;
|
||||
self.user_manager.delete(&old_user.name).await?;
|
||||
}
|
||||
|
||||
// Insert new users
|
||||
|
@ -93,13 +93,17 @@ impl Manager {
|
|||
.iter()
|
||||
.filter(|u| !old_users.iter().any(|old_user| old_user.name == u.name))
|
||||
{
|
||||
self.user_manager.create(new_user)?;
|
||||
self.user_manager.create(new_user).await?;
|
||||
}
|
||||
|
||||
// Update users
|
||||
for user in users {
|
||||
self.user_manager.set_password(&user.name, &user.password)?;
|
||||
self.user_manager.set_is_admin(&user.name, user.admin)?;
|
||||
self.user_manager
|
||||
.set_password(&user.name, &user.password)
|
||||
.await?;
|
||||
self.user_manager
|
||||
.set_is_admin(&user.name, user.admin)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,9 +118,9 @@ mod test {
|
|||
use crate::app::test;
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn apply_saves_misc_settings() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn apply_saves_misc_settings() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
let new_config = Config {
|
||||
settings: Some(settings::NewSettings {
|
||||
album_art_pattern: Some("🖼️\\.jpg".into()),
|
||||
|
@ -125,8 +129,8 @@ mod test {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
ctx.config_manager.apply(&new_config).unwrap();
|
||||
let settings = ctx.settings_manager.read().unwrap();
|
||||
ctx.config_manager.apply(&new_config).await.unwrap();
|
||||
let settings = ctx.settings_manager.read().await.unwrap();
|
||||
let new_settings = new_config.settings.unwrap();
|
||||
assert_eq!(
|
||||
settings.index_album_art_pattern,
|
||||
|
@ -138,9 +142,9 @@ mod test {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_saves_mount_points() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn apply_saves_mount_points() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
|
||||
let new_config = Config {
|
||||
mount_dirs: Some(vec![vfs::MountDir {
|
||||
|
@ -150,36 +154,37 @@ mod test {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
ctx.config_manager.apply(&new_config).unwrap();
|
||||
let actual_mount_dirs: Vec<vfs::MountDir> = ctx.vfs_manager.mount_dirs().unwrap();
|
||||
ctx.config_manager.apply(&new_config).await.unwrap();
|
||||
let actual_mount_dirs: Vec<vfs::MountDir> = ctx.vfs_manager.mount_dirs().await.unwrap();
|
||||
assert_eq!(actual_mount_dirs, new_config.mount_dirs.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_saves_ddns_settings() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn apply_saves_ddns_settings() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
|
||||
let new_config = Config {
|
||||
ydns: Some(ddns::Config {
|
||||
host: "🐸🐸🐸.ydns.eu".into(),
|
||||
username: "kfr🐸g".into(),
|
||||
password: "tasty🐞".into(),
|
||||
ddns_host: "🐸🐸🐸.ydns.eu".into(),
|
||||
ddns_username: "kfr🐸g".into(),
|
||||
ddns_password: "tasty🐞".into(),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
ctx.config_manager.apply(&new_config).unwrap();
|
||||
let actual_ddns = ctx.ddns_manager.config().unwrap();
|
||||
ctx.config_manager.apply(&new_config).await.unwrap();
|
||||
let actual_ddns = ctx.ddns_manager.config().await.unwrap();
|
||||
assert_eq!(actual_ddns, new_config.ydns.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_can_toggle_admin() {
|
||||
#[tokio::test]
|
||||
async fn apply_can_toggle_admin() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.user("Walter", "Tasty🍖", true)
|
||||
.build();
|
||||
.build()
|
||||
.await;
|
||||
|
||||
assert!(ctx.user_manager.list().unwrap()[0].is_admin());
|
||||
assert!(ctx.user_manager.list().await.unwrap()[0].is_admin());
|
||||
|
||||
let new_config = Config {
|
||||
users: Some(vec![user::NewUser {
|
||||
|
@ -189,7 +194,7 @@ mod test {
|
|||
}]),
|
||||
..Default::default()
|
||||
};
|
||||
ctx.config_manager.apply(&new_config).unwrap();
|
||||
assert!(!ctx.user_manager.list().unwrap()[0].is_admin());
|
||||
ctx.config_manager.apply(&new_config).await.unwrap();
|
||||
assert!(!ctx.user_manager.list().await.unwrap()[0].is_admin());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
use base64::prelude::*;
|
||||
use diesel::prelude::*;
|
||||
use log::{debug, error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::db::{self, ddns_config, DB};
|
||||
use crate::db::{self, DB};
|
||||
|
||||
const DDNS_UPDATE_URL: &str = "https://ydns.io/api/v1/update/";
|
||||
|
||||
|
@ -18,15 +16,14 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
DatabaseConnection(#[from] db::Error),
|
||||
#[error(transparent)]
|
||||
Database(#[from] diesel::result::Error),
|
||||
Database(#[from] sqlx::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)]
|
||||
#[diesel(table_name = ddns_config)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct Config {
|
||||
pub host: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub ddns_host: String,
|
||||
pub ddns_username: String,
|
||||
pub ddns_password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -39,15 +36,15 @@ impl Manager {
|
|||
Self { db }
|
||||
}
|
||||
|
||||
fn update_my_ip(&self) -> Result<(), Error> {
|
||||
let config = self.config()?;
|
||||
if config.host.is_empty() || config.username.is_empty() {
|
||||
async fn update_my_ip(&self) -> Result<(), Error> {
|
||||
let config = self.config().await?;
|
||||
if config.ddns_host.is_empty() || config.ddns_username.is_empty() {
|
||||
debug!("Skipping DDNS update because credentials are missing");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.host);
|
||||
let credentials = format!("{}:{}", &config.username, &config.password);
|
||||
let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.ddns_host);
|
||||
let credentials = format!("{}:{}", &config.ddns_username, &config.ddns_password);
|
||||
let response = ureq::get(full_url.as_str())
|
||||
.set(
|
||||
"Authorization",
|
||||
|
@ -62,40 +59,38 @@ impl Manager {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Result<Config, Error> {
|
||||
use crate::db::ddns_config::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
Ok(ddns_config
|
||||
.select((host, username, password))
|
||||
.get_result(&mut connection)?)
|
||||
pub async fn config(&self) -> Result<Config, Error> {
|
||||
Ok(sqlx::query_as!(
|
||||
Config,
|
||||
"SELECT ddns_host, ddns_username, ddns_password FROM config"
|
||||
)
|
||||
.fetch_one(self.db.connect().await?.as_mut())
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub fn set_config(&self, new_config: &Config) -> Result<(), Error> {
|
||||
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)?;
|
||||
pub async fn set_config(&self, new_config: &Config) -> Result<(), Error> {
|
||||
sqlx::query!(
|
||||
"UPDATE config SET ddns_host = $1, ddns_username = $2, ddns_password = $3",
|
||||
new_config.ddns_host,
|
||||
new_config.ddns_username,
|
||||
new_config.ddns_password
|
||||
)
|
||||
.execute(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn begin_periodic_updates(&self) {
|
||||
let cloned = self.clone();
|
||||
std::thread::spawn(move || {
|
||||
cloned.run();
|
||||
tokio::spawn({
|
||||
let ddns = self.clone();
|
||||
async move {
|
||||
loop {
|
||||
if let Err(e) = ddns.update_my_ip().await {
|
||||
error!("Dynamic DNS update error: {:?}", e);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(60 * 30)).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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,6 +1,7 @@
|
|||
use log::error;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::app::{settings, vfs};
|
||||
use crate::db::DB;
|
||||
|
@ -20,7 +21,7 @@ pub struct Index {
|
|||
db: DB,
|
||||
vfs_manager: vfs::Manager,
|
||||
settings_manager: settings::Manager,
|
||||
pending_reindex: Arc<(Mutex<bool>, Condvar)>,
|
||||
pending_reindex: Arc<Notify>,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
|
@ -29,63 +30,45 @@ impl Index {
|
|||
db,
|
||||
vfs_manager,
|
||||
settings_manager,
|
||||
|
||||
pending_reindex: Arc::new((
|
||||
#[allow(clippy::mutex_atomic)]
|
||||
Mutex::new(false),
|
||||
Condvar::new(),
|
||||
)),
|
||||
pending_reindex: Arc::new(Notify::new()),
|
||||
};
|
||||
|
||||
let commands_index = index.clone();
|
||||
std::thread::spawn(move || {
|
||||
commands_index.process_commands();
|
||||
tokio::spawn({
|
||||
let index = index.clone();
|
||||
async move {
|
||||
loop {
|
||||
index.pending_reindex.notified().await;
|
||||
if let Err(e) = index.update().await {
|
||||
error!("Error while updating index: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
index
|
||||
}
|
||||
|
||||
pub fn trigger_reindex(&self) {
|
||||
let (lock, cvar) = &*self.pending_reindex;
|
||||
let mut pending_reindex = lock.lock().unwrap();
|
||||
*pending_reindex = true;
|
||||
cvar.notify_one();
|
||||
self.pending_reindex.notify_one();
|
||||
}
|
||||
|
||||
pub fn begin_periodic_updates(&self) {
|
||||
let auto_index = self.clone();
|
||||
std::thread::spawn(move || {
|
||||
auto_index.automatic_reindex();
|
||||
tokio::spawn({
|
||||
let index = self.clone();
|
||||
async move {
|
||||
loop {
|
||||
index.trigger_reindex();
|
||||
let sleep_duration = index
|
||||
.settings_manager
|
||||
.get_index_sleep_duration()
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Could not retrieve index sleep duration: {}", e);
|
||||
Duration::from_secs(1800)
|
||||
});
|
||||
tokio::time::sleep(sleep_duration).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn process_commands(&self) {
|
||||
loop {
|
||||
{
|
||||
let (lock, cvar) = &*self.pending_reindex;
|
||||
let mut pending = lock.lock().unwrap();
|
||||
while !*pending {
|
||||
pending = cvar.wait(pending).unwrap();
|
||||
}
|
||||
*pending = false;
|
||||
}
|
||||
if let Err(e) = self.update() {
|
||||
error!("Error while updating index: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn automatic_reindex(&self) {
|
||||
loop {
|
||||
self.trigger_reindex();
|
||||
let sleep_duration = self
|
||||
.settings_manager
|
||||
.get_index_sleep_duration()
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Could not retrieve index sleep duration: {}", e);
|
||||
Duration::from_secs(1800)
|
||||
});
|
||||
std::thread::sleep(sleep_duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
use diesel::dsl::sql;
|
||||
use diesel::prelude::*;
|
||||
use diesel::sql_types;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
use crate::db::{self, directories, songs};
|
||||
use crate::db;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum QueryError {
|
||||
#[error(transparent)]
|
||||
Database(#[from] diesel::result::Error),
|
||||
Database(#[from] sqlx::Error),
|
||||
#[error(transparent)]
|
||||
DatabaseConnection(#[from] db::Error),
|
||||
#[error("Song was not found: `{0}`")]
|
||||
|
@ -18,25 +15,21 @@ pub enum QueryError {
|
|||
Vfs(#[from] vfs::Error),
|
||||
}
|
||||
|
||||
sql_function!(
|
||||
#[aggregate]
|
||||
fn random() -> Integer;
|
||||
);
|
||||
|
||||
impl Index {
|
||||
pub fn browse<P>(&self, virtual_path: P) -> Result<Vec<CollectionFile>, QueryError>
|
||||
pub async fn browse<P>(&self, virtual_path: P) -> Result<Vec<CollectionFile>, QueryError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut output = Vec::new();
|
||||
let vfs = self.vfs_manager.get_vfs()?;
|
||||
let mut connection = self.db.connect()?;
|
||||
let vfs = self.vfs_manager.get_vfs().await?;
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
if virtual_path.as_ref().components().count() == 0 {
|
||||
// Browse top-level
|
||||
let real_directories: Vec<Directory> = directories::table
|
||||
.filter(directories::parent.is_null())
|
||||
.load(&mut connection)?;
|
||||
let real_directories =
|
||||
sqlx::query_as!(Directory, "SELECT * FROM directories WHERE parent IS NULL")
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?;
|
||||
let virtual_directories = real_directories
|
||||
.into_iter()
|
||||
.filter_map(|d| d.virtualize(&vfs));
|
||||
|
@ -46,19 +39,28 @@ impl Index {
|
|||
let real_path = vfs.virtual_to_real(virtual_path)?;
|
||||
let real_path_string = real_path.as_path().to_string_lossy().into_owned();
|
||||
|
||||
let real_directories: Vec<Directory> = directories::table
|
||||
.filter(directories::parent.eq(&real_path_string))
|
||||
.order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC"))
|
||||
.load(&mut connection)?;
|
||||
let real_directories = sqlx::query_as!(
|
||||
Directory,
|
||||
"SELECT * FROM directories WHERE parent = $1 ORDER BY path COLLATE NOCASE ASC",
|
||||
real_path_string
|
||||
)
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
let virtual_directories = real_directories
|
||||
.into_iter()
|
||||
.filter_map(|d| d.virtualize(&vfs));
|
||||
|
||||
output.extend(virtual_directories.map(CollectionFile::Directory));
|
||||
|
||||
let real_songs: Vec<Song> = songs::table
|
||||
.filter(songs::parent.eq(&real_path_string))
|
||||
.order(sql::<sql_types::Bool>("path COLLATE NOCASE ASC"))
|
||||
.load(&mut connection)?;
|
||||
let real_songs = sqlx::query_as!(
|
||||
Song,
|
||||
"SELECT * FROM songs WHERE parent = $1 ORDER BY path COLLATE NOCASE ASC",
|
||||
real_path_string
|
||||
)
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs));
|
||||
output.extend(virtual_songs.map(CollectionFile::Song));
|
||||
}
|
||||
|
@ -66,76 +68,88 @@ impl Index {
|
|||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn flatten<P>(&self, virtual_path: P) -> Result<Vec<Song>, QueryError>
|
||||
pub async fn flatten<P>(&self, virtual_path: P) -> Result<Vec<Song>, QueryError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
use self::songs::dsl::*;
|
||||
let vfs = self.vfs_manager.get_vfs()?;
|
||||
let mut connection = self.db.connect()?;
|
||||
let vfs = self.vfs_manager.get_vfs().await?;
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
let real_songs: Vec<Song> = if virtual_path.as_ref().parent().is_some() {
|
||||
let real_songs = if virtual_path.as_ref().parent().is_some() {
|
||||
let real_path = vfs.virtual_to_real(virtual_path)?;
|
||||
let song_path_filter = {
|
||||
let mut path_buf = real_path;
|
||||
path_buf.push("%");
|
||||
path_buf.as_path().to_string_lossy().into_owned()
|
||||
};
|
||||
songs
|
||||
.filter(path.like(&song_path_filter))
|
||||
.order(path)
|
||||
.load(&mut connection)?
|
||||
sqlx::query_as!(
|
||||
Song,
|
||||
"SELECT * FROM songs WHERE path LIKE $1 ORDER BY path COLLATE NOCASE ASC",
|
||||
song_path_filter
|
||||
)
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?
|
||||
} else {
|
||||
songs.order(path).load(&mut connection)?
|
||||
sqlx::query_as!(Song, "SELECT * FROM songs ORDER BY path COLLATE NOCASE ASC")
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?
|
||||
};
|
||||
|
||||
let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs));
|
||||
Ok(virtual_songs.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn get_random_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> {
|
||||
use self::directories::dsl::*;
|
||||
let vfs = self.vfs_manager.get_vfs()?;
|
||||
let mut connection = self.db.connect()?;
|
||||
let real_directories: Vec<Directory> = directories
|
||||
.filter(album.is_not_null())
|
||||
.limit(count)
|
||||
.order(random())
|
||||
.load(&mut connection)?;
|
||||
pub async fn get_random_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> {
|
||||
let vfs = self.vfs_manager.get_vfs().await?;
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
let real_directories = sqlx::query_as!(
|
||||
Directory,
|
||||
"SELECT * FROM directories WHERE album IS NOT NULL ORDER BY RANDOM() DESC LIMIT $1",
|
||||
count
|
||||
)
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
let virtual_directories = real_directories
|
||||
.into_iter()
|
||||
.filter_map(|d| d.virtualize(&vfs));
|
||||
Ok(virtual_directories.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn get_recent_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> {
|
||||
use self::directories::dsl::*;
|
||||
let vfs = self.vfs_manager.get_vfs()?;
|
||||
let mut connection = self.db.connect()?;
|
||||
let real_directories: Vec<Directory> = directories
|
||||
.filter(album.is_not_null())
|
||||
.order(date_added.desc())
|
||||
.limit(count)
|
||||
.load(&mut connection)?;
|
||||
pub async fn get_recent_albums(&self, count: i64) -> Result<Vec<Directory>, QueryError> {
|
||||
let vfs = self.vfs_manager.get_vfs().await?;
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
let real_directories = sqlx::query_as!(
|
||||
Directory,
|
||||
"SELECT * FROM directories WHERE album IS NOT NULL ORDER BY date_added DESC LIMIT $1",
|
||||
count
|
||||
)
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
let virtual_directories = real_directories
|
||||
.into_iter()
|
||||
.filter_map(|d| d.virtualize(&vfs));
|
||||
Ok(virtual_directories.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str) -> Result<Vec<CollectionFile>, QueryError> {
|
||||
let vfs = self.vfs_manager.get_vfs()?;
|
||||
let mut connection = self.db.connect()?;
|
||||
pub async fn search(&self, query: &str) -> Result<Vec<CollectionFile>, QueryError> {
|
||||
let vfs = self.vfs_manager.get_vfs().await?;
|
||||
let mut connection = self.db.connect().await?;
|
||||
let like_test = format!("%{}%", query);
|
||||
let mut output = Vec::new();
|
||||
|
||||
// Find dirs with matching path and parent not matching
|
||||
{
|
||||
use self::directories::dsl::*;
|
||||
let real_directories: Vec<Directory> = directories
|
||||
.filter(path.like(&like_test))
|
||||
.filter(parent.not_like(&like_test))
|
||||
.load(&mut connection)?;
|
||||
let real_directories = sqlx::query_as!(
|
||||
Directory,
|
||||
"SELECT * FROM directories WHERE path LIKE $1 AND parent NOT LIKE $1",
|
||||
like_test
|
||||
)
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
let virtual_directories = real_directories
|
||||
.into_iter()
|
||||
|
@ -146,17 +160,22 @@ impl Index {
|
|||
|
||||
// Find songs with matching title/album/artist and non-matching parent
|
||||
{
|
||||
use self::songs::dsl::*;
|
||||
let real_songs: Vec<Song> = songs
|
||||
.filter(
|
||||
path.like(&like_test)
|
||||
.or(title.like(&like_test))
|
||||
.or(album.like(&like_test))
|
||||
.or(artist.like(&like_test))
|
||||
.or(album_artist.like(&like_test)),
|
||||
)
|
||||
.filter(parent.not_like(&like_test))
|
||||
.load(&mut connection)?;
|
||||
let real_songs = sqlx::query_as!(
|
||||
Song,
|
||||
r#"
|
||||
SELECT * FROM songs
|
||||
WHERE ( path LIKE $1
|
||||
OR title LIKE $1
|
||||
OR album LIKE $1
|
||||
OR artist LIKE $1
|
||||
OR album_artist LIKE $1
|
||||
)
|
||||
AND parent NOT LIKE $1
|
||||
"#,
|
||||
like_test
|
||||
)
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
let virtual_songs = real_songs.into_iter().filter_map(|d| d.virtualize(&vfs));
|
||||
|
||||
|
@ -166,17 +185,20 @@ impl Index {
|
|||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn get_song(&self, virtual_path: &Path) -> Result<Song, QueryError> {
|
||||
let vfs = self.vfs_manager.get_vfs()?;
|
||||
let mut connection = self.db.connect()?;
|
||||
pub async fn get_song(&self, virtual_path: &Path) -> Result<Song, QueryError> {
|
||||
let vfs = self.vfs_manager.get_vfs().await?;
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
let real_path = vfs.virtual_to_real(virtual_path)?;
|
||||
let real_path_string = real_path.as_path().to_string_lossy();
|
||||
|
||||
use self::songs::dsl::*;
|
||||
let real_song: Song = songs
|
||||
.filter(path.eq(real_path_string))
|
||||
.get_result(&mut connection)?;
|
||||
let real_song = sqlx::query_as!(
|
||||
Song,
|
||||
"SELECT * FROM songs WHERE path = $1",
|
||||
real_path_string
|
||||
)
|
||||
.fetch_one(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
match real_song.virtualize(&vfs) {
|
||||
Some(s) => Ok(s),
|
||||
|
|
|
@ -1,32 +1,37 @@
|
|||
use diesel::prelude::*;
|
||||
use std::default::Default;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
use crate::app::test;
|
||||
use crate::db::{directories, songs};
|
||||
use crate::test_name;
|
||||
|
||||
const TEST_MOUNT_NAME: &str = "root";
|
||||
|
||||
#[test]
|
||||
fn update_adds_new_content() {
|
||||
#[tokio::test]
|
||||
async fn update_adds_new_content() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
.build()
|
||||
.await;
|
||||
|
||||
ctx.index.update().unwrap();
|
||||
ctx.index.update().unwrap(); // Validates that subsequent updates don't run into conflicts
|
||||
ctx.index.update().await.unwrap();
|
||||
ctx.index.update().await.unwrap(); // Validates that subsequent updates don't run into conflicts
|
||||
|
||||
let mut connection = ctx.db.connect().unwrap();
|
||||
let all_directories: Vec<Directory> = directories::table.load(&mut connection).unwrap();
|
||||
let all_songs: Vec<Song> = songs::table.load(&mut connection).unwrap();
|
||||
let mut connection = ctx.db.connect().await.unwrap();
|
||||
let all_directories = sqlx::query_as!(Directory, "SELECT * FROM directories")
|
||||
.fetch_all(connection.as_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
let all_songs = sqlx::query_as!(Song, "SELECT * FROM songs")
|
||||
.fetch_all(connection.as_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(all_directories.len(), 6);
|
||||
assert_eq!(all_songs.len(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_removes_missing_content() {
|
||||
#[tokio::test]
|
||||
async fn update_removes_missing_content() {
|
||||
let builder = test::ContextBuilder::new(test_name!());
|
||||
|
||||
let original_collection_dir: PathBuf = ["test-data", "small-collection"].iter().collect();
|
||||
|
@ -42,39 +47,53 @@ fn update_removes_missing_content() {
|
|||
|
||||
let ctx = builder
|
||||
.mount(TEST_MOUNT_NAME, test_collection_dir.to_str().unwrap())
|
||||
.build();
|
||||
.build()
|
||||
.await;
|
||||
|
||||
ctx.index.update().unwrap();
|
||||
ctx.index.update().await.unwrap();
|
||||
|
||||
{
|
||||
let mut connection = ctx.db.connect().unwrap();
|
||||
let all_directories: Vec<Directory> = directories::table.load(&mut connection).unwrap();
|
||||
let all_songs: Vec<Song> = songs::table.load(&mut connection).unwrap();
|
||||
let mut connection = ctx.db.connect().await.unwrap();
|
||||
let all_directories = sqlx::query_as!(Directory, "SELECT * FROM directories")
|
||||
.fetch_all(connection.as_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
let all_songs = sqlx::query_as!(Song, "SELECT * FROM songs")
|
||||
.fetch_all(connection.as_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(all_directories.len(), 6);
|
||||
assert_eq!(all_songs.len(), 13);
|
||||
}
|
||||
|
||||
let khemmis_directory = test_collection_dir.join("Khemmis");
|
||||
std::fs::remove_dir_all(khemmis_directory).unwrap();
|
||||
ctx.index.update().unwrap();
|
||||
ctx.index.update().await.unwrap();
|
||||
{
|
||||
let mut connection = ctx.db.connect().unwrap();
|
||||
let all_directories: Vec<Directory> = directories::table.load(&mut connection).unwrap();
|
||||
let all_songs: Vec<Song> = songs::table.load(&mut connection).unwrap();
|
||||
let mut connection = ctx.db.connect().await.unwrap();
|
||||
let all_directories = sqlx::query_as!(Directory, "SELECT * FROM directories")
|
||||
.fetch_all(connection.as_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
let all_songs = sqlx::query_as!(Song, "SELECT * FROM songs")
|
||||
.fetch_all(connection.as_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(all_directories.len(), 4);
|
||||
assert_eq!(all_songs.len(), 8);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_browse_top_level() {
|
||||
#[tokio::test]
|
||||
async fn can_browse_top_level() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
ctx.index.update().unwrap();
|
||||
.build()
|
||||
.await;
|
||||
ctx.index.update().await.unwrap();
|
||||
|
||||
let root_path = Path::new(TEST_MOUNT_NAME);
|
||||
let files = ctx.index.browse(Path::new("")).unwrap();
|
||||
let files = ctx.index.browse(Path::new("")).await.unwrap();
|
||||
assert_eq!(files.len(), 1);
|
||||
match files[0] {
|
||||
CollectionFile::Directory(ref d) => assert_eq!(d.path, root_path.to_str().unwrap()),
|
||||
|
@ -82,17 +101,18 @@ fn can_browse_top_level() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_browse_directory() {
|
||||
#[tokio::test]
|
||||
async fn can_browse_directory() {
|
||||
let khemmis_path: PathBuf = [TEST_MOUNT_NAME, "Khemmis"].iter().collect();
|
||||
let tobokegao_path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao"].iter().collect();
|
||||
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
ctx.index.update().unwrap();
|
||||
.build()
|
||||
.await;
|
||||
ctx.index.update().await.unwrap();
|
||||
|
||||
let files = ctx.index.browse(Path::new(TEST_MOUNT_NAME)).unwrap();
|
||||
let files = ctx.index.browse(Path::new(TEST_MOUNT_NAME)).await.unwrap();
|
||||
|
||||
assert_eq!(files.len(), 2);
|
||||
match files[0] {
|
||||
|
@ -106,73 +126,79 @@ fn can_browse_directory() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_flatten_root() {
|
||||
#[tokio::test]
|
||||
async fn can_flatten_root() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
ctx.index.update().unwrap();
|
||||
let songs = ctx.index.flatten(Path::new(TEST_MOUNT_NAME)).unwrap();
|
||||
.build()
|
||||
.await;
|
||||
ctx.index.update().await.unwrap();
|
||||
let songs = ctx.index.flatten(Path::new(TEST_MOUNT_NAME)).await.unwrap();
|
||||
assert_eq!(songs.len(), 13);
|
||||
assert_eq!(songs[0].title, Some("Above The Water".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_flatten_directory() {
|
||||
#[tokio::test]
|
||||
async fn can_flatten_directory() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
ctx.index.update().unwrap();
|
||||
.build()
|
||||
.await;
|
||||
ctx.index.update().await.unwrap();
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao"].iter().collect();
|
||||
let songs = ctx.index.flatten(path).unwrap();
|
||||
let songs = ctx.index.flatten(path).await.unwrap();
|
||||
assert_eq!(songs.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_flatten_directory_with_shared_prefix() {
|
||||
#[tokio::test]
|
||||
async fn can_flatten_directory_with_shared_prefix() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
ctx.index.update().unwrap();
|
||||
.build()
|
||||
.await;
|
||||
ctx.index.update().await.unwrap();
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect(); // Prefix of '(Picnic Remixes)'
|
||||
let songs = ctx.index.flatten(path).unwrap();
|
||||
let songs = ctx.index.flatten(path).await.unwrap();
|
||||
assert_eq!(songs.len(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_random_albums() {
|
||||
#[tokio::test]
|
||||
async fn can_get_random_albums() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
ctx.index.update().unwrap();
|
||||
let albums = ctx.index.get_random_albums(1).unwrap();
|
||||
.build()
|
||||
.await;
|
||||
ctx.index.update().await.unwrap();
|
||||
let albums = ctx.index.get_random_albums(1).await.unwrap();
|
||||
assert_eq!(albums.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_recent_albums() {
|
||||
#[tokio::test]
|
||||
async fn can_get_recent_albums() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
ctx.index.update().unwrap();
|
||||
let albums = ctx.index.get_recent_albums(2).unwrap();
|
||||
.build()
|
||||
.await;
|
||||
ctx.index.update().await.unwrap();
|
||||
let albums = ctx.index.get_recent_albums(2).await.unwrap();
|
||||
assert_eq!(albums.len(), 2);
|
||||
assert!(albums[0].date_added >= albums[1].date_added);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_a_song() {
|
||||
#[tokio::test]
|
||||
async fn can_get_a_song() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
.build()
|
||||
.await;
|
||||
|
||||
ctx.index.update().unwrap();
|
||||
ctx.index.update().await.unwrap();
|
||||
|
||||
let picnic_virtual_dir: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect();
|
||||
let song_virtual_path = picnic_virtual_dir.join("05 - シャーベット (Sherbet).mp3");
|
||||
let artwork_virtual_path = picnic_virtual_dir.join("Folder.png");
|
||||
|
||||
let song = ctx.index.get_song(&song_virtual_path).unwrap();
|
||||
let song = ctx.index.get_song(&song_virtual_path).await.unwrap();
|
||||
assert_eq!(song.path, song_virtual_path.to_string_lossy().as_ref());
|
||||
assert_eq!(song.track_number, Some(5));
|
||||
assert_eq!(song.disc_number, None);
|
||||
|
@ -187,29 +213,31 @@ fn can_get_a_song() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexes_embedded_artwork() {
|
||||
#[tokio::test]
|
||||
async fn indexes_embedded_artwork() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
.build()
|
||||
.await;
|
||||
|
||||
ctx.index.update().unwrap();
|
||||
ctx.index.update().await.unwrap();
|
||||
|
||||
let picnic_virtual_dir: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect();
|
||||
let song_virtual_path = picnic_virtual_dir.join("07 - なぜ (Why).mp3");
|
||||
|
||||
let song = ctx.index.get_song(&song_virtual_path).unwrap();
|
||||
let song = ctx.index.get_song(&song_virtual_path).await.unwrap();
|
||||
assert_eq!(
|
||||
song.artwork,
|
||||
Some(song_virtual_path.to_string_lossy().into_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn album_art_pattern_is_case_insensitive() {
|
||||
#[tokio::test]
|
||||
async fn album_art_pattern_is_case_insensitive() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
.build()
|
||||
.await;
|
||||
|
||||
let patterns = vec!["folder", "FOLDER"];
|
||||
|
||||
|
@ -219,12 +247,13 @@ fn album_art_pattern_is_case_insensitive() {
|
|||
album_art_pattern: Some(pattern.to_owned()),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.index.update().unwrap();
|
||||
ctx.index.update().await.unwrap();
|
||||
|
||||
let hunted_virtual_dir: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect();
|
||||
let artwork_virtual_path = hunted_virtual_dir.join("Folder.jpg");
|
||||
let song = &ctx.index.flatten(&hunted_virtual_dir).unwrap()[0];
|
||||
let song = &ctx.index.flatten(&hunted_virtual_dir).await.unwrap()[0];
|
||||
assert_eq!(
|
||||
song.artwork,
|
||||
Some(artwork_virtual_path.to_string_lossy().into_owned())
|
||||
|
|
|
@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize};
|
|||
use std::path::Path;
|
||||
|
||||
use crate::app::vfs::VFS;
|
||||
use crate::db::songs;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CollectionFile {
|
||||
|
@ -10,23 +9,22 @@ pub enum CollectionFile {
|
|||
Song(Song),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Queryable, QueryableByName, Serialize, Deserialize)]
|
||||
#[diesel(table_name = songs)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Song {
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
id: i32,
|
||||
pub id: i64,
|
||||
pub path: String,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub parent: String,
|
||||
pub track_number: Option<i32>,
|
||||
pub disc_number: Option<i32>,
|
||||
pub track_number: Option<i64>,
|
||||
pub disc_number: Option<i64>,
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album_artist: Option<String>,
|
||||
pub year: Option<i32>,
|
||||
pub year: Option<i64>,
|
||||
pub album: Option<String>,
|
||||
pub artwork: Option<String>,
|
||||
pub duration: Option<i32>,
|
||||
pub duration: Option<i64>,
|
||||
pub lyricist: Option<String>,
|
||||
pub composer: Option<String>,
|
||||
pub genre: Option<String>,
|
||||
|
@ -49,18 +47,18 @@ impl Song {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Queryable, Serialize, Deserialize)]
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Directory {
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
id: i32,
|
||||
pub id: i64,
|
||||
pub path: String,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub parent: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub year: Option<i32>,
|
||||
pub year: Option<i64>,
|
||||
pub album: Option<String>,
|
||||
pub artwork: Option<String>,
|
||||
pub date_added: i32,
|
||||
pub date_added: i64,
|
||||
}
|
||||
|
||||
impl Directory {
|
||||
|
|
|
@ -20,7 +20,7 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
IndexClean(#[from] cleaner::Error),
|
||||
#[error(transparent)]
|
||||
Database(#[from] diesel::result::Error),
|
||||
Database(#[from] sqlx::Error),
|
||||
#[error(transparent)]
|
||||
DatabaseConnection(#[from] db::Error),
|
||||
#[error(transparent)]
|
||||
|
@ -28,46 +28,44 @@ pub enum Error {
|
|||
}
|
||||
|
||||
impl Index {
|
||||
pub fn update(&self) -> Result<(), Error> {
|
||||
pub async fn update(&self) -> Result<(), Error> {
|
||||
let start = time::Instant::now();
|
||||
info!("Beginning library index update");
|
||||
|
||||
let album_art_pattern = self.settings_manager.get_index_album_art_pattern().ok();
|
||||
let album_art_pattern = self
|
||||
.settings_manager
|
||||
.get_index_album_art_pattern()
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let cleaner = Cleaner::new(self.db.clone(), self.vfs_manager.clone());
|
||||
cleaner.clean()?;
|
||||
cleaner.clean().await?;
|
||||
|
||||
let (insert_sender, insert_receiver) = crossbeam_channel::unbounded();
|
||||
let inserter_db = self.db.clone();
|
||||
let insertion_thread = std::thread::spawn(move || {
|
||||
let mut inserter = Inserter::new(inserter_db, insert_receiver);
|
||||
inserter.insert();
|
||||
let (insert_sender, insert_receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
let insertion = tokio::spawn({
|
||||
let db = self.db.clone();
|
||||
async {
|
||||
let mut inserter = Inserter::new(db, insert_receiver);
|
||||
inserter.insert().await;
|
||||
}
|
||||
});
|
||||
|
||||
let (collect_sender, collect_receiver) = crossbeam_channel::unbounded();
|
||||
let collector_thread = std::thread::spawn(move || {
|
||||
let collection = tokio::task::spawn_blocking(|| {
|
||||
let collector = Collector::new(collect_receiver, insert_sender, album_art_pattern);
|
||||
collector.collect();
|
||||
});
|
||||
|
||||
let vfs = self.vfs_manager.get_vfs()?;
|
||||
let traverser_thread = std::thread::spawn(move || {
|
||||
let vfs = self.vfs_manager.get_vfs().await?;
|
||||
let traversal = tokio::task::spawn_blocking(move || {
|
||||
let mounts = vfs.mounts();
|
||||
let traverser = Traverser::new(collect_sender);
|
||||
traverser.traverse(mounts.iter().map(|p| p.source.clone()).collect());
|
||||
});
|
||||
|
||||
if let Err(e) = traverser_thread.join() {
|
||||
error!("Error joining on traverser thread: {:?}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = collector_thread.join() {
|
||||
error!("Error joining on collector thread: {:?}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = insertion_thread.join() {
|
||||
error!("Error joining on inserter thread: {:?}", e);
|
||||
}
|
||||
traversal.await.unwrap();
|
||||
collection.await.unwrap();
|
||||
insertion.await.unwrap();
|
||||
|
||||
info!(
|
||||
"Library index update took {} seconds",
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
use diesel::prelude::*;
|
||||
use rayon::prelude::*;
|
||||
use sqlx::{QueryBuilder, Sqlite};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::app::vfs;
|
||||
use crate::db::{self, directories, songs, DB};
|
||||
use crate::db::{self, DB};
|
||||
|
||||
const INDEX_BUILDING_CLEAN_BUFFER_SIZE: usize = 500; // Deletions in each transaction
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Database(#[from] diesel::result::Error),
|
||||
Database(#[from] sqlx::Error),
|
||||
#[error(transparent)]
|
||||
DatabaseConnection(#[from] db::Error),
|
||||
#[error(transparent)]
|
||||
|
@ -29,19 +29,23 @@ impl Cleaner {
|
|||
Self { db, vfs_manager }
|
||||
}
|
||||
|
||||
pub fn clean(&self) -> Result<(), Error> {
|
||||
let vfs = self.vfs_manager.get_vfs()?;
|
||||
pub async fn clean(&self) -> Result<(), Error> {
|
||||
let vfs = self.vfs_manager.get_vfs().await?;
|
||||
|
||||
let all_directories: Vec<String> = {
|
||||
let mut connection = self.db.connect()?;
|
||||
directories::table
|
||||
.select(directories::path)
|
||||
.load(&mut connection)?
|
||||
};
|
||||
let (all_directories, all_songs) = {
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
let all_songs: Vec<String> = {
|
||||
let mut connection = self.db.connect()?;
|
||||
songs::table.select(songs::path).load(&mut connection)?
|
||||
let directories = sqlx::query_scalar!("SELECT path FROM directories")
|
||||
.fetch_all(connection.as_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let songs = sqlx::query_scalar!("SELECT path FROM songs")
|
||||
.fetch_all(connection.as_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(directories, songs)
|
||||
};
|
||||
|
||||
let list_missing_directories = || {
|
||||
|
@ -69,14 +73,26 @@ impl Cleaner {
|
|||
thread_pool.join(list_missing_directories, list_missing_songs);
|
||||
|
||||
{
|
||||
let mut connection = self.db.connect()?;
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
for chunk in missing_directories[..].chunks(INDEX_BUILDING_CLEAN_BUFFER_SIZE) {
|
||||
diesel::delete(directories::table.filter(directories::path.eq_any(chunk)))
|
||||
.execute(&mut connection)?;
|
||||
QueryBuilder::<Sqlite>::new("DELETE FROM directories WHERE path IN ")
|
||||
.push_tuples(chunk, |mut b, path| {
|
||||
b.push_bind(path);
|
||||
})
|
||||
.build()
|
||||
.execute(connection.as_mut())
|
||||
.await?;
|
||||
}
|
||||
|
||||
for chunk in missing_songs[..].chunks(INDEX_BUILDING_CLEAN_BUFFER_SIZE) {
|
||||
diesel::delete(songs::table.filter(songs::path.eq_any(chunk)))
|
||||
.execute(&mut connection)?;
|
||||
QueryBuilder::<Sqlite>::new("DELETE FROM songs WHERE path IN ")
|
||||
.push_tuples(chunk, |mut b, path| {
|
||||
b.push_bind(path);
|
||||
})
|
||||
.build()
|
||||
.execute(connection.as_mut())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
use crossbeam_channel::{Receiver, Sender};
|
||||
use log::error;
|
||||
use regex::Regex;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct Collector {
|
||||
receiver: Receiver<traverser::Directory>,
|
||||
sender: Sender<inserter::Item>,
|
||||
receiver: crossbeam_channel::Receiver<traverser::Directory>,
|
||||
sender: tokio::sync::mpsc::UnboundedSender<inserter::Item>,
|
||||
album_art_pattern: Option<Regex>,
|
||||
}
|
||||
|
||||
impl Collector {
|
||||
pub fn new(
|
||||
receiver: Receiver<traverser::Directory>,
|
||||
sender: Sender<inserter::Item>,
|
||||
receiver: crossbeam_channel::Receiver<traverser::Directory>,
|
||||
sender: tokio::sync::mpsc::UnboundedSender<inserter::Item>,
|
||||
album_art_pattern: Option<Regex>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
use crossbeam_channel::Receiver;
|
||||
use diesel::prelude::*;
|
||||
use log::error;
|
||||
use sqlx::{QueryBuilder, Sqlite};
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
|
||||
use crate::db::{directories, songs, DB};
|
||||
use crate::db::DB;
|
||||
|
||||
const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // Insertions in each transaction
|
||||
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = songs)]
|
||||
pub struct Song {
|
||||
pub path: String,
|
||||
pub parent: String,
|
||||
|
@ -26,8 +24,6 @@ pub struct Song {
|
|||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = directories)]
|
||||
pub struct Directory {
|
||||
pub path: String,
|
||||
pub parent: Option<String>,
|
||||
|
@ -44,14 +40,14 @@ pub enum Item {
|
|||
}
|
||||
|
||||
pub struct Inserter {
|
||||
receiver: Receiver<Item>,
|
||||
receiver: UnboundedReceiver<Item>,
|
||||
new_directories: Vec<Directory>,
|
||||
new_songs: Vec<Song>,
|
||||
db: DB,
|
||||
}
|
||||
|
||||
impl Inserter {
|
||||
pub fn new(db: DB, receiver: Receiver<Item>) -> Self {
|
||||
pub fn new(db: DB, receiver: UnboundedReceiver<Item>) -> Self {
|
||||
let new_directories = Vec::with_capacity(INDEX_BUILDING_INSERT_BUFFER_SIZE);
|
||||
let new_songs = Vec::with_capacity(INDEX_BUILDING_INSERT_BUFFER_SIZE);
|
||||
Self {
|
||||
|
@ -62,63 +58,90 @@ impl Inserter {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self) {
|
||||
while let Ok(item) = self.receiver.recv() {
|
||||
self.insert_item(item);
|
||||
pub async fn insert(&mut self) {
|
||||
while let Some(item) = self.receiver.recv().await {
|
||||
self.insert_item(item).await;
|
||||
}
|
||||
self.flush_directories().await;
|
||||
self.flush_songs().await;
|
||||
}
|
||||
|
||||
fn insert_item(&mut self, insert: Item) {
|
||||
async fn insert_item(&mut self, insert: Item) {
|
||||
match insert {
|
||||
Item::Directory(d) => {
|
||||
self.new_directories.push(d);
|
||||
if self.new_directories.len() >= INDEX_BUILDING_INSERT_BUFFER_SIZE {
|
||||
self.flush_directories();
|
||||
self.flush_directories().await;
|
||||
}
|
||||
}
|
||||
Item::Song(s) => {
|
||||
self.new_songs.push(s);
|
||||
if self.new_songs.len() >= INDEX_BUILDING_INSERT_BUFFER_SIZE {
|
||||
self.flush_songs();
|
||||
self.flush_songs().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn flush_directories(&mut self) {
|
||||
let res = self.db.connect().ok().and_then(|mut connection| {
|
||||
diesel::insert_into(directories::table)
|
||||
.values(&self.new_directories)
|
||||
.execute(&mut *connection) // TODO https://github.com/diesel-rs/diesel/issues/1822
|
||||
.ok()
|
||||
});
|
||||
if res.is_none() {
|
||||
error!("Could not insert new directories in database");
|
||||
}
|
||||
self.new_directories.clear();
|
||||
async fn flush_directories(&mut self) {
|
||||
let Ok(mut connection) = self.db.connect().await else {
|
||||
error!("Could not acquire connection to insert new directories in database");
|
||||
return;
|
||||
};
|
||||
|
||||
let result = QueryBuilder::<Sqlite>::new(
|
||||
"INSERT INTO directories(path, parent, artist, year, album, artwork, date_added) ",
|
||||
)
|
||||
.push_values(&self.new_directories, |mut b, directory| {
|
||||
b.push_bind(&directory.path)
|
||||
.push_bind(&directory.parent)
|
||||
.push_bind(&directory.artist)
|
||||
.push_bind(directory.year)
|
||||
.push_bind(&directory.album)
|
||||
.push_bind(&directory.artwork)
|
||||
.push_bind(directory.date_added);
|
||||
})
|
||||
.build()
|
||||
.execute(connection.as_mut())
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => self.new_directories.clear(),
|
||||
Err(_) => error!("Could not insert new directories in database"),
|
||||
};
|
||||
}
|
||||
|
||||
fn flush_songs(&mut self) {
|
||||
let res = self.db.connect().ok().and_then(|mut connection| {
|
||||
diesel::insert_into(songs::table)
|
||||
.values(&self.new_songs)
|
||||
.execute(&mut *connection) // TODO https://github.com/diesel-rs/diesel/issues/1822
|
||||
.ok()
|
||||
});
|
||||
if res.is_none() {
|
||||
error!("Could not insert new songs in database");
|
||||
}
|
||||
self.new_songs.clear();
|
||||
}
|
||||
}
|
||||
async fn flush_songs(&mut self) {
|
||||
let Ok(mut connection) = self.db.connect().await else {
|
||||
error!("Could not acquire connection to insert new songs in database");
|
||||
return;
|
||||
};
|
||||
|
||||
impl Drop for Inserter {
|
||||
fn drop(&mut self) {
|
||||
if !self.new_directories.is_empty() {
|
||||
self.flush_directories();
|
||||
}
|
||||
if !self.new_songs.is_empty() {
|
||||
self.flush_songs();
|
||||
}
|
||||
let result = QueryBuilder::<Sqlite>::new("INSERT INTO songs(path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration, lyricist, composer, genre, label) ")
|
||||
.push_values(&self.new_songs, |mut b, song| {
|
||||
b.push_bind(&song.path)
|
||||
.push_bind(&song.parent)
|
||||
.push_bind(song.track_number)
|
||||
.push_bind(song.disc_number)
|
||||
.push_bind(&song.title)
|
||||
.push_bind(&song.artist)
|
||||
.push_bind(&song.album_artist)
|
||||
.push_bind(song.year)
|
||||
.push_bind(&song.album)
|
||||
.push_bind(&song.artwork)
|
||||
.push_bind(song.duration)
|
||||
.push_bind(&song.lyricist)
|
||||
.push_bind(&song.composer)
|
||||
.push_bind(&song.genre)
|
||||
.push_bind(&song.label);
|
||||
})
|
||||
.build()
|
||||
.execute(connection.as_mut())
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => self.new_songs.clear(),
|
||||
Err(_) => error!("Could not insert new songs in database"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ impl Manager {
|
|||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn link(&self, username: &str, lastfm_token: &str) -> Result<(), Error> {
|
||||
pub async fn link(&self, username: &str, lastfm_token: &str) -> Result<(), Error> {
|
||||
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY, LASTFM_API_SECRET);
|
||||
let auth_response = scrobbler
|
||||
.authenticate_with_token(lastfm_token)
|
||||
|
@ -52,28 +52,30 @@ impl Manager {
|
|||
|
||||
self.user_manager
|
||||
.lastfm_link(username, &auth_response.name, &auth_response.key)
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn unlink(&self, username: &str) -> Result<(), Error> {
|
||||
pub async fn unlink(&self, username: &str) -> Result<(), Error> {
|
||||
self.user_manager
|
||||
.lastfm_unlink(username)
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn scrobble(&self, username: &str, track: &Path) -> Result<(), Error> {
|
||||
pub async fn scrobble(&self, username: &str, track: &Path) -> Result<(), Error> {
|
||||
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)?;
|
||||
let scrobble = self.scrobble_from_path(track).await?;
|
||||
let auth_token = self.user_manager.get_lastfm_session_key(username).await?;
|
||||
scrobbler.authenticate_with_session_key(&auth_token);
|
||||
scrobbler.scrobble(&scrobble).map_err(Error::Scrobble)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn now_playing(&self, username: &str, track: &Path) -> Result<(), Error> {
|
||||
pub async fn now_playing(&self, username: &str, track: &Path) -> Result<(), Error> {
|
||||
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)?;
|
||||
let scrobble = self.scrobble_from_path(track).await?;
|
||||
let auth_token = self.user_manager.get_lastfm_session_key(username).await?;
|
||||
scrobbler.authenticate_with_session_key(&auth_token);
|
||||
scrobbler
|
||||
.now_playing(&scrobble)
|
||||
|
@ -81,8 +83,8 @@ impl Manager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble, Error> {
|
||||
let song = self.index.get_song(track)?;
|
||||
async fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble, Error> {
|
||||
let song = self.index.get_song(track).await?;
|
||||
Ok(Scrobble::new(
|
||||
song.artist.as_deref().unwrap_or(""),
|
||||
song.title.as_deref().unwrap_or(""),
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
use core::clone::Clone;
|
||||
use diesel::prelude::*;
|
||||
use diesel::sql_types;
|
||||
use diesel::BelongingToDsl;
|
||||
use std::path::Path;
|
||||
use sqlx::{Acquire, QueryBuilder, Sqlite};
|
||||
|
||||
use crate::app::index::Song;
|
||||
use crate::app::vfs;
|
||||
use crate::db::{self, playlist_songs, playlists, users, DB};
|
||||
use crate::db::{self, DB};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Database(#[from] diesel::result::Error),
|
||||
Database(#[from] sqlx::Error),
|
||||
#[error(transparent)]
|
||||
DatabaseConnection(#[from] db::Error),
|
||||
#[error("User not found")]
|
||||
|
@ -33,148 +30,138 @@ impl Manager {
|
|||
Self { db, vfs_manager }
|
||||
}
|
||||
|
||||
pub fn list_playlists(&self, owner: &str) -> Result<Vec<String>, Error> {
|
||||
let mut connection = self.db.connect()?;
|
||||
pub async fn list_playlists(&self, owner: &str) -> Result<Vec<String>, Error> {
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
let user: User = {
|
||||
use self::users::dsl::*;
|
||||
users
|
||||
.filter(name.eq(owner))
|
||||
.select((id,))
|
||||
.first(&mut connection)
|
||||
.optional()?
|
||||
.ok_or(Error::UserNotFound)?
|
||||
};
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
||||
.fetch_optional(connection.as_mut())
|
||||
.await?
|
||||
.ok_or(Error::UserNotFound)?;
|
||||
|
||||
{
|
||||
use self::playlists::dsl::*;
|
||||
let found_playlists: Vec<String> = Playlist::belonging_to(&user)
|
||||
.select(name)
|
||||
.load(&mut connection)?;
|
||||
Ok(found_playlists)
|
||||
}
|
||||
Ok(
|
||||
sqlx::query_scalar!("SELECT name FROM playlists WHERE owner = $1", user_id)
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn save_playlist(
|
||||
pub async 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 vfs = self.vfs_manager.get_vfs().await?;
|
||||
|
||||
{
|
||||
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()?
|
||||
.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)?;
|
||||
|
||||
playlist = {
|
||||
use self::playlists::dsl::*;
|
||||
playlists
|
||||
.select((id, owner))
|
||||
.filter(name.eq(playlist_name).and(owner.eq(user.id)))
|
||||
.get_result(&mut connection)?
|
||||
}
|
||||
struct PlaylistSong {
|
||||
path: String,
|
||||
ordering: i64,
|
||||
}
|
||||
|
||||
let mut new_songs: Vec<NewPlaylistSong> = Vec::with_capacity(content.len());
|
||||
|
||||
let mut new_songs: Vec<PlaylistSong> = Vec::with_capacity(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)
|
||||
.virtual_to_real(path)
|
||||
.ok()
|
||||
.and_then(|p| p.to_str().map(|s| s.to_owned()))
|
||||
{
|
||||
new_songs.push(NewPlaylistSong {
|
||||
playlist: playlist.id,
|
||||
new_songs.push(PlaylistSong {
|
||||
path: real_path,
|
||||
ordering: i as i32,
|
||||
ordering: i as i64,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
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)?;
|
||||
// Create playlist
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
// Insert content
|
||||
diesel::insert_into(playlist_songs::table)
|
||||
.values(&new_songs)
|
||||
.execute(&mut *connection)?; // TODO https://github.com/diesel-rs/diesel/issues/1822
|
||||
Ok(())
|
||||
})?;
|
||||
// Find owner
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
||||
.fetch_optional(connection.as_mut())
|
||||
.await?
|
||||
.ok_or(Error::UserNotFound)?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO playlists (owner, name) VALUES($1, $2)",
|
||||
user_id,
|
||||
playlist_name
|
||||
)
|
||||
.execute(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
let playlist_id = sqlx::query_scalar!(
|
||||
"SELECT id FROM playlists WHERE owner = $1 AND name = $2",
|
||||
user_id,
|
||||
playlist_name
|
||||
)
|
||||
.fetch_one(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
connection.acquire().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM playlist_songs WHERE playlist = $1",
|
||||
playlist_id
|
||||
)
|
||||
.execute(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
if !new_songs.is_empty() {
|
||||
QueryBuilder::<Sqlite>::new("INSERT INTO playlist_songs (playlist, path, ordering) ")
|
||||
.push_values(new_songs, |mut b, song| {
|
||||
b.push_bind(playlist_id)
|
||||
.push_bind(song.path)
|
||||
.push_bind(song.ordering);
|
||||
})
|
||||
.build()
|
||||
.execute(connection.as_mut())
|
||||
.await?;
|
||||
}
|
||||
|
||||
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>;
|
||||
pub async fn read_playlist(
|
||||
&self,
|
||||
playlist_name: &str,
|
||||
owner: &str,
|
||||
) -> Result<Vec<Song>, Error> {
|
||||
let vfs = self.vfs_manager.get_vfs().await?;
|
||||
|
||||
{
|
||||
let mut connection = self.db.connect()?;
|
||||
let songs = {
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
// Find owner
|
||||
let user: User = {
|
||||
use self::users::dsl::*;
|
||||
users
|
||||
.filter(name.eq(owner))
|
||||
.select((id,))
|
||||
.first(&mut connection)
|
||||
.optional()?
|
||||
.ok_or(Error::UserNotFound)?
|
||||
};
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
||||
.fetch_optional(connection.as_mut())
|
||||
.await?
|
||||
.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()?
|
||||
.ok_or(Error::PlaylistNotFound)?
|
||||
};
|
||||
let playlist_id = sqlx::query_scalar!(
|
||||
"SELECT id FROM playlists WHERE name = $1 and owner = $2",
|
||||
playlist_name,
|
||||
user_id
|
||||
)
|
||||
.fetch_optional(connection.as_mut())
|
||||
.await?
|
||||
.ok_or(Error::PlaylistNotFound)?;
|
||||
|
||||
// Select songs. Not using Diesel because we need to LEFT JOIN using a custom column
|
||||
let query = diesel::sql_query(
|
||||
// List songs
|
||||
sqlx::query_as!(
|
||||
Song,
|
||||
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)?;
|
||||
}
|
||||
SELECT s.*
|
||||
FROM playlist_songs ps
|
||||
INNER JOIN songs s ON ps.path = s.path
|
||||
WHERE ps.playlist = $1
|
||||
ORDER BY ps.ordering
|
||||
"#,
|
||||
playlist_id
|
||||
)
|
||||
.fetch_all(connection.as_mut())
|
||||
.await?
|
||||
};
|
||||
|
||||
// Map real path to virtual paths
|
||||
let virtual_songs = songs
|
||||
|
@ -185,64 +172,30 @@ impl Manager {
|
|||
Ok(virtual_songs)
|
||||
}
|
||||
|
||||
pub fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> {
|
||||
let mut connection = self.db.connect()?;
|
||||
pub async fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> {
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
let user: User = {
|
||||
use self::users::dsl::*;
|
||||
users
|
||||
.filter(name.eq(owner))
|
||||
.select((id,))
|
||||
.first(&mut connection)
|
||||
.optional()?
|
||||
.ok_or(Error::UserNotFound)?
|
||||
};
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE name = $1", owner)
|
||||
.fetch_optional(connection.as_mut())
|
||||
.await?
|
||||
.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)? {
|
||||
0 => Err(Error::PlaylistNotFound),
|
||||
_ => Ok(()),
|
||||
}
|
||||
let num_deletions = sqlx::query_scalar!(
|
||||
"DELETE FROM playlists WHERE owner = $1 AND name = $2",
|
||||
user_id,
|
||||
playlist_name
|
||||
)
|
||||
.execute(connection.as_mut())
|
||||
.await?
|
||||
.rows_affected();
|
||||
|
||||
match num_deletions {
|
||||
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,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -255,33 +208,41 @@ mod test {
|
|||
const TEST_PLAYLIST_NAME: &str = "Chill & Grill";
|
||||
const TEST_MOUNT_NAME: &str = "root";
|
||||
|
||||
#[test]
|
||||
fn save_playlist_golden_path() {
|
||||
#[tokio::test]
|
||||
async fn save_playlist_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.user(TEST_USER, TEST_PASSWORD, false)
|
||||
.build();
|
||||
.build()
|
||||
.await;
|
||||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &Vec::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap();
|
||||
let found_playlists = ctx
|
||||
.playlist_manager
|
||||
.list_playlists(TEST_USER)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(found_playlists.len(), 1);
|
||||
assert_eq!(found_playlists[0], TEST_PLAYLIST_NAME);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_playlist_is_idempotent() {
|
||||
#[tokio::test]
|
||||
async fn save_playlist_is_idempotent() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.user(TEST_USER, TEST_PASSWORD, false)
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
.build()
|
||||
.await;
|
||||
|
||||
ctx.index.update().unwrap();
|
||||
ctx.index.update().await.unwrap();
|
||||
|
||||
let playlist_content: Vec<String> = ctx
|
||||
.index
|
||||
.flatten(Path::new(TEST_MOUNT_NAME))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|s| s.path)
|
||||
|
@ -290,51 +251,63 @@ mod test {
|
|||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let songs = ctx
|
||||
.playlist_manager
|
||||
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(songs.len(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_playlist_golden_path() {
|
||||
#[tokio::test]
|
||||
async fn delete_playlist_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.user(TEST_USER, TEST_PASSWORD, false)
|
||||
.build();
|
||||
.build()
|
||||
.await;
|
||||
|
||||
let playlist_content = Vec::new();
|
||||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
ctx.playlist_manager
|
||||
.delete_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap();
|
||||
let found_playlists = ctx
|
||||
.playlist_manager
|
||||
.list_playlists(TEST_USER)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(found_playlists.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_playlist_golden_path() {
|
||||
#[tokio::test]
|
||||
async fn read_playlist_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.user(TEST_USER, TEST_PASSWORD, false)
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
.build()
|
||||
.await;
|
||||
|
||||
ctx.index.update().unwrap();
|
||||
ctx.index.update().await.unwrap();
|
||||
|
||||
let playlist_content: Vec<String> = ctx
|
||||
.index
|
||||
.flatten(Path::new(TEST_MOUNT_NAME))
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|s| s.path)
|
||||
|
@ -343,11 +316,13 @@ mod test {
|
|||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let songs = ctx
|
||||
.playlist_manager
|
||||
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(songs.len(), 13);
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
use diesel::prelude::*;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use std::convert::TryInto;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::db::{self, misc_settings, DB};
|
||||
use crate::db::{self, DB};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
|
@ -19,7 +17,7 @@ pub enum Error {
|
|||
#[error("Index album art pattern is not a valid regex")]
|
||||
IndexAlbumArtPatternInvalid,
|
||||
#[error(transparent)]
|
||||
Database(#[from] diesel::result::Error),
|
||||
Database(#[from] sqlx::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
@ -27,15 +25,15 @@ pub struct AuthSecret {
|
|||
pub key: [u8; 32],
|
||||
}
|
||||
|
||||
#[derive(Debug, Queryable)]
|
||||
#[derive(Debug)]
|
||||
pub struct Settings {
|
||||
pub index_sleep_duration_seconds: i32,
|
||||
pub index_sleep_duration_seconds: i64,
|
||||
pub index_album_art_pattern: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct NewSettings {
|
||||
pub reindex_every_n_seconds: Option<i32>,
|
||||
pub reindex_every_n_seconds: Option<i64>,
|
||||
pub album_art_pattern: Option<String>,
|
||||
}
|
||||
|
||||
|
@ -49,64 +47,57 @@ impl Manager {
|
|||
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::AuthenticationSecretNotFound,
|
||||
e => e.into(),
|
||||
})?;
|
||||
secret
|
||||
pub async fn get_auth_secret(&self) -> Result<AuthSecret, Error> {
|
||||
sqlx::query_scalar!("SELECT auth_secret FROM config")
|
||||
.fetch_one(self.db.connect().await?.as_mut())
|
||||
.await?
|
||||
.try_into()
|
||||
.map_err(|_| Error::AuthenticationSecretInvalid)
|
||||
.map(|key| AuthSecret { key })
|
||||
}
|
||||
|
||||
pub fn get_index_sleep_duration(&self) -> Result<Duration, Error> {
|
||||
let settings = self.read()?;
|
||||
pub async fn get_index_sleep_duration(&self) -> Result<Duration, Error> {
|
||||
let settings = self.read().await?;
|
||||
Ok(Duration::from_secs(
|
||||
settings.index_sleep_duration_seconds as u64,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_index_album_art_pattern(&self) -> Result<Regex, Error> {
|
||||
let settings = self.read()?;
|
||||
pub async fn get_index_album_art_pattern(&self) -> Result<Regex, Error> {
|
||||
let settings = self.read().await?;
|
||||
let regex = Regex::new(&format!("(?i){}", &settings.index_album_art_pattern))
|
||||
.map_err(|_| Error::IndexAlbumArtPatternInvalid)?;
|
||||
Ok(regex)
|
||||
}
|
||||
|
||||
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(|e| match e {
|
||||
diesel::result::Error::NotFound => Error::MiscSettingsNotFound,
|
||||
e => e.into(),
|
||||
})?;
|
||||
|
||||
Ok(settings)
|
||||
pub async fn read(&self) -> Result<Settings, Error> {
|
||||
Ok(sqlx::query_as!(
|
||||
Settings,
|
||||
"SELECT index_sleep_duration_seconds,index_album_art_pattern FROM config"
|
||||
)
|
||||
.fetch_one(self.db.connect().await?.as_mut())
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub fn amend(&self, new_settings: &NewSettings) -> Result<(), Error> {
|
||||
let mut connection = self.db.connect()?;
|
||||
pub async fn amend(&self, new_settings: &NewSettings) -> Result<(), Error> {
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
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))
|
||||
.execute(&mut connection)?;
|
||||
sqlx::query!(
|
||||
"UPDATE config SET index_sleep_duration_seconds = $1",
|
||||
sleep_duration
|
||||
)
|
||||
.execute(connection.as_mut())
|
||||
.await?;
|
||||
}
|
||||
|
||||
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)?;
|
||||
sqlx::query!(
|
||||
"UPDATE config SET index_album_art_pattern = $1",
|
||||
album_art_pattern
|
||||
)
|
||||
.execute(connection.as_mut())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::app::{config, ddns, index::Index, lastfm, playlist, settings, thumbnail, user, vfs};
|
||||
use crate::app::{config, ddns, index::Index, playlist, settings, user, vfs};
|
||||
use crate::db::DB;
|
||||
use crate::test::*;
|
||||
|
||||
|
@ -9,13 +9,10 @@ pub struct Context {
|
|||
pub index: Index,
|
||||
pub config_manager: config::Manager,
|
||||
pub ddns_manager: ddns::Manager,
|
||||
pub lastfm_manager: lastfm::Manager,
|
||||
pub playlist_manager: playlist::Manager,
|
||||
pub settings_manager: settings::Manager,
|
||||
pub thumbnail_manager: thumbnail::Manager,
|
||||
pub user_manager: user::Manager,
|
||||
pub vfs_manager: vfs::Manager,
|
||||
pub test_directory: PathBuf,
|
||||
}
|
||||
|
||||
pub struct ContextBuilder {
|
||||
|
@ -53,14 +50,12 @@ impl ContextBuilder {
|
|||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Context {
|
||||
let cache_output_dir = self.test_directory.join("cache");
|
||||
pub async fn build(self) -> Context {
|
||||
let db_path = self.test_directory.join("db.sqlite");
|
||||
|
||||
let db = DB::new(&db_path).unwrap();
|
||||
let db = DB::new(&db_path).await.unwrap();
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let auth_secret = settings_manager.get_auth_secret().await.unwrap();
|
||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let ddns_manager = ddns::Manager::new(db.clone());
|
||||
|
@ -72,23 +67,18 @@ impl ContextBuilder {
|
|||
);
|
||||
let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone());
|
||||
let playlist_manager = playlist::Manager::new(db.clone(), vfs_manager.clone());
|
||||
let thumbnail_manager = thumbnail::Manager::new(cache_output_dir);
|
||||
let lastfm_manager = lastfm::Manager::new(index.clone(), user_manager.clone());
|
||||
|
||||
config_manager.apply(&self.config).unwrap();
|
||||
config_manager.apply(&self.config).await.unwrap();
|
||||
|
||||
Context {
|
||||
db,
|
||||
index,
|
||||
config_manager,
|
||||
ddns_manager,
|
||||
lastfm_manager,
|
||||
playlist_manager,
|
||||
settings_manager,
|
||||
thumbnail_manager,
|
||||
user_manager,
|
||||
vfs_manager,
|
||||
test_directory: self.test_directory,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
323
src/app/user.rs
323
src/app/user.rs
|
@ -1,4 +1,3 @@
|
|||
use diesel::prelude::*;
|
||||
use pbkdf2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
|
||||
use pbkdf2::Pbkdf2;
|
||||
use rand::rngs::OsRng;
|
||||
|
@ -6,12 +5,12 @@ use serde::{Deserialize, Serialize};
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::app::settings::AuthSecret;
|
||||
use crate::db::{self, users, DB};
|
||||
use crate::db::{self, DB};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Database(#[from] diesel::result::Error),
|
||||
Database(#[from] sqlx::Error),
|
||||
#[error(transparent)]
|
||||
DatabaseConnection(#[from] db::Error),
|
||||
#[error("Cannot use empty username")]
|
||||
|
@ -36,12 +35,10 @@ pub enum Error {
|
|||
BrancaTokenEncoding,
|
||||
}
|
||||
|
||||
#[derive(Debug, Insertable, Queryable)]
|
||||
#[diesel(table_name = users)]
|
||||
#[derive(Debug)]
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub password_hash: String,
|
||||
pub admin: i32,
|
||||
pub admin: i64,
|
||||
}
|
||||
|
||||
impl User {
|
||||
|
@ -90,61 +87,62 @@ impl Manager {
|
|||
Self { db, auth_secret }
|
||||
}
|
||||
|
||||
pub fn create(&self, new_user: &NewUser) -> Result<(), Error> {
|
||||
pub async 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(),
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO users (name, password_hash, admin) VALUES($1, $2, $3)",
|
||||
new_user.name,
|
||||
password_hash,
|
||||
admin: new_user.admin as i32,
|
||||
};
|
||||
new_user.admin
|
||||
)
|
||||
.execute(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
|
||||
diesel::insert_into(users::table)
|
||||
.values(&new_user)
|
||||
.execute(&mut connection)?;
|
||||
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)?;
|
||||
pub async fn delete(&self, username: &str) -> Result<(), Error> {
|
||||
sqlx::query!("DELETE FROM users WHERE name = $1", username)
|
||||
.execute(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_password(&self, username: &str, password: &str) -> Result<(), Error> {
|
||||
pub async 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)?;
|
||||
sqlx::query!(
|
||||
"UPDATE users SET password_hash = $1 WHERE name = $2",
|
||||
hash,
|
||||
username
|
||||
)
|
||||
.execute(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
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)?;
|
||||
pub async fn set_is_admin(&self, username: &str, is_admin: bool) -> Result<(), Error> {
|
||||
sqlx::query!(
|
||||
"UPDATE users SET admin = $1 WHERE name = $2",
|
||||
is_admin,
|
||||
username
|
||||
)
|
||||
.execute(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
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)
|
||||
pub async fn login(&self, username: &str, password: &str) -> Result<AuthToken, Error> {
|
||||
match sqlx::query_scalar!("SELECT password_hash FROM users WHERE name = $1", username)
|
||||
.fetch_optional(self.db.connect().await?.as_mut())
|
||||
.await?
|
||||
{
|
||||
Err(diesel::result::Error::NotFound) => Err(Error::IncorrectUsername),
|
||||
Ok(hash) => {
|
||||
None => Err(Error::IncorrectUsername),
|
||||
Some(hash) => {
|
||||
let hash: String = hash;
|
||||
if verify_password(&hash, password) {
|
||||
let authorization = Authorization {
|
||||
|
@ -156,17 +154,16 @@ impl Manager {
|
|||
Err(Error::IncorrectPassword)
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authenticate(
|
||||
pub async 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)? {
|
||||
if self.exists(&authorization.username).await? {
|
||||
Ok(authorization)
|
||||
} else {
|
||||
Err(Error::IncorrectUsername)
|
||||
|
@ -208,86 +205,76 @@ impl Manager {
|
|||
.map(AuthToken)
|
||||
}
|
||||
|
||||
pub fn count(&self) -> Result<i64, Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
let count = users.count().get_result(&mut connection)?;
|
||||
pub async fn count(&self) -> Result<i32, Error> {
|
||||
let count = sqlx::query_scalar!("SELECT COUNT(*) FROM users")
|
||||
.fetch_one(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Result<Vec<User>, Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
let listed_users = users
|
||||
.select((name, password_hash, admin))
|
||||
.get_results(&mut connection)?;
|
||||
pub async fn list(&self) -> Result<Vec<User>, Error> {
|
||||
let listed_users = sqlx::query_as!(User, "SELECT name, admin FROM users")
|
||||
.fetch_all(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
Ok(listed_users)
|
||||
}
|
||||
|
||||
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)?;
|
||||
Ok(!results.is_empty())
|
||||
pub async fn exists(&self, username: &str) -> Result<bool, Error> {
|
||||
Ok(
|
||||
0 < sqlx::query_scalar!("SELECT COUNT(*) FROM users WHERE name = $1", username)
|
||||
.fetch_one(self.db.connect().await?.as_mut())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
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)?;
|
||||
Ok(is_admin != 0)
|
||||
pub async fn is_admin(&self, username: &str) -> Result<bool, Error> {
|
||||
Ok(
|
||||
0 < sqlx::query_scalar!("SELECT admin FROM users WHERE name = $1", username)
|
||||
.fetch_one(self.db.connect().await?.as_mut())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn read_preferences(&self, username: &str) -> Result<Preferences, Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
let (theme_base, theme_accent, read_lastfm_username) = users
|
||||
.select((web_theme_base, web_theme_accent, lastfm_username))
|
||||
.filter(name.eq(username))
|
||||
.get_result(&mut connection)?;
|
||||
Ok(Preferences {
|
||||
web_theme_base: theme_base,
|
||||
web_theme_accent: theme_accent,
|
||||
lastfm_username: read_lastfm_username,
|
||||
})
|
||||
pub async fn read_preferences(&self, username: &str) -> Result<Preferences, Error> {
|
||||
Ok(sqlx::query_as!(
|
||||
Preferences,
|
||||
"SELECT web_theme_base, web_theme_accent, lastfm_username FROM users WHERE name = $1",
|
||||
username
|
||||
)
|
||||
.fetch_one(self.db.connect().await?.as_mut())
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub fn write_preferences(
|
||||
pub async fn write_preferences(
|
||||
&self,
|
||||
username: &str,
|
||||
preferences: &Preferences,
|
||||
) -> Result<(), Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
diesel::update(users.filter(name.eq(username)))
|
||||
.set((
|
||||
web_theme_base.eq(&preferences.web_theme_base),
|
||||
web_theme_accent.eq(&preferences.web_theme_accent),
|
||||
))
|
||||
.execute(&mut connection)?;
|
||||
sqlx::query!(
|
||||
"UPDATE users SET web_theme_base = $1, web_theme_accent = $2 WHERE name = $3",
|
||||
preferences.web_theme_base,
|
||||
preferences.web_theme_accent,
|
||||
username
|
||||
)
|
||||
.execute(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn lastfm_link(
|
||||
pub async 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)?;
|
||||
sqlx::query!(
|
||||
"UPDATE users SET lastfm_username = $1, lastfm_session_key = $2 WHERE name = $3",
|
||||
lastfm_login,
|
||||
session_key,
|
||||
username
|
||||
)
|
||||
.execute(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -298,27 +285,29 @@ impl Manager {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn get_lastfm_session_key(&self, username: &str) -> Result<String, Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
let token: Option<String> = users
|
||||
.filter(name.eq(username))
|
||||
.select(lastfm_session_key)
|
||||
.get_result(&mut connection)?;
|
||||
pub async fn get_lastfm_session_key(&self, username: &str) -> Result<String, Error> {
|
||||
let token: Option<String> = sqlx::query_scalar!(
|
||||
"SELECT lastfm_session_key FROM users WHERE name = $1",
|
||||
username
|
||||
)
|
||||
.fetch_one(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
token.ok_or(Error::MissingLastFMSessionKey)
|
||||
}
|
||||
|
||||
pub fn is_lastfm_linked(&self, username: &str) -> bool {
|
||||
self.get_lastfm_session_key(username).is_ok()
|
||||
pub async fn is_lastfm_linked(&self, username: &str) -> bool {
|
||||
self.get_lastfm_session_key(username).await.is_ok()
|
||||
}
|
||||
|
||||
pub fn lastfm_unlink(&self, username: &str) -> Result<(), Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let mut connection = self.db.connect()?;
|
||||
pub async fn lastfm_unlink(&self, username: &str) -> Result<(), Error> {
|
||||
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)?;
|
||||
sqlx::query!(
|
||||
"UPDATE users SET lastfm_session_key = $1, lastfm_username = $1 WHERE name = $2",
|
||||
null,
|
||||
username
|
||||
)
|
||||
.execute(self.db.connect().await?.as_mut())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -352,9 +341,9 @@ mod test {
|
|||
const TEST_USERNAME: &str = "Walter";
|
||||
const TEST_PASSWORD: &str = "super_secret!";
|
||||
|
||||
#[test]
|
||||
fn create_delete_user_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn create_delete_user_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
|
@ -362,56 +351,56 @@ mod test {
|
|||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert_eq!(ctx.user_manager.list().unwrap().len(), 1);
|
||||
ctx.user_manager.create(&new_user).await.unwrap();
|
||||
assert_eq!(ctx.user_manager.list().await.unwrap().len(), 1);
|
||||
|
||||
ctx.user_manager.delete(&new_user.name).unwrap();
|
||||
assert_eq!(ctx.user_manager.list().unwrap().len(), 0);
|
||||
ctx.user_manager.delete(&new_user.name).await.unwrap();
|
||||
assert_eq!(ctx.user_manager.list().await.unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_user_with_blank_username() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn cannot_create_user_with_blank_username() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
let new_user = NewUser {
|
||||
name: "".to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
assert!(matches!(
|
||||
ctx.user_manager.create(&new_user).unwrap_err(),
|
||||
ctx.user_manager.create(&new_user).await.unwrap_err(),
|
||||
Error::EmptyUsername
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_user_with_blank_password() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn cannot_create_user_with_blank_password() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: "".to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
assert!(matches!(
|
||||
ctx.user_manager.create(&new_user).unwrap_err(),
|
||||
ctx.user_manager.create(&new_user).await.unwrap_err(),
|
||||
Error::EmptyPassword
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_duplicate_user() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn cannot_create_duplicate_user() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).unwrap_err();
|
||||
ctx.user_manager.create(&new_user).await.unwrap();
|
||||
ctx.user_manager.create(&new_user).await.unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_read_write_preferences() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn can_read_write_preferences() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
|
||||
let new_preferences = Preferences {
|
||||
web_theme_base: Some("very-dark-theme".to_owned()),
|
||||
|
@ -424,19 +413,20 @@ mod test {
|
|||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).await.unwrap();
|
||||
|
||||
ctx.user_manager
|
||||
.write_preferences(TEST_USERNAME, &new_preferences)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let read_preferences = ctx.user_manager.read_preferences("Walter").unwrap();
|
||||
let read_preferences = ctx.user_manager.read_preferences("Walter").await.unwrap();
|
||||
assert_eq!(new_preferences, read_preferences);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_rejects_bad_password() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn login_rejects_bad_password() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
|
@ -444,30 +434,35 @@ mod test {
|
|||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).await.unwrap();
|
||||
assert!(matches!(
|
||||
ctx.user_manager
|
||||
.login(TEST_USERNAME, "not the password")
|
||||
.await
|
||||
.unwrap_err(),
|
||||
Error::IncorrectPassword
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn login_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert!(ctx.user_manager.login(TEST_USERNAME, TEST_PASSWORD).is_ok())
|
||||
ctx.user_manager.create(&new_user).await.unwrap();
|
||||
assert!(ctx
|
||||
.user_manager
|
||||
.login(TEST_USERNAME, TEST_PASSWORD)
|
||||
.await
|
||||
.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_rejects_bad_token() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn authenticate_rejects_bad_token() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
|
@ -475,17 +470,18 @@ mod test {
|
|||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).await.unwrap();
|
||||
let fake_token = AuthToken("fake token".to_owned());
|
||||
assert!(ctx
|
||||
.user_manager
|
||||
.authenticate(&fake_token, AuthorizationScope::PolarisAuth)
|
||||
.await
|
||||
.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn authenticate_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
|
@ -493,14 +489,16 @@ mod test {
|
|||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).await.unwrap();
|
||||
let token = ctx
|
||||
.user_manager
|
||||
.login(TEST_USERNAME, TEST_PASSWORD)
|
||||
.await
|
||||
.unwrap();
|
||||
let authorization = ctx
|
||||
.user_manager
|
||||
.authenticate(&token, AuthorizationScope::PolarisAuth)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
authorization,
|
||||
|
@ -511,9 +509,9 @@ mod test {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_validates_scope() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
#[tokio::test]
|
||||
async fn authenticate_validates_scope() {
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build().await;
|
||||
|
||||
let new_user = NewUser {
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
|
@ -521,14 +519,15 @@ mod test {
|
|||
admin: false,
|
||||
};
|
||||
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).await.unwrap();
|
||||
let token = ctx
|
||||
.user_manager
|
||||
.generate_lastfm_link_token(TEST_USERNAME)
|
||||
.unwrap();
|
||||
let authorization = ctx
|
||||
.user_manager
|
||||
.authenticate(&token, AuthorizationScope::PolarisAuth);
|
||||
.authenticate(&token, AuthorizationScope::PolarisAuth)
|
||||
.await;
|
||||
assert!(matches!(
|
||||
authorization.unwrap_err(),
|
||||
Error::IncorrectAuthorizationScope
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use core::ops::Deref;
|
||||
use diesel::prelude::*;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Acquire, QueryBuilder, Sqlite};
|
||||
use std::path::{self, Path, PathBuf};
|
||||
|
||||
use crate::db::{self, mount_points, DB};
|
||||
use crate::db::{self, DB};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
|
@ -15,11 +15,10 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
DatabaseConnection(#[from] db::Error),
|
||||
#[error(transparent)]
|
||||
Database(#[from] diesel::result::Error),
|
||||
Database(#[from] sqlx::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Eq, Queryable, Serialize)]
|
||||
#[diesel(table_name = mount_points)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub struct MountDir {
|
||||
pub source: String,
|
||||
pub name: String,
|
||||
|
@ -98,31 +97,39 @@ impl Manager {
|
|||
Self { db }
|
||||
}
|
||||
|
||||
pub fn get_vfs(&self) -> Result<VFS, Error> {
|
||||
let mount_dirs = self.mount_dirs()?;
|
||||
pub async fn get_vfs(&self) -> Result<VFS, Error> {
|
||||
let mount_dirs = self.mount_dirs().await?;
|
||||
let mounts = mount_dirs.into_iter().map(|p| p.into()).collect();
|
||||
Ok(VFS::new(mounts))
|
||||
}
|
||||
|
||||
pub fn mount_dirs(&self) -> Result<Vec<MountDir>, Error> {
|
||||
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 async fn mount_dirs(&self) -> Result<Vec<MountDir>, Error> {
|
||||
Ok(
|
||||
sqlx::query_as!(MountDir, "SELECT source, name FROM mount_points")
|
||||
.fetch_all(self.db.connect().await?.as_mut())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_mount_dirs(&self, mount_dirs: &[MountDir]) -> Result<(), Error> {
|
||||
let mut connection = self.db.connect()?;
|
||||
connection.transaction::<_, diesel::result::Error, _>(|connection| {
|
||||
use self::mount_points::dsl::*;
|
||||
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(())
|
||||
})?;
|
||||
pub async fn set_mount_dirs(&self, mount_dirs: &[MountDir]) -> Result<(), Error> {
|
||||
let mut connection = self.db.connect().await?;
|
||||
|
||||
connection.begin().await?;
|
||||
|
||||
sqlx::query!("DELETE FROM mount_points")
|
||||
.execute(connection.as_mut())
|
||||
.await?;
|
||||
|
||||
if !mount_dirs.is_empty() {
|
||||
QueryBuilder::<Sqlite>::new("INSERT INTO mount_points(source, name) ")
|
||||
.push_values(mount_dirs, |mut b, dir| {
|
||||
b.push_bind(&dir.source).push_bind(&dir.name);
|
||||
})
|
||||
.build()
|
||||
.execute(connection.as_mut())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
88
src/db.rs
88
src/db.rs
|
@ -1,15 +1,13 @@
|
|||
use diesel::r2d2::{self, ConnectionManager, PooledConnection};
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use diesel::RunQueryDsl;
|
||||
use diesel_migrations::EmbeddedMigrations;
|
||||
use diesel_migrations::MigrationHarness;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
mod schema;
|
||||
use sqlx::{
|
||||
migrate::Migrator,
|
||||
pool::PoolConnection,
|
||||
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqliteSynchronous},
|
||||
Sqlite,
|
||||
};
|
||||
|
||||
pub use self::schema::*;
|
||||
|
||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
static MIGRATOR: Migrator = sqlx::migrate!("src/db");
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
|
@ -25,74 +23,46 @@ pub enum Error {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct DB {
|
||||
pool: r2d2::Pool<ConnectionManager<SqliteConnection>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ConnectionCustomizer {}
|
||||
impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error>
|
||||
for ConnectionCustomizer
|
||||
{
|
||||
fn on_acquire(&self, connection: &mut SqliteConnection) -> Result<(), diesel::r2d2::Error> {
|
||||
let query = diesel::sql_query(
|
||||
r#"
|
||||
PRAGMA busy_timeout = 60000;
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
"#,
|
||||
);
|
||||
query
|
||||
.execute(connection)
|
||||
.map_err(diesel::r2d2::Error::QueryError)?;
|
||||
Ok(())
|
||||
}
|
||||
pool: SqlitePool,
|
||||
}
|
||||
|
||||
impl DB {
|
||||
pub fn new(path: &Path) -> Result<DB, Error> {
|
||||
pub async fn new(path: &Path) -> Result<DB, Error> {
|
||||
let directory = path.parent().unwrap();
|
||||
std::fs::create_dir_all(directory).map_err(|e| Error::Io(directory.to_owned(), e))?;
|
||||
let manager = ConnectionManager::<SqliteConnection>::new(path.to_string_lossy());
|
||||
let pool = diesel::r2d2::Pool::builder()
|
||||
.connection_customizer(Box::new(ConnectionCustomizer {}))
|
||||
.build(manager)
|
||||
.or(Err(Error::ConnectionPoolBuild))?;
|
||||
|
||||
let pool = SqlitePool::connect_lazy_with(
|
||||
SqliteConnectOptions::new()
|
||||
.create_if_missing(true)
|
||||
.filename(path)
|
||||
.journal_mode(SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal),
|
||||
);
|
||||
|
||||
let db = DB { pool };
|
||||
db.migrate_up()?;
|
||||
db.migrate_up().await?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
pub fn connect(&self) -> Result<PooledConnection<ConnectionManager<SqliteConnection>>, Error> {
|
||||
self.pool.get().or(Err(Error::ConnectionPool))
|
||||
pub async fn connect(&self) -> Result<PoolConnection<Sqlite>, Error> {
|
||||
self.pool.acquire().await.map_err(|_| Error::ConnectionPool)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn migrate_down(&self) -> Result<(), Error> {
|
||||
let mut connection = self.connect()?;
|
||||
connection
|
||||
.revert_all_migrations(MIGRATIONS)
|
||||
.and(Ok(()))
|
||||
.or(Err(Error::Migration))
|
||||
}
|
||||
|
||||
fn migrate_up(&self) -> Result<(), Error> {
|
||||
let mut connection = self.connect()?;
|
||||
connection
|
||||
.run_pending_migrations(MIGRATIONS)
|
||||
async fn migrate_up(&self) -> Result<(), Error> {
|
||||
MIGRATOR
|
||||
.run(&self.pool)
|
||||
.await
|
||||
.and(Ok(()))
|
||||
.or(Err(Error::Migration))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_migrations() {
|
||||
#[tokio::test]
|
||||
async fn run_migrations() {
|
||||
use crate::test::*;
|
||||
use crate::test_name;
|
||||
let output_dir = prepare_test_directory(test_name!());
|
||||
let db_path = output_dir.join("db.sqlite");
|
||||
let db = DB::new(&db_path).unwrap();
|
||||
|
||||
db.migrate_down().unwrap();
|
||||
db.migrate_up().unwrap();
|
||||
let db = DB::new(&db_path).await.unwrap();
|
||||
db.migrate_up().await.unwrap();
|
||||
}
|
||||
|
|
95
src/db/20240711080449_init.sql
Normal file
95
src/db/20240711080449_init.sql
Normal file
|
@ -0,0 +1,95 @@
|
|||
CREATE TABLE config (
|
||||
id INTEGER PRIMARY KEY NOT NULL CHECK(id = 0),
|
||||
auth_secret BLOB NOT NULL DEFAULT (randomblob(32)),
|
||||
index_sleep_duration_seconds INTEGER NOT NULL,
|
||||
index_album_art_pattern TEXT NOT NULL,
|
||||
ddns_host TEXT NOT NULL,
|
||||
ddns_username TEXT NOT NULL,
|
||||
ddns_password TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO config (
|
||||
id,
|
||||
auth_secret,
|
||||
index_sleep_duration_seconds,
|
||||
index_album_art_pattern,
|
||||
ddns_host,
|
||||
ddns_username,
|
||||
ddns_password
|
||||
) VALUES (
|
||||
0,
|
||||
randomblob(32),
|
||||
1800,
|
||||
"Folder.(jpeg|jpg|png)",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
);
|
||||
|
||||
CREATE TABLE mount_points (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
UNIQUE(name)
|
||||
);
|
||||
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
admin INTEGER NOT NULL,
|
||||
lastfm_username TEXT,
|
||||
lastfm_session_key TEXT,
|
||||
web_theme_base TEXT,
|
||||
web_theme_accent TEXT,
|
||||
UNIQUE(name)
|
||||
);
|
||||
|
||||
CREATE TABLE directories (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
parent TEXT,
|
||||
artist TEXT,
|
||||
year INTEGER,
|
||||
album TEXT,
|
||||
artwork TEXT,
|
||||
date_added INTEGER DEFAULT 0 NOT NULL,
|
||||
UNIQUE(path) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
CREATE TABLE songs (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
parent TEXT NOT NULL,
|
||||
track_number INTEGER,
|
||||
disc_number INTEGER,
|
||||
title TEXT,
|
||||
artist TEXT,
|
||||
album_artist TEXT,
|
||||
year INTEGER,
|
||||
album TEXT,
|
||||
artwork TEXT,
|
||||
duration INTEGER,
|
||||
lyricist TEXT,
|
||||
composer TEXT,
|
||||
genre TEXT,
|
||||
label TEXT,
|
||||
UNIQUE(path) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
CREATE TABLE playlists (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
owner INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
FOREIGN KEY(owner) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE(owner, name) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
CREATE TABLE playlist_songs (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
playlist INTEGER NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
ordering INTEGER NOT NULL,
|
||||
FOREIGN KEY(playlist) REFERENCES playlists(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
UNIQUE(playlist, ordering) ON CONFLICT REPLACE
|
||||
);
|
103
src/db/schema.rs
103
src/db/schema.rs
|
@ -1,103 +0,0 @@
|
|||
table! {
|
||||
ddns_config (id) {
|
||||
id -> Integer,
|
||||
host -> Text,
|
||||
username -> Text,
|
||||
password -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
directories (id) {
|
||||
id -> Integer,
|
||||
path -> Text,
|
||||
parent -> Nullable<Text>,
|
||||
artist -> Nullable<Text>,
|
||||
year -> Nullable<Integer>,
|
||||
album -> Nullable<Text>,
|
||||
artwork -> Nullable<Text>,
|
||||
date_added -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
misc_settings (id) {
|
||||
id -> Integer,
|
||||
auth_secret -> Binary,
|
||||
index_sleep_duration_seconds -> Integer,
|
||||
index_album_art_pattern -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
mount_points (id) {
|
||||
id -> Integer,
|
||||
source -> Text,
|
||||
name -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
playlist_songs (id) {
|
||||
id -> Integer,
|
||||
playlist -> Integer,
|
||||
path -> Text,
|
||||
ordering -> Integer,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
playlists (id) {
|
||||
id -> Integer,
|
||||
owner -> Integer,
|
||||
name -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
songs (id) {
|
||||
id -> Integer,
|
||||
path -> Text,
|
||||
parent -> Text,
|
||||
track_number -> Nullable<Integer>,
|
||||
disc_number -> Nullable<Integer>,
|
||||
title -> Nullable<Text>,
|
||||
artist -> Nullable<Text>,
|
||||
album_artist -> Nullable<Text>,
|
||||
year -> Nullable<Integer>,
|
||||
album -> Nullable<Text>,
|
||||
artwork -> Nullable<Text>,
|
||||
duration -> Nullable<Integer>,
|
||||
lyricist -> Nullable<Text>,
|
||||
composer -> Nullable<Text>,
|
||||
genre -> Nullable<Text>,
|
||||
label -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
users (id) {
|
||||
id -> Integer,
|
||||
name -> Text,
|
||||
password_hash -> Text,
|
||||
admin -> Integer,
|
||||
lastfm_username -> Nullable<Text>,
|
||||
lastfm_session_key -> Nullable<Text>,
|
||||
web_theme_base -> Nullable<Text>,
|
||||
web_theme_accent -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
joinable!(playlist_songs -> playlists (playlist));
|
||||
joinable!(playlists -> users (owner));
|
||||
|
||||
allow_tables_to_appear_in_same_query!(
|
||||
ddns_config,
|
||||
directories,
|
||||
misc_settings,
|
||||
mount_points,
|
||||
playlist_songs,
|
||||
playlists,
|
||||
songs,
|
||||
users,
|
||||
);
|
23
src/main.rs
23
src/main.rs
|
@ -1,12 +1,8 @@
|
|||
#![cfg_attr(all(windows, feature = "ui"), windows_subsystem = "windows")]
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
use log::info;
|
||||
use log::{error, info};
|
||||
use options::CLIOptions;
|
||||
use simplelog::{
|
||||
ColorChoice, CombinedLogger, LevelFilter, SharedLogger, TermLogger, TerminalMode, WriteLogger,
|
||||
};
|
||||
|
@ -27,6 +23,8 @@ mod utils;
|
|||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
App(#[from] app::Error),
|
||||
#[error("Could not start web services")]
|
||||
ServiceStartup(std::io::Error),
|
||||
#[error("Could not parse command line arguments:\n\n{0}")]
|
||||
CliArgsParsing(getopts::Fail),
|
||||
#[cfg(unix)]
|
||||
|
@ -139,16 +137,21 @@ fn main() -> Result<(), Error> {
|
|||
info!("Swagger files location is {:#?}", paths.swagger_dir_path);
|
||||
info!("Web client files location is {:#?}", paths.web_dir_path);
|
||||
|
||||
async_main(cli_options, paths)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn async_main(cli_options: CLIOptions, paths: paths::Paths) -> Result<(), Error> {
|
||||
// Create and run app
|
||||
let app = app::App::new(cli_options.port.unwrap_or(5050), paths)?;
|
||||
let app = app::App::new(cli_options.port.unwrap_or(5050), paths).await?;
|
||||
app.index.begin_periodic_updates();
|
||||
app.ddns_manager.begin_periodic_updates();
|
||||
|
||||
// Start server
|
||||
info!("Starting up server");
|
||||
std::thread::spawn(move || {
|
||||
let _ = service::run(app);
|
||||
});
|
||||
if let Err(e) = service::launch(app) {
|
||||
return Err(Error::ServiceStartup(e));
|
||||
}
|
||||
|
||||
// Send readiness notification
|
||||
#[cfg(unix)]
|
||||
|
|
10
src/paths.rs
10
src/paths.rs
|
@ -80,23 +80,23 @@ impl Paths {
|
|||
pub fn new(cli_options: &CLIOptions) -> Self {
|
||||
let mut paths = Self::from_build();
|
||||
if let Some(path) = &cli_options.cache_dir_path {
|
||||
paths.cache_dir_path = path.clone();
|
||||
path.clone_into(&mut paths.cache_dir_path);
|
||||
}
|
||||
if let Some(path) = &cli_options.config_file_path {
|
||||
paths.config_file_path = Some(path.clone());
|
||||
}
|
||||
if let Some(path) = &cli_options.database_file_path {
|
||||
paths.db_file_path = path.clone();
|
||||
path.clone_into(&mut paths.db_file_path);
|
||||
}
|
||||
#[cfg(unix)]
|
||||
if let Some(path) = &cli_options.pid_file_path {
|
||||
paths.pid_file_path = path.clone();
|
||||
path.clone_into(&mut paths.pid_file_path);
|
||||
}
|
||||
if let Some(path) = &cli_options.swagger_dir_path {
|
||||
paths.swagger_dir_path = path.clone();
|
||||
path.clone_into(&mut paths.swagger_dir_path);
|
||||
}
|
||||
if let Some(path) = &cli_options.web_dir_path {
|
||||
paths.web_dir_path = path.clone();
|
||||
path.clone_into(&mut paths.web_dir_path);
|
||||
}
|
||||
|
||||
let log_to_file = cli_options.log_file_path.is_some() || !cli_options.foreground;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use actix_web::{
|
||||
dev::Service,
|
||||
middleware::{Compress, Logger, NormalizePath},
|
||||
rt::System,
|
||||
web::{self, ServiceConfig},
|
||||
App as ActixApp, HttpServer,
|
||||
};
|
||||
|
@ -43,9 +42,9 @@ pub fn make_config(app: App) -> impl FnOnce(&mut ServiceConfig) + Clone {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn run(app: App) -> Result<(), std::io::Error> {
|
||||
pub fn launch(app: App) -> Result<(), std::io::Error> {
|
||||
let address = ("0.0.0.0", app.port);
|
||||
System::new().block_on(
|
||||
tokio::spawn(
|
||||
HttpServer::new(move || {
|
||||
ActixApp::new()
|
||||
.wrap(Logger::default())
|
||||
|
@ -72,5 +71,6 @@ pub fn run(app: App) -> Result<(), std::io::Error> {
|
|||
e
|
||||
})?
|
||||
.run(),
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ use base64::prelude::*;
|
|||
use futures_util::future::err;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::str;
|
||||
|
||||
|
@ -141,25 +141,27 @@ impl FromRequest for Auth {
|
|||
// Auth via bearer token in query parameter
|
||||
if let Ok(query) = query_params_future.await {
|
||||
let auth_token = user::AuthToken(query.auth_token.clone());
|
||||
let authorization = block(move || {
|
||||
user_manager.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
|
||||
})
|
||||
.await?;
|
||||
return Ok(Auth {
|
||||
username: authorization.username,
|
||||
});
|
||||
if let Ok(auth) = user_manager
|
||||
.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
|
||||
.await
|
||||
{
|
||||
return Ok(Auth {
|
||||
username: auth.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auth via bearer token in authorization header
|
||||
if let Ok(bearer_auth) = bearer_auth_future.await {
|
||||
let auth_token = user::AuthToken(bearer_auth.token().to_owned());
|
||||
let authorization = block(move || {
|
||||
user_manager.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
|
||||
})
|
||||
.await?;
|
||||
return Ok(Auth {
|
||||
username: authorization.username,
|
||||
});
|
||||
if let Ok(auth) = user_manager
|
||||
.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
|
||||
.await
|
||||
{
|
||||
return Ok(Auth {
|
||||
username: auth.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(ErrorUnauthorized(APIError::AuthenticationRequired))
|
||||
|
@ -185,21 +187,18 @@ impl FromRequest for AdminRights {
|
|||
let auth_future = Auth::from_request(request, payload);
|
||||
|
||||
Box::pin(async move {
|
||||
let user_manager_count = user_manager.clone();
|
||||
let user_count = block(move || user_manager_count.count()).await;
|
||||
let user_count = user_manager.count().await;
|
||||
match user_count {
|
||||
Err(e) => return Err(e.into()),
|
||||
Err(_) => return Err(ErrorInternalServerError(APIError::Internal)),
|
||||
Ok(0) => return Ok(AdminRights { auth: None }),
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let auth = auth_future.await?;
|
||||
let username = auth.username.clone();
|
||||
let is_admin = block(move || user_manager.is_admin(&username)).await?;
|
||||
if is_admin {
|
||||
Ok(AdminRights { auth: Some(auth) })
|
||||
} else {
|
||||
Err(ErrorForbidden(APIError::AdminPermissionRequired))
|
||||
match user_manager.is_admin(&auth.username).await {
|
||||
Ok(true) => Ok(AdminRights { auth: Some(auth) }),
|
||||
Ok(false) => Err(ErrorForbidden(APIError::AdminPermissionRequired)),
|
||||
Err(_) => Err(ErrorInternalServerError(APIError::Internal)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -228,18 +227,6 @@ impl Responder for MediaFile {
|
|||
}
|
||||
}
|
||||
|
||||
async fn block<F, I, E>(f: F) -> Result<I, APIError>
|
||||
where
|
||||
F: FnOnce() -> Result<I, E> + Send + 'static,
|
||||
I: Send + 'static,
|
||||
E: Send + std::fmt::Debug + 'static + Into<APIError>,
|
||||
{
|
||||
actix_web::web::block(f)
|
||||
.await
|
||||
.map_err(|_| APIError::Internal)
|
||||
.and_then(|r| r.map_err(|e| e.into()))
|
||||
}
|
||||
|
||||
#[get("/version")]
|
||||
async fn version() -> Json<dto::Version> {
|
||||
let current_version = dto::Version {
|
||||
|
@ -253,14 +240,13 @@ async fn version() -> Json<dto::Version> {
|
|||
async fn initial_setup(
|
||||
user_manager: Data<user::Manager>,
|
||||
) -> Result<Json<dto::InitialSetup>, APIError> {
|
||||
let initial_setup = block(move || -> Result<dto::InitialSetup, APIError> {
|
||||
let users = user_manager.list()?;
|
||||
let initial_setup = {
|
||||
let users = user_manager.list().await?;
|
||||
let has_any_admin = users.iter().any(|u| u.is_admin());
|
||||
Ok(dto::InitialSetup {
|
||||
dto::InitialSetup {
|
||||
has_any_users: has_any_admin,
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
};
|
||||
Ok(Json(initial_setup))
|
||||
}
|
||||
|
||||
|
@ -270,7 +256,7 @@ async fn apply_config(
|
|||
config_manager: Data<config::Manager>,
|
||||
config: Json<dto::Config>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
block(move || config_manager.apply(&config.to_owned().into())).await?;
|
||||
config_manager.apply(&config.to_owned().into()).await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -279,7 +265,7 @@ async fn get_settings(
|
|||
settings_manager: Data<settings::Manager>,
|
||||
_admin_rights: AdminRights,
|
||||
) -> Result<Json<dto::Settings>, APIError> {
|
||||
let settings = block(move || settings_manager.read()).await?;
|
||||
let settings = settings_manager.read().await?;
|
||||
Ok(Json(settings.into()))
|
||||
}
|
||||
|
||||
|
@ -289,7 +275,9 @@ async fn put_settings(
|
|||
settings_manager: Data<settings::Manager>,
|
||||
new_settings: Json<dto::NewSettings>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
block(move || settings_manager.amend(&new_settings.to_owned().into())).await?;
|
||||
settings_manager
|
||||
.amend(&new_settings.to_owned().into())
|
||||
.await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -298,7 +286,7 @@ async fn list_mount_dirs(
|
|||
vfs_manager: Data<vfs::Manager>,
|
||||
_admin_rights: AdminRights,
|
||||
) -> Result<Json<Vec<dto::MountDir>>, APIError> {
|
||||
let mount_dirs = block(move || vfs_manager.mount_dirs()).await?;
|
||||
let mount_dirs = vfs_manager.mount_dirs().await?;
|
||||
let mount_dirs = mount_dirs.into_iter().map(|m| m.into()).collect();
|
||||
Ok(Json(mount_dirs))
|
||||
}
|
||||
|
@ -310,7 +298,7 @@ async fn put_mount_dirs(
|
|||
new_mount_dirs: Json<Vec<dto::MountDir>>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
let new_mount_dirs: Vec<MountDir> = new_mount_dirs.iter().cloned().map(|m| m.into()).collect();
|
||||
block(move || vfs_manager.set_mount_dirs(&new_mount_dirs)).await?;
|
||||
vfs_manager.set_mount_dirs(&new_mount_dirs).await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -319,7 +307,7 @@ async fn get_ddns_config(
|
|||
ddns_manager: Data<ddns::Manager>,
|
||||
_admin_rights: AdminRights,
|
||||
) -> Result<Json<dto::DDNSConfig>, APIError> {
|
||||
let ddns_config = block(move || ddns_manager.config()).await?;
|
||||
let ddns_config = ddns_manager.config().await?;
|
||||
Ok(Json(ddns_config.into()))
|
||||
}
|
||||
|
||||
|
@ -329,7 +317,9 @@ async fn put_ddns_config(
|
|||
ddns_manager: Data<ddns::Manager>,
|
||||
new_ddns_config: Json<dto::DDNSConfig>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
block(move || ddns_manager.set_config(&new_ddns_config.to_owned().into())).await?;
|
||||
ddns_manager
|
||||
.set_config(&new_ddns_config.to_owned().into())
|
||||
.await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -338,7 +328,7 @@ async fn list_users(
|
|||
user_manager: Data<user::Manager>,
|
||||
_admin_rights: AdminRights,
|
||||
) -> Result<Json<Vec<dto::User>>, APIError> {
|
||||
let users = block(move || user_manager.list()).await?;
|
||||
let users = user_manager.list().await?;
|
||||
let users = users.into_iter().map(|u| u.into()).collect();
|
||||
Ok(Json(users))
|
||||
}
|
||||
|
@ -350,7 +340,7 @@ async fn create_user(
|
|||
new_user: Json<dto::NewUser>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
let new_user = new_user.to_owned().into();
|
||||
block(move || user_manager.create(&new_user)).await?;
|
||||
user_manager.create(&new_user).await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -367,16 +357,14 @@ async fn update_user(
|
|||
}
|
||||
}
|
||||
|
||||
block(move || -> Result<(), APIError> {
|
||||
if let Some(password) = &user_update.new_password {
|
||||
user_manager.set_password(&name, password)?;
|
||||
}
|
||||
if let Some(is_admin) = &user_update.new_is_admin {
|
||||
user_manager.set_is_admin(&name, *is_admin)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
if let Some(password) = &user_update.new_password {
|
||||
user_manager.set_password(&name, password).await?;
|
||||
}
|
||||
|
||||
if let Some(is_admin) = &user_update.new_is_admin {
|
||||
user_manager.set_is_admin(&name, *is_admin).await?;
|
||||
}
|
||||
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -391,7 +379,7 @@ async fn delete_user(
|
|||
return Err(APIError::DeletingOwnAccount);
|
||||
}
|
||||
}
|
||||
block(move || user_manager.delete(&name)).await?;
|
||||
user_manager.delete(&name).await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -400,7 +388,7 @@ async fn get_preferences(
|
|||
user_manager: Data<user::Manager>,
|
||||
auth: Auth,
|
||||
) -> Result<Json<user::Preferences>, APIError> {
|
||||
let preferences = block(move || user_manager.read_preferences(&auth.username)).await?;
|
||||
let preferences = user_manager.read_preferences(&auth.username).await?;
|
||||
Ok(Json(preferences))
|
||||
}
|
||||
|
||||
|
@ -410,7 +398,9 @@ async fn put_preferences(
|
|||
auth: Auth,
|
||||
preferences: Json<user::Preferences>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
block(move || user_manager.write_preferences(&auth.username, &preferences)).await?;
|
||||
user_manager
|
||||
.write_preferences(&auth.username, &preferences)
|
||||
.await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -429,18 +419,18 @@ async fn login(
|
|||
credentials: Json<dto::Credentials>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
let username = credentials.username.clone();
|
||||
let (user::AuthToken(token), is_admin) =
|
||||
block(move || -> Result<(user::AuthToken, bool), APIError> {
|
||||
let auth_token = user_manager.login(&credentials.username, &credentials.password)?;
|
||||
let is_admin = user_manager.is_admin(&credentials.username)?;
|
||||
Ok((auth_token, is_admin))
|
||||
})
|
||||
|
||||
let user::AuthToken(token) = user_manager
|
||||
.login(&credentials.username, &credentials.password)
|
||||
.await?;
|
||||
let is_admin = user_manager.is_admin(&credentials.username).await?;
|
||||
|
||||
let authorization = dto::Authorization {
|
||||
username: username.clone(),
|
||||
token,
|
||||
is_admin,
|
||||
};
|
||||
|
||||
let response = HttpResponse::Ok().json(authorization);
|
||||
Ok(response)
|
||||
}
|
||||
|
@ -450,7 +440,7 @@ async fn browse_root(
|
|||
index: Data<Index>,
|
||||
_auth: Auth,
|
||||
) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
|
||||
let result = block(move || index.browse(Path::new(""))).await?;
|
||||
let result = index.browse(Path::new("")).await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
|
@ -460,17 +450,14 @@ async fn browse(
|
|||
_auth: Auth,
|
||||
path: web::Path<String>,
|
||||
) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
|
||||
let result = block(move || {
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
index.browse(Path::new(path.as_ref()))
|
||||
})
|
||||
.await?;
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
let result = index.browse(Path::new(path.as_ref())).await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[get("/flatten")]
|
||||
async fn flatten_root(index: Data<Index>, _auth: Auth) -> Result<Json<Vec<index::Song>>, APIError> {
|
||||
let songs = block(move || index.flatten(Path::new(""))).await?;
|
||||
let songs = index.flatten(Path::new("")).await?;
|
||||
Ok(Json(songs))
|
||||
}
|
||||
|
||||
|
@ -480,23 +467,20 @@ async fn flatten(
|
|||
_auth: Auth,
|
||||
path: web::Path<String>,
|
||||
) -> Result<Json<Vec<index::Song>>, APIError> {
|
||||
let songs = block(move || {
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
index.flatten(Path::new(path.as_ref()))
|
||||
})
|
||||
.await?;
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
let songs = index.flatten(Path::new(path.as_ref())).await?;
|
||||
Ok(Json(songs))
|
||||
}
|
||||
|
||||
#[get("/random")]
|
||||
async fn random(index: Data<Index>, _auth: Auth) -> Result<Json<Vec<index::Directory>>, APIError> {
|
||||
let result = block(move || index.get_random_albums(20)).await?;
|
||||
let result = index.get_random_albums(20).await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
#[get("/recent")]
|
||||
async fn recent(index: Data<Index>, _auth: Auth) -> Result<Json<Vec<index::Directory>>, APIError> {
|
||||
let result = block(move || index.get_recent_albums(20)).await?;
|
||||
let result = index.get_recent_albums(20).await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
|
@ -505,7 +489,7 @@ async fn search_root(
|
|||
index: Data<Index>,
|
||||
_auth: Auth,
|
||||
) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
|
||||
let result = block(move || index.search("")).await?;
|
||||
let result = index.search("").await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
|
@ -515,7 +499,7 @@ async fn search(
|
|||
_auth: Auth,
|
||||
query: web::Path<String>,
|
||||
) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
|
||||
let result = block(move || index.search(&query)).await?;
|
||||
let result = index.search(&query).await?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
|
@ -525,13 +509,9 @@ async fn get_audio(
|
|||
_auth: Auth,
|
||||
path: web::Path<String>,
|
||||
) -> Result<MediaFile, APIError> {
|
||||
let audio_path = block(move || {
|
||||
let vfs = vfs_manager.get_vfs()?;
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
vfs.virtual_to_real(Path::new(path.as_ref()))
|
||||
})
|
||||
.await?;
|
||||
|
||||
let vfs = vfs_manager.get_vfs().await?;
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
let audio_path = vfs.virtual_to_real(Path::new(path.as_ref()))?;
|
||||
let named_file = NamedFile::open(audio_path).map_err(|_| APIError::AudioFileIOError)?;
|
||||
Ok(MediaFile::new(named_file))
|
||||
}
|
||||
|
@ -545,19 +525,11 @@ async fn get_thumbnail(
|
|||
options_input: web::Query<dto::ThumbnailOptions>,
|
||||
) -> Result<MediaFile, APIError> {
|
||||
let options = thumbnail::Options::from(options_input.0);
|
||||
|
||||
let thumbnail_path = block(move || -> Result<PathBuf, APIError> {
|
||||
let vfs = vfs_manager.get_vfs()?;
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
let image_path = vfs.virtual_to_real(Path::new(path.as_ref()))?;
|
||||
thumbnails_manager
|
||||
.get_thumbnail(&image_path, &options)
|
||||
.map_err(|e| e.into())
|
||||
})
|
||||
.await?;
|
||||
|
||||
let vfs = vfs_manager.get_vfs().await?;
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
let image_path = vfs.virtual_to_real(Path::new(path.as_ref()))?;
|
||||
let thumbnail_path = thumbnails_manager.get_thumbnail(&image_path, &options)?;
|
||||
let named_file = NamedFile::open(thumbnail_path).map_err(|_| APIError::ThumbnailFileIOError)?;
|
||||
|
||||
Ok(MediaFile::new(named_file))
|
||||
}
|
||||
|
||||
|
@ -566,7 +538,7 @@ async fn list_playlists(
|
|||
playlist_manager: Data<playlist::Manager>,
|
||||
auth: Auth,
|
||||
) -> Result<Json<Vec<dto::ListPlaylistsEntry>>, APIError> {
|
||||
let playlist_names = block(move || playlist_manager.list_playlists(&auth.username)).await?;
|
||||
let playlist_names = playlist_manager.list_playlists(&auth.username).await?;
|
||||
let playlists: Vec<dto::ListPlaylistsEntry> = playlist_names
|
||||
.into_iter()
|
||||
.map(|p| dto::ListPlaylistsEntry { name: p })
|
||||
|
@ -582,7 +554,9 @@ async fn save_playlist(
|
|||
name: web::Path<String>,
|
||||
playlist: Json<dto::SavePlaylistInput>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
block(move || playlist_manager.save_playlist(&name, &auth.username, &playlist.tracks)).await?;
|
||||
playlist_manager
|
||||
.save_playlist(&name, &auth.username, &playlist.tracks)
|
||||
.await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -592,7 +566,9 @@ async fn read_playlist(
|
|||
auth: Auth,
|
||||
name: web::Path<String>,
|
||||
) -> Result<Json<Vec<index::Song>>, APIError> {
|
||||
let songs = block(move || playlist_manager.read_playlist(&name, &auth.username)).await?;
|
||||
let songs = playlist_manager
|
||||
.read_playlist(&name, &auth.username)
|
||||
.await?;
|
||||
Ok(Json(songs))
|
||||
}
|
||||
|
||||
|
@ -602,7 +578,9 @@ async fn delete_playlist(
|
|||
auth: Auth,
|
||||
name: web::Path<String>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
block(move || playlist_manager.delete_playlist(&name, &auth.username)).await?;
|
||||
playlist_manager
|
||||
.delete_playlist(&name, &auth.username)
|
||||
.await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -613,15 +591,13 @@ async fn lastfm_now_playing(
|
|||
auth: Auth,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
block(move || -> Result<(), APIError> {
|
||||
if !user_manager.is_lastfm_linked(&auth.username) {
|
||||
return Err(APIError::LastFMAccountNotLinked);
|
||||
}
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
lastfm_manager.now_playing(&auth.username, Path::new(path.as_ref()))?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
if !user_manager.is_lastfm_linked(&auth.username).await {
|
||||
return Err(APIError::LastFMAccountNotLinked);
|
||||
}
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
lastfm_manager
|
||||
.now_playing(&auth.username, Path::new(path.as_ref()))
|
||||
.await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -632,15 +608,13 @@ async fn lastfm_scrobble(
|
|||
auth: Auth,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
block(move || -> Result<(), APIError> {
|
||||
if !user_manager.is_lastfm_linked(&auth.username) {
|
||||
return Err(APIError::LastFMAccountNotLinked);
|
||||
}
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
lastfm_manager.scrobble(&auth.username, Path::new(path.as_ref()))?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
if !user_manager.is_lastfm_linked(&auth.username).await {
|
||||
return Err(APIError::LastFMAccountNotLinked);
|
||||
}
|
||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
||||
lastfm_manager
|
||||
.scrobble(&auth.username, Path::new(path.as_ref()))
|
||||
.await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
|
@ -649,8 +623,7 @@ async fn lastfm_link_token(
|
|||
lastfm_manager: Data<lastfm::Manager>,
|
||||
auth: Auth,
|
||||
) -> Result<Json<dto::LastFMLinkToken>, APIError> {
|
||||
let user::AuthToken(value) =
|
||||
block(move || lastfm_manager.generate_link_token(&auth.username)).await?;
|
||||
let user::AuthToken(value) = lastfm_manager.generate_link_token(&auth.username)?;
|
||||
Ok(Json(dto::LastFMLinkToken { value }))
|
||||
}
|
||||
|
||||
|
@ -660,27 +633,27 @@ async fn lastfm_link(
|
|||
user_manager: Data<user::Manager>,
|
||||
payload: web::Query<dto::LastFMLink>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
let popup_content_string = block(move || {
|
||||
let auth_token = user::AuthToken(payload.auth_token.clone());
|
||||
let authorization =
|
||||
user_manager.authenticate(&auth_token, user::AuthorizationScope::LastFMLink)?;
|
||||
let lastfm_token = &payload.token;
|
||||
lastfm_manager.link(&authorization.username, lastfm_token)?;
|
||||
let auth_token = user::AuthToken(payload.auth_token.clone());
|
||||
let authorization = user_manager
|
||||
.authenticate(&auth_token, user::AuthorizationScope::LastFMLink)
|
||||
.await?;
|
||||
let lastfm_token = &payload.token;
|
||||
lastfm_manager
|
||||
.link(&authorization.username, lastfm_token)
|
||||
.await?;
|
||||
|
||||
// Percent decode
|
||||
let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy();
|
||||
// Percent decode
|
||||
let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy();
|
||||
|
||||
// Base64 decode
|
||||
let popup_content = BASE64_STANDARD_NO_PAD
|
||||
.decode(base64_content.as_bytes())
|
||||
.map_err(|_| APIError::LastFMLinkContentBase64DecodeError)?;
|
||||
// Base64 decode
|
||||
let popup_content = BASE64_STANDARD_NO_PAD
|
||||
.decode(base64_content.as_bytes())
|
||||
.map_err(|_| APIError::LastFMLinkContentBase64DecodeError)?;
|
||||
|
||||
// UTF-8 decode
|
||||
str::from_utf8(&popup_content)
|
||||
.map_err(|_| APIError::LastFMLinkContentEncodingError)
|
||||
.map(|s| s.to_owned())
|
||||
})
|
||||
.await?;
|
||||
// UTF-8 decode
|
||||
let popup_content_string = str::from_utf8(&popup_content)
|
||||
.map_err(|_| APIError::LastFMLinkContentEncodingError)
|
||||
.map(|s| s.to_owned())?;
|
||||
|
||||
Ok(HttpResponse::build(StatusCode::OK)
|
||||
.content_type("text/html; charset=utf-8")
|
||||
|
@ -692,6 +665,6 @@ async fn lastfm_unlink(
|
|||
lastfm_manager: Data<lastfm::Manager>,
|
||||
auth: Auth,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
block(move || lastfm_manager.unlink(&auth.username)).await?;
|
||||
lastfm_manager.unlink(&auth.username).await?;
|
||||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use actix_test::TestServer;
|
||||
use actix_web::{
|
||||
middleware::{Compress, Logger},
|
||||
rt::{System, SystemRunner},
|
||||
web::Bytes,
|
||||
App as ActixApp,
|
||||
};
|
||||
|
@ -18,7 +17,6 @@ use crate::service::test::TestService;
|
|||
use crate::test::*;
|
||||
|
||||
pub struct ActixTestService {
|
||||
system_runner: SystemRunner,
|
||||
authorization: Option<dto::Authorization>,
|
||||
server: TestServer,
|
||||
}
|
||||
|
@ -26,7 +24,7 @@ pub struct ActixTestService {
|
|||
pub type ServiceType = ActixTestService;
|
||||
|
||||
impl ActixTestService {
|
||||
fn process_internal<T: Serialize + Clone + 'static>(
|
||||
async fn process_internal<T: Serialize + Clone + 'static>(
|
||||
&mut self,
|
||||
request: &Request<T>,
|
||||
) -> (Builder, Option<Bytes>) {
|
||||
|
@ -50,9 +48,7 @@ impl ActixTestService {
|
|||
actix_request = actix_request.bearer_auth(&authorization.token);
|
||||
}
|
||||
|
||||
let mut actix_response = self
|
||||
.system_runner
|
||||
.block_on(async move { actix_request.send_json(&body).await.unwrap() });
|
||||
let mut actix_response = actix_request.send_json(&body).await.unwrap();
|
||||
|
||||
let mut response_builder = Response::builder().status(actix_response.status());
|
||||
let headers = response_builder.headers_mut().unwrap();
|
||||
|
@ -62,10 +58,7 @@ impl ActixTestService {
|
|||
|
||||
let is_success = actix_response.status().is_success();
|
||||
let body = if is_success {
|
||||
Some(
|
||||
self.system_runner
|
||||
.block_on(async move { actix_response.body().await.unwrap() }),
|
||||
)
|
||||
Some(actix_response.body().await.unwrap())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -75,7 +68,7 @@ impl ActixTestService {
|
|||
}
|
||||
|
||||
impl TestService for ActixTestService {
|
||||
fn new(test_name: &str) -> Self {
|
||||
async fn new(test_name: &str) -> Self {
|
||||
let output_dir = prepare_test_directory(test_name);
|
||||
|
||||
let paths = Paths {
|
||||
|
@ -89,9 +82,8 @@ impl TestService for ActixTestService {
|
|||
web_dir_path: ["test-data", "web"].iter().collect(),
|
||||
};
|
||||
|
||||
let app = App::new(5050, paths).unwrap();
|
||||
let app = App::new(5050, paths).await.unwrap();
|
||||
|
||||
let system_runner = System::new();
|
||||
let server = actix_test::start(move || {
|
||||
let config = make_config(app.clone());
|
||||
ActixApp::new()
|
||||
|
@ -102,31 +94,33 @@ impl TestService for ActixTestService {
|
|||
|
||||
ActixTestService {
|
||||
authorization: None,
|
||||
system_runner,
|
||||
server,
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch<T: Serialize + Clone + 'static>(&mut self, request: &Request<T>) -> Response<()> {
|
||||
let (response_builder, _body) = self.process_internal(request);
|
||||
async fn fetch<T: Serialize + Clone + 'static>(
|
||||
&mut self,
|
||||
request: &Request<T>,
|
||||
) -> Response<()> {
|
||||
let (response_builder, _body) = self.process_internal(request).await;
|
||||
response_builder.body(()).unwrap()
|
||||
}
|
||||
|
||||
fn fetch_bytes<T: Serialize + Clone + 'static>(
|
||||
async fn fetch_bytes<T: Serialize + Clone + 'static>(
|
||||
&mut self,
|
||||
request: &Request<T>,
|
||||
) -> Response<Vec<u8>> {
|
||||
let (response_builder, body) = self.process_internal(request);
|
||||
let (response_builder, body) = self.process_internal(request).await;
|
||||
response_builder
|
||||
.body(body.unwrap().deref().to_owned())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>(
|
||||
async fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>(
|
||||
&mut self,
|
||||
request: &Request<T>,
|
||||
) -> Response<U> {
|
||||
let (response_builder, body) = self.process_internal(request);
|
||||
let (response_builder, body) = self.process_internal(request).await;
|
||||
let body = serde_json::from_slice(&body.unwrap()).unwrap();
|
||||
response_builder.body(body).unwrap()
|
||||
}
|
||||
|
|
|
@ -139,9 +139,9 @@ pub struct DDNSConfig {
|
|||
impl From<DDNSConfig> for ddns::Config {
|
||||
fn from(c: DDNSConfig) -> Self {
|
||||
Self {
|
||||
host: c.host,
|
||||
username: c.username,
|
||||
password: c.password,
|
||||
ddns_host: c.host,
|
||||
ddns_username: c.username,
|
||||
ddns_password: c.password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -149,9 +149,9 @@ impl From<DDNSConfig> for ddns::Config {
|
|||
impl From<ddns::Config> for DDNSConfig {
|
||||
fn from(c: ddns::Config) -> Self {
|
||||
Self {
|
||||
host: c.host,
|
||||
username: c.username,
|
||||
password: c.password,
|
||||
host: c.ddns_host,
|
||||
username: c.ddns_username,
|
||||
password: c.ddns_password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,7 +204,7 @@ impl From<Config> for config::Config {
|
|||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct NewSettings {
|
||||
pub album_art_pattern: Option<String>,
|
||||
pub reindex_every_n_seconds: Option<i32>,
|
||||
pub reindex_every_n_seconds: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<NewSettings> for settings::NewSettings {
|
||||
|
@ -219,7 +219,7 @@ impl From<NewSettings> for settings::NewSettings {
|
|||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub album_art_pattern: String,
|
||||
pub reindex_every_n_seconds: i32,
|
||||
pub reindex_every_n_seconds: i64,
|
||||
}
|
||||
|
||||
impl From<settings::Settings> for Settings {
|
||||
|
|
|
@ -18,7 +18,7 @@ pub enum APIError {
|
|||
#[error("Could not encode Branca token")]
|
||||
BrancaTokenEncoding,
|
||||
#[error("Database error:\n\n{0}")]
|
||||
Database(diesel::result::Error),
|
||||
Database(sqlx::Error),
|
||||
#[error("DDNS update query failed with HTTP status {0}")]
|
||||
DdnsUpdateQueryFailed(u16),
|
||||
#[error("Cannot delete your own account")]
|
||||
|
|
|
@ -26,18 +26,19 @@ use crate::service::test::constants::*;
|
|||
pub use crate::service::actix::test::ServiceType;
|
||||
|
||||
pub trait TestService {
|
||||
fn new(test_name: &str) -> Self;
|
||||
fn fetch<T: Serialize + Clone + 'static>(&mut self, request: &Request<T>) -> Response<()>;
|
||||
fn fetch_bytes<T: Serialize + Clone + 'static>(
|
||||
async fn new(test_name: &str) -> Self;
|
||||
async fn fetch<T: Serialize + Clone + 'static>(&mut self, request: &Request<T>)
|
||||
-> Response<()>;
|
||||
async fn fetch_bytes<T: Serialize + Clone + 'static>(
|
||||
&mut self,
|
||||
request: &Request<T>,
|
||||
) -> Response<Vec<u8>>;
|
||||
fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>(
|
||||
async fn fetch_json<T: Serialize + Clone + 'static, U: DeserializeOwned>(
|
||||
&mut self,
|
||||
request: &Request<T>,
|
||||
) -> Response<U>;
|
||||
|
||||
fn complete_initial_setup(&mut self) {
|
||||
async fn complete_initial_setup(&mut self) {
|
||||
let configuration = dto::Config {
|
||||
users: Some(vec![
|
||||
dto::NewUser {
|
||||
|
@ -58,40 +59,43 @@ pub trait TestService {
|
|||
..Default::default()
|
||||
};
|
||||
let request = protocol::apply_config(configuration);
|
||||
let response = self.fetch(&request);
|
||||
let response = self.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
fn login_internal(&mut self, username: &str, password: &str) {
|
||||
async fn login_internal(&mut self, username: &str, password: &str) {
|
||||
let request = protocol::login(username, password);
|
||||
let response = self.fetch_json::<_, dto::Authorization>(&request);
|
||||
let response = self.fetch_json::<_, dto::Authorization>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let authorization = response.into_body();
|
||||
self.set_authorization(Some(authorization));
|
||||
}
|
||||
|
||||
fn login_admin(&mut self) {
|
||||
self.login_internal(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN);
|
||||
async fn login_admin(&mut self) {
|
||||
self.login_internal(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn login(&mut self) {
|
||||
self.login_internal(TEST_USERNAME, TEST_PASSWORD);
|
||||
async fn login(&mut self) {
|
||||
self.login_internal(TEST_USERNAME, TEST_PASSWORD).await;
|
||||
}
|
||||
|
||||
fn logout(&mut self) {
|
||||
async fn logout(&mut self) {
|
||||
self.set_authorization(None);
|
||||
}
|
||||
|
||||
fn set_authorization(&mut self, authorization: Option<dto::Authorization>);
|
||||
|
||||
fn index(&mut self) {
|
||||
async fn index(&mut self) {
|
||||
let request = protocol::trigger_index();
|
||||
let response = self.fetch(&request);
|
||||
let response = self.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
loop {
|
||||
let browse_request = protocol::browse(Path::new(""));
|
||||
let response = self.fetch_json::<(), Vec<index::CollectionFile>>(&browse_request);
|
||||
let response = self
|
||||
.fetch_json::<(), Vec<index::CollectionFile>>(&browse_request)
|
||||
.await;
|
||||
let entries = response.body();
|
||||
if !entries.is_empty() {
|
||||
break;
|
||||
|
@ -101,7 +105,9 @@ pub trait TestService {
|
|||
|
||||
loop {
|
||||
let flatten_request = protocol::flatten(Path::new(""));
|
||||
let response = self.fetch_json::<_, Vec<index::Song>>(&flatten_request);
|
||||
let response = self
|
||||
.fetch_json::<_, Vec<index::Song>>(&flatten_request)
|
||||
.await;
|
||||
let entries = response.body();
|
||||
if !entries.is_empty() {
|
||||
break;
|
||||
|
|
|
@ -5,20 +5,20 @@ use crate::service::dto;
|
|||
use crate::service::test::{protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn returns_api_version() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn returns_api_version() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::version();
|
||||
let response = service.fetch_json::<_, dto::Version>(&request);
|
||||
let response = service.fetch_json::<_, dto::Version>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_setup_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn initial_setup_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::initial_setup();
|
||||
{
|
||||
let response = service.fetch_json::<_, dto::InitialSetup>(&request);
|
||||
let response = service.fetch_json::<_, dto::InitialSetup>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let initial_setup = response.body();
|
||||
assert_eq!(
|
||||
|
@ -28,9 +28,9 @@ fn initial_setup_golden_path() {
|
|||
}
|
||||
);
|
||||
}
|
||||
service.complete_initial_setup();
|
||||
service.complete_initial_setup().await;
|
||||
{
|
||||
let response = service.fetch_json::<_, dto::InitialSetup>(&request);
|
||||
let response = service.fetch_json::<_, dto::InitialSetup>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let initial_setup = response.body();
|
||||
assert_eq!(
|
||||
|
@ -42,40 +42,44 @@ fn initial_setup_golden_path() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_index_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
#[actix_web::test]
|
||||
async fn trigger_index_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
|
||||
let request = protocol::random();
|
||||
|
||||
let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 0);
|
||||
|
||||
service.index();
|
||||
service.index().await;
|
||||
|
||||
let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_index_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn trigger_index_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
let request = protocol::trigger_index();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_index_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn trigger_index_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
let request = protocol::trigger_index();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
|
|
@ -5,33 +5,33 @@ use crate::service::dto;
|
|||
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn login_rejects_bad_username() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn login_rejects_bad_username() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
|
||||
let request = protocol::login("garbage", TEST_PASSWORD);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_rejects_bad_password() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn login_rejects_bad_password() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
|
||||
let request = protocol::login(TEST_USERNAME, "garbage");
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn login_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
|
||||
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
|
||||
let response = service.fetch_json::<_, dto::Authorization>(&request);
|
||||
let response = service.fetch_json::<_, dto::Authorization>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let authorization = response.body();
|
||||
|
@ -40,73 +40,73 @@ fn login_golden_path() {
|
|||
assert!(!authorization.token.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_via_bearer_http_header_rejects_bad_token() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn authentication_via_bearer_http_header_rejects_bad_token() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
|
||||
let mut request = protocol::random();
|
||||
let bearer = headers::Authorization::bearer("garbage").unwrap();
|
||||
request.headers_mut().typed_insert(bearer);
|
||||
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_via_bearer_http_header_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn authentication_via_bearer_http_header_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
|
||||
let authorization = {
|
||||
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
|
||||
let response = service.fetch_json::<_, dto::Authorization>(&request);
|
||||
let response = service.fetch_json::<_, dto::Authorization>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
response.into_body()
|
||||
};
|
||||
|
||||
service.logout();
|
||||
service.logout().await;
|
||||
|
||||
let mut request = protocol::random();
|
||||
let bearer = headers::Authorization::bearer(&authorization.token).unwrap();
|
||||
request.headers_mut().typed_insert(bearer);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_via_query_param_rejects_bad_token() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn authentication_via_query_param_rejects_bad_token() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
|
||||
let mut request = protocol::random();
|
||||
*request.uri_mut() = (request.uri().to_string() + "?auth_token=garbage-token")
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_via_query_param_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn authentication_via_query_param_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
|
||||
let authorization = {
|
||||
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
|
||||
let response = service.fetch_json::<_, dto::Authorization>(&request);
|
||||
let response = service.fetch_json::<_, dto::Authorization>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
response.into_body()
|
||||
};
|
||||
|
||||
service.logout();
|
||||
service.logout().await;
|
||||
|
||||
let mut request = protocol::random();
|
||||
*request.uri_mut() = format!("{}?auth_token={}", request.uri(), authorization.token)
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
|
|
@ -5,214 +5,230 @@ use crate::app::index;
|
|||
use crate::service::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn browse_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn browse_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::browse(&PathBuf::new());
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browse_root() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn browse_root() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::browse(&PathBuf::new());
|
||||
let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::CollectionFile>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browse_directory() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn browse_directory() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect();
|
||||
let request = protocol::browse(&path);
|
||||
let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::CollectionFile>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browse_bad_directory() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn browse_bad_directory() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = ["not_my_collection"].iter().collect();
|
||||
let request = protocol::browse(&path);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flatten_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn flatten_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::flatten(&PathBuf::new());
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flatten_root() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn flatten_root() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::flatten(&PathBuf::new());
|
||||
let response = service.fetch_json::<_, Vec<index::Song>>(&request);
|
||||
let response = service.fetch_json::<_, Vec<index::Song>>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flatten_directory() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn flatten_directory() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::flatten(Path::new(TEST_MOUNT_NAME));
|
||||
let response = service.fetch_json::<_, Vec<index::Song>>(&request);
|
||||
let response = service.fetch_json::<_, Vec<index::Song>>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flatten_bad_directory() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn flatten_bad_directory() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = ["not_my_collection"].iter().collect();
|
||||
let request = protocol::flatten(&path);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn random_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::random();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn random_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::random();
|
||||
let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_with_trailing_slash() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn random_with_trailing_slash() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let mut request = protocol::random();
|
||||
add_trailing_slash(&mut request);
|
||||
let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn recent_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::recent();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn recent_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::recent();
|
||||
let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recent_with_trailing_slash() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn recent_with_trailing_slash() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let mut request = protocol::recent();
|
||||
add_trailing_slash(&mut request);
|
||||
let response = service.fetch_json::<_, Vec<index::Directory>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn search_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::search("");
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_without_query() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn search_without_query() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::search("");
|
||||
let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::CollectionFile>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_with_query() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn search_with_query() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::search("door");
|
||||
let response = service.fetch_json::<_, Vec<index::CollectionFile>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::CollectionFile>>(&request)
|
||||
.await;
|
||||
let results = response.body();
|
||||
assert_eq!(results.len(), 1);
|
||||
match results[0] {
|
||||
|
|
|
@ -4,60 +4,60 @@ use crate::service::dto;
|
|||
use crate::service::test::{protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn get_ddns_config_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn get_ddns_config_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::get_ddns_config();
|
||||
service.complete_initial_setup();
|
||||
service.complete_initial_setup().await;
|
||||
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
service.login();
|
||||
let response = service.fetch(&request);
|
||||
service.login().await;
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_ddns_config_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
#[actix_web::test]
|
||||
async fn get_ddns_config_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
|
||||
let request = protocol::get_ddns_config();
|
||||
let response = service.fetch_json::<_, dto::DDNSConfig>(&request);
|
||||
let response = service.fetch_json::<_, dto::DDNSConfig>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn put_ddns_config_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn put_ddns_config_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::put_ddns_config(dto::DDNSConfig {
|
||||
host: "test".to_owned(),
|
||||
username: "test".to_owned(),
|
||||
password: "test".to_owned(),
|
||||
});
|
||||
service.complete_initial_setup();
|
||||
service.complete_initial_setup().await;
|
||||
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
service.login();
|
||||
let response = service.fetch(&request);
|
||||
service.login().await;
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn put_ddns_config_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
#[actix_web::test]
|
||||
async fn put_ddns_config_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
|
||||
let request = protocol::put_ddns_config(dto::DDNSConfig {
|
||||
host: "test".to_owned(),
|
||||
username: "test".to_owned(),
|
||||
password: "test".to_owned(),
|
||||
});
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
|
|
@ -5,56 +5,58 @@ use crate::service::dto;
|
|||
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn lastfm_scrobble_ignores_unlinked_user() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn lastfm_scrobble_ignores_unlinked_user() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
|
||||
.iter()
|
||||
.collect();
|
||||
|
||||
let request = protocol::lastfm_scrobble(&path);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::NO_CONTENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lastfm_now_playing_ignores_unlinked_user() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn lastfm_now_playing_ignores_unlinked_user() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
|
||||
.iter()
|
||||
.collect();
|
||||
|
||||
let request = protocol::lastfm_now_playing(&path);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::NO_CONTENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lastfm_link_token_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn lastfm_link_token_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::lastfm_link_token();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lastfm_link_token_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn lastfm_link_token_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::lastfm_link_token();
|
||||
let response = service.fetch_json::<_, dto::LastFMLinkToken>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, dto::LastFMLinkToken>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let link_token = response.body();
|
||||
assert!(!link_token.value.is_empty());
|
||||
|
|
|
@ -5,33 +5,33 @@ use crate::service::dto::ThumbnailSize;
|
|||
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn audio_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn audio_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
|
||||
.iter()
|
||||
.collect();
|
||||
|
||||
let request = protocol::audio(&path);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn audio_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
|
||||
.iter()
|
||||
.collect();
|
||||
|
||||
let request = protocol::audio(&path);
|
||||
let response = service.fetch_bytes(&request);
|
||||
let response = service.fetch_bytes(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.body().len(), 24_142);
|
||||
assert_eq!(
|
||||
|
@ -40,13 +40,13 @@ fn audio_golden_path() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_does_not_encode_content() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn audio_does_not_encode_content() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
|
||||
.iter()
|
||||
|
@ -59,7 +59,7 @@ fn audio_does_not_encode_content() {
|
|||
HeaderValue::from_str("gzip, deflate, br").unwrap(),
|
||||
);
|
||||
|
||||
let response = service.fetch_bytes(&request);
|
||||
let response = service.fetch_bytes(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.body().len(), 24_142);
|
||||
assert_eq!(response.headers().get(header::TRANSFER_ENCODING), None);
|
||||
|
@ -69,13 +69,13 @@ fn audio_does_not_encode_content() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_partial_content() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn audio_partial_content() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
|
||||
.iter()
|
||||
|
@ -88,7 +88,7 @@ fn audio_partial_content() {
|
|||
HeaderValue::from_str("bytes=100-299").unwrap(),
|
||||
);
|
||||
|
||||
let response = service.fetch_bytes(&request);
|
||||
let response = service.fetch_bytes(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
|
||||
assert_eq!(response.body().len(), 200);
|
||||
assert_eq!(
|
||||
|
@ -97,22 +97,22 @@ fn audio_partial_content() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_bad_path_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn audio_bad_path_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = ["not_my_collection"].iter().collect();
|
||||
|
||||
let request = protocol::audio(&path);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumbnail_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn thumbnail_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "Folder.jpg"]
|
||||
.iter()
|
||||
|
@ -121,17 +121,17 @@ fn thumbnail_requires_auth() {
|
|||
let size = None;
|
||||
let pad = None;
|
||||
let request = protocol::thumbnail(&path, size, pad);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumbnail_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn thumbnail_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "Folder.jpg"]
|
||||
.iter()
|
||||
|
@ -140,60 +140,60 @@ fn thumbnail_golden_path() {
|
|||
let size = None;
|
||||
let pad = None;
|
||||
let request = protocol::thumbnail(&path, size, pad);
|
||||
let response = service.fetch_bytes(&request);
|
||||
let response = service.fetch_bytes(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumbnail_bad_path_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn thumbnail_bad_path_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = ["not_my_collection"].iter().collect();
|
||||
|
||||
let size = None;
|
||||
let pad = None;
|
||||
let request = protocol::thumbnail(&path, size, pad);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumbnail_size_default() {
|
||||
thumbnail_size(&test_name!(), None, None, 400);
|
||||
#[actix_web::test]
|
||||
async fn thumbnail_size_default() {
|
||||
thumbnail_size(&test_name!(), None, None, 400).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thumbnail_size_small() {
|
||||
thumbnail_size(&test_name!(), Some(ThumbnailSize::Small), None, 400);
|
||||
#[actix_web::test]
|
||||
async fn thumbnail_size_small() {
|
||||
thumbnail_size(&test_name!(), Some(ThumbnailSize::Small), None, 400).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[actix_web::test]
|
||||
#[cfg(not(tarpaulin))]
|
||||
fn thumbnail_size_large() {
|
||||
thumbnail_size(&test_name!(), Some(ThumbnailSize::Large), None, 1200);
|
||||
async fn thumbnail_size_large() {
|
||||
thumbnail_size(&test_name!(), Some(ThumbnailSize::Large), None, 1200).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[actix_web::test]
|
||||
#[cfg(not(tarpaulin))]
|
||||
fn thumbnail_size_native() {
|
||||
thumbnail_size(&test_name!(), Some(ThumbnailSize::Native), None, 1423);
|
||||
async fn thumbnail_size_native() {
|
||||
thumbnail_size(&test_name!(), Some(ThumbnailSize::Native), None, 1423).await;
|
||||
}
|
||||
|
||||
fn thumbnail_size(name: &str, size: Option<ThumbnailSize>, pad: Option<bool>, expected: u32) {
|
||||
let mut service = ServiceType::new(name);
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
service.index();
|
||||
service.login();
|
||||
async fn thumbnail_size(name: &str, size: Option<ThumbnailSize>, pad: Option<bool>, expected: u32) {
|
||||
let mut service = ServiceType::new(name).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
service.index().await;
|
||||
service.login().await;
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic", "Folder.png"]
|
||||
.iter()
|
||||
.collect();
|
||||
|
||||
let request = protocol::thumbnail(&path, size, pad);
|
||||
let response = service.fetch_bytes(&request);
|
||||
let response = service.fetch_bytes(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let thumbnail = image::load_from_memory(response.body()).unwrap().to_rgb8();
|
||||
assert_eq!(thumbnail.width(), expected);
|
||||
|
|
|
@ -5,130 +5,132 @@ use crate::service::dto;
|
|||
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn list_playlists_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn list_playlists_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::playlists();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_playlists_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn list_playlists_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
let request = protocol::playlists();
|
||||
let response = service.fetch_json::<_, Vec<dto::ListPlaylistsEntry>>(&request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<dto::ListPlaylistsEntry>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_playlist_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn save_playlist_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
|
||||
let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_playlist_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn save_playlist_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
|
||||
let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_playlist_large() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn save_playlist_large() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let tracks = (0..100_000)
|
||||
.map(|_| "My Super Cool Song".to_string())
|
||||
.collect();
|
||||
let my_playlist = dto::SavePlaylistInput { tracks };
|
||||
let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_playlist_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn get_playlist_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_playlist_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn get_playlist_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
{
|
||||
let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
|
||||
let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
|
||||
let response = service.fetch_json::<_, Vec<index::Song>>(&request);
|
||||
let response = service.fetch_json::<_, Vec<index::Song>>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_playlist_bad_name_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn get_playlist_bad_name_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_playlist_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn delete_playlist_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::delete_playlist(TEST_PLAYLIST_NAME);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_playlist_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn delete_playlist_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
{
|
||||
let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
|
||||
let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
let request = protocol::delete_playlist(TEST_PLAYLIST_NAME);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_playlist_bad_name_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn delete_playlist_bad_name_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::delete_playlist(TEST_PLAYLIST_NAME);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
|
|
@ -4,72 +4,72 @@ use crate::service::dto::{self, Settings};
|
|||
use crate::service::test::{protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn get_settings_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn get_settings_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
|
||||
let request = protocol::get_settings();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_settings_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn get_settings_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::get_settings();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_settings_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
#[actix_web::test]
|
||||
async fn get_settings_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
|
||||
let request = protocol::get_settings();
|
||||
let response = service.fetch_json::<_, dto::Settings>(&request);
|
||||
let response = service.fetch_json::<_, dto::Settings>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn put_settings_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn put_settings_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
let request = protocol::put_settings(dto::NewSettings::default());
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn put_settings_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn put_settings_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
let request = protocol::put_settings(dto::NewSettings::default());
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn put_settings_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
#[actix_web::test]
|
||||
async fn put_settings_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
|
||||
let request = protocol::put_settings(dto::NewSettings {
|
||||
album_art_pattern: Some("test_pattern".to_owned()),
|
||||
reindex_every_n_seconds: Some(31),
|
||||
});
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let request = protocol::get_settings();
|
||||
let response = service.fetch_json::<_, dto::Settings>(&request);
|
||||
let response = service.fetch_json::<_, dto::Settings>(&request).await;
|
||||
let settings = response.body();
|
||||
assert_eq!(
|
||||
settings,
|
||||
|
|
|
@ -3,19 +3,19 @@ use http::StatusCode;
|
|||
use crate::service::test::{add_trailing_slash, protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn can_get_swagger_index() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn can_get_swagger_index() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::swagger_index();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_swagger_index_with_trailing_slash() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn can_get_swagger_index_with_trailing_slash() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let mut request = protocol::swagger_index();
|
||||
add_trailing_slash(&mut request);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
|
|
@ -6,53 +6,53 @@ use crate::service::dto;
|
|||
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn list_users_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn list_users_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
let request = protocol::list_users();
|
||||
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
service.login();
|
||||
let response = service.fetch(&request);
|
||||
service.login().await;
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_users_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
#[actix_web::test]
|
||||
async fn list_users_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
let request = protocol::list_users();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_user_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn create_user_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
let request = protocol::create_user(dto::NewUser {
|
||||
name: "Walter".into(),
|
||||
password: "secret".into(),
|
||||
admin: false,
|
||||
});
|
||||
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
service.login();
|
||||
let response = service.fetch(&request);
|
||||
service.login().await;
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_user_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
#[actix_web::test]
|
||||
async fn create_user_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login_admin().await;
|
||||
|
||||
let new_user = dto::NewUser {
|
||||
name: "Walter".into(),
|
||||
|
@ -60,39 +60,39 @@ fn create_user_golden_path() {
|
|||
admin: false,
|
||||
};
|
||||
let request = protocol::create_user(new_user);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_user_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn update_user_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
let request = protocol::update_user("Walter", dto::UserUpdate::default());
|
||||
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
service.login();
|
||||
let response = service.fetch(&request);
|
||||
service.login().await;
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_user_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn update_user_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
let request = protocol::update_user("Walter", dto::UserUpdate::default());
|
||||
|
||||
service.login_admin();
|
||||
let response = service.fetch(&request);
|
||||
service.login_admin().await;
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_user_cannot_unadmin_self() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn update_user_cannot_unadmin_self() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
let request = protocol::update_user(
|
||||
TEST_USERNAME_ADMIN,
|
||||
dto::UserUpdate {
|
||||
|
@ -101,80 +101,80 @@ fn update_user_cannot_unadmin_self() {
|
|||
},
|
||||
);
|
||||
|
||||
service.login_admin();
|
||||
let response = service.fetch(&request);
|
||||
service.login_admin().await;
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_user_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn delete_user_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
let request = protocol::delete_user("Walter");
|
||||
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
service.login();
|
||||
let response = service.fetch(&request);
|
||||
service.login().await;
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_user_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn delete_user_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
let request = protocol::delete_user("Walter");
|
||||
|
||||
service.login_admin();
|
||||
let response = service.fetch(&request);
|
||||
service.login_admin().await;
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_user_cannot_delete_self() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
#[actix_web::test]
|
||||
async fn delete_user_cannot_delete_self() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
let request = protocol::delete_user(TEST_USERNAME_ADMIN);
|
||||
service.login_admin();
|
||||
let response = service.fetch(&request);
|
||||
service.login_admin().await;
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_preferences_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn get_preferences_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::get_preferences();
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_preferences_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn get_preferences_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::get_preferences();
|
||||
let response = service.fetch_json::<_, user::Preferences>(&request);
|
||||
let response = service.fetch_json::<_, user::Preferences>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn put_preferences_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn put_preferences_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::put_preferences(user::Preferences::default());
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn put_preferences_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
#[actix_web::test]
|
||||
async fn put_preferences_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
service.complete_initial_setup().await;
|
||||
service.login().await;
|
||||
|
||||
let request = protocol::put_preferences(user::Preferences::default());
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ use http::StatusCode;
|
|||
use crate::service::test::{protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn serves_web_client() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
#[actix_web::test]
|
||||
async fn serves_web_client() {
|
||||
let mut service = ServiceType::new(&test_name!()).await;
|
||||
let request = protocol::web_index();
|
||||
let response = service.fetch_bytes(&request);
|
||||
let response = service.fetch_bytes(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
cargo install diesel_cli --no-default-features --features "sqlite-bundled"
|
||||
|
||||
mkdir tmp
|
||||
diesel --database-url tmp/print-schema.sqlite setup
|
||||
diesel --database-url tmp/print-schema.sqlite migration run
|
||||
rmdir /q /s tmp
|
Loading…
Add table
Reference in a new issue