Merge branch 'rocket'
This commit is contained in:
commit
94602317ad
24 changed files with 2145 additions and 1909 deletions
1
.gitmodules
vendored
1
.gitmodules
vendored
|
@ -1,3 +1,4 @@
|
||||||
[submodule "web"]
|
[submodule "web"]
|
||||||
path = web
|
path = web
|
||||||
url = https://github.com/agersant/polaris-web.git
|
url = https://github.com/agersant/polaris-web.git
|
||||||
|
branch = .
|
||||||
|
|
|
@ -6,5 +6,5 @@ rust:
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- rust: beta
|
- rust: stable
|
||||||
- rust: nightly
|
- rust: beta
|
1739
Cargo.lock
generated
1739
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
22
Cargo.toml
22
Cargo.toml
|
@ -1,7 +1,8 @@
|
||||||
[package]
|
[package]
|
||||||
name = "polaris"
|
name = "polaris"
|
||||||
version = "0.8.0"
|
version = "0.9.0"
|
||||||
authors = ["Antoine Gersant <antoine.gersant@lesforges.org>"]
|
authors = ["Antoine Gersant <antoine.gersant@lesforges.org>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
ui = []
|
ui = []
|
||||||
|
@ -9,37 +10,34 @@ ui = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ape = "0.2.0"
|
ape = "0.2.0"
|
||||||
app_dirs = "1.1.1"
|
app_dirs = "1.1.1"
|
||||||
base64 = "0.9.3"
|
base64 = "0.10.0"
|
||||||
diesel = { version = "1.3.3", features = ["sqlite"] }
|
diesel = { version = "1.3.3", features = ["sqlite"] }
|
||||||
diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
|
diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
|
||||||
error-chain = "0.12.0"
|
error-chain = "0.12.0"
|
||||||
getopts = "0.2.15"
|
getopts = "0.2.15"
|
||||||
hyper = "0.12.11"
|
|
||||||
id3 = "0.2.3"
|
id3 = "0.2.3"
|
||||||
image = "0.20.0"
|
image = "0.20.0"
|
||||||
iron = "0.6.0"
|
|
||||||
rustfm-scrobble = { git = "https://github.com/agersant/rustfm-scrobble" }
|
rustfm-scrobble = { git = "https://github.com/agersant/rustfm-scrobble" }
|
||||||
lewton = "0.9.1"
|
lewton = "0.9.1"
|
||||||
log = "0.4.5"
|
log = "0.4.5"
|
||||||
metaflac = "0.1.8"
|
metaflac = "0.1.8"
|
||||||
mount = "0.4.0"
|
|
||||||
mp3-duration = "0.1.0"
|
mp3-duration = "0.1.0"
|
||||||
params = { git = "https://github.com/agersant/params" }
|
|
||||||
rand = "0.5.5"
|
rand = "0.5.5"
|
||||||
regex = "1.0.5"
|
regex = "1.0.5"
|
||||||
ring = "0.13.2"
|
ring = "0.13.5"
|
||||||
reqwest = "0.9.2"
|
reqwest = "0.9.2"
|
||||||
router = "0.6.0"
|
rocket = "0.4.0"
|
||||||
rust-crypto = "0.2.36"
|
rust-crypto = "0.2.36"
|
||||||
secure-session = "0.3.1"
|
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
staticfile = "0.5.0"
|
|
||||||
simplelog = "0.5.2"
|
simplelog = "0.5.2"
|
||||||
toml = "0.4.5"
|
toml = "0.4.5"
|
||||||
typemap = "0.3"
|
|
||||||
url = "1.2.0"
|
[dependencies.rocket_contrib]
|
||||||
|
version = "0.4.0"
|
||||||
|
default_features = false
|
||||||
|
features = ["json", "serve"]
|
||||||
|
|
||||||
[dependencies.rusqlite]
|
[dependencies.rusqlite]
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|
|
@ -55,7 +55,7 @@ environment:
|
||||||
# or test failure in the matching channels/targets from failing the entire build.
|
# or test failure in the matching channels/targets from failing the entire build.
|
||||||
matrix:
|
matrix:
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- channel: nightly
|
- channel: stable
|
||||||
- channel: beta
|
- channel: beta
|
||||||
|
|
||||||
## Install Script ##
|
## Install Script ##
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version='1.0' encoding='windows-1252'?>
|
<?xml version='1.0' encoding='windows-1252'?>
|
||||||
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi' xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi' xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||||
<Product Name='Polaris' Id='A9B98E78-65E3-4002-BF73-B026A1A5473C' UpgradeCode='FF16B075-1D36-47F4-BE37-D95BBC1A412C' Language='1033' Codepage='1252' Version='0.8.0' Manufacturer='Permafrost'>
|
<Product Name='Polaris' Id='9298DCA7-8FEB-48B2-89F9-8C60BC320D07' UpgradeCode='FF16B075-1D36-47F4-BE37-D95BBC1A412C' Language='1033' Codepage='1252' Version='0.9.0' Manufacturer='Permafrost'>
|
||||||
|
|
||||||
<Package Id='*' Keywords='Installer' Platform='x64' InstallScope='perUser' Description='Polaris Installer' Manufacturer='Permafrost' Languages='1033' Compressed='yes' SummaryCodepage='1252' />
|
<Package Id='*' Keywords='Installer' Platform='x64' InstallScope='perUser' Description='Polaris Installer' Manufacturer='Permafrost' Languages='1033' Compressed='yes' SummaryCodepage='1252' />
|
||||||
|
|
||||||
|
|
1075
src/api.rs
1075
src/api.rs
File diff suppressed because it is too large
Load diff
564
src/api_tests.rs
Normal file
564
src/api_tests.rs
Normal file
|
@ -0,0 +1,564 @@
|
||||||
|
use rocket::http::hyper::header::*;
|
||||||
|
use rocket::http::uri::Uri;
|
||||||
|
use rocket::http::Status;
|
||||||
|
use rocket::local::Client;
|
||||||
|
use std::fs;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::{thread, time};
|
||||||
|
|
||||||
|
use crate::api;
|
||||||
|
use crate::config;
|
||||||
|
use crate::db;
|
||||||
|
use crate::ddns;
|
||||||
|
use crate::index;
|
||||||
|
use crate::server;
|
||||||
|
use crate::vfs;
|
||||||
|
|
||||||
|
const TEST_USERNAME: &str = "test_user";
|
||||||
|
const TEST_PASSWORD: &str = "test_password";
|
||||||
|
const TEST_MOUNT_NAME: &str = "collection";
|
||||||
|
const TEST_MOUNT_SOURCE: &str = "test/collection";
|
||||||
|
|
||||||
|
struct TestEnvironment {
|
||||||
|
pub client: Client,
|
||||||
|
command_sender: Arc<index::CommandSender>,
|
||||||
|
db: Arc<db::DB>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestEnvironment {
|
||||||
|
pub fn update_index(&self) {
|
||||||
|
index::update(self.db.deref()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TestEnvironment {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.command_sender.deref().exit().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_test_environment(db_name: &str) -> TestEnvironment {
|
||||||
|
let mut db_path = PathBuf::new();
|
||||||
|
db_path.push("test");
|
||||||
|
db_path.push(db_name);
|
||||||
|
if db_path.exists() {
|
||||||
|
fs::remove_file(&db_path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = Arc::new(db::DB::new(&db_path).unwrap());
|
||||||
|
|
||||||
|
let web_dir_path = PathBuf::from("web");
|
||||||
|
let command_sender = index::init(db.clone());
|
||||||
|
|
||||||
|
let server = server::get_server(
|
||||||
|
5050,
|
||||||
|
"/",
|
||||||
|
"/api",
|
||||||
|
&web_dir_path,
|
||||||
|
db.clone(),
|
||||||
|
command_sender.clone(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let client = Client::new(server).unwrap();
|
||||||
|
TestEnvironment {
|
||||||
|
client,
|
||||||
|
command_sender,
|
||||||
|
db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_initial_setup(client: &Client) {
|
||||||
|
let configuration = config::Config {
|
||||||
|
album_art_pattern: None,
|
||||||
|
prefix_url: None,
|
||||||
|
reindex_every_n_seconds: None,
|
||||||
|
ydns: None,
|
||||||
|
users: Some(vec![config::ConfigUser {
|
||||||
|
name: TEST_USERNAME.into(),
|
||||||
|
password: TEST_PASSWORD.into(),
|
||||||
|
admin: true,
|
||||||
|
}]),
|
||||||
|
mount_dirs: Some(vec![vfs::MountPoint {
|
||||||
|
name: TEST_MOUNT_NAME.into(),
|
||||||
|
source: TEST_MOUNT_SOURCE.into(),
|
||||||
|
}]),
|
||||||
|
};
|
||||||
|
let body = serde_json::to_string(&configuration).unwrap();
|
||||||
|
let response = client.put("/api/settings").body(&body).dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_auth(client: &Client) {
|
||||||
|
let credentials = api::AuthCredentials {
|
||||||
|
username: TEST_USERNAME.into(),
|
||||||
|
password: TEST_PASSWORD.into(),
|
||||||
|
};
|
||||||
|
let body = serde_json::to_string(&credentials).unwrap();
|
||||||
|
let response = client.post("/api/auth").body(body).dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version() {
|
||||||
|
let env = get_test_environment("api_version.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
let mut response = client.get("/api/version").dispatch();
|
||||||
|
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: api::Version = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json, api::Version { major: 3, minor: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_setup() {
|
||||||
|
let env = get_test_environment("api_initial_setup.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/initial_setup").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: api::InitialSetup = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
response_json,
|
||||||
|
api::InitialSetup {
|
||||||
|
has_any_users: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
complete_initial_setup(client);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/initial_setup").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: api::InitialSetup = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
response_json,
|
||||||
|
api::InitialSetup {
|
||||||
|
has_any_users: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings() {
|
||||||
|
let env = get_test_environment("api_settings.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
complete_initial_setup(client);
|
||||||
|
|
||||||
|
{
|
||||||
|
let response = client.get("/api/settings").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
do_auth(client);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/settings").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: config::Config = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
response_json,
|
||||||
|
config::Config {
|
||||||
|
album_art_pattern: Some("Folder.(jpg|png)".to_string()),
|
||||||
|
reindex_every_n_seconds: Some(1800),
|
||||||
|
mount_dirs: Some(vec![vfs::MountPoint {
|
||||||
|
name: TEST_MOUNT_NAME.into(),
|
||||||
|
source: TEST_MOUNT_SOURCE.into()
|
||||||
|
}]),
|
||||||
|
prefix_url: None,
|
||||||
|
users: Some(vec![config::ConfigUser {
|
||||||
|
name: TEST_USERNAME.into(),
|
||||||
|
password: "".into(),
|
||||||
|
admin: true
|
||||||
|
}]),
|
||||||
|
ydns: Some(ddns::DDNSConfig {
|
||||||
|
host: "".into(),
|
||||||
|
username: "".into(),
|
||||||
|
password: "".into()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut configuration = config::Config {
|
||||||
|
album_art_pattern: Some("my_pattern".to_owned()),
|
||||||
|
reindex_every_n_seconds: Some(3600),
|
||||||
|
mount_dirs: Some(vec![
|
||||||
|
vfs::MountPoint {
|
||||||
|
name: TEST_MOUNT_NAME.into(),
|
||||||
|
source: TEST_MOUNT_SOURCE.into(),
|
||||||
|
},
|
||||||
|
vfs::MountPoint {
|
||||||
|
name: "more_music".into(),
|
||||||
|
source: "test/collection".into(),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
prefix_url: Some("my_prefix".to_owned()),
|
||||||
|
users: Some(vec![
|
||||||
|
config::ConfigUser {
|
||||||
|
name: "test_user".into(),
|
||||||
|
password: "some_password".into(),
|
||||||
|
admin: true,
|
||||||
|
},
|
||||||
|
config::ConfigUser {
|
||||||
|
name: "other_user".into(),
|
||||||
|
password: "some_other_password".into(),
|
||||||
|
admin: false,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
ydns: Some(ddns::DDNSConfig {
|
||||||
|
host: "my_host".into(),
|
||||||
|
username: "my_username".into(),
|
||||||
|
password: "my_password".into(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = serde_json::to_string(&configuration).unwrap();
|
||||||
|
|
||||||
|
configuration.users = Some(vec![
|
||||||
|
config::ConfigUser {
|
||||||
|
name: "test_user".into(),
|
||||||
|
password: "".into(),
|
||||||
|
admin: true,
|
||||||
|
},
|
||||||
|
config::ConfigUser {
|
||||||
|
name: "other_user".into(),
|
||||||
|
password: "".into(),
|
||||||
|
admin: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
client.put("/api/settings").body(body).dispatch();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/settings").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: config::Config = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json, configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preferences() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trigger_index() {
|
||||||
|
let env = get_test_environment("api_trigger_index.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
complete_initial_setup(client);
|
||||||
|
do_auth(client);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/random").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::Directory> = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let response = client.post("/api/trigger_index").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeout = time::Duration::from_secs(5);
|
||||||
|
thread::sleep(timeout);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/random").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::Directory> = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth() {
|
||||||
|
let env = get_test_environment("api_auth.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
complete_initial_setup(client);
|
||||||
|
|
||||||
|
{
|
||||||
|
let credentials = api::AuthCredentials {
|
||||||
|
username: "garbage".into(),
|
||||||
|
password: "garbage".into(),
|
||||||
|
};
|
||||||
|
let response = client
|
||||||
|
.post("/api/auth")
|
||||||
|
.body(serde_json::to_string(&credentials).unwrap())
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Unauthorized);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let credentials = api::AuthCredentials {
|
||||||
|
username: TEST_USERNAME.into(),
|
||||||
|
password: "garbage".into(),
|
||||||
|
};
|
||||||
|
let response = client
|
||||||
|
.post("/api/auth")
|
||||||
|
.body(serde_json::to_string(&credentials).unwrap())
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Unauthorized);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let credentials = api::AuthCredentials {
|
||||||
|
username: TEST_USERNAME.into(),
|
||||||
|
password: TEST_PASSWORD.into(),
|
||||||
|
};
|
||||||
|
let response = client
|
||||||
|
.post("/api/auth")
|
||||||
|
.body(serde_json::to_string(&credentials).unwrap())
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
assert_eq!(response.cookies()[0].name(), "session");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn browse() {
|
||||||
|
let env = get_test_environment("api_browse.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
complete_initial_setup(client);
|
||||||
|
do_auth(client);
|
||||||
|
env.update_index();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/browse").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::CollectionFile> =
|
||||||
|
serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut next;
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/browse/collection").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::CollectionFile> =
|
||||||
|
serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 2);
|
||||||
|
|
||||||
|
match response_json[0] {
|
||||||
|
index::CollectionFile::Directory(ref d) => {
|
||||||
|
next = d.path.clone();
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/browse/collection/Khemmis
|
||||||
|
{
|
||||||
|
let url = format!("/api/browse/{}", Uri::percent_encode(&next));
|
||||||
|
let mut response = client.get(url).dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::CollectionFile> =
|
||||||
|
serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 1);
|
||||||
|
match response_json[0] {
|
||||||
|
index::CollectionFile::Directory(ref d) => {
|
||||||
|
next = d.path.clone();
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/browse/collection/Khemmis/Hunted
|
||||||
|
{
|
||||||
|
let url = format!("/api/browse/{}", Uri::percent_encode(&next));
|
||||||
|
let mut response = client.get(url).dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::CollectionFile> =
|
||||||
|
serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flatten() {
|
||||||
|
let env = get_test_environment("api_flatten.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
complete_initial_setup(client);
|
||||||
|
do_auth(client);
|
||||||
|
env.update_index();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/flatten").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::Song> = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/flatten/collection").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::Song> = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn random() {
|
||||||
|
let env = get_test_environment("api_random.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
complete_initial_setup(client);
|
||||||
|
do_auth(client);
|
||||||
|
env.update_index();
|
||||||
|
|
||||||
|
let mut response = client.get("/api/random").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::Directory> = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recent() {
|
||||||
|
let env = get_test_environment("api_recent.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
complete_initial_setup(client);
|
||||||
|
do_auth(client);
|
||||||
|
env.update_index();
|
||||||
|
|
||||||
|
let mut response = client.get("/api/recent").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::Directory> = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search() {
|
||||||
|
let env = get_test_environment("api_search.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
complete_initial_setup(client);
|
||||||
|
do_auth(client);
|
||||||
|
env.update_index();
|
||||||
|
|
||||||
|
let mut response = client.get("/api/search/door").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::CollectionFile> = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 1);
|
||||||
|
match response_json[0] {
|
||||||
|
index::CollectionFile::Song(ref s) => assert_eq!(s.title, Some("Beyond The Door".into())),
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serve() {
|
||||||
|
let env = get_test_environment("api_serve.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
complete_initial_setup(client);
|
||||||
|
do_auth(client);
|
||||||
|
env.update_index();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/serve/collection%2FKhemmis%2FHunted%2F02%20-%20Candlelight.mp3").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let body = response.body().unwrap();
|
||||||
|
let body = body.into_bytes().unwrap();
|
||||||
|
assert_eq!(body.len(), 24_142);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/serve/collection%2FKhemmis%2FHunted%2F02%20-%20Candlelight.mp3")
|
||||||
|
.header(Range::bytes(100, 299))
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::PartialContent);
|
||||||
|
let body = response.body().unwrap();
|
||||||
|
let body = body.into_bytes().unwrap();
|
||||||
|
assert_eq!(body.len(), 200);
|
||||||
|
assert_eq!(response.headers().get_one("Content-Length").unwrap(), "200");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn playlists() {
|
||||||
|
let env = get_test_environment("api_playlists.sqlite");
|
||||||
|
let client = &env.client;
|
||||||
|
complete_initial_setup(client);
|
||||||
|
do_auth(client);
|
||||||
|
env.update_index();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/playlists").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<api::ListPlaylistsEntry> =
|
||||||
|
serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let songs: Vec<index::Song>;
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/flatten").dispatch();
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
songs = serde_json::from_str(&response_body).unwrap();
|
||||||
|
}
|
||||||
|
let my_playlist = api::SavePlaylistInput {
|
||||||
|
tracks: songs[2..6].into_iter().map(|s| s.path.clone()).collect(),
|
||||||
|
};
|
||||||
|
let response = client
|
||||||
|
.put("/api/playlist/my_playlist")
|
||||||
|
.body(serde_json::to_string(&my_playlist).unwrap())
|
||||||
|
.dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/playlists").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<api::ListPlaylistsEntry> =
|
||||||
|
serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
response_json,
|
||||||
|
vec![api::ListPlaylistsEntry {
|
||||||
|
name: "my_playlist".into()
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/playlist/my_playlist").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<index::Song> = serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let response = client.delete("/api/playlist/my_playlist").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut response = client.get("/api/playlists").dispatch();
|
||||||
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
let response_body = response.body_string().unwrap();
|
||||||
|
let response_json: Vec<api::ListPlaylistsEntry> =
|
||||||
|
serde_json::from_str(&response_body).unwrap();
|
||||||
|
assert_eq!(response_json.len(), 0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,19 +2,18 @@ use core::ops::Deref;
|
||||||
use diesel;
|
use diesel;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde_json;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path;
|
use std::path;
|
||||||
use toml;
|
use toml;
|
||||||
|
|
||||||
use db::ConnectionSource;
|
use crate::db::ConnectionSource;
|
||||||
use db::DB;
|
use crate::db::DB;
|
||||||
use db::{ddns_config, misc_settings, mount_points, users};
|
use crate::db::{ddns_config, misc_settings, mount_points, users};
|
||||||
use ddns::DDNSConfig;
|
use crate::ddns::DDNSConfig;
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
use user::*;
|
use crate::user::*;
|
||||||
use vfs::MountPoint;
|
use crate::vfs::MountPoint;
|
||||||
|
|
||||||
#[derive(Debug, Queryable)]
|
#[derive(Debug, Queryable)]
|
||||||
pub struct MiscSettings {
|
pub struct MiscSettings {
|
||||||
|
@ -61,12 +60,6 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_json(content: &str) -> Result<Config> {
|
|
||||||
let mut config = serde_json::from_str::<Config>(content)?;
|
|
||||||
config.clean_paths()?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_toml_file(path: &path::Path) -> Result<Config> {
|
pub fn parse_toml_file(path: &path::Path) -> Result<Config> {
|
||||||
info!("Config file path: {}", path.to_string_lossy());
|
info!("Config file path: {}", path.to_string_lossy());
|
||||||
let mut config_file = fs::File::open(path)?;
|
let mut config_file = fs::File::open(path)?;
|
||||||
|
@ -100,7 +93,8 @@ where
|
||||||
index_album_art_pattern,
|
index_album_art_pattern,
|
||||||
index_sleep_duration_seconds,
|
index_sleep_duration_seconds,
|
||||||
prefix_url,
|
prefix_url,
|
||||||
)).get_result(connection.deref())?;
|
))
|
||||||
|
.get_result(connection.deref())?;
|
||||||
config.album_art_pattern = Some(art_pattern);
|
config.album_art_pattern = Some(art_pattern);
|
||||||
config.reindex_every_n_seconds = Some(sleep_duration);
|
config.reindex_every_n_seconds = Some(sleep_duration);
|
||||||
config.prefix_url = if url != "" { Some(url) } else { None };
|
config.prefix_url = if url != "" { Some(url) } else { None };
|
||||||
|
@ -124,7 +118,8 @@ where
|
||||||
name,
|
name,
|
||||||
password: "".to_owned(),
|
password: "".to_owned(),
|
||||||
admin: admin != 0,
|
admin: admin != 0,
|
||||||
}).collect::<_>(),
|
})
|
||||||
|
.collect::<_>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let ydns = ddns_config
|
let ydns = ddns_config
|
||||||
|
@ -194,7 +189,8 @@ where
|
||||||
.iter()
|
.iter()
|
||||||
.find(|old_name| *old_name == &u.name)
|
.find(|old_name| *old_name == &u.name)
|
||||||
.is_none()
|
.is_none()
|
||||||
}).collect::<_>();
|
})
|
||||||
|
.collect::<_>();
|
||||||
for config_user in &insert_users {
|
for config_user in &insert_users {
|
||||||
let new_user = User::new(&config_user.name, &config_user.password);
|
let new_user = User::new(&config_user.name, &config_user.password);
|
||||||
diesel::insert_into(users::table)
|
diesel::insert_into(users::table)
|
||||||
|
@ -242,7 +238,8 @@ where
|
||||||
host.eq(ydns.host.clone()),
|
host.eq(ydns.host.clone()),
|
||||||
username.eq(ydns.username.clone()),
|
username.eq(ydns.username.clone()),
|
||||||
password.eq(ydns.password.clone()),
|
password.eq(ydns.password.clone()),
|
||||||
)).execute(connection.deref())?;
|
))
|
||||||
|
.execute(connection.deref())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref prefix_url) = new_config.prefix_url {
|
if let Some(ref prefix_url) = new_config.prefix_url {
|
||||||
|
|
|
@ -6,7 +6,7 @@ use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard};
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
|
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ impl ConnectionSource for DB {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _get_test_db(name: &str) -> DB {
|
pub fn _get_test_db(name: &str) -> DB {
|
||||||
use config;
|
use crate::config;
|
||||||
let config_path = Path::new("test/config.toml");
|
let config_path = Path::new("test/config.toml");
|
||||||
let config = config::parse_toml_file(&config_path).unwrap();
|
let config = config::parse_toml_file(&config_path).unwrap();
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ use std::io;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
use db::ddns_config;
|
use crate::db::ddns_config;
|
||||||
use db::{ConnectionSource, DB};
|
use crate::db::{ConnectionSource, DB};
|
||||||
use errors;
|
use crate::errors;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Queryable, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Queryable, Serialize)]
|
||||||
#[table_name = "ddns_config"]
|
#[table_name = "ddns_config"]
|
||||||
|
|
|
@ -3,14 +3,12 @@ use core;
|
||||||
use diesel;
|
use diesel;
|
||||||
use diesel_migrations;
|
use diesel_migrations;
|
||||||
use getopts;
|
use getopts;
|
||||||
use hyper;
|
|
||||||
use id3;
|
use id3;
|
||||||
use image;
|
use image;
|
||||||
use iron::status::Status;
|
|
||||||
use iron::IronError;
|
|
||||||
use lewton;
|
use lewton;
|
||||||
use metaflac;
|
use metaflac;
|
||||||
use regex;
|
use regex;
|
||||||
|
use rocket;
|
||||||
use rustfm_scrobble;
|
use rustfm_scrobble;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use std;
|
use std;
|
||||||
|
@ -25,7 +23,6 @@ error_chain! {
|
||||||
Encoding(core::str::Utf8Error);
|
Encoding(core::str::Utf8Error);
|
||||||
Flac(metaflac::Error);
|
Flac(metaflac::Error);
|
||||||
GetOpts(getopts::Fail);
|
GetOpts(getopts::Fail);
|
||||||
Hyper(hyper::Error);
|
|
||||||
Id3(id3::Error);
|
Id3(id3::Error);
|
||||||
Image(image::ImageError);
|
Image(image::ImageError);
|
||||||
Io(std::io::Error);
|
Io(std::io::Error);
|
||||||
|
@ -33,51 +30,27 @@ error_chain! {
|
||||||
Time(std::time::SystemTimeError);
|
Time(std::time::SystemTimeError);
|
||||||
Toml(toml::de::Error);
|
Toml(toml::de::Error);
|
||||||
Regex(regex::Error);
|
Regex(regex::Error);
|
||||||
|
RocketConfig(rocket::config::ConfigError);
|
||||||
Scrobbler(rustfm_scrobble::ScrobblerError);
|
Scrobbler(rustfm_scrobble::ScrobblerError);
|
||||||
Vorbis(lewton::VorbisError);
|
Vorbis(lewton::VorbisError);
|
||||||
}
|
}
|
||||||
|
|
||||||
errors {
|
errors {
|
||||||
DaemonError {}
|
DaemonError {}
|
||||||
AuthenticationRequired {}
|
|
||||||
AdminPrivilegeRequired {}
|
|
||||||
MissingConfig {}
|
|
||||||
MissingPreferences {}
|
|
||||||
MissingUsername {}
|
|
||||||
MissingPassword {}
|
|
||||||
MissingPlaylist {}
|
|
||||||
IncorrectCredentials {}
|
IncorrectCredentials {}
|
||||||
CannotServeDirectory {}
|
|
||||||
UnsupportedFileType {}
|
|
||||||
FileNotFound {}
|
|
||||||
MissingIndexVersion {}
|
|
||||||
MissingPlaylistName {}
|
|
||||||
EncodingError {}
|
EncodingError {}
|
||||||
MissingLastFMCredentials {}
|
MissingLastFMCredentials {}
|
||||||
LastFMAuthError {}
|
|
||||||
LastFMDeserializationError {}
|
|
||||||
MissingDesiredResponse {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for IronError {
|
impl<'r> rocket::response::Responder<'r> for Error {
|
||||||
fn from(err: Error) -> IronError {
|
fn respond_to(self, _: &rocket::request::Request) -> rocket::response::Result<'r> {
|
||||||
match err {
|
let mut build = rocket::response::Response::build();
|
||||||
e @ Error(ErrorKind::AuthenticationRequired, _) => {
|
build
|
||||||
IronError::new(e, Status::Unauthorized)
|
.status(match self.0 {
|
||||||
}
|
ErrorKind::IncorrectCredentials => rocket::http::Status::Unauthorized,
|
||||||
e @ Error(ErrorKind::AdminPrivilegeRequired, _) => IronError::new(e, Status::Forbidden),
|
_ => rocket::http::Status::InternalServerError,
|
||||||
e @ Error(ErrorKind::MissingUsername, _) => IronError::new(e, Status::BadRequest),
|
})
|
||||||
e @ Error(ErrorKind::MissingPassword, _) => IronError::new(e, Status::BadRequest),
|
.ok()
|
||||||
e @ Error(ErrorKind::IncorrectCredentials, _) => {
|
|
||||||
IronError::new(e, Status::Unauthorized)
|
|
||||||
}
|
|
||||||
e @ Error(ErrorKind::CannotServeDirectory, _) => IronError::new(e, Status::BadRequest),
|
|
||||||
e @ Error(ErrorKind::UnsupportedFileType, _) => IronError::new(e, Status::BadRequest),
|
|
||||||
e @ Error(ErrorKind::MissingLastFMCredentials, _) => {
|
|
||||||
IronError::new(e, Status::Unauthorized)
|
|
||||||
}
|
|
||||||
e => IronError::new(e, Status::InternalServerError),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
122
src/index.rs
122
src/index.rs
|
@ -14,14 +14,14 @@ use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
use config::MiscSettings;
|
use crate::config::MiscSettings;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use db;
|
use crate::db;
|
||||||
use db::ConnectionSource;
|
use crate::db::{directories, misc_settings, songs};
|
||||||
use db::{directories, misc_settings, songs};
|
use crate::db::{ConnectionSource, DB};
|
||||||
use errors;
|
use crate::errors;
|
||||||
use metadata;
|
use crate::metadata;
|
||||||
use vfs::{VFSSource, VFS};
|
use crate::vfs::{VFSSource, VFS};
|
||||||
|
|
||||||
const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // Insertions in each transaction
|
const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // Insertions in each transaction
|
||||||
const INDEX_BUILDING_CLEAN_BUFFER_SIZE: usize = 500; // Insertions in each transaction
|
const INDEX_BUILDING_CLEAN_BUFFER_SIZE: usize = 500; // Insertions in each transaction
|
||||||
|
@ -32,17 +32,72 @@ no_arg_sql_function!(
|
||||||
"Represents the SQL RANDOM() function"
|
"Represents the SQL RANDOM() function"
|
||||||
);
|
);
|
||||||
|
|
||||||
pub enum Command {
|
enum Command {
|
||||||
REINDEX,
|
REINDEX,
|
||||||
|
EXIT,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Queryable, QueryableByName, Serialize)]
|
struct CommandReceiver {
|
||||||
|
receiver: Receiver<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandReceiver {
|
||||||
|
fn new(receiver: Receiver<Command>) -> CommandReceiver {
|
||||||
|
CommandReceiver { receiver }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CommandSender {
|
||||||
|
sender: Mutex<Sender<Command>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandSender {
|
||||||
|
fn new(sender: Sender<Command>) -> CommandSender {
|
||||||
|
CommandSender {
|
||||||
|
sender: Mutex::new(sender),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger_reindex(&self) -> Result<(), errors::Error> {
|
||||||
|
let sender = self.sender.lock().unwrap();
|
||||||
|
match sender.send(Command::REINDEX) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => bail!("Trigger reindex channel error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn exit(&self) -> Result<(), errors::Error> {
|
||||||
|
let sender = self.sender.lock().unwrap();
|
||||||
|
match sender.send(Command::EXIT) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => bail!("Index exit channel error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(db: Arc<DB>) -> Arc<CommandSender> {
|
||||||
|
let (index_sender, index_receiver) = channel();
|
||||||
|
let command_sender = Arc::new(CommandSender::new(index_sender));
|
||||||
|
let command_receiver = CommandReceiver::new(index_receiver);
|
||||||
|
|
||||||
|
// Start update loop
|
||||||
|
let db_ref = db.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let db = db_ref.deref();
|
||||||
|
update_loop(db, &command_receiver);
|
||||||
|
});
|
||||||
|
|
||||||
|
command_sender
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Queryable, QueryableByName, Serialize, Deserialize)]
|
||||||
#[table_name = "songs"]
|
#[table_name = "songs"]
|
||||||
pub struct Song {
|
pub struct Song {
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
id: i32,
|
id: i32,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub parent: String,
|
pub parent: String,
|
||||||
pub track_number: Option<i32>,
|
pub track_number: Option<i32>,
|
||||||
pub disc_number: Option<i32>,
|
pub disc_number: Option<i32>,
|
||||||
|
@ -55,12 +110,12 @@ pub struct Song {
|
||||||
pub duration: Option<i32>,
|
pub duration: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Queryable, Serialize)]
|
#[derive(Debug, PartialEq, Queryable, Serialize, Deserialize)]
|
||||||
pub struct Directory {
|
pub struct Directory {
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
id: i32,
|
id: i32,
|
||||||
pub path: String,
|
pub path: String,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub parent: Option<String>,
|
pub parent: Option<String>,
|
||||||
pub artist: Option<String>,
|
pub artist: Option<String>,
|
||||||
pub year: Option<i32>,
|
pub year: Option<i32>,
|
||||||
|
@ -69,7 +124,7 @@ pub struct Directory {
|
||||||
pub date_added: i32,
|
pub date_added: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum CollectionFile {
|
pub enum CollectionFile {
|
||||||
Directory(Directory),
|
Directory(Directory),
|
||||||
Song(Song),
|
Song(Song),
|
||||||
|
@ -318,7 +373,8 @@ where
|
||||||
.filter(|ref song_path| {
|
.filter(|ref song_path| {
|
||||||
let path = Path::new(&song_path);
|
let path = Path::new(&song_path);
|
||||||
!path.exists() || vfs.real_to_virtual(path).is_err()
|
!path.exists() || vfs.real_to_virtual(path).is_err()
|
||||||
}).collect::<Vec<_>>();
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
{
|
{
|
||||||
let connection = db.get_connection();
|
let connection = db.get_connection();
|
||||||
|
@ -343,7 +399,8 @@ where
|
||||||
.filter(|ref directory_path| {
|
.filter(|ref directory_path| {
|
||||||
let path = Path::new(&directory_path);
|
let path = Path::new(&directory_path);
|
||||||
!path.exists() || vfs.real_to_virtual(path).is_err()
|
!path.exists() || vfs.real_to_virtual(path).is_err()
|
||||||
}).collect::<Vec<_>>();
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
{
|
{
|
||||||
let connection = db.get_connection();
|
let connection = db.get_connection();
|
||||||
|
@ -396,24 +453,21 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_loop<T>(db: &T, command_buffer: &Receiver<Command>)
|
fn update_loop<T>(db: &T, command_buffer: &CommandReceiver)
|
||||||
where
|
where
|
||||||
T: ConnectionSource + VFSSource,
|
T: ConnectionSource + VFSSource,
|
||||||
{
|
{
|
||||||
loop {
|
loop {
|
||||||
// Wait for a command
|
// Wait for a command
|
||||||
if let Err(e) = command_buffer.recv() {
|
if command_buffer.receiver.recv().is_err() {
|
||||||
error!("Error while waiting on index command buffer: {}", e);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush the buffer to ignore spammy requests
|
// Flush the buffer to ignore spammy requests
|
||||||
loop {
|
loop {
|
||||||
match command_buffer.try_recv() {
|
match command_buffer.receiver.try_recv() {
|
||||||
Err(TryRecvError::Disconnected) => {
|
Err(TryRecvError::Disconnected) => return,
|
||||||
error!("Error while flushing index command buffer");
|
Ok(Command::EXIT) => return,
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(TryRecvError::Empty) => break,
|
Err(TryRecvError::Empty) => break,
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
}
|
}
|
||||||
|
@ -426,15 +480,14 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn self_trigger<T>(db: &T, command_buffer: &Arc<Mutex<Sender<Command>>>)
|
pub fn self_trigger<T>(db: &T, command_buffer: &Arc<CommandSender>)
|
||||||
where
|
where
|
||||||
T: ConnectionSource,
|
T: ConnectionSource,
|
||||||
{
|
{
|
||||||
loop {
|
loop {
|
||||||
{
|
{
|
||||||
let command_buffer = command_buffer.lock().unwrap();
|
|
||||||
let command_buffer = command_buffer.deref();
|
let command_buffer = command_buffer.deref();
|
||||||
if let Err(e) = command_buffer.send(Command::REINDEX) {
|
if let Err(e) = command_buffer.trigger_reindex() {
|
||||||
error!("Error while writing to index command buffer: {}", e);
|
error!("Error while writing to index command buffer: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -484,15 +537,16 @@ fn virtualize_directory(vfs: &VFS, mut directory: Directory) -> Option<Directory
|
||||||
Some(directory)
|
Some(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn browse<T>(db: &T, virtual_path: &Path) -> Result<Vec<CollectionFile>, errors::Error>
|
pub fn browse<T, P>(db: &T, virtual_path: P) -> Result<Vec<CollectionFile>, errors::Error>
|
||||||
where
|
where
|
||||||
T: ConnectionSource + VFSSource,
|
T: ConnectionSource + VFSSource,
|
||||||
|
P: AsRef<Path>,
|
||||||
{
|
{
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
let vfs = db.get_vfs()?;
|
let vfs = db.get_vfs()?;
|
||||||
let connection = db.get_connection();
|
let connection = db.get_connection();
|
||||||
|
|
||||||
if virtual_path.components().count() == 0 {
|
if virtual_path.as_ref().components().count() == 0 {
|
||||||
// Browse top-level
|
// Browse top-level
|
||||||
let real_directories: Vec<Directory> = directories::table
|
let real_directories: Vec<Directory> = directories::table
|
||||||
.filter(directories::parent.is_null())
|
.filter(directories::parent.is_null())
|
||||||
|
@ -528,15 +582,16 @@ where
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn flatten<T>(db: &T, virtual_path: &Path) -> Result<Vec<Song>, errors::Error>
|
pub fn flatten<T, P>(db: &T, virtual_path: P) -> Result<Vec<Song>, errors::Error>
|
||||||
where
|
where
|
||||||
T: ConnectionSource + VFSSource,
|
T: ConnectionSource + VFSSource,
|
||||||
|
P: AsRef<Path>,
|
||||||
{
|
{
|
||||||
use self::songs::dsl::*;
|
use self::songs::dsl::*;
|
||||||
let vfs = db.get_vfs()?;
|
let vfs = db.get_vfs()?;
|
||||||
let connection = db.get_connection();
|
let connection = db.get_connection();
|
||||||
|
|
||||||
let real_songs: Vec<Song> = if virtual_path.parent() != None {
|
let real_songs: Vec<Song> = if virtual_path.as_ref().parent() != None {
|
||||||
let real_path = vfs.virtual_to_real(virtual_path)?;
|
let real_path = vfs.virtual_to_real(virtual_path)?;
|
||||||
let like_path = real_path.as_path().to_string_lossy().into_owned() + "%";
|
let like_path = real_path.as_path().to_string_lossy().into_owned() + "%";
|
||||||
songs
|
songs
|
||||||
|
@ -623,7 +678,8 @@ where
|
||||||
.or(album.like(&like_test))
|
.or(album.like(&like_test))
|
||||||
.or(artist.like(&like_test))
|
.or(artist.like(&like_test))
|
||||||
.or(album_artist.like(&like_test)),
|
.or(album_artist.like(&like_test)),
|
||||||
).filter(parent.not_like(&like_test))
|
)
|
||||||
|
.filter(parent.not_like(&like_test))
|
||||||
.load(connection.deref())?;
|
.load(connection.deref())?;
|
||||||
|
|
||||||
let virtual_songs = real_songs
|
let virtual_songs = real_songs
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use rustfm_scrobble::{Scrobble, Scrobbler};
|
use rustfm_scrobble::{Scrobble, Scrobbler};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use db::ConnectionSource;
|
use crate::db::ConnectionSource;
|
||||||
use errors;
|
use crate::errors;
|
||||||
use index;
|
use crate::index;
|
||||||
use user;
|
use crate::user;
|
||||||
use vfs::VFSSource;
|
use crate::vfs::VFSSource;
|
||||||
|
|
||||||
const LASTFM_API_KEY: &str = "02b96c939a2b451c31dfd67add1f696e";
|
const LASTFM_API_KEY: &str = "02b96c939a2b451c31dfd67add1f696e";
|
||||||
const LASTFM_API_SECRET: &str = "0f25a80ceef4b470b5cb97d99d4b3420";
|
const LASTFM_API_SECRET: &str = "0f25a80ceef4b470b5cb97d99d4b3420";
|
||||||
|
@ -60,12 +60,7 @@ where
|
||||||
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into());
|
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into());
|
||||||
let auth_response = scrobbler.authenticate_with_token(token.to_string())?;
|
let auth_response = scrobbler.authenticate_with_token(token.to_string())?;
|
||||||
|
|
||||||
user::lastfm_link(
|
user::lastfm_link(db, username, &auth_response.name, &auth_response.key)
|
||||||
db,
|
|
||||||
username,
|
|
||||||
&auth_response.name,
|
|
||||||
&auth_response.key,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unlink<T>(db: &T, username: &str) -> Result<(), errors::Error>
|
pub fn unlink<T>(db: &T, username: &str) -> Result<(), errors::Error>
|
||||||
|
|
72
src/main.rs
72
src/main.rs
|
@ -1,4 +1,5 @@
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
#![feature(proc_macro_hygiene, decl_macro)]
|
||||||
#![allow(proc_macro_derive_resolution_fallback)]
|
#![allow(proc_macro_derive_resolution_fallback)]
|
||||||
|
|
||||||
extern crate ape;
|
extern crate ape;
|
||||||
|
@ -13,30 +14,24 @@ extern crate diesel_migrations;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate error_chain;
|
extern crate error_chain;
|
||||||
extern crate getopts;
|
extern crate getopts;
|
||||||
extern crate hyper;
|
|
||||||
extern crate id3;
|
extern crate id3;
|
||||||
extern crate image;
|
extern crate image;
|
||||||
extern crate iron;
|
|
||||||
extern crate lewton;
|
extern crate lewton;
|
||||||
extern crate metaflac;
|
extern crate metaflac;
|
||||||
extern crate mount;
|
|
||||||
extern crate mp3_duration;
|
extern crate mp3_duration;
|
||||||
extern crate params;
|
|
||||||
extern crate rand;
|
extern crate rand;
|
||||||
extern crate regex;
|
extern crate regex;
|
||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
extern crate ring;
|
extern crate ring;
|
||||||
extern crate router;
|
#[macro_use]
|
||||||
|
extern crate rocket;
|
||||||
|
extern crate rocket_contrib;
|
||||||
extern crate rustfm_scrobble;
|
extern crate rustfm_scrobble;
|
||||||
extern crate secure_session;
|
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_derive;
|
extern crate serde_derive;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
extern crate staticfile;
|
|
||||||
extern crate toml;
|
extern crate toml;
|
||||||
extern crate typemap;
|
|
||||||
extern crate url;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
extern crate simplelog;
|
extern crate simplelog;
|
||||||
|
@ -57,17 +52,15 @@ use std::io::prelude::*;
|
||||||
use unix_daemonize::{daemonize_redirect, ChdirMode};
|
use unix_daemonize::{daemonize_redirect, ChdirMode};
|
||||||
|
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
use getopts::Options;
|
use getopts::Options;
|
||||||
use iron::prelude::*;
|
|
||||||
use mount::Mount;
|
|
||||||
use simplelog::{Level, LevelFilter, SimpleLogger, TermLogger};
|
use simplelog::{Level, LevelFilter, SimpleLogger, TermLogger};
|
||||||
use staticfile::Static;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::Arc;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod api_tests;
|
||||||
mod config;
|
mod config;
|
||||||
mod db;
|
mod db;
|
||||||
mod ddns;
|
mod ddns;
|
||||||
|
@ -77,6 +70,7 @@ mod lastfm;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
mod playlist;
|
mod playlist;
|
||||||
mod serve;
|
mod serve;
|
||||||
|
mod server;
|
||||||
mod thumbnails;
|
mod thumbnails;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod user;
|
mod user;
|
||||||
|
@ -204,6 +198,7 @@ fn run() -> Result<()> {
|
||||||
let db = Arc::new(db::DB::new(&db_path)?);
|
let db = Arc::new(db::DB::new(&db_path)?);
|
||||||
|
|
||||||
// Parse config
|
// Parse config
|
||||||
|
info!("Parsing configuration");
|
||||||
let config_file_name = matches.opt_str("c");
|
let config_file_name = matches.opt_str("c");
|
||||||
let config_file_path = config_file_name.map(|p| Path::new(p.as_str()).to_path_buf());
|
let config_file_path = config_file_name.map(|p| Path::new(p.as_str()).to_path_buf());
|
||||||
if let Some(path) = config_file_path {
|
if let Some(path) = config_file_path {
|
||||||
|
@ -213,30 +208,22 @@ fn run() -> Result<()> {
|
||||||
let config = config::read(db.deref())?;
|
let config = config::read(db.deref())?;
|
||||||
|
|
||||||
// Init index
|
// Init index
|
||||||
let (index_sender, index_receiver) = channel();
|
info!("Initializing index");
|
||||||
let index_sender = Arc::new(Mutex::new(index_sender));
|
let command_sender = index::init(db.clone());
|
||||||
let db_ref = db.clone();
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let db = db_ref.deref();
|
|
||||||
index::update_loop(db, &index_receiver);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Trigger auto-indexing
|
// Trigger auto-indexing
|
||||||
let db_ref = db.clone();
|
let db_auto_index = db.clone();
|
||||||
let sender_ref = index_sender.clone();
|
let command_sender_auto_index = command_sender.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
index::self_trigger(db_ref.deref(), &sender_ref);
|
index::self_trigger(db_auto_index.deref(), &command_sender_auto_index);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mount API
|
// API mount target
|
||||||
let prefix_url = config.prefix_url.unwrap_or_else(|| "".to_string());
|
let prefix_url = config.prefix_url.unwrap_or_else(|| "".to_string());
|
||||||
let api_url = format!("{}/api", &prefix_url);
|
let api_url = format!("{}/api", &prefix_url);
|
||||||
info!("Mounting API on {}", api_url);
|
info!("Mounting API on {}", api_url);
|
||||||
let mut mount = Mount::new();
|
|
||||||
let handler = api::get_handler(&db.clone(), &index_sender)?;
|
|
||||||
mount.mount(&api_url, handler);
|
|
||||||
|
|
||||||
// Mount static files
|
// Static files mount target
|
||||||
let web_dir_name = matches.opt_str("w");
|
let web_dir_name = matches.opt_str("w");
|
||||||
let mut default_web_dir = utils::get_data_root()?;
|
let mut default_web_dir = utils::get_data_root()?;
|
||||||
default_web_dir.push("web");
|
default_web_dir.push("web");
|
||||||
|
@ -246,8 +233,8 @@ fn run() -> Result<()> {
|
||||||
info!("Static files location is {}", web_dir_path.display());
|
info!("Static files location is {}", web_dir_path.display());
|
||||||
let static_url = format!("/{}", &prefix_url);
|
let static_url = format!("/{}", &prefix_url);
|
||||||
info!("Mounting static files on {}", static_url);
|
info!("Mounting static files on {}", static_url);
|
||||||
mount.mount(&static_url, Static::new(web_dir_path));
|
|
||||||
|
|
||||||
|
// Start server
|
||||||
info!("Starting up server");
|
info!("Starting up server");
|
||||||
let port: u16 = matches
|
let port: u16 = matches
|
||||||
.opt_str("p")
|
.opt_str("p")
|
||||||
|
@ -255,24 +242,27 @@ fn run() -> Result<()> {
|
||||||
.parse()
|
.parse()
|
||||||
.or(Err("invalid port number"))?;
|
.or(Err("invalid port number"))?;
|
||||||
|
|
||||||
let mut server = match Iron::new(mount).http(("0.0.0.0", port)) {
|
let server = server::get_server(
|
||||||
Ok(s) => s,
|
port,
|
||||||
Err(e) => bail!("Error starting up server: {}", e),
|
&static_url,
|
||||||
};
|
&api_url,
|
||||||
|
&web_dir_path,
|
||||||
|
db.clone(),
|
||||||
|
command_sender,
|
||||||
|
)?;
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
server.launch();
|
||||||
|
});
|
||||||
|
|
||||||
// Start DDNS updates
|
// Start DDNS updates
|
||||||
let db_ref = db.clone();
|
let db_ddns = db.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
ddns::run(db_ref.deref());
|
ddns::run(db_ddns.deref());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run UI
|
// Run UI
|
||||||
ui::run();
|
ui::run();
|
||||||
|
|
||||||
info!("Shutting down server");
|
info!("Shutting down server");
|
||||||
if let Err(e) = server.close() {
|
|
||||||
bail!("Error shutting down server: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@ use regex::Regex;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
use utils;
|
use crate::utils;
|
||||||
use utils::AudioFormat;
|
use crate::utils::AudioFormat;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct SongTags {
|
pub struct SongTags {
|
||||||
|
|
|
@ -7,12 +7,12 @@ use diesel::BelongingToDsl;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use db;
|
use crate::db;
|
||||||
use db::ConnectionSource;
|
use crate::db::ConnectionSource;
|
||||||
use db::{playlist_songs, playlists, users};
|
use crate::db::{playlist_songs, playlists, users};
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
use index::{self, Song};
|
use crate::index::{self, Song};
|
||||||
use vfs::VFSSource;
|
use crate::vfs::VFSSource;
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
#[table_name = "playlists"]
|
#[table_name = "playlists"]
|
||||||
|
@ -262,7 +262,7 @@ fn test_delete_playlist() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fill_playlist() {
|
fn test_fill_playlist() {
|
||||||
use index;
|
use crate::index;
|
||||||
|
|
||||||
let db = db::_get_test_db("fill_playlist.sqlite");
|
let db = db::_get_test_db("fill_playlist.sqlite");
|
||||||
index::update(&db).unwrap();
|
index::update(&db).unwrap();
|
||||||
|
|
238
src/serve.rs
238
src/serve.rs
|
@ -1,53 +1,15 @@
|
||||||
use iron::headers::{
|
use rocket;
|
||||||
AcceptRanges, ByteRangeSpec, ContentLength, ContentRange, ContentRangeSpec, Range, RangeUnit,
|
use rocket::http::hyper::header::*;
|
||||||
};
|
use rocket::http::Status;
|
||||||
use iron::modifier::Modifier;
|
use rocket::Response;
|
||||||
use iron::modifiers::Header;
|
use rocket::response::{self, Responder};
|
||||||
use iron::prelude::*;
|
|
||||||
use iron::response::WriteBody;
|
|
||||||
use iron::status::{self, Status};
|
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::fs::{self, File};
|
use std::convert::From;
|
||||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
use std::fs::File;
|
||||||
use std::path::Path;
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
|
use std::str::FromStr;
|
||||||
use errors::{Error, ErrorKind};
|
|
||||||
|
|
||||||
pub fn deliver(path: &Path, range_header: Option<&Range>) -> IronResult<Response> {
|
|
||||||
match fs::metadata(path) {
|
|
||||||
Ok(meta) => meta,
|
|
||||||
Err(e) => {
|
|
||||||
let status = match e.kind() {
|
|
||||||
io::ErrorKind::NotFound => status::NotFound,
|
|
||||||
io::ErrorKind::PermissionDenied => status::Forbidden,
|
|
||||||
_ => status::InternalServerError,
|
|
||||||
};
|
|
||||||
return Err(IronError::new(e, status));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let accept_range_header = Header(AcceptRanges(vec![RangeUnit::Bytes]));
|
|
||||||
let range_header = range_header.cloned();
|
|
||||||
|
|
||||||
match range_header {
|
|
||||||
None => Ok(Response::with((status::Ok, path, accept_range_header))),
|
|
||||||
Some(range) => match range {
|
|
||||||
Range::Bytes(vec_range) => {
|
|
||||||
if let Ok(partial_file) = PartialFile::from_path(path, vec_range) {
|
|
||||||
Ok(Response::with((
|
|
||||||
status::Ok,
|
|
||||||
partial_file,
|
|
||||||
accept_range_header,
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Err(Error::from(ErrorKind::FileNotFound).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Ok(Response::with(status::RangeNotSatisfiable)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum PartialFileRange {
|
pub enum PartialFileRange {
|
||||||
AllFrom(u64),
|
AllFrom(u64),
|
||||||
FromTo(u64, u64),
|
FromTo(u64, u64),
|
||||||
|
@ -64,11 +26,6 @@ impl From<ByteRangeSpec> for PartialFileRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PartialFile {
|
|
||||||
file: File,
|
|
||||||
range: PartialFileRange,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<ByteRangeSpec>> for PartialFileRange {
|
impl From<Vec<ByteRangeSpec>> for PartialFileRange {
|
||||||
fn from(v: Vec<ByteRangeSpec>) -> PartialFileRange {
|
fn from(v: Vec<ByteRangeSpec>) -> PartialFileRange {
|
||||||
match v.into_iter().next() {
|
match v.into_iter().next() {
|
||||||
|
@ -78,90 +35,119 @@ impl From<Vec<ByteRangeSpec>> for PartialFileRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialFile {
|
pub struct RangeResponder<R> {
|
||||||
pub fn new<Range>(file: File, range: Range) -> PartialFile
|
original: R,
|
||||||
where
|
}
|
||||||
Range: Into<PartialFileRange>,
|
|
||||||
{
|
impl<'r, R: Responder<'r>> RangeResponder<R> {
|
||||||
let range = range.into();
|
pub fn new(original: R) -> RangeResponder<R> {
|
||||||
PartialFile { file, range }
|
RangeResponder { original }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_path<P: AsRef<Path>, Range>(path: P, range: Range) -> Result<PartialFile, io::Error>
|
fn ignore_range(self, request: &rocket::request::Request, file_length: Option<u64>) -> response::Result<'r> {
|
||||||
where
|
let mut response = self.original.respond_to(request)?;
|
||||||
Range: Into<PartialFileRange>,
|
if let Some(content_length) = file_length {
|
||||||
{
|
response.set_header(ContentLength(content_length));
|
||||||
let file = File::open(path.as_ref())?;
|
}
|
||||||
Ok(Self::new(file, range))
|
response.set_status(Status::Ok);
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reject_range(self, file_length: Option<u64>) -> response::Result<'r> {
|
||||||
|
let mut response = Response::build()
|
||||||
|
.status(Status::RangeNotSatisfiable)
|
||||||
|
.finalize();
|
||||||
|
if file_length.is_some() {
|
||||||
|
let content_range = ContentRange(ContentRangeSpec::Bytes {
|
||||||
|
range: None,
|
||||||
|
instance_length: file_length,
|
||||||
|
});
|
||||||
|
response.set_header(content_range);
|
||||||
|
}
|
||||||
|
response.set_status(Status::RangeNotSatisfiable);
|
||||||
|
Ok(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Modifier<Response> for PartialFile {
|
fn truncate_range(range: &PartialFileRange, file_length: &Option<u64>) -> Option<(u64, u64)> {
|
||||||
fn modify(self, res: &mut Response) {
|
use self::PartialFileRange::*;
|
||||||
use self::PartialFileRange::*;
|
|
||||||
let metadata: Option<_> = self.file.metadata().ok();
|
match (range, file_length) {
|
||||||
|
(FromTo(from, to), Some(file_length)) => {
|
||||||
|
if from <= to && from < file_length {
|
||||||
|
Some((*from, cmp::min(*to, file_length - 1)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(AllFrom(from), Some(file_length)) => {
|
||||||
|
if from < file_length {
|
||||||
|
Some((*from, file_length - 1))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Last(last), Some(file_length)) => {
|
||||||
|
if last < file_length {
|
||||||
|
Some((file_length - last, file_length - 1))
|
||||||
|
} else {
|
||||||
|
Some((0, file_length - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(_, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> Responder<'r> for RangeResponder<File> {
|
||||||
|
fn respond_to(mut self, request: &rocket::request::Request) -> response::Result<'r> {
|
||||||
|
|
||||||
|
let metadata: Option<_> = self.original.metadata().ok();
|
||||||
let file_length: Option<u64> = metadata.map(|m| m.len());
|
let file_length: Option<u64> = metadata.map(|m| m.len());
|
||||||
let range: Option<(u64, u64)> = match (self.range, file_length) {
|
|
||||||
(FromTo(from, to), Some(file_length)) => {
|
let range_header = request.headers().get_one("Range");
|
||||||
if from <= to && from < file_length {
|
let range_header = match range_header {
|
||||||
Some((from, cmp::min(to, file_length - 1)))
|
None => return self.ignore_range(request, file_length),
|
||||||
} else {
|
Some(h) => h,
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(AllFrom(from), Some(file_length)) => {
|
|
||||||
if from < file_length {
|
|
||||||
Some((from, file_length - 1))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Last(last), Some(file_length)) => {
|
|
||||||
if last < file_length {
|
|
||||||
Some((file_length - last, file_length - 1))
|
|
||||||
} else {
|
|
||||||
Some((0, file_length - 1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(_, None) => None,
|
|
||||||
};
|
};
|
||||||
if let Some(range) = range {
|
|
||||||
|
let vec_range = match Range::from_str(range_header) {
|
||||||
|
Ok(Range::Bytes(v)) => v,
|
||||||
|
_ => {
|
||||||
|
warn!("Ignoring range header that could not be parse {:?}, file length is {:?}", range_header, file_length);
|
||||||
|
return self.ignore_range(request, file_length);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let partial_file_range = match vec_range.into_iter().next() {
|
||||||
|
None => PartialFileRange::AllFrom(0),
|
||||||
|
Some(byte_range) => PartialFileRange::from(byte_range),
|
||||||
|
};
|
||||||
|
|
||||||
|
let range: Option<(u64, u64)> = truncate_range(&partial_file_range, &file_length);
|
||||||
|
|
||||||
|
if let Some((from, to)) = range {
|
||||||
let content_range = ContentRange(ContentRangeSpec::Bytes {
|
let content_range = ContentRange(ContentRangeSpec::Bytes {
|
||||||
range: Some(range),
|
range: range,
|
||||||
instance_length: file_length,
|
instance_length: file_length,
|
||||||
});
|
});
|
||||||
let content_len = range.1 - range.0 + 1;
|
let content_len = to - from + 1;
|
||||||
res.headers.set(ContentLength(content_len));
|
|
||||||
res.headers.set(content_range);
|
match self.original.seek(SeekFrom::Start(from)) {
|
||||||
let partial_content = PartialContentBody {
|
Ok(_) => (),
|
||||||
file: self.file,
|
Err(_) => return Err(rocket::http::Status::InternalServerError),
|
||||||
offset: range.0,
|
}
|
||||||
len: content_len,
|
let partial_original = self.original.take(content_len);
|
||||||
};
|
let response = Response::build()
|
||||||
res.status = Some(Status::PartialContent);
|
.status(Status::PartialContent)
|
||||||
res.body = Some(Box::new(partial_content));
|
.header(ContentLength(content_len))
|
||||||
|
.header(content_range)
|
||||||
|
.streamed_body(partial_original)
|
||||||
|
.finalize();
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
} else {
|
} else {
|
||||||
if let Some(file_length) = file_length {
|
warn!("Rejecting unsatisfiable range header {:?}, file length is {:?}", &partial_file_range, &file_length);
|
||||||
res.headers.set(ContentRange(ContentRangeSpec::Bytes {
|
self.reject_range(file_length)
|
||||||
range: None,
|
|
||||||
instance_length: Some(file_length),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
res.status = Some(Status::RangeNotSatisfiable);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PartialContentBody {
|
|
||||||
pub file: File,
|
|
||||||
pub offset: u64,
|
|
||||||
pub len: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WriteBody for PartialContentBody {
|
|
||||||
fn write_body(&mut self, res: &mut Write) -> io::Result<()> {
|
|
||||||
self.file.seek(SeekFrom::Start(self.offset))?;
|
|
||||||
let mut limiter = <File as Read>::by_ref(&mut self.file).take(self.len);
|
|
||||||
io::copy(&mut limiter, res).map(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
28
src/server.rs
Normal file
28
src/server.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use rocket;
|
||||||
|
use rocket_contrib::serve::StaticFiles;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::api;
|
||||||
|
use crate::db::DB;
|
||||||
|
use crate::errors;
|
||||||
|
use crate::index::CommandSender;
|
||||||
|
|
||||||
|
pub fn get_server(
|
||||||
|
port: u16,
|
||||||
|
static_url: &str,
|
||||||
|
api_url: &str,
|
||||||
|
web_dir_path: &PathBuf,
|
||||||
|
db: Arc<DB>,
|
||||||
|
command_sender: Arc<CommandSender>,
|
||||||
|
) -> Result<rocket::Rocket, errors::Error> {
|
||||||
|
let config = rocket::Config::build(rocket::config::Environment::Production)
|
||||||
|
.port(port)
|
||||||
|
.finalize()?;
|
||||||
|
|
||||||
|
Ok(rocket::custom(config)
|
||||||
|
.manage(db)
|
||||||
|
.manage(command_sender)
|
||||||
|
.mount(&static_url, StaticFiles::from(web_dir_path))
|
||||||
|
.mount(&api_url, api::get_routes()))
|
||||||
|
}
|
|
@ -11,8 +11,8 @@ use std::fs::{DirBuilder, File};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::path::*;
|
use std::path::*;
|
||||||
|
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
use utils;
|
use crate::utils;
|
||||||
|
|
||||||
const THUMBNAILS_PATH: &str = "thumbnails";
|
const THUMBNAILS_PATH: &str = "thumbnails";
|
||||||
|
|
||||||
|
|
24
src/user.rs
24
src/user.rs
|
@ -4,9 +4,9 @@ use diesel::prelude::*;
|
||||||
use rand;
|
use rand;
|
||||||
use ring::{digest, pbkdf2};
|
use ring::{digest, pbkdf2};
|
||||||
|
|
||||||
use db::users;
|
use crate::db::users;
|
||||||
use db::ConnectionSource;
|
use crate::db::ConnectionSource;
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
#[derive(Debug, Insertable, Queryable)]
|
#[derive(Debug, Insertable, Queryable)]
|
||||||
#[table_name = "users"]
|
#[table_name = "users"]
|
||||||
|
@ -58,14 +58,15 @@ fn verify_password(
|
||||||
password_salt,
|
password_salt,
|
||||||
attempted_password.as_bytes(),
|
attempted_password.as_bytes(),
|
||||||
password_hash,
|
password_hash,
|
||||||
).is_ok()
|
)
|
||||||
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn auth<T>(db: &T, username: &str, password: &str) -> Result<bool>
|
pub fn auth<T>(db: &T, username: &str, password: &str) -> Result<bool>
|
||||||
where
|
where
|
||||||
T: ConnectionSource,
|
T: ConnectionSource,
|
||||||
{
|
{
|
||||||
use db::users::dsl::*;
|
use crate::db::users::dsl::*;
|
||||||
let connection = db.get_connection();
|
let connection = db.get_connection();
|
||||||
match users
|
match users
|
||||||
.select((password_hash, password_salt))
|
.select((password_hash, password_salt))
|
||||||
|
@ -82,7 +83,7 @@ pub fn count<T>(db: &T) -> Result<i64>
|
||||||
where
|
where
|
||||||
T: ConnectionSource,
|
T: ConnectionSource,
|
||||||
{
|
{
|
||||||
use db::users::dsl::*;
|
use crate::db::users::dsl::*;
|
||||||
let connection = db.get_connection();
|
let connection = db.get_connection();
|
||||||
let count = users.count().get_result(connection.deref())?;
|
let count = users.count().get_result(connection.deref())?;
|
||||||
Ok(count)
|
Ok(count)
|
||||||
|
@ -92,7 +93,7 @@ pub fn is_admin<T>(db: &T, username: &str) -> Result<bool>
|
||||||
where
|
where
|
||||||
T: ConnectionSource,
|
T: ConnectionSource,
|
||||||
{
|
{
|
||||||
use db::users::dsl::*;
|
use crate::db::users::dsl::*;
|
||||||
let connection = db.get_connection();
|
let connection = db.get_connection();
|
||||||
let is_admin: i32 = users
|
let is_admin: i32 = users
|
||||||
.filter(name.eq(username))
|
.filter(name.eq(username))
|
||||||
|
@ -105,13 +106,14 @@ pub fn lastfm_link<T>(db: &T, username: &str, lastfm_login: &str, session_key: &
|
||||||
where
|
where
|
||||||
T: ConnectionSource,
|
T: ConnectionSource,
|
||||||
{
|
{
|
||||||
use db::users::dsl::*;
|
use crate::db::users::dsl::*;
|
||||||
let connection = db.get_connection();
|
let connection = db.get_connection();
|
||||||
diesel::update(users.filter(name.eq(username)))
|
diesel::update(users.filter(name.eq(username)))
|
||||||
.set((
|
.set((
|
||||||
lastfm_username.eq(lastfm_login),
|
lastfm_username.eq(lastfm_login),
|
||||||
lastfm_session_key.eq(session_key),
|
lastfm_session_key.eq(session_key),
|
||||||
)).execute(connection.deref())?;
|
))
|
||||||
|
.execute(connection.deref())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +121,7 @@ pub fn get_lastfm_session_key<T>(db: &T, username: &str) -> Result<String>
|
||||||
where
|
where
|
||||||
T: ConnectionSource,
|
T: ConnectionSource,
|
||||||
{
|
{
|
||||||
use db::users::dsl::*;
|
use crate::db::users::dsl::*;
|
||||||
let connection = db.get_connection();
|
let connection = db.get_connection();
|
||||||
let token = users
|
let token = users
|
||||||
.filter(name.eq(username))
|
.filter(name.eq(username))
|
||||||
|
@ -135,7 +137,7 @@ pub fn lastfm_unlink<T>(db: &T, username: &str) -> Result<()>
|
||||||
where
|
where
|
||||||
T: ConnectionSource,
|
T: ConnectionSource,
|
||||||
{
|
{
|
||||||
use db::users::dsl::*;
|
use crate::db::users::dsl::*;
|
||||||
let connection = db.get_connection();
|
let connection = db.get_connection();
|
||||||
diesel::update(users.filter(name.eq(username)))
|
diesel::update(users.filter(name.eq(username)))
|
||||||
.set((lastfm_session_key.eq(""), lastfm_username.eq("")))
|
.set((lastfm_session_key.eq(""), lastfm_username.eq("")))
|
||||||
|
|
12
src/utils.rs
12
src/utils.rs
|
@ -2,7 +2,7 @@ use app_dirs::{app_root, AppDataType, AppInfo};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
const APP_INFO: AppInfo = AppInfo {
|
const APP_INFO: AppInfo = AppInfo {
|
||||||
|
@ -64,16 +64,6 @@ fn test_get_audio_format() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_song(path: &Path) -> bool {
|
|
||||||
get_audio_format(path).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_song() {
|
|
||||||
assert!(is_song(Path::new("animals/🐷/my🐖file.mp3")));
|
|
||||||
assert!(!is_song(Path::new("animals/🐷/my🐖file.jpg")));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_image(path: &Path) -> bool {
|
pub fn is_image(path: &Path) -> bool {
|
||||||
let extension = match path.extension() {
|
let extension = match path.extension() {
|
||||||
Some(e) => e,
|
Some(e) => e,
|
||||||
|
|
14
src/vfs.rs
14
src/vfs.rs
|
@ -4,9 +4,9 @@ use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use db::mount_points;
|
use crate::db::mount_points;
|
||||||
use db::{ConnectionSource, DB};
|
use crate::db::{ConnectionSource, DB};
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
pub trait VFSSource {
|
pub trait VFSSource {
|
||||||
fn get_vfs(&self) -> Result<VFS>;
|
fn get_vfs(&self) -> Result<VFS>;
|
||||||
|
@ -51,9 +51,9 @@ impl VFS {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn real_to_virtual(&self, real_path: &Path) -> Result<PathBuf> {
|
pub fn real_to_virtual<P: AsRef<Path>>(&self, real_path: P) -> Result<PathBuf> {
|
||||||
for (name, target) in &self.mount_points {
|
for (name, target) in &self.mount_points {
|
||||||
if let Ok(p) = real_path.strip_prefix(target) {
|
if let Ok(p) = real_path.as_ref().strip_prefix(target) {
|
||||||
let mount_path = Path::new(&name);
|
let mount_path = Path::new(&name);
|
||||||
return if p.components().count() == 0 {
|
return if p.components().count() == 0 {
|
||||||
Ok(mount_path.to_path_buf())
|
Ok(mount_path.to_path_buf())
|
||||||
|
@ -65,10 +65,10 @@ impl VFS {
|
||||||
bail!("Real path has no match in VFS")
|
bail!("Real path has no match in VFS")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn virtual_to_real(&self, virtual_path: &Path) -> Result<PathBuf> {
|
pub fn virtual_to_real<P: AsRef<Path>>(&self, virtual_path: P) -> Result<PathBuf> {
|
||||||
for (name, target) in &self.mount_points {
|
for (name, target) in &self.mount_points {
|
||||||
let mount_path = Path::new(&name);
|
let mount_path = Path::new(&name);
|
||||||
if let Ok(p) = virtual_path.strip_prefix(mount_path) {
|
if let Ok(p) = virtual_path.as_ref().strip_prefix(mount_path) {
|
||||||
return if p.components().count() == 0 {
|
return if p.components().count() == 0 {
|
||||||
Ok(target.clone())
|
Ok(target.clone())
|
||||||
} else {
|
} else {
|
||||||
|
|
2
web
2
web
|
@ -1 +1 @@
|
||||||
Subproject commit 484a1099806c0805964d19006c6128c75384bb30
|
Subproject commit 8cc2cbaee0c5ede96a6c0b66ce1c2c36a94473bc
|
Loading…
Add table
Reference in a new issue