diff --git a/.sqlx/query-fbf4d83d9836cf85d01059e679bcbf7dc463eea0579afacc0267f0f8c33640d3.json b/.sqlx/query-d08992cf2c132fedbed21b94d545e154fa2a7a2a2bf79fd033341d1bb5a6c0f2.json similarity index 69% rename from .sqlx/query-fbf4d83d9836cf85d01059e679bcbf7dc463eea0579afacc0267f0f8c33640d3.json rename to .sqlx/query-d08992cf2c132fedbed21b94d545e154fa2a7a2a2bf79fd033341d1bb5a6c0f2.json index 73ed5af..f47d543 100644 --- a/.sqlx/query-fbf4d83d9836cf85d01059e679bcbf7dc463eea0579afacc0267f0f8c33640d3.json +++ b/.sqlx/query-d08992cf2c132fedbed21b94d545e154fa2a7a2a2bf79fd033341d1bb5a6c0f2.json @@ -1,31 +1,33 @@ { - "db_name": "SQLite", - "query": "SELECT * FROM users WHERE name = ?", + "db_name": "PostgreSQL", + "query": "SELECT * FROM users WHERE name = $1", "describe": { "columns": [ { - "name": "id", "ordinal": 0, - "type_info": "Int64" + "name": "id", + "type_info": "Int8" }, { - "name": "name", "ordinal": 1, + "name": "name", "type_info": "Text" }, { - "name": "password", "ordinal": 2, + "name": "password", "type_info": "Text" }, { - "name": "is_admin", "ordinal": 3, + "name": "is_admin", "type_info": "Bool" } ], "parameters": { - "Right": 1 + "Left": [ + "Text" + ] }, "nullable": [ false, @@ -34,5 +36,5 @@ false ] }, - "hash": "fbf4d83d9836cf85d01059e679bcbf7dc463eea0579afacc0267f0f8c33640d3" + "hash": "d08992cf2c132fedbed21b94d545e154fa2a7a2a2bf79fd033341d1bb5a6c0f2" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 9c88f1c..6048321 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,4 @@ { "sqltools.useNodeRuntime": true, - "sqltools.connections": [ - { - "previewLimit": 50, - "driver": "SQLite", - "name": "rave-users", - "database": "${workspaceFolder:rave}/users.db" - } - ] + "sqltools.connections": [] } \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 6d4bf86..100b6ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,12 @@ poem = { version = "1.3.58", features = [ quick-xml = { version = "0.30.0", features = ["serialize"] } serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" -sqlx = { version = "0.7.2", features = ["time", "sqlite", "runtime-tokio"] } -time = { version = "0.3.29", features = ["serde-human-readable", "macros", "parsing"] } +sqlx = { version = "0.7.2", features = ["time", "postgres", "runtime-tokio"] } +time = { version = "0.3.29", features = [ + "serde-human-readable", + "macros", + "parsing", +] } tokio = { version = "1.32.0", features = ["full"] } tracing = { version = "0.1.37", features = ["async-await"] } tracing-subscriber = { version = "0.3.17", features = [ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..601fd5c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3' + +services: + dev-db: + image: postgres:15 + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 12345:5432 + volumes: + - /tmp/rave-dev-db:/var/lib/postgresql/data diff --git a/migrations/0001_create-user.sql b/migrations/0001_create-user.sql index 581d7c5..7768f6f 100644 --- a/migrations/0001_create-user.sql +++ b/migrations/0001_create-user.sql @@ -1,9 +1,9 @@ -- Add migration script here CREATE TABLE users ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, + id BIGSERIAL NOT NULL PRIMARY KEY, + name TEXT NOT NULL UNIQUE, password TEXT NOT NULL, is_admin BOOLEAN NOT NULL DEFAULT FALSE ); -INSERT INTO users (name, password, is_admin) -VALUES ('admin', 'admin', TRUE); \ No newline at end of file +INSERT INTO users (id, name, password, is_admin) +VALUES (0, 'admin', 'admin', TRUE); \ No newline at end of file diff --git a/migrations/0002_create-albums.sql b/migrations/0002_create-albums.sql new file mode 100644 index 0000000..b91f8ae --- /dev/null +++ b/migrations/0002_create-albums.sql @@ -0,0 +1,15 @@ +-- Add migration script here +CREATE TABLE IF NOT EXISTS album ( + id BIGSERIAL NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + artist TEXT, + artist_id INTEGER, + cover_art TEXT, + song_count INTEGER, + duration INTEGER, + play_count INTEGER, + created timestamptz DEFAULT current_timestamp, + starred timestamptz DEFAULT current_timestamp, + year INTEGER, + genre TEXT +); \ No newline at end of file diff --git a/migrations/0003_create-tracks.sql b/migrations/0003_create-tracks.sql new file mode 100644 index 0000000..2b1bca5 --- /dev/null +++ b/migrations/0003_create-tracks.sql @@ -0,0 +1,21 @@ +-- Add migration script here +CREATE TABLE IF NOT EXISTS tracks ( + id BIGSERIAL NOT NULL PRIMARY KEY, + parent INTEGER NOT NULL REFERENCES album(id), + is_dir BOOLEAN NOT NULL DEFAULT FALSE, + title TEXT NOT NULL, + album TEXT, + artist TEXT, + track INTEGER, + year INTEGER, + genre TEXT, + cover_art TEXT, + duration INTEGER, + path TEXT, + play_count INTEGER, + created timestamptz DEFAULT current_timestamp, + starred timestamptz, + album_id TEXT, + artist_id TEXT, + disc_number INTEGER +) \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 885d2c2..0ca9a96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,17 @@ #![warn(clippy::pedantic, clippy::nursery)] -#![deny(clippy::unwrap_used, clippy::panic)] +#![deny(clippy::unwrap_used)] #![allow(clippy::module_name_repetitions, clippy::too_many_lines)] use std::time::Duration; use color_eyre::Result; use poem::{ - get, listener::TcpListener, middleware, web::{CompressionAlgo, CompressionLevel}, Endpoint, EndpointExt, Route, }; -use rest::build; -use sqlx::SqlitePool; +use sqlx::PgPool; use tracing::info; use tracing_subscriber::{fmt, EnvFilter}; @@ -21,6 +19,7 @@ mod authentication; mod random_types; mod rest; mod subsonic; +mod ui; mod user; mod utils; @@ -37,7 +36,7 @@ async fn main() -> Result<()> { sqlx::migrate!().run(&pool).await?; - let route = route.with(middleware::AddData::new(pool)); + let route = route.with(utils::middleware::DbConnectionMiddleware::new(pool)); let server = create_server(); @@ -52,18 +51,18 @@ async fn main() -> Result<()> { Ok(()) } -async fn create_pool() -> SqlitePool { +async fn create_pool() -> PgPool { let url = std::env::var("DATABASE_URL").expect("DATABASE_URL not set"); - SqlitePool::connect(&url) + PgPool::connect(&url) .await .expect("Failed to connect to database") } fn create_route() -> Box> { Route::new() - .at("/", get(hello_world)) - .nest("/rest", build()) + .nest("/", ui::build()) + .nest("/rest", rest::build()) .with(middleware::CatchPanic::new()) .with( middleware::Compression::new() @@ -108,11 +107,6 @@ fn install_tracing() -> Result<()> { Ok(()) } -#[poem::handler] -const fn hello_world() -> &'static str { - "Hello, world!" -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/rest/get_album.rs b/src/rest/get_album.rs index 3385c49..7c80765 100644 --- a/src/rest/get_album.rs +++ b/src/rest/get_album.rs @@ -1,20 +1,19 @@ -use poem::web::{Data, Query}; -use serde::Deserialize; -use sqlx::SqlitePool; - use crate::{ authentication::Authentication, subsonic::{AlbumId3, Child, Error, MediaType, SubsonicResponse}, - utils, + utils::{self, middleware::DbConn}, }; +use poem::web::{Data, Query}; +use serde::Deserialize; + #[poem::handler] pub async fn get_album( - Data(pool): Data<&SqlitePool>, + Data(conn): Data<&DbConn>, auth: Authentication, Query(params): Query, ) -> SubsonicResponse { - let u = utils::verify_user(pool, auth).await; + let u = utils::verify_user(conn.clone(), auth).await; match u { Ok(_) => {} diff --git a/src/rest/get_album_list.rs b/src/rest/get_album_list.rs index 2ab6568..c5f24e6 100644 --- a/src/rest/get_album_list.rs +++ b/src/rest/get_album_list.rs @@ -1,21 +1,20 @@ use poem::web::{Data, Query}; use serde::Deserialize; -use sqlx::SqlitePool; use crate::{ authentication::Authentication, random_types::SortType, subsonic::{Child, Error, SubsonicResponse}, - utils, + utils::{self, middleware::DbConn}, }; #[poem::handler] pub async fn get_album_list( - Data(pool): Data<&SqlitePool>, + Data(conn): Data<&DbConn>, auth: Authentication, Query(params): Query, ) -> SubsonicResponse { - let u = utils::verify_user(pool, auth).await; + let u = utils::verify_user(conn.clone(), auth).await; match u { Ok(_) => {} diff --git a/src/rest/get_album_list2.rs b/src/rest/get_album_list2.rs index f2e7e92..cb504ba 100644 --- a/src/rest/get_album_list2.rs +++ b/src/rest/get_album_list2.rs @@ -1,20 +1,19 @@ use poem::web::{Data, Query}; -use sqlx::SqlitePool; use crate::{ authentication::Authentication, rest::get_album_list::GetAlbumListParams, subsonic::{AlbumId3, SubsonicResponse}, - utils, + utils::{self, middleware::DbConn}, }; #[poem::handler] pub async fn get_album_list2( - Data(pool): Data<&SqlitePool>, + Data(conn): Data<&DbConn>, auth: Authentication, Query(params): Query, ) -> SubsonicResponse { - let u = utils::verify_user(pool, auth).await; + let u = utils::verify_user(conn.clone(), auth).await; match u { Ok(_) => {} diff --git a/src/rest/get_license.rs b/src/rest/get_license.rs index cc46eea..4f9cfa9 100644 --- a/src/rest/get_license.rs +++ b/src/rest/get_license.rs @@ -1,11 +1,14 @@ use poem::web::Data; -use sqlx::SqlitePool; -use crate::{authentication::Authentication, subsonic::SubsonicResponse, utils}; +use crate::{ + authentication::Authentication, + subsonic::SubsonicResponse, + utils::{self, middleware::DbConn}, +}; #[poem::handler] -pub async fn get_license(Data(pool): Data<&SqlitePool>, auth: Authentication) -> SubsonicResponse { - let u = utils::verify_user(pool, auth).await; +pub async fn get_license(Data(conn): Data<&DbConn>, auth: Authentication) -> SubsonicResponse { + let u = utils::verify_user(conn.clone(), auth).await; match u { Ok(_) => {} diff --git a/src/rest/get_music_folders.rs b/src/rest/get_music_folders.rs index 152a3b5..dc8352c 100644 --- a/src/rest/get_music_folders.rs +++ b/src/rest/get_music_folders.rs @@ -1,18 +1,17 @@ use poem::web::Data; -use sqlx::SqlitePool; use crate::{ authentication::Authentication, subsonic::{MusicFolder, SubsonicResponse}, - utils, + utils::{self, middleware::DbConn}, }; #[poem::handler] pub async fn get_music_folders( - Data(pool): Data<&SqlitePool>, + Data(conn): Data<&DbConn>, auth: Authentication, ) -> SubsonicResponse { - let u = utils::verify_user(pool, auth).await; + let u = utils::verify_user(conn.clone(), auth).await; match u { Ok(_) => {} diff --git a/src/rest/ping.rs b/src/rest/ping.rs index cf816a6..c0031b4 100644 --- a/src/rest/ping.rs +++ b/src/rest/ping.rs @@ -1,11 +1,14 @@ use poem::web::Data; -use sqlx::SqlitePool; -use crate::{authentication::Authentication, subsonic::SubsonicResponse, utils}; +use crate::{ + authentication::Authentication, + subsonic::SubsonicResponse, + utils::{self, middleware::DbConn}, +}; #[poem::handler] -pub async fn ping(Data(pool): Data<&SqlitePool>, auth: Authentication) -> SubsonicResponse { - let u = utils::verify_user(pool, auth).await; +pub async fn ping(Data(conn): Data<&DbConn>, auth: Authentication) -> SubsonicResponse { + let u = utils::verify_user(conn.clone(), auth).await; match u { Ok(_) => SubsonicResponse::new_empty(), diff --git a/src/rest/stream.rs b/src/rest/stream.rs index edc765c..800182e 100644 --- a/src/rest/stream.rs +++ b/src/rest/stream.rs @@ -4,19 +4,21 @@ use poem::{ IntoResponse, Response, }; use serde::Deserialize; -use sqlx::SqlitePool; -use crate::{authentication::Authentication, utils}; +use crate::{ + authentication::Authentication, + utils::{self, middleware::DbConn}, +}; const SONG: &[u8] = include_bytes!("../../../data.mp3"); #[poem::handler] pub async fn stream( - Data(pool): Data<&SqlitePool>, + Data(conn): Data<&DbConn>, auth: Authentication, Query(_params): Query, ) -> Response { - let u = utils::verify_user(pool, auth).await; + let u = utils::verify_user(conn.clone(), auth).await; match u { Ok(_) => {} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..70337ac --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,5 @@ +use poem::{Endpoint, EndpointExt, Route}; + +pub fn build() -> Box> { + Route::new().boxed() +} diff --git a/src/user.rs b/src/user.rs index 71b1706..be73f9a 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,6 +1,7 @@ use color_eyre::Result; use serde::{Deserialize, Serialize}; -use sqlx::SqlitePool; + +use crate::utils::middleware::DbConn; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct User { @@ -20,10 +21,10 @@ impl User { } } -pub async fn get_user(pool: &SqlitePool, name: &str) -> Result> { - Ok( - sqlx::query_as!(User, "SELECT * FROM users WHERE name = ?", name) - .fetch_optional(pool) - .await?, - ) +pub async fn get_user(conn: DbConn, name: &str) -> Result> { + let user = sqlx::query_as!(User, "SELECT * FROM users WHERE name = $1", name) + .fetch_optional(&*conn) + .await?; + + Ok(user) } diff --git a/src/utils.rs b/src/utils.rs index e0e07f5..828d6be 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,3 @@ -use sqlx::SqlitePool; use tracing::error; use crate::{ @@ -7,11 +6,10 @@ use crate::{ user::{get_user, User}, }; -pub async fn verify_user( - pool: &SqlitePool, - auth: Authentication, -) -> Result { - let user = get_user(pool, &auth.username).await; +use self::middleware::DbConn; + +pub async fn verify_user(conn: DbConn, auth: Authentication) -> Result { + let user = get_user(conn, &auth.username).await; match user { Ok(Some(u)) => { @@ -34,3 +32,5 @@ pub async fn verify_user( } } } + +pub mod middleware; diff --git a/src/utils/middleware.rs b/src/utils/middleware.rs new file mode 100644 index 0000000..d6e0c5b --- /dev/null +++ b/src/utils/middleware.rs @@ -0,0 +1,47 @@ +use std::sync::Arc; + +use poem::{Endpoint, Middleware}; +use sqlx::PgPool; + +pub type DbConn = Arc; + +#[derive(Debug)] +pub struct DbConnectionMiddleware { + db: PgPool, +} + +impl DbConnectionMiddleware { + pub const fn new(db: PgPool) -> Self { + Self { db } + } +} + +impl Middleware for DbConnectionMiddleware { + type Output = DbConnectionMwEndpoint; + + fn transform(&self, ep: E) -> Self::Output { + DbConnectionMwEndpoint { + inner: ep, + db: self.db.clone(), + } + } +} + +#[derive(Debug)] +pub struct DbConnectionMwEndpoint { + inner: E, + db: PgPool, +} + +#[poem::async_trait] +impl Endpoint for DbConnectionMwEndpoint { + type Output = E::Output; + + async fn call(&self, mut req: poem::Request) -> Result { + let conn = Arc::new(self.db.clone()); + + req.extensions_mut().insert(conn); + + self.inner.call(req).await + } +}