Merge branch 'rocket'

This commit is contained in:
Antoine Gersant 2019-02-27 22:13:10 -08:00
commit 94602317ad
24 changed files with 2145 additions and 1909 deletions

1
.gitmodules vendored
View file

@ -1,3 +1,4 @@
[submodule "web"]
path = web
url = https://github.com/agersant/polaris-web.git
branch = .

View file

@ -6,5 +6,5 @@ rust:
matrix:
allow_failures:
- rust: beta
- rust: nightly
- rust: stable
- rust: beta

1739
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,8 @@
[package]
name = "polaris"
version = "0.8.0"
version = "0.9.0"
authors = ["Antoine Gersant <antoine.gersant@lesforges.org>"]
edition = "2018"
[features]
ui = []
@ -9,37 +10,34 @@ ui = []
[dependencies]
ape = "0.2.0"
app_dirs = "1.1.1"
base64 = "0.9.3"
base64 = "0.10.0"
diesel = { version = "1.3.3", features = ["sqlite"] }
diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
error-chain = "0.12.0"
getopts = "0.2.15"
hyper = "0.12.11"
id3 = "0.2.3"
image = "0.20.0"
iron = "0.6.0"
rustfm-scrobble = { git = "https://github.com/agersant/rustfm-scrobble" }
lewton = "0.9.1"
log = "0.4.5"
metaflac = "0.1.8"
mount = "0.4.0"
mp3-duration = "0.1.0"
params = { git = "https://github.com/agersant/params" }
rand = "0.5.5"
regex = "1.0.5"
ring = "0.13.2"
ring = "0.13.5"
reqwest = "0.9.2"
router = "0.6.0"
rocket = "0.4.0"
rust-crypto = "0.2.36"
secure-session = "0.3.1"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
staticfile = "0.5.0"
simplelog = "0.5.2"
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]
version = "0.14.0"

View file

@ -55,7 +55,7 @@ environment:
# or test failure in the matching channels/targets from failing the entire build.
matrix:
allow_failures:
- channel: nightly
- channel: stable
- channel: beta
## Install Script ##

View file

@ -1,6 +1,6 @@
<?xml version='1.0' encoding='windows-1252'?>
<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' />

1075
src/api.rs

File diff suppressed because it is too large Load diff

564
src/api_tests.rs Normal file
View 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);
}
}

View file

