diff --git a/Cargo.lock b/Cargo.lock index e18146e..9e75ab8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,6 +684,27 @@ dependencies = [ "crypto-common 0.2.2", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -1560,6 +1581,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -1790,6 +1812,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "p256" version = "0.13.2" @@ -2151,6 +2179,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.19", +] + [[package]] name = "reqwest" version = "0.12.28" @@ -2758,6 +2797,7 @@ dependencies = [ "base64", "chrono", "cookie", + "dirs", "maud", "rand 0.9.5", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 09073a2..0b6fc46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ path = "src/main.rs" axum = "0.8" maud = { version = "0.27", features = ["axum"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -sqlx = { version = "0.8", features = [ "runtime-tokio" ] } +sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite", "migrate" ] } atproto-identity = "0.14.5" atproto-oauth = "0.14.5" serde = { version = "1", features = ["derive"] } @@ -26,4 +26,5 @@ anyhow = "1" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } reqwest-middleware = "0.4" reqwest-chain = "1.0" -urlencoding = "2.1" \ No newline at end of file +urlencoding = "2.1" +dirs = "6.0.0" diff --git a/README.md b/README.md index 5113c29..6d2b5b7 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,8 @@ 3. Success! ## Database Instructions -1. Run `cargo install sqlx-cli` -2. Create the database. `sqlx db create --database-url sqlite:mods.db` -3. Run sql migrations `sqlx migrate run --database-url sqlite:mods.db` +`sqlx-cli` is provided by the nix devShell for authoring new migrations: +`sqlx migrate add `. ## Quick-Start Contribution Guide @@ -30,4 +29,4 @@ TO-DO: - Moderation Tools ### PHASE 4 - - Forum \ No newline at end of file + - Forum diff --git a/flake.nix b/flake.nix index 08f68bd..ca476af 100644 --- a/flake.nix +++ b/flake.nix @@ -82,6 +82,7 @@ buildInputs = rustBuildInputs; nativeBuildInputs = with pkgs; [ rustToolchain + sqlx-cli ]; shellHook = '' # For rust-analyzer 'hover' tooltips to work. diff --git a/migrations/mods.sql b/migrations/20260807000000_mods.sql similarity index 98% rename from migrations/mods.sql rename to migrations/20260807000000_mods.sql index 6520010..4c8baaf 100644 --- a/migrations/mods.sql +++ b/migrations/20260807000000_mods.sql @@ -19,7 +19,7 @@ CREATE TABLE IF NOT EXISTS mods download_url TEXT NOT NULL, icon_url TEXT, - mod_color TEXT, + mod_color TEXT ); CREATE TABLE IF NOT EXISTS authors diff --git a/src/config.rs b/src/config.rs index 8fa95c2..0f86115 100644 --- a/src/config.rs +++ b/src/config.rs @@ -69,6 +69,16 @@ impl Config { } } + /// Path to the SQLite database file, under the platform's local data + /// directory (e.g. `~/.local/share/starhaven/database.db` on Linux, or + /// `$XDG_DATA_HOME` if set). Not user-configurable: in production this + /// is expected to be pointed elsewhere by setting `XDG_DATA_HOME` (e.g. + /// to a systemd `StateDirectory`), not an app-specific env var. + pub fn database_path() -> std::path::PathBuf { + let base = dirs::data_local_dir().expect("no local data directory for this platform"); + base.join("starhaven").join("database.db") + } + /// The OAuth client id (the `client-metadata.json` URL). pub fn oauth_client_id(&self) -> String { format!("{}/client-metadata.json", self.external_base) diff --git a/src/main.rs b/src/main.rs index 1228ba7..c5a4aba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,9 @@ use state::AppState; #[tokio::main] async fn main() { - let state = AppState::new(Config::from_env()); + let state = AppState::new(Config::from_env()) + .await + .expect("failed to initialize application state"); let app = Router::new() .merge(oauth::router()) diff --git a/src/state.rs b/src/state.rs index 1d488d6..c8e1ba8 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,6 +7,8 @@ use std::sync::{Arc, Mutex}; use atproto_identity::resolve::{ HickoryDnsResolver, InnerIdentityResolver, SharedIdentityResolver, }; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use sqlx::SqlitePool; use crate::config::Config; use crate::oauth::login::PersistedOAuthRequest; @@ -28,11 +30,13 @@ pub struct Inner { /// `/callback`. In-memory and lost across restarts; an interrupted login /// just needs to be retried. pub oauth_requests: Mutex>, + /// SQLite connection pool. + pub db: SqlitePool, } impl AppState { /// Build application state from configuration. - pub fn new(config: Config) -> Self { + pub async fn new(config: Config) -> anyhow::Result { let http_client = reqwest::Client::new(); let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[])); let identity_resolver = SharedIdentityResolver(Arc::new(InnerIdentityResolver { @@ -41,12 +45,26 @@ impl AppState { plc_hostname: "plc.directory".to_string(), })); - AppState(Arc::new(Inner { + // Create and migrate db if necessary. + let database_path = Config::database_path(); + if let Some(parent) = database_path.parent() { + std::fs::create_dir_all(parent)?; + } + let connect_options = SqliteConnectOptions::new() + .filename(&database_path) + .create_if_missing(true); + let db = SqlitePoolOptions::new() + .connect_with(connect_options) + .await?; + sqlx::migrate!().run(&db).await?; + + Ok(AppState(Arc::new(Inner { config, http_client, identity_resolver, oauth_requests: Mutex::new(HashMap::new()), - })) + db, + }))) } }