diff --git a/Cargo.lock b/Cargo.lock index 3e700cb..7d31c53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "atomic" version = "0.6.0" @@ -207,6 +219,8 @@ dependencies = [ "futures", "futures-util", "problem_details", + "rusqlite", + "rusqlite_migration", "serde", "tokio", "tokio-util", @@ -311,6 +325,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -498,12 +524,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "headers" version = "0.4.0" @@ -824,7 +868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", ] [[package]] @@ -880,6 +924,17 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1233,6 +1288,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.6.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rusqlite_migration" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923b42e802f7dc20a0a6b5e097ba7c83fe4289da07e49156fecf6af08aa9cd1c" +dependencies = [ + "log", + "rusqlite", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2128,6 +2207,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.5" diff --git a/casket-backend/Cargo.toml b/casket-backend/Cargo.toml index 49c474b..86d5061 100644 --- a/casket-backend/Cargo.toml +++ b/casket-backend/Cargo.toml @@ -18,4 +18,8 @@ serde = { version = "1.0.217", features = ["derive"] } tracing = "0.1.41" tracing-log = "0.2.0" tracing-subscriber = "0.3.19" -problem_details = { version = "0.7.0", features = ["axum"] } \ No newline at end of file +problem_details = { version = "0.7.0", features = ["axum"] } + +# database +rusqlite = { version = "0.32.1", features = ["bundled"] } +rusqlite_migration = "1.3.1" diff --git a/casket-backend/src/config.rs b/casket-backend/src/config.rs index 4d49115..a9ed25f 100644 --- a/casket-backend/src/config.rs +++ b/casket-backend/src/config.rs @@ -9,7 +9,8 @@ pub const CONFIG_LOCATIONS: [&str; 2] = ["casket-backend/casket.toml", "/config/ pub struct Config { pub server: Server, pub files: Files, - pub oidc: Oidc + pub oidc: Oidc, + pub database: Database, } #[derive(Deserialize, Clone, Debug)] @@ -27,6 +28,12 @@ pub struct Oidc { pub oidc_endpoint: String } +#[derive(Deserialize, Clone, Debug)] +pub struct Database { + pub path: PathBuf, +} + + pub fn get_config() -> figment::Result { CONFIG_LOCATIONS .iter() diff --git a/casket-backend/src/db/mod.rs b/casket-backend/src/db/mod.rs new file mode 100644 index 0000000..a8d6933 --- /dev/null +++ b/casket-backend/src/db/mod.rs @@ -0,0 +1,2 @@ +pub mod repository; +pub mod sqlite; diff --git a/casket-backend/src/db/repository.rs b/casket-backend/src/db/repository.rs new file mode 100644 index 0000000..127f79d --- /dev/null +++ b/casket-backend/src/db/repository.rs @@ -0,0 +1,34 @@ +use crate::db::repository::insert::File; +use crate::db::repository::models::{FileModel, UserModel}; +use problem_details::ProblemDetails; + +pub trait Repository { + fn migrate(&self) -> impl std::future::Future> + Send; + fn get_or_create_user(&self, id: String) -> impl std::future::Future> + Send; + fn create_file(&self, file: File) -> impl std::future::Future> + Send; + + fn bump_version(&self, file_model: FileModel) -> impl std::future::Future> + Send; + + fn get_file(&self, user_id: String, path: String) -> impl std::future::Future> + Send; +} + +pub mod models { + pub struct UserModel { + pub uuid: String, + } + + pub struct FileModel { + pub id: u64, + pub user_id: String, + pub path: String, + pub version: u64, + } +} + +pub mod insert { + pub struct File { + pub user_id: String, + pub path: String, + pub version: u64, + } +} diff --git a/casket-backend/src/db/sqlite.rs b/casket-backend/src/db/sqlite.rs new file mode 100644 index 0000000..2272dce --- /dev/null +++ b/casket-backend/src/db/sqlite.rs @@ -0,0 +1,79 @@ +use std::path::PathBuf; +use crate::db::repository::insert::File; +use crate::db::repository::models::{FileModel, UserModel}; +use crate::db::repository::Repository; +use crate::errors::to_internal_error; +use problem_details::ProblemDetails; +use rusqlite::Connection; +use rusqlite_migration::{Migrations, M}; +use std::sync::{Arc, Mutex}; +use tokio::task::spawn_blocking; + +#[derive(Clone)] +pub struct Sqlite { + connection: Arc>, +} + +impl Sqlite { + pub fn from_path(path: &PathBuf) -> Sqlite { + Sqlite { + connection: Arc::new(Mutex::new(Connection::open(path).unwrap())), + } + } +} + +impl Repository for Sqlite { + async fn migrate(&self) -> Result<(), ProblemDetails> { + let migrations = Migrations::new(vec![ + M::up("CREATE TABLE users(uuid TEXT PRIMARY KEY, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);"), + M::up("CREATE TABLE files(id INTEGER PRIMARY KEY, user_id TEXT, file_path TEXT, version INTEGER, FOREIGN KEY(user_id) REFERENCES users(uuid));"), + M::up("CREATE INDEX files_user_path_idx ON files(user_id, file_path);"), + ]); + let connection = self.connection.clone(); + spawn_blocking(move || match connection.lock() { + Ok(mut conn) => { + migrations.to_latest(&mut conn).unwrap(); + } + Err(err) => { + panic!("Failed to lock connection: {err}"); + } + }) + .await + .map_err(to_internal_error) + } + + async fn get_or_create_user(&self, id: String) -> Result { + todo!() + } + + async fn create_file(&self, file: File) -> Result { + todo!() + } + + async fn bump_version(&self, file_model: FileModel) -> Result<(), ProblemDetails> { + todo!() + } + + async fn get_file(&self, user_id: String, path: String) -> Result { + let conn = self.connection.clone(); + spawn_blocking(move || { + match conn.lock() { + Ok(connection) => { + let mut statement = connection.prepare("SELECT id, user_id, path, version FROM files WHERE user_id = ?1 AND path = ?2").unwrap(); + statement.query_row([user_id, path], |row| { + Ok(FileModel { + id: row.get(0).unwrap(), + user_id: row.get(1).unwrap(), + path: row.get(2).unwrap(), + version: row.get(3).unwrap(), + }) + }).unwrap() + } + Err(err) => { + panic!("Failed to lock connection: {err}"); + } + } + }).await + .map_err(to_internal_error) + } +} diff --git a/casket-backend/src/main.rs b/casket-backend/src/main.rs index 18bf680..845a50f 100644 --- a/casket-backend/src/main.rs +++ b/casket-backend/src/main.rs @@ -2,12 +2,15 @@ mod auth; mod config; +mod db; mod errors; mod extractor_helper; mod files; mod routes; use crate::config::Config; +use crate::db::repository::Repository; +use crate::db::sqlite::Sqlite; use axum::{middleware, Router}; use axum_jwks::Jwks; use std::env; @@ -19,6 +22,13 @@ use tracing::{debug, error, info}; pub struct AppState { config: config::Config, pub jwks: Jwks, + pub sqlite: Sqlite, +} + +impl AppState { + pub fn get_repo(&self) -> &impl Repository { + &self.sqlite + } } #[tokio::main] @@ -31,8 +41,13 @@ async fn main() { Ok(config) => { debug!("Config loaded {:?}", &config,); let jwks = load_jwks(&config).await; + let db = connect_database(&config).await; let bind = format!("{}:{}", &config.server.bind_address, &config.server.port); - let state = AppState { config, jwks }; + let state: AppState = AppState { + config, + jwks, + sqlite: db, + }; let app = Router::new() .merge(routes::routes()) .route_layer(middleware::from_fn_with_state( @@ -56,6 +71,12 @@ async fn main() { } } +async fn connect_database(config: &Config) -> Sqlite { + let sqlite = db::sqlite::Sqlite::from_path(&config.database.path); + sqlite.migrate().await.unwrap(); + sqlite +} + async fn load_jwks(config: &Config) -> Jwks { Jwks::from_oidc_url(&config.oidc.oidc_endpoint, None) .await