@ -2,19 +2,18 @@ use core::ops::Deref;
use diesel;
use diesel::prelude::*;
use regex::Regex;
use serde_json;
use std::fs;
use std::io::Read;
use std::path;
use toml;
use db::ConnectionSource;
use db::DB;
use db::{ddns_config, misc_settings, mount_points, users};
use ddns::DDNSConfig;
use errors::*;
use user::*;
use vfs::MountPoint;
use crate::db::ConnectionSource;
use crate::db::DB;
use crate::db::{ddns_config, misc_settings, mount_points, users};
use crate::ddns::DDNSConfig;
use crate::errors::*;
use crate::user::*;
use crate::vfs::MountPoint;
#[derive(Debug, Queryable)]
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> {
info!("Config file path: {}", path.to_string_lossy());
let mut config_file = fs::File::open(path)?;
@ -100,7 +93,8 @@ where
index_album_art_pattern,
index_sleep_duration_seconds,
prefix_url,
)).get_result(connection.deref())?;
))
.get_result(connection.deref())?;
config.album_art_pattern = Some(art_pattern);
config.reindex_every_n_seconds = Some(sleep_duration);
config.prefix_url = if url != "" { Some(url) } else { None };
@ -124,7 +118,8 @@ where
name,
password: "".to_owned(),
admin: admin != 0,
}).collect::<_>(),
})
.collect::<_>(),
);
let ydns = ddns_config
@ -194,7 +189,8 @@ where
.iter()
.find(|old_name| *old_name == &u.name)
.is_none()
}).collect::<_>();
})
.collect::<_>();
for config_user in &insert_users {
let new_user = User::new(&config_user.name, &config_user.password);
diesel::insert_into(users::table)
@ -242,7 +238,8 @@ where
host.eq(ydns.host.clone()),
username.eq(ydns.username.clone()),
password.eq(ydns.password.clone()),
)).execute(connection.deref())?;
))
.execute(connection.deref())?;
}
if let Some(ref prefix_url) = new_config.prefix_url {

View file

@ -6,7 +6,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, MutexGuard};
use errors::*;
use crate::errors::*;
mod schema;
@ -85,7 +85,7 @@ impl ConnectionSource for DB {
}
pub fn _get_test_db(name: &str) -> DB {
use config;
use crate::config;
let config_path = Path::new("test/config.toml");
let config = config::parse_toml_file(&config_path).unwrap();

View file

@ -5,9 +5,9 @@ use std::io;
use std::thread;
use std::time;
use db::ddns_config;
use db::{ConnectionSource, DB};
use errors;
use crate::db::ddns_config;
use crate::db::{ConnectionSource, DB};
use crate::errors;
#[derive(Clone, Debug, Deserialize, Insertable, PartialEq, Queryable, Serialize)]
#[table_name = "ddns_config"]

View file

@ -3,14 +3,12 @@ use core;
use diesel;
use diesel_migrations;
use getopts;
use hyper;
use id3;
use image;
use iron::status::Status;
use iron::IronError;
use lewton;
use metaflac;
use regex;
use rocket;
use rustfm_scrobble;
use serde_json;
use std;
@ -25,7 +23,6 @@ error_chain! {
Encoding(core::str::Utf8Error);
Flac(metaflac::Error);
GetOpts(getopts::Fail);
Hyper(hyper::Error);
Id3(id3::Error);
Image(image::ImageError);
Io(std::io::Error);
@ -33,51 +30,27 @@ error_chain! {
Time(std::time::SystemTimeError);
Toml(toml::de::Error);
Regex(regex::Error);
RocketConfig(rocket::config::ConfigError);
Scrobbler(rustfm_scrobble::ScrobblerError);
Vorbis(lewton::VorbisError);
}
errors {
DaemonError {}
AuthenticationRequired {}
AdminPrivilegeRequired {}
MissingConfig {}
MissingPreferences {}
MissingUsername {}
MissingPassword {}
MissingPlaylist {}
IncorrectCredentials {}
CannotServeDirectory {}
UnsupportedFileType {}
FileNotFound {}
MissingIndexVersion {}
MissingPlaylistName {}
EncodingError {}
MissingLastFMCredentials {}
LastFMAuthError {}
LastFMDeserializationError {}
MissingDesiredResponse {}
}
}
impl From<Error> for IronError {
fn from(err: Error) -> IronError {
match err {
e @ Error(ErrorKind::AuthenticationRequired, _) => {
IronError::new(e, Status::Unauthorized)
}
e @ Error(ErrorKind::AdminPrivilegeRequired, _) => IronError::new(e, Status::Forbidden),
e @ Error(ErrorKind::MissingUsername, _) => IronError::new(e, Status::BadRequest),
e @ Error(ErrorKind::MissingPassword, _) => IronError::new(e, Status::BadRequest),
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),
}
impl<'r> rocket::response::Responder<'r> for Error {
fn respond_to(self, _: &rocket::request::Request) -> rocket::response::Result<'r> {
let mut build = rocket::response::Response::build();
build
.status(match self.0 {
ErrorKind::IncorrectCredentials => rocket::http::Status::Unauthorized,
_ => rocket::http::Status::InternalServerError,
})
.ok()
}
}

View file

@ -14,14 +14,14 @@ use std::sync::{Arc, Mutex};
use std::thread;
use std::time;
use config::MiscSettings;
use crate::config::MiscSettings;
#[cfg(test)]
use db;
use db::ConnectionSource;
use db::{directories, misc_settings, songs};
use errors;
use metadata;
use vfs::{VFSSource, VFS};
use crate::db;
use crate::db::{directories, misc_settings, songs};
use crate::db::{ConnectionSource, DB};
use crate::errors;
use crate::metadata;
use crate::vfs::{VFSSource, VFS};
const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // 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"
);
pub enum Command {
enum Command {
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"]
pub struct Song {
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
id: i32,
pub path: String,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub parent: String,
pub track_number: Option<i32>,
pub disc_number: Option<i32>,
@ -55,12 +110,12 @@ pub struct Song {
pub duration: Option<i32>,
}
#[derive(Debug, Queryable, Serialize)]
#[derive(Debug, PartialEq, Queryable, Serialize, Deserialize)]
pub struct Directory {
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
id: i32,
pub path: String,
#[serde(skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub parent: Option<String>,
pub artist: Option<String>,
pub year: Option<i32>,
@ -69,7 +124,7 @@ pub struct Directory {
pub date_added: i32,
}
#[derive(Debug, Serialize)]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub enum CollectionFile {
Directory(Directory),
Song(Song),
@ -318,7 +373,8 @@ where
.filter(|ref song_path| {
let path = Path::new(&song_path);
!path.exists() || vfs.real_to_virtual(path).is_err()
}).collect::<Vec<_>>();
})
.collect::<Vec<_>>();
{
let connection = db.get_connection();
@ -343,7 +399,8 @@ where
.filter(|ref directory_path| {
let path = Path::new(&directory_path);
!path.exists() || vfs.real_to_virtual(path).is_err()
}).collect::<Vec<_>>();
})
.collect::<Vec<_>>();
{
let connection = db.get_connection();
@ -396,24 +453,21 @@ where
Ok(())
}
pub fn update_loop<T>(db: &T, command_buffer: &Receiver<Command>)
fn update_loop<T>(db: &T, command_buffer: &CommandReceiver)
where
T: ConnectionSource + VFSSource,
{
loop {
// Wait for a command
if let Err(e) = command_buffer.recv() {
error!("Error while waiting on index command buffer: {}", e);
if command_buffer.receiver.recv().is_err() {
return;
}
// Flush the buffer to ignore spammy requests
loop {
match command_buffer.try_recv() {
Err(TryRecvError::Disconnected) => {
error!("Error while flushing index command buffer");
return;
}
match command_buffer.receiver.try_recv() {
Err(TryRecvError::Disconnected) => return,
Ok(Command::EXIT) => return,
Err(TryRecvError::Empty) => break,
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
T: ConnectionSource,
{
loop {
{
let command_buffer = command_buffer.lock().unwrap();
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);
return;
}
@ -484,15 +537,16 @@ fn virtualize_directory(vfs: &VFS, mut directory: Directory) -> Option<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
T: ConnectionSource + VFSSource,
P: AsRef<Path>,
{
let mut output = Vec::new();
let vfs = db.get_vfs()?;
let connection = db.get_connection();
if virtual_path.components().count() == 0 {
if virtual_path.as_ref().components().count() == 0 {
// Browse top-level
let real_directories: Vec<Directory> = directories::table
.filter(directories::parent.is_null())
@ -528,15 +582,16 @@ where
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
T: ConnectionSource + VFSSource,
P: AsRef<Path>,
{
use self::songs::dsl::*;
let vfs = db.get_vfs()?;
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 like_path = real_path.as_path().to_string_lossy().into_owned() + "%";
songs
@ -623,7 +678,8 @@ where
.or(album.like(&like_test))
.or(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())?;
let virtual_songs = real_songs

View file

@ -1,11 +1,11 @@
use rustfm_scrobble::{Scrobble, Scrobbler};
use std::path::Path;
use db::ConnectionSource;
use errors;
use index;
use user;
use vfs::VFSSource;
use crate::db::ConnectionSource;
use crate::errors;
use crate::index;
use crate::user;
use crate::vfs::VFSSource;
const LASTFM_API_KEY: &str = "02b96c939a2b451c31dfd67add1f696e";
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 auth_response = scrobbler.authenticate_with_token(token.to_string())?;
user::lastfm_link(
db,
username,
&auth_response.name,
&auth_response.key,
)
user::lastfm_link(db, username, &auth_response.name, &auth_response.key)
}
pub fn unlink<T>(db: &T, username: &str) -> Result<(), errors::Error>

View file

@ -1,4 +1,5 @@
#![recursion_limit = "256"]
#![feature(proc_macro_hygiene, decl_macro)]
#![allow(proc_macro_derive_resolution_fallback)]
extern crate ape;
@ -13,30 +14,24 @@ extern crate diesel_migrations;
#[macro_use]
extern crate error_chain;
extern crate getopts;
extern crate hyper;
extern crate id3;
extern crate image;
extern crate iron;
extern crate lewton;
extern crate metaflac;
extern crate mount;
extern crate mp3_duration;
extern crate params;
extern crate rand;
extern crate regex;
extern crate reqwest;
extern crate ring;
extern crate router;
#[macro_use]
extern crate rocket;
extern crate rocket_contrib;
extern crate rustfm_scrobble;
extern crate secure_session;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate staticfile;
extern crate toml;
extern crate typemap;
extern crate url;
#[macro_use]
extern crate log;
extern crate simplelog;
@ -57,17 +52,15 @@ use std::io::prelude::*;
use unix_daemonize::{daemonize_redirect, ChdirMode};
use core::ops::Deref;
use errors::*;
use crate::errors::*;
use getopts::Options;
use iron::prelude::*;
use mount::Mount;
use simplelog::{Level, LevelFilter, SimpleLogger, TermLogger};
use staticfile::Static;
use std::path::Path;
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
mod api;
#[cfg(test)]
mod api_tests;
mod config;
mod db;
mod ddns;
@ -77,6 +70,7 @@ mod lastfm;
mod metadata;
mod playlist;
mod serve;
mod server;
mod thumbnails;
mod ui;
mod user;
@ -204,6 +198,7 @@ fn run() -> Result<()> {
let db = Arc::new(db::DB::new(&db_path)?);
// Parse config
info!("Parsing configuration");
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());
if let Some(path) = config_file_path {
@ -213,30 +208,22 @@ fn run() -> Result<()> {
let config = config::read(db.deref())?;
// Init index
let (index_sender, index_receiver) = channel();
let index_sender = Arc::new(Mutex::new(index_sender));
let db_ref = db.clone();
std::thread::spawn(move || {
let db = db_ref.deref();
index::update_loop(db, &index_receiver);
});
info!("Initializing index");
let command_sender = index::init(db.clone());
// Trigger auto-indexing
let db_ref = db.clone();
let sender_ref = index_sender.clone();
let db_auto_index = db.clone();
let command_sender_auto_index = command_sender.clone();
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 api_url = format!("{}/api", &prefix_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 mut default_web_dir = utils::get_data_root()?;
default_web_dir.push("web");
@ -246,8 +233,8 @@ fn run() -> Result<()> {
info!("Static files location is {}", web_dir_path.display());
let static_url = format!("/{}", &prefix_url);
info!("Mounting static files on {}", static_url);
mount.mount(&static_url, Static::new(web_dir_path));
// Start server
info!("Starting up server");
let port: u16 = matches
.opt_str("p")
@ -255,24 +242,27 @@ fn run() -> Result<()> {
.parse()
.or(Err("invalid port number"))?;
let mut server = match Iron::new(mount).http(("0.0.0.0", port)) {
Ok(s) => s,
Err(e) => bail!("Error starting up server: {}", e),
};
let server = server::get_server(
port,
&static_url,
&api_url,
&web_dir_path,
db.clone(),
command_sender,
)?;
std::thread::spawn(move || {
server.launch();
});
// Start DDNS updates
let db_ref = db.clone();
let db_ddns = db.clone();
std::thread::spawn(move || {
ddns::run(db_ref.deref());
ddns::run(db_ddns.deref());
});
// Run UI
ui::run();
info!("Shutting down server");
if let Err(e) = server.close() {
bail!("Error shutting down server: {}", e);
}
Ok(())
}

View file

@ -7,9 +7,9 @@ use regex::Regex;
use std::fs;
use std::path::Path;
use errors::*;
use utils;
use utils::AudioFormat;
use crate::errors::*;
use crate::utils;
use crate::utils::AudioFormat;
#[derive(Debug, Clone, PartialEq)]
pub struct SongTags {

View file

@ -7,12 +7,12 @@ use diesel::BelongingToDsl;
use std::path::Path;
#[cfg(test)]
use db;
use db::ConnectionSource;
use db::{playlist_songs, playlists, users};
use errors::*;
use index::{self, Song};
use vfs::VFSSource;
use crate::db;
use crate::db::ConnectionSource;
use crate::db::{playlist_songs, playlists, users};
use crate::errors::*;
use crate::index::{self, Song};
use crate::vfs::VFSSource;
#[derive(Insertable)]
#[table_name = "playlists"]
@ -262,7 +262,7 @@ fn test_delete_playlist() {
#[test]
fn test_fill_playlist() {
use index;
use crate::index;
let db = db::_get_test_db("fill_playlist.sqlite");
index::update(&db).unwrap();

View file

@ -1,53 +1,15 @@
use iron::headers::{
AcceptRanges, ByteRangeSpec, ContentLength, ContentRange, ContentRangeSpec, Range, RangeUnit,
};
use iron::modifier::Modifier;
use iron::modifiers::Header;
use iron::prelude::*;
use iron::response::WriteBody;
use iron::status::{self, Status};
use rocket;
use rocket::http::hyper::header::*;
use rocket::http::Status;
use rocket::Response;
use rocket::response::{self, Responder};
use std::cmp;
use std::fs::{self, File};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
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)),
},
}
}
use std::convert::From;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::str::FromStr;
#[derive(Debug)]
pub enum PartialFileRange {
AllFrom(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 {
fn from(v: Vec<ByteRangeSpec>) -> PartialFileRange {
match v.into_iter().next() {
@ -78,90 +35,119 @@ impl From<Vec<ByteRangeSpec>> for PartialFileRange {
}
}
impl PartialFile {
pub fn new<Range>(file: File, range: Range) -> PartialFile
where
Range: Into<PartialFileRange>,
{
let range = range.into();
PartialFile { file, range }
pub struct RangeResponder<R> {
original: R,
}
impl<'r, R: Responder<'r>> RangeResponder<R> {
pub fn new(original: R) -> RangeResponder<R> {
RangeResponder { original }
}
pub fn from_path<P: AsRef<Path>, Range>(path: P, range: Range) -> Result<PartialFile, io::Error>
where
Range: Into<PartialFileRange>,
{
let file = File::open(path.as_ref())?;
Ok(Self::new(file, range))
fn ignore_range(self, request: &rocket::request::Request, file_length: Option<u64>) -> response::Result<'r> {
let mut response = self.original.respond_to(request)?;
if let Some(content_length) = file_length {
response.set_header(ContentLength(content_length));
}
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 modify(self, res: &mut Response) {
use self::PartialFileRange::*;
let metadata: Option<_> = self.file.metadata().ok();
fn truncate_range(range: &PartialFileRange, file_length: &Option<u64>) -> Option<(u64, u64)> {
use self::PartialFileRange::*;
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 range: Option<(u64, u64)> = match (self.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,
let range_header = request.headers().get_one("Range");
let range_header = match range_header {
None => return self.ignore_range(request, file_length),
Some(h) => h,
};
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 {
range: Some(range),
range: range,
instance_length: file_length,
});
let content_len = range.1 - range.0 + 1;
res.headers.set(ContentLength(content_len));
res.headers.set(content_range);
let partial_content = PartialContentBody {
file: self.file,
offset: range.0,
len: content_len,
};
res.status = Some(Status::PartialContent);
res.body = Some(Box::new(partial_content));
let content_len = to - from + 1;
match self.original.seek(SeekFrom::Start(from)) {
Ok(_) => (),
Err(_) => return Err(rocket::http::Status::InternalServerError),
}
let partial_original = self.original.take(content_len);
let response = Response::build()
.status(Status::PartialContent)
.header(ContentLength(content_len))
.header(content_range)
.streamed_body(partial_original)
.finalize();
Ok(response)
} else {
if let Some(file_length) = file_length {
res.headers.set(ContentRange(ContentRangeSpec::Bytes {
range: None,
instance_length: Some(file_length),
}));
};
res.status = Some(Status::RangeNotSatisfiable);
warn!("Rejecting unsatisfiable range header {:?}, file length is {:?}", &partial_file_range, &file_length);
self.reject_range(file_length)
}
}
}
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
View 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()))
}

View file

@ -11,8 +11,8 @@ use std::fs::{DirBuilder, File};
use std::hash::{Hash, Hasher};
use std::path::*;
use errors::*;
use utils;
use crate::errors::*;
use crate::utils;
const THUMBNAILS_PATH: &str = "thumbnails";

View file

@ -4,9 +4,9 @@ use diesel::prelude::*;
use rand;
use ring::{digest, pbkdf2};
use db::users;
use db::ConnectionSource;
use errors::*;
use crate::db::users;
use crate::db::ConnectionSource;
use crate::errors::*;
#[derive(Debug, Insertable, Queryable)]
#[table_name = "users"]
@ -58,14 +58,15 @@ fn verify_password(
password_salt,
attempted_password.as_bytes(),
password_hash,
).is_ok()
)
.is_ok()
}
pub fn auth<T>(db: &T, username: &str, password: &str) -> Result<bool>
where
T: ConnectionSource,
{
use db::users::dsl::*;
use crate::db::users::dsl::*;
let connection = db.get_connection();
match users
.select((password_hash, password_salt))
@ -82,7 +83,7 @@ pub fn count<T>(db: &T) -> Result<i64>
where
T: ConnectionSource,
{
use db::users::dsl::*;
use crate::db::users::dsl::*;
let connection = db.get_connection();
let count = users.count().get_result(connection.deref())?;
Ok(count)
@ -92,7 +93,7 @@ pub fn is_admin<T>(db: &T, username: &str) -> Result<bool>
where
T: ConnectionSource,
{
use db::users::dsl::*;
use crate::db::users::dsl::*;
let connection = db.get_connection();
let is_admin: i32 = users
.filter(name.eq(username))
@ -105,13 +106,14 @@ pub fn lastfm_link<T>(db: &T, username: &str, lastfm_login: &str, session_key: &
where
T: ConnectionSource,
{
use db::users::dsl::*;
use crate::db::users::dsl::*;
let connection = db.get_connection();
diesel::update(users.filter(name.eq(username)))
.set((
lastfm_username.eq(lastfm_login),
lastfm_session_key.eq(session_key),
)).execute(connection.deref())?;
))
.execute(connection.deref())?;
Ok(())
}
@ -119,7 +121,7 @@ pub fn get_lastfm_session_key<T>(db: &T, username: &str) -> Result<String>
where
T: ConnectionSource,
{
use db::users::dsl::*;
use crate::db::users::dsl::*;
let connection = db.get_connection();
let token = users
.filter(name.eq(username))
@ -135,7 +137,7 @@ pub fn lastfm_unlink<T>(db: &T, username: &str) -> Result<()>
where
T: ConnectionSource,
{
use db::users::dsl::*;
use crate::db::users::dsl::*;
let connection = db.get_connection();
diesel::update(users.filter(name.eq(username)))
.set((lastfm_session_key.eq(""), lastfm_username.eq("")))

View file

@ -2,7 +2,7 @@ use app_dirs::{app_root, AppDataType, AppInfo};
use std::fs;
use std::path::{Path, PathBuf};
use errors::*;
use crate::errors::*;
#[cfg(not(target_os = "linux"))]
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 {
let extension = match path.extension() {
Some(e) => e,

View file

@ -4,9 +4,9 @@ use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use db::mount_points;
use db::{ConnectionSource, DB};
use errors::*;
use crate::db::mount_points;
use crate::db::{ConnectionSource, DB};
use crate::errors::*;
pub trait VFSSource {
fn get_vfs(&self) -> Result<VFS>;
@ -51,9 +51,9 @@ impl VFS {
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 {
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);
return if p.components().count() == 0 {
Ok(mount_path.to_path_buf())
@ -65,10 +65,10 @@ impl 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 {
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 {
Ok(target.clone())
} else {

2
web

@ -1 +1 @@
Subproject commit 484a1099806c0805964d19006c6128c75384bb30
Subproject commit 8cc2cbaee0c5ede96a6c0b66ce1c2c36a94473bc