Merge 4ad8d922f7
into release
This commit is contained in:
commit
77b363a2ba
41 changed files with 961 additions and 1264 deletions
.gitignoreCargo.lockCargo.tomlbuild.rs
res
unix
windows
src
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,6 +9,7 @@ TestConfig.toml
|
|||
|
||||
# Runtime artifacts
|
||||
*.sqlite
|
||||
polaris.log
|
||||
/thumbnails
|
||||
|
||||
# Release process artifacts (usually runs on CI)
|
||||
|
|
77
Cargo.lock
generated
77
Cargo.lock
generated
|
@ -448,6 +448,12 @@ dependencies = [
|
|||
"byte-tools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boxfnonce"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5988cb1d626264ac94100be357308f29ff7cbdd3b36bda27f450a4ee3f713426"
|
||||
|
||||
[[package]]
|
||||
name = "branca"
|
||||
version = "0.10.0"
|
||||
|
@ -691,6 +697,16 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "daemonize"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70c24513e34f53b640819f0ac9f705b673fcf4006d7aab8778bee72ebfc89815"
|
||||
dependencies = [
|
||||
"boxfnonce",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "0.8.6"
|
||||
|
@ -903,6 +919,12 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon"
|
||||
version = "0.3.3"
|
||||
|
@ -1555,6 +1577,29 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b78760a249b7611363d02cfbd56974e1957faf2caa4fce36d4207b7edc803b1"
|
||||
|
||||
[[package]]
|
||||
name = "native-windows-derive"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e12bdd46113e604a98d04f19f79249e1679be21a65eaa1dbadec16ba00c94f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.54",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-windows-gui"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe8d44e6cea6bba40a302d1ab3ee50c6d9f9714ab94a776b0db0a5521c49c9ce"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"lazy_static",
|
||||
"winapi 0.3.9",
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "net2"
|
||||
version = "0.2.36"
|
||||
|
@ -1813,8 +1858,10 @@ dependencies = [
|
|||
"branca",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"daemonize",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"fs_extra",
|
||||
"futures-util",
|
||||
"getopts",
|
||||
"headers",
|
||||
|
@ -1827,6 +1874,8 @@ dependencies = [
|
|||
"metaflac",
|
||||
"mp3-duration",
|
||||
"mp4ameta",
|
||||
"native-windows-derive",
|
||||
"native-windows-gui",
|
||||
"num_cpus",
|
||||
"opus_headers",
|
||||
"pbkdf2",
|
||||
|
@ -1843,11 +1892,9 @@ dependencies = [
|
|||
"thiserror",
|
||||
"time 0.2.23",
|
||||
"toml",
|
||||
"unix-daemonize",
|
||||
"ureq",
|
||||
"url",
|
||||
"uuid",
|
||||
"winapi 0.3.9",
|
||||
"winres",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2659,15 +2706,6 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "unix-daemonize"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "531faed80732acaa13d1016c66d6a9180b5045c4fcef8daa20bb2baf46b13907"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
|
@ -2705,12 +2743,6 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
|
||||
|
||||
[[package]]
|
||||
name = "v_escape"
|
||||
version = "0.14.1"
|
||||
|
@ -2920,6 +2952,15 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winres"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff4fb510bbfe5b8992ff15f77a2e6fe6cf062878f0eda00c0f44963a807ca5dc"
|
||||
dependencies = [
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapped-vec"
|
||||
version = "0.2.1"
|
||||
|
|
13
Cargo.toml
13
Cargo.toml
|
@ -3,11 +3,12 @@ name = "polaris"
|
|||
version = "0.13.2"
|
||||
authors = ["Antoine Gersant <antoine.gersant@lesforges.org>"]
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
|
||||
[features]
|
||||
default = ["bundle-sqlite"]
|
||||
bundle-sqlite = ["libsqlite3-sys"]
|
||||
ui = ["uuid", "winapi"]
|
||||
ui = ["native-windows-gui", "native-windows-derive"]
|
||||
|
||||
[dependencies]
|
||||
actix-files = { version = "0.4" }
|
||||
|
@ -59,12 +60,16 @@ default_features = false
|
|||
features = ["bmp", "gif", "jpeg", "png"]
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
uuid = { version="0.8", optional = true }
|
||||
winapi = { version = "0.3.3", features = ["winuser", "libloaderapi", "shellapi", "errhandlingapi"], optional = true }
|
||||
native-windows-gui = {version = "1.0.7", default-features = false, features = ["cursor", "image-decoder", "message-window", "menu", "tray-notification"], optional = true }
|
||||
native-windows-derive = {version = "1.0.2", optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
daemonize = "0.4.1"
|
||||
sd-notify = "0.1.0"
|
||||
unix-daemonize = "0.1.2"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
headers = "0.3"
|
||||
fs_extra = "1.2.0"
|
||||
|
|
9
build.rs
Normal file
9
build.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
#[cfg(windows)]
|
||||
fn main() {
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("./res/windows/application/icon_polaris_512.ico");
|
||||
res.compile().unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn main() {}
|
|
@ -3,7 +3,7 @@ echo "Creating output directory"
|
|||
mkdir -p release/tmp/polaris
|
||||
|
||||
echo "Copying package files"
|
||||
cp -r web docs/swagger src migrations test-data Cargo.toml Cargo.lock rust-toolchain res/unix/Makefile release/tmp/polaris
|
||||
cp -r web docs/swagger src migrations test-data build.rs Cargo.toml Cargo.lock rust-toolchain res/unix/Makefile release/tmp/polaris
|
||||
|
||||
echo "Creating tarball"
|
||||
tar -zc -C release/tmp -f release/polaris.tar.gz polaris
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAware>true</dpiAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
|
@ -1,7 +0,0 @@
|
|||
#define IDI_POLARIS 0x101
|
||||
#define IDI_POLARIS_TRAY 0x102
|
||||
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "application.manifest"
|
||||
|
||||
IDI_POLARIS ICON "icon_polaris_512.ico"
|
||||
IDI_POLARIS_TRAY ICON "icon_polaris_outline_64.ico"
|
BIN
res/windows/application/icon_polaris_outline_16.png
Normal file
BIN
res/windows/application/icon_polaris_outline_16.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 1.8 KiB |
Binary file not shown.
Before (image error) Size: 31 KiB |
|
@ -2,10 +2,6 @@ if (!(Test-Path env:POLARIS_VERSION)) {
|
|||
throw "POLARIS_VERSION environment variable is not defined"
|
||||
}
|
||||
|
||||
"Compiling resource file"
|
||||
$rc_exe = Join-Path "C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64" RC.exe
|
||||
& $rc_exe /fo res\windows\application\application.res res\windows\application\application.rc
|
||||
|
||||
""
|
||||
"Compiling executable"
|
||||
# TODO: Uncomment the following once Polaris can do variable expansion of %LOCALAPPDATA%
|
||||
|
@ -17,8 +13,8 @@ $rc_exe = Join-Path "C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64
|
|||
# $env:POLARIS_LOG_DIR = "$INSTALL_DIR"
|
||||
# $env:POLARIS_CACHE_DIR = "$INSTALL_DIR"
|
||||
# $env:POLARIS_PID_DIR = "$INSTALL_DIR"
|
||||
cargo rustc --release --features "ui" -- -C link-args="/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup res\windows\application\application.res"
|
||||
cargo rustc --release -- -o ".\target\release\polaris-cli.exe" -C link-args="res\windows\application\application.res"
|
||||
cargo rustc --release --features "ui" -- -o ".\target\release\polaris.exe"
|
||||
cargo rustc --release -- -o ".\target\release\polaris-cli.exe"
|
||||
|
||||
""
|
||||
"Creating output directory"
|
||||
|
|
|
@ -1,40 +1,10 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
use crate::app::{settings, user, vfs};
|
||||
use crate::db::DB;
|
||||
use crate::app::{ddns, settings, test, user, vfs};
|
||||
use crate::test_name;
|
||||
|
||||
#[cfg(test)]
|
||||
fn get_test_db(name: &str) -> DB {
|
||||
let mut db_path = PathBuf::new();
|
||||
db_path.push("test-output");
|
||||
fs::create_dir_all(&db_path).unwrap();
|
||||
|
||||
db_path.push(name);
|
||||
if db_path.exists() {
|
||||
fs::remove_file(&db_path).unwrap();
|
||||
}
|
||||
|
||||
DB::new(&db_path).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_saves_misc_settings() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().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());
|
||||
let config_manager = Manager::new(
|
||||
settings_manager.clone(),
|
||||
user_manager.clone(),
|
||||
vfs_manager.clone(),
|
||||
ddns_manager.clone(),
|
||||
);
|
||||
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_config = Config {
|
||||
settings: Some(settings::NewSettings {
|
||||
album_art_pattern: Some("🖼️\\.jpg".into()),
|
||||
|
@ -44,8 +14,8 @@ fn apply_saves_misc_settings() {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
config_manager.apply(&new_config).unwrap();
|
||||
let settings = settings_manager.read().unwrap();
|
||||
ctx.config_manager.apply(&new_config).unwrap();
|
||||
let settings = ctx.settings_manager.read().unwrap();
|
||||
let new_settings = new_config.settings.unwrap();
|
||||
assert_eq!(
|
||||
settings.album_art_pattern,
|
||||
|
@ -59,18 +29,7 @@ fn apply_saves_misc_settings() {
|
|||
|
||||
#[test]
|
||||
fn apply_saves_mount_points() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().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());
|
||||
let config_manager = Manager::new(
|
||||
settings_manager.clone(),
|
||||
user_manager.clone(),
|
||||
vfs_manager.clone(),
|
||||
ddns_manager.clone(),
|
||||
);
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_config = Config {
|
||||
mount_dirs: Some(vec![vfs::MountDir {
|
||||
|
@ -80,27 +39,14 @@ fn apply_saves_mount_points() {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
config_manager.apply(&new_config).unwrap();
|
||||
let actual_mount_dirs: Vec<vfs::MountDir> = vfs_manager.mount_dirs().unwrap();
|
||||
ctx.config_manager.apply(&new_config).unwrap();
|
||||
let actual_mount_dirs: Vec<vfs::MountDir> = ctx.vfs_manager.mount_dirs().unwrap();
|
||||
assert_eq!(actual_mount_dirs, new_config.mount_dirs.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_saves_ddns_settings() {
|
||||
use crate::app::ddns;
|
||||
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().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());
|
||||
let config_manager = Manager::new(
|
||||
settings_manager.clone(),
|
||||
user_manager.clone(),
|
||||
vfs_manager.clone(),
|
||||
ddns_manager.clone(),
|
||||
);
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_config = Config {
|
||||
ydns: Some(ddns::Config {
|
||||
|
@ -111,36 +57,18 @@ fn apply_saves_ddns_settings() {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
config_manager.apply(&new_config).unwrap();
|
||||
let actual_ddns = ddns_manager.config().unwrap();
|
||||
ctx.config_manager.apply(&new_config).unwrap();
|
||||
let actual_ddns = ctx.ddns_manager.config().unwrap();
|
||||
assert_eq!(actual_ddns, new_config.ydns.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_can_toggle_admin() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().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());
|
||||
let config_manager = Manager::new(
|
||||
settings_manager.clone(),
|
||||
user_manager.clone(),
|
||||
vfs_manager.clone(),
|
||||
ddns_manager.clone(),
|
||||
);
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.user("Walter", "Tasty🍖", true)
|
||||
.build();
|
||||
|
||||
let initial_config = Config {
|
||||
users: Some(vec![user::NewUser {
|
||||
name: "Walter".into(),
|
||||
password: "Tasty🍖".into(),
|
||||
admin: true,
|
||||
}]),
|
||||
..Default::default()
|
||||
};
|
||||
config_manager.apply(&initial_config).unwrap();
|
||||
assert!(user_manager.list().unwrap()[0].is_admin());
|
||||
assert!(ctx.user_manager.list().unwrap()[0].is_admin());
|
||||
|
||||
let new_config = Config {
|
||||
users: Some(vec![user::NewUser {
|
||||
|
@ -150,6 +78,6 @@ fn apply_can_toggle_admin() {
|
|||
}]),
|
||||
..Default::default()
|
||||
};
|
||||
config_manager.apply(&new_config).unwrap();
|
||||
assert!(!user_manager.list().unwrap()[0].is_admin());
|
||||
ctx.config_manager.apply(&new_config).unwrap();
|
||||
assert!(!ctx.user_manager.list().unwrap()[0].is_admin());
|
||||
}
|
||||
|
|
|
@ -258,7 +258,7 @@ fn read_mp4(path: &Path) -> Result<SongTags> {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_metadata() {
|
||||
fn reads_file_metadata() {
|
||||
let sample_tags = SongTags {
|
||||
disc_number: Some(3),
|
||||
track_number: Some(1),
|
||||
|
@ -309,7 +309,7 @@ fn test_read_metadata() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_artwork() {
|
||||
fn reads_embedded_artwork() {
|
||||
assert!(
|
||||
read(Path::new("test-data/artwork/sample.mp3"))
|
||||
.unwrap()
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
use diesel::prelude::*;
|
||||
use std::default::Default;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
use crate::app::{index::Index, settings, vfs};
|
||||
use crate::db::{self, directories, songs};
|
||||
use crate::app::test;
|
||||
use crate::db::{directories, songs};
|
||||
use crate::test_name;
|
||||
|
||||
fn get_context(test_name: &str) -> (db::DB, Index) {
|
||||
let db = db::get_test_db(test_name);
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let index = Index::new(db.clone(), vfs_manager, settings_manager);
|
||||
(db, index)
|
||||
}
|
||||
const TEST_MOUNT_NAME: &str = "root";
|
||||
|
||||
#[test]
|
||||
fn test_populate() {
|
||||
let (db, index) = get_context(&test_name!());
|
||||
index.update().unwrap();
|
||||
index.update().unwrap(); // Validates that subsequent updates don't run into conflicts
|
||||
fn update_adds_new_content() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
|
||||
let connection = db.connect().unwrap();
|
||||
ctx.index.update().unwrap();
|
||||
ctx.index.update().unwrap(); // Validates that subsequent updates don't run into conflicts
|
||||
|
||||
let connection = ctx.db.connect().unwrap();
|
||||
let all_directories: Vec<Directory> = directories::table.load(&connection).unwrap();
|
||||
let all_songs: Vec<Song> = songs::table.load(&connection).unwrap();
|
||||
assert_eq!(all_directories.len(), 6);
|
||||
|
@ -28,29 +26,154 @@ fn test_populate() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_metadata() {
|
||||
let target: PathBuf = ["test-data", "small-collection", "Tobokegao", "Picnic"]
|
||||
.iter()
|
||||
.collect();
|
||||
fn update_removes_missing_content() {
|
||||
let builder = test::ContextBuilder::new(test_name!());
|
||||
|
||||
let mut song_path = target.clone();
|
||||
song_path.push("05 - シャーベット (Sherbet).mp3");
|
||||
let original_collection_dir: PathBuf = ["test-data", "small-collection"].iter().collect();
|
||||
let test_collection_dir: PathBuf = builder.test_directory.join("small-collection");
|
||||
|
||||
let mut artwork_path = target.clone();
|
||||
artwork_path.push("Folder.png");
|
||||
let copy_options = fs_extra::dir::CopyOptions::new();
|
||||
fs_extra::dir::copy(
|
||||
&original_collection_dir,
|
||||
&builder.test_directory,
|
||||
©_options,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (db, index) = get_context(&test_name!());
|
||||
index.update().unwrap();
|
||||
let ctx = builder
|
||||
.mount(TEST_MOUNT_NAME, test_collection_dir.to_str().unwrap())
|
||||
.build();
|
||||
|
||||
let connection = db.connect().unwrap();
|
||||
let songs: Vec<Song> = songs::table
|
||||
.filter(songs::title.eq("シャーベット (Sherbet)"))
|
||||
.load(&connection)
|
||||
.unwrap();
|
||||
ctx.index.update().unwrap();
|
||||
|
||||
assert_eq!(songs.len(), 1);
|
||||
let song = &songs[0];
|
||||
assert_eq!(song.path, song_path.to_string_lossy().as_ref());
|
||||
{
|
||||
let connection = ctx.db.connect().unwrap();
|
||||
let all_directories: Vec<Directory> = directories::table.load(&connection).unwrap();
|
||||
let all_songs: Vec<Song> = songs::table.load(&connection).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();
|
||||
{
|
||||
let connection = ctx.db.connect().unwrap();
|
||||
let all_directories: Vec<Directory> = directories::table.load(&connection).unwrap();
|
||||
let all_songs: Vec<Song> = songs::table.load(&connection).unwrap();
|
||||
assert_eq!(all_directories.len(), 4);
|
||||
assert_eq!(all_songs.len(), 8);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
|
||||
let root_path = Path::new(TEST_MOUNT_NAME);
|
||||
let files = ctx.index.browse(Path::new("")).unwrap();
|
||||
assert_eq!(files.len(), 1);
|
||||
match files[0] {
|
||||
CollectionFile::Directory(ref d) => assert_eq!(d.path, root_path.to_str().unwrap()),
|
||||
_ => panic!("Expected directory"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
|
||||
let files = ctx.index.browse(Path::new(TEST_MOUNT_NAME)).unwrap();
|
||||
|
||||
assert_eq!(files.len(), 2);
|
||||
match files[0] {
|
||||
CollectionFile::Directory(ref d) => assert_eq!(d.path, khemmis_path.to_str().unwrap()),
|
||||
_ => panic!("Expected directory"),
|
||||
}
|
||||
|
||||
match files[1] {
|
||||
CollectionFile::Directory(ref d) => assert_eq!(d.path, tobokegao_path.to_str().unwrap()),
|
||||
_ => panic!("Expected directory"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
assert_eq!(songs.len(), 13);
|
||||
assert_eq!(songs[0].title, Some("Above The Water".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_flatten_directory() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
ctx.index.update().unwrap();
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao"].iter().collect();
|
||||
let songs = ctx.index.flatten(&path).unwrap();
|
||||
assert_eq!(songs.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect(); // Prefix of '(Picnic Remixes)'
|
||||
let songs = ctx.index.flatten(&path).unwrap();
|
||||
assert_eq!(songs.len(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
assert_eq!(albums.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
assert_eq!(albums.len(), 2);
|
||||
assert!(albums[0].date_added >= albums[1].date_added);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_get_a_song() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
|
||||
ctx.index.update().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();
|
||||
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);
|
||||
assert_eq!(song.title, Some("シャーベット (Sherbet)".to_owned()));
|
||||
|
@ -60,152 +183,54 @@ fn test_metadata() {
|
|||
assert_eq!(song.year, Some(2016));
|
||||
assert_eq!(
|
||||
song.artwork,
|
||||
Some(artwork_path.to_string_lossy().into_owned())
|
||||
Some(artwork_virtual_path.to_string_lossy().into_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_artwork_pattern_case_insensitive() {
|
||||
let target: PathBuf = ["test-data", "small-collection", "Khemmis", "Hunted"]
|
||||
.iter()
|
||||
.collect();
|
||||
fn indexes_embedded_artwork() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
|
||||
let mut song_path = target.clone();
|
||||
song_path.push("05 - Hunted.mp3");
|
||||
ctx.index.update().unwrap();
|
||||
|
||||
let mut artwork_path = target.clone();
|
||||
artwork_path.push("folder.jpg");
|
||||
let picnic_virtual_dir: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect();
|
||||
let song_virtual_path = picnic_virtual_dir.join("07 - なぜ (Why).mp3");
|
||||
|
||||
let (db, index) = get_context(&test_name!());
|
||||
index.update().unwrap();
|
||||
|
||||
let connection = db.connect().unwrap();
|
||||
let songs: Vec<Song> = songs::table
|
||||
.filter(songs::title.eq("Hunted"))
|
||||
.load(&connection)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(songs.len(), 1);
|
||||
let song = &songs[0];
|
||||
let song = ctx.index.get_song(&song_virtual_path).unwrap();
|
||||
assert_eq!(
|
||||
song.artwork.as_ref().unwrap().to_lowercase(),
|
||||
artwork_path.to_string_lossy().to_lowercase()
|
||||
song.artwork,
|
||||
Some(song_virtual_path.to_string_lossy().into_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_embedded_artwork() {
|
||||
let song_path: PathBuf = [
|
||||
"test-data",
|
||||
"small-collection",
|
||||
"Tobokegao",
|
||||
"Picnic",
|
||||
"07 - なぜ (Why).mp3",
|
||||
]
|
||||
.iter()
|
||||
.collect();
|
||||
fn album_art_pattern_is_case_insensitive() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.mount(TEST_MOUNT_NAME, "test-data/small-collection")
|
||||
.build();
|
||||
|
||||
let (db, index) = get_context(&test_name!());
|
||||
index.update().unwrap();
|
||||
|
||||
let connection = db.connect().unwrap();
|
||||
let songs: Vec<Song> = songs::table
|
||||
.filter(songs::title.eq("なぜ (Why?)"))
|
||||
.load(&connection)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(songs.len(), 1);
|
||||
let song = &songs[0];
|
||||
assert_eq!(song.artwork, Some(song_path.to_string_lossy().into_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_browse_top_level() {
|
||||
let mut root_path = PathBuf::new();
|
||||
root_path.push("root");
|
||||
|
||||
let (_db, index) = get_context(&test_name!());
|
||||
index.update().unwrap();
|
||||
|
||||
let results = index.browse(Path::new("")).unwrap();
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
match results[0] {
|
||||
CollectionFile::Directory(ref d) => assert_eq!(d.path, root_path.to_str().unwrap()),
|
||||
_ => panic!("Expected directory"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_browse() {
|
||||
let khemmis_path: PathBuf = ["root", "Khemmis"].iter().collect();
|
||||
let tobokegao_path: PathBuf = ["root", "Tobokegao"].iter().collect();
|
||||
|
||||
let (_db, index) = get_context(&test_name!());
|
||||
index.update().unwrap();
|
||||
|
||||
let results = index.browse(Path::new("root")).unwrap();
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
match results[0] {
|
||||
CollectionFile::Directory(ref d) => assert_eq!(d.path, khemmis_path.to_str().unwrap()),
|
||||
_ => panic!("Expected directory"),
|
||||
}
|
||||
match results[1] {
|
||||
CollectionFile::Directory(ref d) => assert_eq!(d.path, tobokegao_path.to_str().unwrap()),
|
||||
_ => panic!("Expected directory"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flatten() {
|
||||
let (_db, index) = get_context(&test_name!());
|
||||
index.update().unwrap();
|
||||
|
||||
// Flatten all
|
||||
let results = index.flatten(Path::new("root")).unwrap();
|
||||
assert_eq!(results.len(), 13);
|
||||
assert_eq!(results[0].title, Some("Above The Water".to_owned()));
|
||||
|
||||
// Flatten a directory
|
||||
let path: PathBuf = ["root", "Tobokegao"].iter().collect();
|
||||
let results = index.flatten(&path).unwrap();
|
||||
assert_eq!(results.len(), 8);
|
||||
|
||||
// Flatten a directory that is a prefix of another directory (Picnic Remixes)
|
||||
let path: PathBuf = ["root", "Tobokegao", "Picnic"].iter().collect();
|
||||
let results = index.flatten(&path).unwrap();
|
||||
assert_eq!(results.len(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_random() {
|
||||
let (_db, index) = get_context(&test_name!());
|
||||
index.update().unwrap();
|
||||
|
||||
let results = index.get_random_albums(1).unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recent() {
|
||||
let (_db, index) = get_context(&test_name!());
|
||||
index.update().unwrap();
|
||||
|
||||
let results = index.get_recent_albums(2).unwrap();
|
||||
assert_eq!(results.len(), 2);
|
||||
assert!(results[0].date_added >= results[1].date_added);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_song() {
|
||||
let (_db, index) = get_context(&test_name!());
|
||||
index.update().unwrap();
|
||||
|
||||
let song_path: PathBuf = ["root", "Khemmis", "Hunted", "02 - Candlelight.mp3"]
|
||||
let patterns = vec!["folder", "FOLDER"]
|
||||
.iter()
|
||||
.collect();
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let song = index.get_song(&song_path).unwrap();
|
||||
assert_eq!(song.title.unwrap(), "Candlelight");
|
||||
for pattern in patterns.into_iter() {
|
||||
ctx.settings_manager
|
||||
.amend(&settings::NewSettings {
|
||||
album_art_pattern: Some(pattern),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
ctx.index.update().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];
|
||||
assert_eq!(
|
||||
song.artwork,
|
||||
Some(artwork_virtual_path.to_string_lossy().into_owned())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ impl Cleaner {
|
|||
};
|
||||
|
||||
let thread_pool = rayon::ThreadPoolBuilder::new().build()?;
|
||||
let (missing_songs, missing_directories) =
|
||||
let (missing_directories, missing_songs) =
|
||||
thread_pool.join(list_missing_directories, list_missing_songs);
|
||||
|
||||
{
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::db::DB;
|
||||
use crate::paths::Paths;
|
||||
|
||||
pub mod config;
|
||||
pub mod ddns;
|
||||
pub mod index;
|
||||
|
@ -7,3 +13,74 @@ pub mod settings;
|
|||
pub mod thumbnail;
|
||||
pub mod user;
|
||||
pub mod vfs;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
||||
|
||||
#[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,
|
||||
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,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(port: u16, paths: Paths) -> anyhow::Result<Self> {
|
||||
let db = DB::new(&paths.db_file_path)?;
|
||||
fs::create_dir_all(&paths.web_dir_path)?;
|
||||
fs::create_dir_all(&paths.swagger_dir_path)?;
|
||||
|
||||
let thumbnails_dir_path = paths.cache_dir_path.join("thumbnails");
|
||||
|
||||
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 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());
|
||||
let config_manager = config::Manager::new(
|
||||
settings_manager.clone(),
|
||||
user_manager.clone(),
|
||||
vfs_manager.clone(),
|
||||
ddns_manager.clone(),
|
||||
);
|
||||
let playlist_manager = playlist::Manager::new(db.clone(), vfs_manager.clone());
|
||||
let thumbnail_manager = thumbnail::Manager::new(thumbnails_dir_path);
|
||||
let lastfm_manager = lastfm::Manager::new(index.clone(), user_manager.clone());
|
||||
|
||||
if let Some(config_path) = paths.config_file_path {
|
||||
let config = config::Config::from_path(&config_path)?;
|
||||
config_manager.apply(&config)?;
|
||||
}
|
||||
|
||||
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,
|
||||
config_manager,
|
||||
ddns_manager,
|
||||
lastfm_manager,
|
||||
playlist_manager,
|
||||
settings_manager,
|
||||
thumbnail_manager,
|
||||
user_manager,
|
||||
vfs_manager,
|
||||
db,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,99 +1,118 @@
|
|||
use core::clone::Clone;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
use crate::app::{index::Index, settings, vfs};
|
||||
use crate::db;
|
||||
use crate::app::test;
|
||||
use crate::test_name;
|
||||
|
||||
const TEST_USER: &str = "test_user";
|
||||
const TEST_PASSWORD: &str = "password";
|
||||
const TEST_PLAYLIST_NAME: &str = "Chill & Grill";
|
||||
const TEST_MOUNT_NAME: &str = "root";
|
||||
|
||||
#[test]
|
||||
fn test_create_playlist() {
|
||||
let db = db::get_test_db(&test_name!());
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let manager = Manager::new(db, vfs_manager);
|
||||
fn save_playlist_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.user(TEST_USER, TEST_PASSWORD, false)
|
||||
.build();
|
||||
|
||||
let found_playlists = manager.list_playlists("test_user").unwrap();
|
||||
assert!(found_playlists.is_empty());
|
||||
|
||||
manager
|
||||
.save_playlist("chill_and_grill", "test_user", &Vec::new())
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &Vec::new())
|
||||
.unwrap();
|
||||
let found_playlists = manager.list_playlists("test_user").unwrap();
|
||||
assert_eq!(found_playlists.len(), 1);
|
||||
assert_eq!(found_playlists[0], "chill_and_grill");
|
||||
|
||||
let found_playlists = manager.list_playlists("someone_else");
|
||||
assert!(found_playlists.is_err());
|
||||
let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap();
|
||||
assert_eq!(found_playlists.len(), 1);
|
||||
assert_eq!(found_playlists[0], TEST_PLAYLIST_NAME);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_playlist() {
|
||||
let db = db::get_test_db(&test_name!());
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let manager = Manager::new(db, vfs_manager);
|
||||
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();
|
||||
|
||||
let playlist_content = Vec::new();
|
||||
ctx.index.update().unwrap();
|
||||
|
||||
manager
|
||||
.save_playlist("chill_and_grill", "test_user", &playlist_content)
|
||||
.unwrap();
|
||||
manager
|
||||
.save_playlist("mellow_bungalow", "test_user", &playlist_content)
|
||||
.unwrap();
|
||||
let found_playlists = manager.list_playlists("test_user").unwrap();
|
||||
assert_eq!(found_playlists.len(), 2);
|
||||
|
||||
manager
|
||||
.delete_playlist("chill_and_grill", "test_user")
|
||||
.unwrap();
|
||||
let found_playlists = manager.list_playlists("test_user").unwrap();
|
||||
assert_eq!(found_playlists.len(), 1);
|
||||
assert_eq!(found_playlists[0], "mellow_bungalow");
|
||||
|
||||
let delete_result = manager.delete_playlist("mellow_bungalow", "someone_else");
|
||||
assert!(delete_result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fill_playlist() {
|
||||
let db = db::get_test_db(&test_name!());
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager);
|
||||
let manager = Manager::new(db, vfs_manager);
|
||||
|
||||
index.update().unwrap();
|
||||
|
||||
let mut playlist_content: Vec<String> = index
|
||||
.flatten(Path::new("root"))
|
||||
let playlist_content: Vec<String> = ctx
|
||||
.index
|
||||
.flatten(Path::new(TEST_MOUNT_NAME))
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|s| s.path)
|
||||
.collect();
|
||||
assert_eq!(playlist_content.len(), 13);
|
||||
|
||||
let first_song = playlist_content[0].clone();
|
||||
playlist_content.push(first_song);
|
||||
assert_eq!(playlist_content.len(), 14);
|
||||
|
||||
manager
|
||||
.save_playlist("all_the_music", "test_user", &playlist_content)
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.unwrap();
|
||||
|
||||
let songs = manager.read_playlist("all_the_music", "test_user").unwrap();
|
||||
assert_eq!(songs.len(), 14);
|
||||
assert_eq!(songs[0].title, Some("Above The Water".to_owned()));
|
||||
assert_eq!(songs[13].title, Some("Above The Water".to_owned()));
|
||||
|
||||
let first_song_path: PathBuf = ["root", "Khemmis", "Hunted", "01 - Above The Water.mp3"]
|
||||
.iter()
|
||||
.collect();
|
||||
assert_eq!(songs[0].path, first_song_path.to_str().unwrap());
|
||||
|
||||
// Save again to verify that we don't dupe the content
|
||||
manager
|
||||
.save_playlist("all_the_music", "test_user", &playlist_content)
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.unwrap();
|
||||
let songs = manager.read_playlist("all_the_music", "test_user").unwrap();
|
||||
assert_eq!(songs.len(), 14);
|
||||
|
||||
let songs = ctx
|
||||
.playlist_manager
|
||||
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
||||
.unwrap();
|
||||
assert_eq!(songs.len(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_playlist_golden_path() {
|
||||
let ctx = test::ContextBuilder::new(test_name!())
|
||||
.user(TEST_USER, TEST_PASSWORD, false)
|
||||
.build();
|
||||
|
||||
let playlist_content = Vec::new();
|
||||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.unwrap();
|
||||
|
||||
ctx.playlist_manager
|
||||
.delete_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
||||
.unwrap();
|
||||
|
||||
let found_playlists = ctx.playlist_manager.list_playlists(TEST_USER).unwrap();
|
||||
assert_eq!(found_playlists.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
|
||||
ctx.index.update().unwrap();
|
||||
|
||||
let playlist_content: Vec<String> = ctx
|
||||
.index
|
||||
.flatten(Path::new(TEST_MOUNT_NAME))
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|s| s.path)
|
||||
.collect();
|
||||
assert_eq!(playlist_content.len(), 13);
|
||||
|
||||
ctx.playlist_manager
|
||||
.save_playlist(TEST_PLAYLIST_NAME, TEST_USER, &playlist_content)
|
||||
.unwrap();
|
||||
|
||||
let songs = ctx
|
||||
.playlist_manager
|
||||
.read_playlist(TEST_PLAYLIST_NAME, TEST_USER)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(songs.len(), 13);
|
||||
assert_eq!(songs[0].title, Some("Above The Water".to_owned()));
|
||||
|
||||
let first_song_path: PathBuf = [
|
||||
TEST_MOUNT_NAME,
|
||||
"Khemmis",
|
||||
"Hunted",
|
||||
"01 - Above The Water.mp3",
|
||||
]
|
||||
.iter()
|
||||
.collect();
|
||||
assert_eq!(songs[0].path, first_song_path.to_str().unwrap());
|
||||
}
|
||||
|
|
94
src/app/test.rs
Normal file
94
src/app/test.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::app::{config, ddns, index::Index, lastfm, playlist, settings, thumbnail, user, vfs};
|
||||
use crate::db::DB;
|
||||
use crate::test::*;
|
||||
|
||||
pub struct Context {
|
||||
pub db: DB,
|
||||
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 {
|
||||
config: config::Config,
|
||||
pub test_directory: PathBuf,
|
||||
}
|
||||
|
||||
impl ContextBuilder {
|
||||
pub fn new(test_name: String) -> Self {
|
||||
Self {
|
||||
test_directory: prepare_test_directory(&test_name),
|
||||
config: config::Config::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user(mut self, name: &str, password: &str, is_admin: bool) -> Self {
|
||||
self.config
|
||||
.users
|
||||
.get_or_insert(Vec::new())
|
||||
.push(user::NewUser {
|
||||
name: name.to_owned(),
|
||||
password: password.to_owned(),
|
||||
admin: is_admin,
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mount(mut self, name: &str, source: &str) -> Self {
|
||||
self.config
|
||||
.mount_dirs
|
||||
.get_or_insert(Vec::new())
|
||||
.push(vfs::MountDir {
|
||||
name: name.to_owned(),
|
||||
source: source.to_owned(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Context {
|
||||
let cache_output_dir = self.test_directory.join("cache");
|
||||
let db_path = self.test_directory.join("db.sqlite");
|
||||
|
||||
let db = DB::new(&db_path).unwrap();
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().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());
|
||||
let config_manager = config::Manager::new(
|
||||
settings_manager.clone(),
|
||||
user_manager.clone(),
|
||||
vfs_manager.clone(),
|
||||
ddns_manager.clone(),
|
||||
);
|
||||
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();
|
||||
|
||||
Context {
|
||||
db,
|
||||
index,
|
||||
config_manager,
|
||||
ddns_manager,
|
||||
lastfm_manager,
|
||||
playlist_manager,
|
||||
settings_manager,
|
||||
thumbnail_manager,
|
||||
user_manager,
|
||||
vfs_manager,
|
||||
test_directory: self.test_directory,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,10 +19,6 @@ impl Manager {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_directory(&self) -> &Path {
|
||||
&self.thumbnails_dir_path
|
||||
}
|
||||
|
||||
pub fn get_thumbnail(&self, image_path: &Path, thumbnailoptions: &Options) -> Result<PathBuf> {
|
||||
match self.retrieve_thumbnail(image_path, thumbnailoptions) {
|
||||
Some(path) => Ok(path),
|
||||
|
|
|
@ -75,7 +75,7 @@ fn read_opus(_: &Path) -> Result<DynamicImage> {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_artwork() {
|
||||
fn can_read_artwork_data() {
|
||||
let ext_img = image::open("test-data/artwork/Folder.png")
|
||||
.unwrap()
|
||||
.to_rgb8();
|
||||
|
|
|
@ -1,103 +1,70 @@
|
|||
use super::*;
|
||||
use crate::app::settings;
|
||||
use crate::db::DB;
|
||||
use crate::app::test;
|
||||
use crate::test_name;
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn get_test_db(name: &str) -> DB {
|
||||
let mut db_path = std::path::PathBuf::new();
|
||||
db_path.push("test-output");
|
||||
std::fs::create_dir_all(&db_path).unwrap();
|
||||
|
||||
db_path.push(name);
|
||||
if db_path.exists() {
|
||||
std::fs::remove_file(&db_path).unwrap();
|
||||
}
|
||||
|
||||
DB::new(&db_path).unwrap()
|
||||
}
|
||||
const TEST_USERNAME: &str = "Walter";
|
||||
const TEST_PASSWORD: &str = "super_secret!";
|
||||
|
||||
#[test]
|
||||
fn create_delete_user_golden_path() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: "Walter".to_owned(),
|
||||
password: "super_secret!".to_owned(),
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
assert_eq!(user_manager.list().unwrap().len(), 0);
|
||||
user_manager.create(&new_user).unwrap();
|
||||
assert_eq!(user_manager.list().unwrap().len(), 1);
|
||||
user_manager.delete(&new_user.name).unwrap();
|
||||
assert_eq!(user_manager.list().unwrap().len(), 0);
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert_eq!(ctx.user_manager.list().unwrap().len(), 1);
|
||||
|
||||
ctx.user_manager.delete(&new_user.name).unwrap();
|
||||
assert_eq!(ctx.user_manager.list().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_user_with_blank_username() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: "".to_owned(),
|
||||
password: "super_secret!".to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
user_manager.create(&new_user).unwrap_err(),
|
||||
ctx.user_manager.create(&new_user).unwrap_err(),
|
||||
Error::EmptyUsername
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_user_with_blank_password() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: "Walter".to_owned(),
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: "".to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
user_manager.create(&new_user).unwrap_err(),
|
||||
ctx.user_manager.create(&new_user).unwrap_err(),
|
||||
Error::EmptyPassword
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_create_duplicate_user() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: "Walter".to_owned(),
|
||||
password: "super_secret!".to_owned(),
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
user_manager.create(&new_user).unwrap_err();
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_read_write_preferences() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_preferences = Preferences {
|
||||
web_theme_base: Some("very-dark-theme".to_owned()),
|
||||
|
@ -106,40 +73,34 @@ fn can_read_write_preferences() {
|
|||
};
|
||||
|
||||
let new_user = NewUser {
|
||||
name: "Walter".to_owned(),
|
||||
password: "super_secret!".to_owned(),
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
|
||||
user_manager
|
||||
.write_preferences("Walter", &new_preferences)
|
||||
ctx.user_manager
|
||||
.write_preferences(TEST_USERNAME, &new_preferences)
|
||||
.unwrap();
|
||||
|
||||
let read_preferences = user_manager.read_preferences("Walter").unwrap();
|
||||
let read_preferences = ctx.user_manager.read_preferences("Walter").unwrap();
|
||||
assert_eq!(new_preferences, read_preferences);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_rejects_bad_password() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let username = "Walter";
|
||||
let password = "super_secret!";
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert_eq!(
|
||||
user_manager
|
||||
.login(username, "not the password")
|
||||
ctx.user_manager
|
||||
.login(TEST_USERNAME, "not the password")
|
||||
.unwrap_err(),
|
||||
Error::IncorrectPassword
|
||||
)
|
||||
|
@ -147,72 +108,57 @@ fn login_rejects_bad_password() {
|
|||
|
||||
#[test]
|
||||
fn login_golden_path() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let username = "Walter";
|
||||
let password = "super_secret!";
|
||||
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
let new_user = NewUser {
|
||||
name: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
assert!(user_manager.login(username, password).is_ok())
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
assert!(ctx.user_manager.login(TEST_USERNAME, TEST_PASSWORD).is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_rejects_bad_token() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let username = "Walter";
|
||||
let password = "super_secret!";
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
let fake_token = AuthToken("fake token".to_owned());
|
||||
assert!(user_manager
|
||||
assert!(ctx
|
||||
.user_manager
|
||||
.authenticate(&fake_token, AuthorizationScope::PolarisAuth)
|
||||
.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_golden_path() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let username = "Walter";
|
||||
let password = "super_secret!";
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
let token = user_manager.login(username, password).unwrap();
|
||||
let authorization = user_manager
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
let token = ctx
|
||||
.user_manager
|
||||
.login(TEST_USERNAME, TEST_PASSWORD)
|
||||
.unwrap();
|
||||
let authorization = ctx
|
||||
.user_manager
|
||||
.authenticate(&token, AuthorizationScope::PolarisAuth)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
authorization,
|
||||
Authorization {
|
||||
username: username.to_owned(),
|
||||
username: TEST_USERNAME.to_owned(),
|
||||
scope: AuthorizationScope::PolarisAuth,
|
||||
}
|
||||
)
|
||||
|
@ -220,23 +166,22 @@ fn authenticate_golden_path() {
|
|||
|
||||
#[test]
|
||||
fn authenticate_validates_scope() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let username = "Walter";
|
||||
let password = "super_secret!";
|
||||
let ctx = test::ContextBuilder::new(test_name!()).build();
|
||||
|
||||
let new_user = NewUser {
|
||||
name: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
name: TEST_USERNAME.to_owned(),
|
||||
password: TEST_PASSWORD.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
let token = user_manager.generate_lastfm_link_token(username).unwrap();
|
||||
let authorization = user_manager.authenticate(&token, AuthorizationScope::PolarisAuth);
|
||||
ctx.user_manager.create(&new_user).unwrap();
|
||||
let token = ctx
|
||||
.user_manager
|
||||
.generate_lastfm_link_token(TEST_USERNAME)
|
||||
.unwrap();
|
||||
let authorization = ctx
|
||||
.user_manager
|
||||
.authenticate(&token, AuthorizationScope::PolarisAuth);
|
||||
assert_eq!(
|
||||
authorization.unwrap_err(),
|
||||
Error::IncorrectAuthorizationScope
|
||||
|
|
|
@ -3,60 +3,42 @@ use std::path::{Path, PathBuf};
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_virtual_to_real() {
|
||||
fn converts_virtual_to_real() {
|
||||
let vfs = VFS::new(vec![Mount {
|
||||
name: "root".to_owned(),
|
||||
source: Path::new("test_dir").to_owned(),
|
||||
}]);
|
||||
|
||||
let mut correct_path = PathBuf::new();
|
||||
correct_path.push("test_dir");
|
||||
correct_path.push("somewhere");
|
||||
correct_path.push("something.png");
|
||||
|
||||
let mut virtual_path = PathBuf::new();
|
||||
virtual_path.push("root");
|
||||
virtual_path.push("somewhere");
|
||||
virtual_path.push("something.png");
|
||||
|
||||
let found_path = vfs.virtual_to_real(virtual_path.as_path()).unwrap();
|
||||
assert!(found_path.to_str() == correct_path.to_str());
|
||||
let real_path: PathBuf = ["test_dir", "somewhere", "something.png"].iter().collect();
|
||||
let virtual_path: PathBuf = ["root", "somewhere", "something.png"].iter().collect();
|
||||
let converted_path = vfs.virtual_to_real(virtual_path.as_path()).unwrap();
|
||||
assert_eq!(converted_path, real_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_virtual_to_real_no_trail() {
|
||||
fn converts_virtual_to_real_top_level() {
|
||||
let vfs = VFS::new(vec![Mount {
|
||||
name: "root".to_owned(),
|
||||
source: Path::new("test_dir").to_owned(),
|
||||
}]);
|
||||
let correct_path = Path::new("test_dir");
|
||||
let found_path = vfs.virtual_to_real(Path::new("root")).unwrap();
|
||||
assert!(found_path.to_str() == correct_path.to_str());
|
||||
let real_path = Path::new("test_dir");
|
||||
let converted_path = vfs.virtual_to_real(Path::new("root")).unwrap();
|
||||
assert_eq!(converted_path, real_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_real_to_virtual() {
|
||||
fn converts_real_to_virtual() {
|
||||
let vfs = VFS::new(vec![Mount {
|
||||
name: "root".to_owned(),
|
||||
source: Path::new("test_dir").to_owned(),
|
||||
}]);
|
||||
|
||||
let mut correct_path = PathBuf::new();
|
||||
correct_path.push("root");
|
||||
correct_path.push("somewhere");
|
||||
correct_path.push("something.png");
|
||||
|
||||
let mut real_path = PathBuf::new();
|
||||
real_path.push("test_dir");
|
||||
real_path.push("somewhere");
|
||||
real_path.push("something.png");
|
||||
|
||||
let found_path = vfs.real_to_virtual(real_path.as_path()).unwrap();
|
||||
assert!(found_path == correct_path);
|
||||
let virtual_path: PathBuf = ["root", "somewhere", "something.png"].iter().collect();
|
||||
let real_path: PathBuf = ["test_dir", "somewhere", "something.png"].iter().collect();
|
||||
let converted_path = vfs.real_to_virtual(real_path.as_path()).unwrap();
|
||||
assert_eq!(converted_path, virtual_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_path_string() {
|
||||
fn cleans_path_string() {
|
||||
let mut correct_path = path::PathBuf::new();
|
||||
if cfg!(target_os = "windows") {
|
||||
correct_path.push("C:\\");
|
||||
|
|
|
@ -3,7 +3,7 @@ use diesel::r2d2::{self, ConnectionManager, PooledConnection};
|
|||
use diesel::sqlite::SqliteConnection;
|
||||
use diesel::RunQueryDsl;
|
||||
use diesel_migrations;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
mod schema;
|
||||
|
||||
|
@ -16,7 +16,6 @@ embed_migrations!("migrations");
|
|||
#[derive(Clone)]
|
||||
pub struct DB {
|
||||
pool: r2d2::Pool<ConnectionManager<SqliteConnection>>,
|
||||
location: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -42,22 +41,16 @@ impl diesel::r2d2::CustomizeConnection<SqliteConnection, diesel::r2d2::Error>
|
|||
|
||||
impl DB {
|
||||
pub fn new(path: &Path) -> Result<DB> {
|
||||
std::fs::create_dir_all(&path.parent().unwrap())?;
|
||||
let manager = ConnectionManager::<SqliteConnection>::new(path.to_string_lossy());
|
||||
let pool = diesel::r2d2::Pool::builder()
|
||||
.connection_customizer(Box::new(ConnectionCustomizer {}))
|
||||
.build(manager)?;
|
||||
let db = DB {
|
||||
pool: pool,
|
||||
location: path.to_owned(),
|
||||
};
|
||||
let db = DB { pool: pool };
|
||||
db.migrate_up()?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
pub fn location(&self) -> &Path {
|
||||
&self.location
|
||||
}
|
||||
|
||||
pub fn connect(&self) -> Result<PooledConnection<ConnectionManager<SqliteConnection>>> {
|
||||
self.pool.get().map_err(Error::new)
|
||||
}
|
||||
|
@ -87,42 +80,14 @@ impl DB {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn get_test_db(name: &str) -> DB {
|
||||
use crate::app::{config, ddns, settings, user, vfs};
|
||||
|
||||
let mut db_path = std::path::PathBuf::new();
|
||||
db_path.push("test-output");
|
||||
std::fs::create_dir_all(&db_path).unwrap();
|
||||
|
||||
db_path.push(name);
|
||||
if db_path.exists() {
|
||||
std::fs::remove_file(&db_path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().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());
|
||||
let config_manager =
|
||||
config::Manager::new(settings_manager, user_manager, vfs_manager, ddns_manager);
|
||||
|
||||
let config_path = Path::new("test-data/config.toml");
|
||||
let config = config::Config::from_path(&config_path).unwrap();
|
||||
config_manager.apply(&config).unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migrations_up() {
|
||||
get_test_db("migrations_up.sqlite");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migrations_down() {
|
||||
let db = get_test_db("migrations_down.sqlite");
|
||||
db.migrate_down().unwrap();
|
||||
db.migrate_up().unwrap();
|
||||
}
|
||||
|
|
154
src/main.rs
154
src/main.rs
|
@ -1,3 +1,4 @@
|
|||
#![cfg_attr(all(windows, feature = "ui"), windows_subsystem = "windows")]
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
#[macro_use]
|
||||
|
@ -6,12 +7,15 @@ extern crate diesel;
|
|||
extern crate diesel_migrations;
|
||||
|
||||
use anyhow::*;
|
||||
use log::{error, info};
|
||||
use simplelog::{LevelFilter, SimpleLogger, TermLogger, TerminalMode};
|
||||
use log::info;
|
||||
use simplelog::{CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod app;
|
||||
mod db;
|
||||
mod options;
|
||||
mod paths;
|
||||
mod service;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
@ -19,75 +23,46 @@ mod ui;
|
|||
mod utils;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn daemonize(
|
||||
foreground: bool,
|
||||
pid_file_path: &Option<std::path::PathBuf>,
|
||||
log_file_path: &Option<std::path::PathBuf>,
|
||||
) -> Result<()> {
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use unix_daemonize::{daemonize_redirect, ChdirMode};
|
||||
|
||||
fn daemonize(foreground: bool, pid_file_path: &PathBuf) -> Result<()> {
|
||||
if foreground {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let log_path = log_file_path.clone().unwrap_or_else(|| {
|
||||
let mut path = PathBuf::from(option_env!("POLARIS_LOG_DIR").unwrap_or("."));
|
||||
path.push("polaris.log");
|
||||
path
|
||||
});
|
||||
fs::create_dir_all(&log_path.parent().unwrap())?;
|
||||
|
||||
let pid = match daemonize_redirect(Some(&log_path), Some(&log_path), ChdirMode::NoChdir) {
|
||||
Ok(p) => p,
|
||||
Err(e) => bail!("Daemonize error: {:#?}", e),
|
||||
};
|
||||
|
||||
let pid_path = pid_file_path.clone().unwrap_or_else(|| {
|
||||
let mut path = PathBuf::from(option_env!("POLARIS_PID_DIR").unwrap_or("."));
|
||||
path.push("polaris.pid");
|
||||
path
|
||||
});
|
||||
fs::create_dir_all(&pid_path.parent().unwrap())?;
|
||||
|
||||
let mut file = fs::File::create(pid_path)?;
|
||||
file.write_all(pid.to_string().as_bytes())?;
|
||||
if let Some(parent) = pid_file_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let daemonize = daemonize::Daemonize::new()
|
||||
.pid_file(pid_file_path)
|
||||
.working_directory(".");
|
||||
daemonize.start()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn notify_ready() {
|
||||
fn notify_ready() -> Result<()> {
|
||||
if let Ok(true) = sd_notify::booted() {
|
||||
if let Err(e) = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]) {
|
||||
error!("Unable to send ready notification: {}", e);
|
||||
}
|
||||
sd_notify::notify(true, &[sd_notify::NotifyState::Ready])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn notify_ready() {}
|
||||
|
||||
fn init_logging(cli_options: &options::CLIOptions) -> Result<()> {
|
||||
let log_level = cli_options.log_level.unwrap_or(LevelFilter::Info);
|
||||
fn init_logging(log_level: LevelFilter, log_file_path: &PathBuf) -> Result<()> {
|
||||
let log_config = simplelog::ConfigBuilder::new()
|
||||
.set_location_level(LevelFilter::Error)
|
||||
.build();
|
||||
|
||||
#[cfg(unix)]
|
||||
let prefer_term_logger = cli_options.foreground;
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let prefer_term_logger = true;
|
||||
|
||||
if prefer_term_logger {
|
||||
match TermLogger::init(log_level, log_config.clone(), TerminalMode::Stdout) {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => error!("Error starting terminal logger: {}", e),
|
||||
}
|
||||
if let Some(parent) = log_file_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
SimpleLogger::init(log_level, log_config)?;
|
||||
|
||||
CombinedLogger::init(vec![
|
||||
TermLogger::new(log_level, log_config.clone(), TerminalMode::Mixed),
|
||||
WriteLogger::new(
|
||||
log_level,
|
||||
log_config.clone(),
|
||||
fs::File::create(log_file_path)?,
|
||||
),
|
||||
])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -104,60 +79,41 @@ fn main() -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let paths = paths::Paths::new(&cli_options);
|
||||
|
||||
// Logging
|
||||
let log_level = cli_options.log_level.unwrap_or(LevelFilter::Info);
|
||||
init_logging(log_level, &paths.log_file_path)?;
|
||||
|
||||
// Fork
|
||||
#[cfg(unix)]
|
||||
daemonize(
|
||||
cli_options.foreground,
|
||||
&cli_options.pid_file_path,
|
||||
&cli_options.log_file_path,
|
||||
)?;
|
||||
daemonize(cli_options.foreground, &paths.pid_file_path)?;
|
||||
|
||||
init_logging(&cli_options)?;
|
||||
info!("Cache files location is {:#?}", paths.cache_dir_path);
|
||||
info!("Config files location is {:#?}", paths.config_file_path);
|
||||
info!("Database file location is {:#?}", paths.db_file_path);
|
||||
info!("Log file location is {:#?}", paths.log_file_path);
|
||||
#[cfg(unix)]
|
||||
if !cli_options.foreground {
|
||||
info!("Pid file location is {:#?}", paths.pid_file_path);
|
||||
}
|
||||
info!("Swagger files location is {:#?}", paths.swagger_dir_path);
|
||||
info!("Web client files location is {:#?}", paths.web_dir_path);
|
||||
|
||||
// Create service context
|
||||
let mut context_builder = service::ContextBuilder::new();
|
||||
if let Some(port) = cli_options.port {
|
||||
context_builder = context_builder.port(port);
|
||||
}
|
||||
if let Some(path) = cli_options.config_file_path {
|
||||
info!("Config file location is {:#?}", path);
|
||||
context_builder = context_builder.config_file_path(path);
|
||||
}
|
||||
if let Some(path) = cli_options.database_file_path {
|
||||
context_builder = context_builder.database_file_path(path);
|
||||
}
|
||||
if let Some(path) = cli_options.web_dir_path {
|
||||
context_builder = context_builder.web_dir_path(path);
|
||||
}
|
||||
if let Some(path) = cli_options.swagger_dir_path {
|
||||
context_builder = context_builder.swagger_dir_path(path);
|
||||
}
|
||||
if let Some(path) = cli_options.cache_dir_path {
|
||||
context_builder = context_builder.cache_dir_path(path);
|
||||
}
|
||||
|
||||
let context = context_builder.build()?;
|
||||
info!("Database file location is {:#?}", context.db.location());
|
||||
info!("Web client files location is {:#?}", context.web_dir_path);
|
||||
info!("Swagger files location is {:#?}", context.swagger_dir_path);
|
||||
info!(
|
||||
"Thumbnails files location is {:#?}",
|
||||
context.thumbnail_manager.get_directory()
|
||||
);
|
||||
|
||||
// Begin collection scans
|
||||
context.index.begin_periodic_updates();
|
||||
|
||||
// Start DDNS updates
|
||||
context.ddns_manager.begin_periodic_updates();
|
||||
// Create and run app
|
||||
let app = app::App::new(cli_options.port.unwrap_or(5050), paths)?;
|
||||
app.index.begin_periodic_updates();
|
||||
app.ddns_manager.begin_periodic_updates();
|
||||
|
||||
// Start server
|
||||
info!("Starting up server");
|
||||
std::thread::spawn(move || {
|
||||
let _ = service::run(context);
|
||||
let _ = service::run(app);
|
||||
});
|
||||
|
||||
// Send readiness notification
|
||||
notify_ready();
|
||||
#[cfg(unix)]
|
||||
notify_ready()?;
|
||||
|
||||
// Run UI
|
||||
ui::run();
|
||||
|
|
|
@ -7,6 +7,7 @@ pub struct CLIOptions {
|
|||
#[cfg(unix)]
|
||||
pub foreground: bool,
|
||||
pub log_file_path: Option<PathBuf>,
|
||||
#[cfg(unix)]
|
||||
pub pid_file_path: Option<PathBuf>,
|
||||
pub config_file_path: Option<PathBuf>,
|
||||
pub database_file_path: Option<PathBuf>,
|
||||
|
@ -36,6 +37,7 @@ impl Manager {
|
|||
#[cfg(unix)]
|
||||
foreground: matches.opt_present("f"),
|
||||
log_file_path: matches.opt_str("log").map(PathBuf::from),
|
||||
#[cfg(unix)]
|
||||
pid_file_path: matches.opt_str("pid").map(PathBuf::from),
|
||||
config_file_path: matches.opt_str("c").map(PathBuf::from),
|
||||
database_file_path: matches.opt_str("d").map(PathBuf::from),
|
||||
|
|
106
src/paths.rs
Normal file
106
src/paths.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::options::CLIOptions;
|
||||
|
||||
pub struct Paths {
|
||||
pub cache_dir_path: PathBuf,
|
||||
pub config_file_path: Option<PathBuf>,
|
||||
pub db_file_path: PathBuf,
|
||||
pub log_file_path: PathBuf,
|
||||
#[cfg(unix)]
|
||||
pub pid_file_path: PathBuf,
|
||||
pub swagger_dir_path: PathBuf,
|
||||
pub web_dir_path: PathBuf,
|
||||
}
|
||||
|
||||
// TODO Make this the only implementation when we can expand %LOCALAPPDATA% correctly on Windows
|
||||
// And fix the installer accordingly (`release_script.ps1`)
|
||||
#[cfg(not(windows))]
|
||||
impl Default for Paths {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cache_dir_path: ["."].iter().collect(),
|
||||
config_file_path: None,
|
||||
db_file_path: [".", "db.sqlite"].iter().collect(),
|
||||
log_file_path: [".", "polaris.log"].iter().collect(),
|
||||
pid_file_path: [".", "polaris.pid"].iter().collect(),
|
||||
swagger_dir_path: [".", "docs", "swagger"].iter().collect(),
|
||||
web_dir_path: [".", "web"].iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl Default for Paths {
|
||||
fn default() -> Self {
|
||||
let local_app_data = std::env::var("LOCALAPPDATA").map(PathBuf::from).unwrap();
|
||||
let install_directory: PathBuf =
|
||||
local_app_data.join(["Permafrost", "Polaris"].iter().collect::<PathBuf>());
|
||||
Self {
|
||||
cache_dir_path: install_directory.clone(),
|
||||
config_file_path: None,
|
||||
db_file_path: install_directory.join("db.sqlite"),
|
||||
log_file_path: install_directory.join("polaris.log"),
|
||||
swagger_dir_path: install_directory.join("swagger"),
|
||||
web_dir_path: install_directory.join("web"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Paths {
|
||||
fn from_build() -> Self {
|
||||
let defaults = Self::default();
|
||||
Self {
|
||||
db_file_path: option_env!("POLARIS_DB_DIR")
|
||||
.map(PathBuf::from)
|
||||
.map(|p| p.join("db.sqlite"))
|
||||
.unwrap_or(defaults.db_file_path),
|
||||
config_file_path: None,
|
||||
cache_dir_path: option_env!("POLARIS_CACHE_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(defaults.cache_dir_path),
|
||||
log_file_path: option_env!("POLARIS_LOG_DIR")
|
||||
.map(PathBuf::from)
|
||||
.map(|p| p.join("polaris.log"))
|
||||
.unwrap_or(defaults.log_file_path),
|
||||
#[cfg(unix)]
|
||||
pid_file_path: option_env!("POLARIS_PID_DIR")
|
||||
.map(PathBuf::from)
|
||||
.map(|p| p.join("polaris.pid"))
|
||||
.unwrap_or(defaults.pid_file_path),
|
||||
swagger_dir_path: option_env!("POLARIS_SWAGGER_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(defaults.swagger_dir_path),
|
||||
web_dir_path: option_env!("POLARIS_WEB_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(defaults.web_dir_path),
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
if let Some(path) = &cli_options.log_file_path {
|
||||
paths.log_file_path = path.clone();
|
||||
}
|
||||
#[cfg(unix)]
|
||||
if let Some(path) = &cli_options.pid_file_path {
|
||||
paths.pid_file_path = path.clone();
|
||||
}
|
||||
if let Some(path) = &cli_options.swagger_dir_path {
|
||||
paths.swagger_dir_path = path.clone();
|
||||
}
|
||||
if let Some(path) = &cli_options.web_dir_path {
|
||||
paths.web_dir_path = path.clone();
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
}
|
|
@ -2,58 +2,58 @@ use actix_web::{
|
|||
middleware::{normalize::TrailingSlash, Compress, Logger, NormalizePath},
|
||||
rt::System,
|
||||
web::{self, ServiceConfig},
|
||||
App, HttpServer,
|
||||
App as ActixApp, HttpServer,
|
||||
};
|
||||
use anyhow::*;
|
||||
use log::error;
|
||||
|
||||
use crate::service;
|
||||
use crate::app::App;
|
||||
|
||||
mod api;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
||||
|
||||
pub fn make_config(context: service::Context) -> impl FnOnce(&mut ServiceConfig) + Clone {
|
||||
pub fn make_config(app: App) -> impl FnOnce(&mut ServiceConfig) + Clone {
|
||||
move |cfg: &mut ServiceConfig| {
|
||||
let encryption_key = cookie::Key::derive_from(&context.auth_secret.key[..]);
|
||||
cfg.app_data(web::Data::new(context.index))
|
||||
.app_data(web::Data::new(context.config_manager))
|
||||
.app_data(web::Data::new(context.ddns_manager))
|
||||
.app_data(web::Data::new(context.lastfm_manager))
|
||||
.app_data(web::Data::new(context.playlist_manager))
|
||||
.app_data(web::Data::new(context.settings_manager))
|
||||
.app_data(web::Data::new(context.thumbnail_manager))
|
||||
.app_data(web::Data::new(context.user_manager))
|
||||
.app_data(web::Data::new(context.vfs_manager))
|
||||
let encryption_key = cookie::Key::derive_from(&app.auth_secret.key[..]);
|
||||
cfg.app_data(web::Data::new(app.index))
|
||||
.app_data(web::Data::new(app.config_manager))
|
||||
.app_data(web::Data::new(app.ddns_manager))
|
||||
.app_data(web::Data::new(app.lastfm_manager))
|
||||
.app_data(web::Data::new(app.playlist_manager))
|
||||
.app_data(web::Data::new(app.settings_manager))
|
||||
.app_data(web::Data::new(app.thumbnail_manager))
|
||||
.app_data(web::Data::new(app.user_manager))
|
||||
.app_data(web::Data::new(app.vfs_manager))
|
||||
.app_data(web::Data::new(encryption_key))
|
||||
.service(
|
||||
web::scope(&context.api_url)
|
||||
web::scope("/api")
|
||||
.configure(api::make_config())
|
||||
.wrap_fn(api::http_auth_middleware)
|
||||
.wrap(NormalizePath::new(TrailingSlash::Trim)),
|
||||
)
|
||||
.service(
|
||||
actix_files::Files::new(&context.swagger_url, context.swagger_dir_path)
|
||||
actix_files::Files::new("/swagger", app.swagger_dir_path)
|
||||
.redirect_to_slash_directory()
|
||||
.index_file("index.html"),
|
||||
)
|
||||
.service(
|
||||
actix_files::Files::new(&context.web_url, context.web_dir_path)
|
||||
actix_files::Files::new("/", app.web_dir_path)
|
||||
.redirect_to_slash_directory()
|
||||
.index_file("index.html"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(context: service::Context) -> Result<()> {
|
||||
pub fn run(app: App) -> Result<()> {
|
||||
System::run(move || {
|
||||
let address = format!("0.0.0.0:{}", context.port);
|
||||
let address = format!("0.0.0.0:{}", app.port);
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
ActixApp::new()
|
||||
.wrap(Logger::default())
|
||||
.wrap(Compress::default())
|
||||
.configure(make_config(context.clone()))
|
||||
.configure(make_config(app.clone()))
|
||||
})
|
||||
.disable_signals()
|
||||
.bind(address)
|
||||
|
|
|
@ -4,18 +4,19 @@ use actix_web::{
|
|||
test,
|
||||
test::*,
|
||||
web::Bytes,
|
||||
App,
|
||||
App as ActixApp,
|
||||
};
|
||||
use http::{response::Builder, Method, Request, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::paths::Paths;
|
||||
use crate::service::actix::*;
|
||||
use crate::service::dto;
|
||||
use crate::service::test::TestService;
|
||||
use crate::test::*;
|
||||
|
||||
pub struct ActixTestService {
|
||||
system_runner: SystemRunner,
|
||||
|
@ -76,27 +77,25 @@ impl ActixTestService {
|
|||
|
||||
impl TestService for ActixTestService {
|
||||
fn new(test_name: &str) -> Self {
|
||||
let mut db_path: PathBuf = ["test-output", test_name].iter().collect();
|
||||
fs::create_dir_all(&db_path).unwrap();
|
||||
db_path.push("db.sqlite");
|
||||
let output_dir = prepare_test_directory(test_name);
|
||||
|
||||
if db_path.exists() {
|
||||
fs::remove_file(&db_path).unwrap();
|
||||
}
|
||||
let paths = Paths {
|
||||
cache_dir_path: ["test-output", test_name].iter().collect(),
|
||||
config_file_path: None,
|
||||
db_file_path: output_dir.join("db.sqlite"),
|
||||
#[cfg(unix)]
|
||||
pid_file_path: output_dir.join("polaris.pid"),
|
||||
log_file_path: output_dir.join("polaris.log"),
|
||||
swagger_dir_path: ["docs", "swagger"].iter().collect(),
|
||||
web_dir_path: ["test-data", "web"].iter().collect(),
|
||||
};
|
||||
|
||||
let context = service::ContextBuilder::new()
|
||||
.port(5050)
|
||||
.database_file_path(db_path)
|
||||
.web_dir_path(Path::new("test-data/web").into())
|
||||
.swagger_dir_path(["docs", "swagger"].iter().collect())
|
||||
.cache_dir_path(["test-output", test_name].iter().collect())
|
||||
.build()
|
||||
.unwrap();
|
||||
let app = App::new(5050, paths).unwrap();
|
||||
|
||||
let system_runner = System::new("test");
|
||||
let server = test::start(move || {
|
||||
let config = make_config(context.clone());
|
||||
App::new()
|
||||
let config = make_config(app.clone());
|
||||
ActixApp::new()
|
||||
.wrap(Logger::default())
|
||||
.wrap(Compress::default())
|
||||
.configure(config)
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::app::{config, ddns, index::Index, lastfm, playlist, settings, thumbnail, user, vfs};
|
||||
use crate::db::DB;
|
||||
|
||||
mod dto;
|
||||
mod error;
|
||||
|
||||
|
@ -12,196 +6,3 @@ mod test;
|
|||
|
||||
mod actix;
|
||||
pub use actix::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pub port: u16,
|
||||
pub auth_secret: settings::AuthSecret,
|
||||
pub web_dir_path: PathBuf,
|
||||
pub swagger_dir_path: PathBuf,
|
||||
pub web_url: String,
|
||||
pub swagger_url: String,
|
||||
pub api_url: String,
|
||||
pub db: DB,
|
||||
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,
|
||||
}
|
||||
|
||||
struct Paths {
|
||||
db_dir_path: PathBuf,
|
||||
web_dir_path: PathBuf,
|
||||
swagger_dir_path: PathBuf,
|
||||
cache_dir_path: PathBuf,
|
||||
}
|
||||
|
||||
// TODO Make this the only implementation when we can expand %LOCALAPPDATA% correctly on Windows
|
||||
// And fix the installer accordingly (`release_script.ps1`)
|
||||
#[cfg(not(windows))]
|
||||
impl Default for Paths {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
db_dir_path: ["."].iter().collect(),
|
||||
web_dir_path: [".", "web"].iter().collect(),
|
||||
swagger_dir_path: [".", "docs", "swagger"].iter().collect(),
|
||||
cache_dir_path: ["."].iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl Default for Paths {
|
||||
fn default() -> Self {
|
||||
let local_app_data = std::env::var("LOCALAPPDATA").map(PathBuf::from).unwrap();
|
||||
let install_directory: PathBuf =
|
||||
local_app_data.join(["Permafrost", "Polaris"].iter().collect::<PathBuf>());
|
||||
Self {
|
||||
db_dir_path: install_directory.clone(),
|
||||
web_dir_path: install_directory.join("web"),
|
||||
swagger_dir_path: install_directory.join("swagger"),
|
||||
cache_dir_path: install_directory.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Paths {
|
||||
fn new() -> Self {
|
||||
let defaults = Self::default();
|
||||
Self {
|
||||
db_dir_path: option_env!("POLARIS_DB_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(defaults.db_dir_path),
|
||||
web_dir_path: option_env!("POLARIS_WEB_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(defaults.web_dir_path),
|
||||
swagger_dir_path: option_env!("POLARIS_SWAGGER_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(defaults.swagger_dir_path),
|
||||
cache_dir_path: option_env!("POLARIS_CACHE_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(defaults.cache_dir_path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextBuilder {
|
||||
port: Option<u16>,
|
||||
config_file_path: Option<PathBuf>,
|
||||
database_file_path: Option<PathBuf>,
|
||||
web_dir_path: Option<PathBuf>,
|
||||
swagger_dir_path: Option<PathBuf>,
|
||||
cache_dir_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ContextBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
port: None,
|
||||
config_file_path: None,
|
||||
database_file_path: None,
|
||||
web_dir_path: None,
|
||||
swagger_dir_path: None,
|
||||
cache_dir_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(self) -> anyhow::Result<Context> {
|
||||
let paths = Paths::new();
|
||||
|
||||
let db_path = self
|
||||
.database_file_path
|
||||
.unwrap_or(paths.db_dir_path.join("db.sqlite"));
|
||||
fs::create_dir_all(&db_path.parent().unwrap())?;
|
||||
let db = DB::new(&db_path)?;
|
||||
|
||||
let web_dir_path = self.web_dir_path.unwrap_or(paths.web_dir_path);
|
||||
fs::create_dir_all(&web_dir_path)?;
|
||||
|
||||
let swagger_dir_path = self.swagger_dir_path.unwrap_or(paths.swagger_dir_path);
|
||||
fs::create_dir_all(&swagger_dir_path)?;
|
||||
|
||||
let thumbnails_dir_path = self
|
||||
.cache_dir_path
|
||||
.unwrap_or(paths.cache_dir_path)
|
||||
.join("thumbnails");
|
||||
|
||||
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 ddns_manager = ddns::Manager::new(db.clone());
|
||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||
let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone());
|
||||
let config_manager = config::Manager::new(
|
||||
settings_manager.clone(),
|
||||
user_manager.clone(),
|
||||
vfs_manager.clone(),
|
||||
ddns_manager.clone(),
|
||||
);
|
||||
let playlist_manager = playlist::Manager::new(db.clone(), vfs_manager.clone());
|
||||
let thumbnail_manager = thumbnail::Manager::new(thumbnails_dir_path);
|
||||
let lastfm_manager = lastfm::Manager::new(index.clone(), user_manager.clone());
|
||||
|
||||
if let Some(config_path) = self.config_file_path {
|
||||
let config = config::Config::from_path(&config_path)?;
|
||||
config_manager.apply(&config)?;
|
||||
}
|
||||
|
||||
let auth_secret = settings_manager.get_auth_secret()?;
|
||||
|
||||
Ok(Context {
|
||||
port: self.port.unwrap_or(5050),
|
||||
auth_secret,
|
||||
api_url: "/api".to_owned(),
|
||||
swagger_url: "/swagger".to_owned(),
|
||||
web_url: "/".to_owned(),
|
||||
web_dir_path,
|
||||
swagger_dir_path,
|
||||
index,
|
||||
config_manager,
|
||||
ddns_manager,
|
||||
lastfm_manager,
|
||||
playlist_manager,
|
||||
settings_manager,
|
||||
thumbnail_manager,
|
||||
user_manager,
|
||||
vfs_manager,
|
||||
db,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = Some(port);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn config_file_path(mut self, path: PathBuf) -> Self {
|
||||
self.config_file_path = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn database_file_path(mut self, path: PathBuf) -> Self {
|
||||
self.database_file_path = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn web_dir_path(mut self, path: PathBuf) -> Self {
|
||||
self.web_dir_path = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn swagger_dir_path(mut self, path: PathBuf) -> Self {
|
||||
self.swagger_dir_path = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cache_dir_path(mut self, path: PathBuf) -> Self {
|
||||
self.cache_dir_path = Some(path);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::service::test::{protocol, ServiceType, TestService};
|
|||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn test_returns_api_version() {
|
||||
fn returns_api_version() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::version();
|
||||
let response = service.fetch_json::<_, dto::Version>(&request);
|
||||
|
@ -14,7 +14,7 @@ fn test_returns_api_version() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_initial_setup_golden_path() {
|
||||
fn initial_setup_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::initial_setup();
|
||||
{
|
||||
|
@ -43,7 +43,7 @@ fn test_initial_setup_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_index_golden_path() {
|
||||
fn trigger_index_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -62,7 +62,7 @@ fn test_trigger_index_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_index_requires_auth() {
|
||||
fn trigger_index_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
let request = protocol::trigger_index();
|
||||
|
@ -71,7 +71,7 @@ fn test_trigger_index_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_trigger_index_requires_admin() {
|
||||
fn trigger_index_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
|
|
@ -52,7 +52,7 @@ fn validate_no_cookies<T>(response: &Response<T>) {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_login_rejects_bad_username() {
|
||||
fn login_rejects_bad_username() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -62,7 +62,7 @@ fn test_login_rejects_bad_username() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_login_rejects_bad_password() {
|
||||
fn login_rejects_bad_password() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -72,7 +72,7 @@ fn test_login_rejects_bad_password() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_login_golden_path() {
|
||||
fn login_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -89,7 +89,7 @@ fn test_login_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_requests_without_auth_header_do_not_set_cookies() {
|
||||
fn requests_without_auth_header_do_not_set_cookies() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -102,7 +102,7 @@ fn test_requests_without_auth_header_do_not_set_cookies() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_basic_http_header_rejects_bad_username() {
|
||||
fn authentication_via_basic_http_header_rejects_bad_username() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -115,7 +115,7 @@ fn test_authentication_via_basic_http_header_rejects_bad_username() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_basic_http_header_rejects_bad_password() {
|
||||
fn authentication_via_basic_http_header_rejects_bad_password() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -128,7 +128,7 @@ fn test_authentication_via_basic_http_header_rejects_bad_password() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_basic_http_header_golden_path() {
|
||||
fn authentication_via_basic_http_header_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -143,7 +143,7 @@ fn test_authentication_via_basic_http_header_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_bearer_http_header_rejects_bad_token() {
|
||||
fn authentication_via_bearer_http_header_rejects_bad_token() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -156,7 +156,7 @@ fn test_authentication_via_bearer_http_header_rejects_bad_token() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_bearer_http_header_golden_path() {
|
||||
fn authentication_via_bearer_http_header_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -179,7 +179,7 @@ fn test_authentication_via_bearer_http_header_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_query_param_rejects_bad_token() {
|
||||
fn authentication_via_query_param_rejects_bad_token() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -193,7 +193,7 @@ fn test_authentication_via_query_param_rejects_bad_token() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_query_param_golden_path() {
|
||||
fn authentication_via_query_param_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::service::test::{add_trailing_slash, constants::*, protocol, ServiceTy
|
|||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn test_browse_requires_auth() {
|
||||
fn browse_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::browse(&PathBuf::new());
|
||||
let response = service.fetch(&request);
|
||||
|
@ -14,7 +14,7 @@ fn test_browse_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_browse_root() {
|
||||
fn browse_root() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -29,7 +29,7 @@ fn test_browse_root() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_browse_directory() {
|
||||
fn browse_directory() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -45,7 +45,7 @@ fn test_browse_directory() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_browse_bad_directory() {
|
||||
fn browse_bad_directory() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -57,7 +57,7 @@ fn test_browse_bad_directory() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_flatten_requires_auth() {
|
||||
fn flatten_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::flatten(&PathBuf::new());
|
||||
let response = service.fetch(&request);
|
||||
|
@ -65,7 +65,7 @@ fn test_flatten_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_flatten_root() {
|
||||
fn flatten_root() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -80,7 +80,7 @@ fn test_flatten_root() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_flatten_directory() {
|
||||
fn flatten_directory() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -95,7 +95,7 @@ fn test_flatten_directory() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_flatten_bad_directory() {
|
||||
fn flatten_bad_directory() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -107,7 +107,7 @@ fn test_flatten_bad_directory() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_random_requires_auth() {
|
||||
fn random_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::random();
|
||||
let response = service.fetch(&request);
|
||||
|
@ -115,7 +115,7 @@ fn test_random_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_random_golden_path() {
|
||||
fn random_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -130,7 +130,7 @@ fn test_random_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_random_with_trailing_slash() {
|
||||
fn random_with_trailing_slash() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -146,7 +146,7 @@ fn test_random_with_trailing_slash() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_recent_requires_auth() {
|
||||
fn recent_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::recent();
|
||||
let response = service.fetch(&request);
|
||||
|
@ -154,7 +154,7 @@ fn test_recent_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_recent_golden_path() {
|
||||
fn recent_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -169,7 +169,7 @@ fn test_recent_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_recent_with_trailing_slash() {
|
||||
fn recent_with_trailing_slash() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -185,7 +185,7 @@ fn test_recent_with_trailing_slash() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_requires_auth() {
|
||||
fn search_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::search("");
|
||||
let response = service.fetch(&request);
|
||||
|
@ -193,7 +193,7 @@ fn test_search_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_without_query() {
|
||||
fn search_without_query() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -204,7 +204,7 @@ fn test_search_without_query() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_with_query() {
|
||||
fn search_with_query() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::service::test::{protocol, ServiceType, TestService};
|
|||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn test_get_ddns_config_requires_admin() {
|
||||
fn get_ddns_config_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::get_ddns_config();
|
||||
service.complete_initial_setup();
|
||||
|
@ -19,7 +19,7 @@ fn test_get_ddns_config_requires_admin() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_ddns_config_golden_path() {
|
||||
fn get_ddns_config_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -30,7 +30,7 @@ fn test_get_ddns_config_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_put_ddns_config_requires_admin() {
|
||||
fn put_ddns_config_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::put_ddns_config(dto::DDNSConfig {
|
||||
host: "test".to_owned(),
|
||||
|
@ -48,7 +48,7 @@ fn test_put_ddns_config_requires_admin() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_put_ddns_config_golden_path() {
|
||||
fn put_ddns_config_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
|||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn test_lastfm_scrobble_ignores_unlinked_user() {
|
||||
fn lastfm_scrobble_ignores_unlinked_user() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -23,7 +23,7 @@ fn test_lastfm_scrobble_ignores_unlinked_user() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_lastfm_now_playing_ignores_unlinked_user() {
|
||||
fn lastfm_now_playing_ignores_unlinked_user() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -59,4 +59,3 @@ fn lastfm_link_token_golden_path() {
|
|||
let link_token = response.body();
|
||||
assert!(!link_token.value.is_empty());
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
|||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn test_audio_requires_auth() {
|
||||
fn audio_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "02 - Candlelight.mp3"]
|
||||
|
@ -18,7 +18,7 @@ fn test_audio_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_audio_golden_path() {
|
||||
fn audio_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -36,7 +36,7 @@ fn test_audio_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_audio_partial_content() {
|
||||
fn audio_partial_content() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -64,7 +64,7 @@ fn test_audio_partial_content() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_audio_bad_path_returns_not_found() {
|
||||
fn audio_bad_path_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -77,7 +77,7 @@ fn test_audio_bad_path_returns_not_found() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_thumbnail_requires_auth() {
|
||||
fn thumbnail_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
|
||||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted", "Folder.jpg"]
|
||||
|
@ -91,7 +91,7 @@ fn test_thumbnail_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_thumbnail_golden_path() {
|
||||
fn thumbnail_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -109,7 +109,7 @@ fn test_thumbnail_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_thumbnail_bad_path_returns_not_found() {
|
||||
fn thumbnail_bad_path_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
|||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn test_list_playlists_requires_auth() {
|
||||
fn list_playlists_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::playlists();
|
||||
let response = service.fetch(&request);
|
||||
|
@ -14,7 +14,7 @@ fn test_list_playlists_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_playlists_golden_path() {
|
||||
fn list_playlists_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -24,7 +24,7 @@ fn test_list_playlists_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_playlist_requires_auth() {
|
||||
fn save_playlist_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let my_playlist = dto::SavePlaylistInput { tracks: Vec::new() };
|
||||
let request = protocol::save_playlist(TEST_PLAYLIST_NAME, my_playlist);
|
||||
|
@ -33,7 +33,7 @@ fn test_save_playlist_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_playlist_golden_path() {
|
||||
fn save_playlist_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -45,7 +45,7 @@ fn test_save_playlist_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_playlist_large() {
|
||||
fn save_playlist_large() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -60,7 +60,7 @@ fn test_save_playlist_large() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_playlist_requires_auth() {
|
||||
fn get_playlist_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
|
||||
let response = service.fetch(&request);
|
||||
|
@ -68,7 +68,7 @@ fn test_get_playlist_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_playlist_golden_path() {
|
||||
fn get_playlist_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -86,7 +86,7 @@ fn test_get_playlist_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_playlist_bad_name_returns_not_found() {
|
||||
fn get_playlist_bad_name_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -97,7 +97,7 @@ fn test_get_playlist_bad_name_returns_not_found() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_playlist_requires_auth() {
|
||||
fn delete_playlist_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::delete_playlist(TEST_PLAYLIST_NAME);
|
||||
let response = service.fetch(&request);
|
||||
|
@ -105,7 +105,7 @@ fn test_delete_playlist_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_playlist_golden_path() {
|
||||
fn delete_playlist_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -123,7 +123,7 @@ fn test_delete_playlist_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_playlist_bad_name_returns_not_found() {
|
||||
fn delete_playlist_bad_name_returns_not_found() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::service::test::{protocol, ServiceType, TestService};
|
|||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn test_get_settings_requires_auth() {
|
||||
fn get_settings_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -15,7 +15,7 @@ fn test_get_settings_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_settings_requires_admin() {
|
||||
fn get_settings_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -26,7 +26,7 @@ fn test_get_settings_requires_admin() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_settings_golden_path() {
|
||||
fn get_settings_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
@ -37,7 +37,7 @@ fn test_get_settings_golden_path() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_put_settings_requires_auth() {
|
||||
fn put_settings_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
let request = protocol::put_settings(dto::NewSettings::default());
|
||||
|
@ -46,7 +46,7 @@ fn test_put_settings_requires_auth() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_put_settings_requires_admin() {
|
||||
fn put_settings_requires_admin() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
@ -56,7 +56,7 @@ fn test_put_settings_requires_admin() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_put_settings_golden_path() {
|
||||
fn put_settings_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login_admin();
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::service::test::{add_trailing_slash, protocol, ServiceType, TestServic
|
|||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn test_swagger_can_get_index() {
|
||||
fn can_get_swagger_index() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::swagger_index();
|
||||
let response = service.fetch(&request);
|
||||
|
@ -13,7 +13,7 @@ fn test_swagger_can_get_index() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_swagger_can_get_index_with_trailing_slash() {
|
||||
fn can_get_swagger_index_with_trailing_slash() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let mut request = protocol::swagger_index();
|
||||
add_trailing_slash(&mut request);
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::service::test::{protocol, ServiceType, TestService};
|
|||
use crate::test_name;
|
||||
|
||||
#[test]
|
||||
fn test_serves_web_client() {
|
||||
fn serves_web_client() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::web_index();
|
||||
let response = service.fetch_bytes(&request);
|
||||
|
|
13
src/test.rs
13
src/test.rs
|
@ -1,3 +1,5 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! test_name {
|
||||
() => {{
|
||||
|
@ -5,5 +7,14 @@ macro_rules! test_name {
|
|||
let file_name = file_name.replace("/", "-");
|
||||
let file_name = file_name.replace("\\", "-");
|
||||
format!("{}-line-{}", file_name, line!())
|
||||
}};
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn prepare_test_directory<T: AsRef<str>>(test_name: T) -> PathBuf {
|
||||
let output_dir: PathBuf = [".", "test-output", test_name.as_ref()].iter().collect();
|
||||
if output_dir.is_dir() {
|
||||
std::fs::remove_dir_all(&output_dir).unwrap();
|
||||
}
|
||||
std::fs::create_dir_all(&output_dir).unwrap();
|
||||
return output_dir;
|
||||
}
|
||||
|
|
|
@ -1,282 +1,44 @@
|
|||
use log::info;
|
||||
use std;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use uuid;
|
||||
use winapi;
|
||||
use winapi::shared::minwindef::{DWORD, LOWORD, LPARAM, LRESULT, UINT, WPARAM};
|
||||
use winapi::shared::windef::HWND;
|
||||
use winapi::um::{shellapi, winuser};
|
||||
use native_windows_derive::NwgUi;
|
||||
use native_windows_gui::{self as nwg, NativeUi};
|
||||
|
||||
const IDI_POLARIS_TRAY: isize = 0x102;
|
||||
const UID_NOTIFICATION_ICON: u32 = 0;
|
||||
const MESSAGE_NOTIFICATION_ICON: u32 = winuser::WM_USER + 1;
|
||||
const MESSAGE_NOTIFICATION_ICON_QUIT: u32 = winuser::WM_USER + 2;
|
||||
const TRAY_ICON: &[u8] =
|
||||
include_bytes!("../../res/windows/application/icon_polaris_outline_16.png");
|
||||
|
||||
pub trait ToWin {
|
||||
type Out;
|
||||
fn to_win(&self) -> Self::Out;
|
||||
#[derive(Default, NwgUi)]
|
||||
pub struct SystemTray {
|
||||
#[nwg_control]
|
||||
window: nwg::MessageWindow,
|
||||
|
||||
#[nwg_resource(source_bin: Some(TRAY_ICON))]
|
||||
icon: nwg::Icon,
|
||||
|
||||
#[nwg_control(icon: Some(&data.icon), tip: Some("Polaris"))]
|
||||
#[nwg_events(MousePressLeftUp: [SystemTray::show_menu], OnContextMenu: [SystemTray::show_menu])]
|
||||
tray: nwg::TrayNotification,
|
||||
|
||||
#[nwg_control(parent: window, popup: true)]
|
||||
tray_menu: nwg::Menu,
|
||||
|
||||
#[nwg_control(parent: tray_menu, text: "Quit Polaris")]
|
||||
#[nwg_events(OnMenuItemSelected: [SystemTray::exit])]
|
||||
exit_menu_item: nwg::MenuItem,
|
||||
}
|
||||
|
||||
impl<'a> ToWin for &'a str {
|
||||
type Out = Vec<u16>;
|
||||
|
||||
fn to_win(&self) -> Self::Out {
|
||||
OsStr::new(self)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToWin for uuid::Uuid {
|
||||
type Out = winapi::shared::guiddef::GUID;
|
||||
|
||||
fn to_win(&self) -> Self::Out {
|
||||
let bytes = self.as_bytes();
|
||||
let end = [
|
||||
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
|
||||
];
|
||||
|
||||
winapi::shared::guiddef::GUID {
|
||||
Data1: ((bytes[0] as u32) << 24
|
||||
| (bytes[1] as u32) << 16
|
||||
| (bytes[2] as u32) << 8
|
||||
| (bytes[3] as u32)),
|
||||
Data2: ((bytes[4] as u16) << 8 | (bytes[5] as u16)),
|
||||
Data3: ((bytes[6] as u16) << 8 | (bytes[7] as u16)),
|
||||
Data4: end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Constructible {
|
||||
type Out;
|
||||
fn new() -> Self::Out;
|
||||
}
|
||||
|
||||
impl Constructible for shellapi::NOTIFYICONDATAW {
|
||||
type Out = shellapi::NOTIFYICONDATAW;
|
||||
|
||||
fn new() -> Self::Out {
|
||||
let mut version_union: shellapi::NOTIFYICONDATAW_u = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
let version = version_union.uVersion_mut();
|
||||
*version = shellapi::NOTIFYICON_VERSION_4;
|
||||
}
|
||||
|
||||
shellapi::NOTIFYICONDATAW {
|
||||
cbSize: std::mem::size_of::<shellapi::NOTIFYICONDATAW>() as u32,
|
||||
hWnd: std::ptr::null_mut(),
|
||||
uFlags: 0,
|
||||
guidItem: uuid::Uuid::nil().to_win(),
|
||||
hIcon: std::ptr::null_mut(),
|
||||
uID: 0,
|
||||
uCallbackMessage: 0,
|
||||
szTip: [0; 128],
|
||||
dwState: 0,
|
||||
dwStateMask: 0,
|
||||
szInfo: [0; 256],
|
||||
u: version_union,
|
||||
szInfoTitle: [0; 64],
|
||||
dwInfoFlags: 0,
|
||||
hBalloonIcon: std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_window() -> Option<HWND> {
|
||||
let class_name = "Polaris-class".to_win();
|
||||
let window_name = "Polaris-window".to_win();
|
||||
|
||||
unsafe {
|
||||
let module_handle = winapi::um::libloaderapi::GetModuleHandleW(std::ptr::null());
|
||||
let wnd = winuser::WNDCLASSW {
|
||||
style: 0,
|
||||
lpfnWndProc: Some(window_proc),
|
||||
hInstance: module_handle,
|
||||
hIcon: std::ptr::null_mut(),
|
||||
hCursor: std::ptr::null_mut(),
|
||||
lpszClassName: class_name.as_ptr(),
|
||||
hbrBackground: winuser::COLOR_WINDOW as winapi::shared::windef::HBRUSH,
|
||||
lpszMenuName: std::ptr::null_mut(),
|
||||
cbClsExtra: 0,
|
||||
cbWndExtra: 0,
|
||||
};
|
||||
|
||||
let atom = winuser::RegisterClassW(&wnd);
|
||||
if atom == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let window_handle = winuser::CreateWindowExW(
|
||||
0,
|
||||
atom as winapi::shared::ntdef::LPCWSTR,
|
||||
window_name.as_ptr(),
|
||||
winuser::WS_DISABLED,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
winuser::GetDesktopWindow(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
|
||||
if window_handle.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
return Some(window_handle);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_notification_icon(window: HWND) {
|
||||
let mut tooltip = [0 as winapi::um::winnt::WCHAR; 128];
|
||||
for (&x, p) in "Polaris".to_win().iter().zip(tooltip.iter_mut()) {
|
||||
*p = x;
|
||||
impl SystemTray {
|
||||
fn show_menu(&self) {
|
||||
let (x, y) = nwg::GlobalCursor::position();
|
||||
self.tray_menu.popup(x, y);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let module = winapi::um::libloaderapi::GetModuleHandleW(std::ptr::null());
|
||||
let icon = winuser::LoadIconW(module, std::mem::transmute(IDI_POLARIS_TRAY));
|
||||
let mut flags = shellapi::NIF_MESSAGE | shellapi::NIF_TIP;
|
||||
if !icon.is_null() {
|
||||
flags |= shellapi::NIF_ICON;
|
||||
}
|
||||
|
||||
let mut icon_data = shellapi::NOTIFYICONDATAW::new();
|
||||
icon_data.hWnd = window;
|
||||
icon_data.uID = UID_NOTIFICATION_ICON;
|
||||
icon_data.uFlags = flags;
|
||||
icon_data.hIcon = icon;
|
||||
icon_data.uCallbackMessage = MESSAGE_NOTIFICATION_ICON;
|
||||
icon_data.szTip = tooltip;
|
||||
|
||||
shellapi::Shell_NotifyIconW(shellapi::NIM_ADD, &mut icon_data);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_notification_icon(window: HWND) {
|
||||
let mut icon_data = shellapi::NOTIFYICONDATAW::new();
|
||||
icon_data.hWnd = window;
|
||||
icon_data.uID = UID_NOTIFICATION_ICON;
|
||||
unsafe {
|
||||
shellapi::Shell_NotifyIconW(shellapi::NIM_DELETE, &mut icon_data);
|
||||
}
|
||||
}
|
||||
|
||||
fn open_notification_context_menu(window: HWND) {
|
||||
info!("Opening notification icon context menu");
|
||||
let quit_string = "Quit Polaris".to_win();
|
||||
|
||||
unsafe {
|
||||
let context_menu = winuser::CreatePopupMenu();
|
||||
if context_menu.is_null() {
|
||||
return;
|
||||
}
|
||||
winuser::InsertMenuW(
|
||||
context_menu,
|
||||
0,
|
||||
winuser::MF_STRING,
|
||||
MESSAGE_NOTIFICATION_ICON_QUIT as usize,
|
||||
quit_string.as_ptr(),
|
||||
);
|
||||
|
||||
let mut cursor_position = winapi::shared::windef::POINT { x: 0, y: 0 };
|
||||
winuser::GetCursorPos(&mut cursor_position);
|
||||
|
||||
winuser::SetForegroundWindow(window);
|
||||
let flags = winuser::TPM_RIGHTALIGN | winuser::TPM_BOTTOMALIGN | winuser::TPM_RIGHTBUTTON;
|
||||
winuser::TrackPopupMenu(
|
||||
context_menu,
|
||||
flags,
|
||||
cursor_position.x,
|
||||
cursor_position.y,
|
||||
0,
|
||||
window,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
winuser::PostMessageW(window, 0, 0, 0);
|
||||
|
||||
info!("Closing notification context menu");
|
||||
winuser::DestroyMenu(context_menu);
|
||||
}
|
||||
}
|
||||
|
||||
fn quit(window: HWND) {
|
||||
info!("Shutting down UI");
|
||||
unsafe {
|
||||
winuser::PostMessageW(window, winuser::WM_CLOSE, 0, 0);
|
||||
fn exit(&self) {
|
||||
nwg::stop_thread_dispatch();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
info!("Starting up UI (Windows)");
|
||||
|
||||
create_window().expect("Could not initialize window");
|
||||
|
||||
let mut message = winuser::MSG {
|
||||
hwnd: std::ptr::null_mut(),
|
||||
message: 0,
|
||||
wParam: 0,
|
||||
lParam: 0,
|
||||
time: 0,
|
||||
pt: winapi::shared::windef::POINT { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
loop {
|
||||
let status: i32;
|
||||
unsafe {
|
||||
status = winuser::GetMessageW(&mut message, std::ptr::null_mut(), 0, 0);
|
||||
if status == -1 {
|
||||
panic!(
|
||||
"GetMessageW error: {}",
|
||||
winapi::um::errhandlingapi::GetLastError()
|
||||
);
|
||||
}
|
||||
if status == 0 {
|
||||
break;
|
||||
}
|
||||
winuser::TranslateMessage(&message);
|
||||
winuser::DispatchMessageW(&message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe extern "system" fn window_proc(
|
||||
window: HWND,
|
||||
msg: UINT,
|
||||
w_param: WPARAM,
|
||||
l_param: LPARAM,
|
||||
) -> LRESULT {
|
||||
match msg {
|
||||
winuser::WM_CREATE => {
|
||||
add_notification_icon(window);
|
||||
}
|
||||
|
||||
MESSAGE_NOTIFICATION_ICON => match LOWORD(l_param as DWORD) as u32 {
|
||||
winuser::WM_RBUTTONUP => {
|
||||
open_notification_context_menu(window);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
|
||||
winuser::WM_COMMAND => match LOWORD(w_param as DWORD) as u32 {
|
||||
MESSAGE_NOTIFICATION_ICON_QUIT => {
|
||||
quit(window);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
|
||||
winuser::WM_DESTROY => {
|
||||
remove_notification_icon(window);
|
||||
winuser::PostQuitMessage(0);
|
||||
}
|
||||
|
||||
_ => (),
|
||||
};
|
||||
|
||||
return winuser::DefWindowProcW(window, msg, w_param, l_param);
|
||||
info!("Starting up UI (Windows system tray)");
|
||||
nwg::init().expect("Failed to init Native Windows GUI");
|
||||
let _ui = SystemTray::build_ui(Default::default()).expect("Failed to build tray UI");
|
||||
nwg::dispatch_thread_events();
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ pub fn get_audio_format(path: &Path) -> Option<AudioFormat> {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_audio_format() {
|
||||
fn can_guess_audio_format() {
|
||||
assert_eq!(get_audio_format(Path::new("animals/🐷/my🐖file.jpg")), None);
|
||||
assert_eq!(
|
||||
get_audio_format(Path::new("animals/🐷/my🐖file.flac")),
|
||||
|
|
Loading…
Add table
Reference in a new issue