diff --git a/.gitignore b/.gitignore index 4159f90..9b63f9d 100644 --- a/.gitignore +++ b/.gitignore @@ -34,10 +34,11 @@ build/ logs/ *.log -# Database files +# Database files (now includes the specific dev database) *.sqlite *.sqlite3 *.db +owlynews.sqlite3* # Dependency directories node_modules/ diff --git a/backend-rust/Cargo.lock b/backend-rust/Cargo.lock index feab1af..6c9a181 100644 --- a/backend-rust/Cargo.lock +++ b/backend-rust/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -908,6 +917,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.8.4" @@ -973,6 +991,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1079,6 +1107,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owly-news-summariser" version = "0.1.0" @@ -1090,6 +1124,8 @@ dependencies = [ "serde_json", "sqlx", "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -1256,6 +1292,50 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rsa" version = "0.9.8" @@ -1421,6 +1501,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1767,6 +1856,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -1892,6 +1990,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1944,6 +2072,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2045,6 +2179,28 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.61.2" diff --git a/backend-rust/Cargo.toml b/backend-rust/Cargo.toml index 77060af..14d93d0 100644 --- a/backend-rust/Cargo.toml +++ b/backend-rust/Cargo.toml @@ -11,3 +11,5 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "sqlite", "macros", "migrate", "chrono", "json"] } dotenv = "0.15" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/backend-rust/src/api.rs b/backend-rust/src/api.rs index 24cb919..ebbadbf 100644 --- a/backend-rust/src/api.rs +++ b/backend-rust/src/api.rs @@ -1,3 +1,3 @@ -mod handlers; -mod middleware; -mod routes; +pub mod handlers; +pub mod middleware; +pub mod routes; diff --git a/backend-rust/src/api/handlers.rs b/backend-rust/src/api/handlers.rs index e69de29..a005354 100644 --- a/backend-rust/src/api/handlers.rs +++ b/backend-rust/src/api/handlers.rs @@ -0,0 +1,39 @@ +use axum::Json; +use axum::extract::State; +use serde_json::Value; +use sqlx::SqlitePool; + +pub async fn get_articles(State(pool): State) -> Result, AppError> { + // TODO: Article logic + Ok(Json(serde_json::json!({"articles": []}))) +} + +pub async fn get_summaries(State(pool): State) -> Result, AppError> { + // TODO: Summaries logic + Ok(Json(serde_json::json!({"summaries": []}))) +} + +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; + +pub struct AppError(anyhow::Error); + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", self.0), + ) + .into_response() + } +} + +impl From for AppError +where + E: Into, { + fn from(err: E) -> Self { + Self(err.into()) + } +} diff --git a/backend-rust/src/api/routes.rs b/backend-rust/src/api/routes.rs index e69de29..242d273 100644 --- a/backend-rust/src/api/routes.rs +++ b/backend-rust/src/api/routes.rs @@ -0,0 +1,11 @@ +use axum::Router; +use axum::routing::get; +use sqlx::SqlitePool; +use crate::api::handlers; + +pub fn routes() -> Router { + Router::new() + .route("/articles", get(handlers::get_articles)) + .route("/summaries", get(handlers::get_summaries)) + // Add more routes as needed +} diff --git a/backend-rust/src/config.rs b/backend-rust/src/config.rs index 5c82f7c..781f38f 100644 --- a/backend-rust/src/config.rs +++ b/backend-rust/src/config.rs @@ -1,21 +1,53 @@ use std::env; +use std::path::PathBuf; #[derive(Debug, Clone)] pub struct Config { pub db_path: String, pub migration_path: String, + pub host: String, + pub port: u16, } impl Config { pub fn from_env() -> Self { Self { - db_path: env::var("DB_URL").unwrap_or_else(|_| "owlynews.sqlite3".to_string()), + db_path: Self::get_db_path(), migration_path: std::env::var("MIGRATION_PATH") .unwrap_or_else(|_| "./migrations".to_string()), + host: env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), + port: env::var("PORT") + .unwrap_or_else(|_| "1337".to_string()) + .parse() + .expect("PORT must be a number"), + } + } + + fn get_db_path() -> String { + if let Ok(db_path) = env::var("DATABASE_PATH") { + return db_path; + } + + if cfg!(debug_assertions) { + // Development: Use backend-rust directory + // TODO: Change later + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("owlynews.sqlite3"); + path.to_str().unwrap().to_string() + } else { + // Production: Use standard Linux applications data directory + "/var/lib/owly-news-summariser/owlynews.sqlite3".to_string() } } pub fn database_url(&self) -> String { format!("sqlite:{}", self.db_path) } + + pub fn ensure_db_directory(&self) -> Result<(), std::io::Error> { + if let Some(parent) = std::path::Path::new(&self.db_path).parent() { + std::fs::create_dir_all(parent)?; + } + Ok(()) + } } diff --git a/backend-rust/src/db.rs b/backend-rust/src/db.rs index fcd9564..480528f 100644 --- a/backend-rust/src/db.rs +++ b/backend-rust/src/db.rs @@ -1,23 +1,31 @@ +use crate::config::Config; use anyhow::Result; use sqlx::migrate::Migrator; use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection}; -use sqlx::{Connection, SqlitePool}; +use sqlx::{Pool, Sqlite, SqlitePool}; use std::str::FromStr; +use tracing::info; pub const MIGRATOR: Migrator = sqlx::migrate!("./migrations"); -pub async fn initialize_db(db_path: &str, _migrations_dir: &str) -> Result { - let options = - SqliteConnectOptions::from_str(&format!("sqlite:{}", db_path))?.create_if_missing(true); - let mut conn = SqliteConnection::connect_with(&options).await?; +pub async fn initialize_db(config: &Config) -> Result> { + config.ensure_db_directory()?; - MIGRATOR.run(&mut conn).await?; + let options = SqliteConnectOptions::from_str(&config.database_url())? + .create_if_missing(true) + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .foreign_keys(true); - Ok(conn) -} + let pool = SqlitePool::connect_with(options).await?; -pub async fn create_pool(opts: SqliteConnectOptions) -> Result { - let pool = SqlitePool::connect_with(opts).await?; + MIGRATOR.run(&pool).await?; + info!("Database migrations completed successfully"); + + Ok(pool) +} + +pub async fn create_pool(opts: SqliteConnectOptions) -> Result { + let pool = SqlitePool::connect_with(opts).await?; Ok(pool) } diff --git a/backend-rust/src/main.rs b/backend-rust/src/main.rs index 5eb4b24..295d5ef 100644 --- a/backend-rust/src/main.rs +++ b/backend-rust/src/main.rs @@ -5,35 +5,74 @@ mod models; mod services; use crate::config::Config; +use anyhow::Result; +use axum::Router; +use axum::routing::get; use sqlx::sqlite::SqliteConnectOptions; use std::str::FromStr; +use tokio::signal; +use tokio::signal::ctrl_c; +use tokio::signal::unix::signal; +use tracing::{error, info}; +use tracing_subscriber; #[tokio::main] -async fn main() { +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_target(false) + .compact() + .init(); + let config = Config::from_env(); - match db::initialize_db(&config.db_path, &config.migration_path).await { - Ok(_conn) => { - println!("Database initialized successfully"); + let pool = db::initialize_db(&config).await?; - let options = SqliteConnectOptions::from_str(&config.database_url()) - .expect("Invalid database URL") - .create_if_missing(true); + let app = create_app(pool); - match db::create_pool(options).await { - Ok(_pool) => { - println!("Database pool created successfully") - // App logic here - } - Err(e) => { - println!("Error creating database pool: {:?}", e); - std::process::exit(1); - } - } - } - Err(e) => { - println!("Error initializing database: {:?}", e); - std::process::exit(1); - } - } + let listener = + tokio::net::TcpListener::bind(format!("{}:{}", config.host, config.port)).await?; + info!("Server starting on {}:{}", config.host, config.port); + + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await?; + + Ok(()) +} + +fn create_app(pool: sqlx::SqlitePool) -> Router { + Router::new() + .route("/health", get(health_check)) + .nest("/api", api::routes::routes()) + .with_state(pool) +} + +async fn health_check() -> &'static str { + "OK" +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install CTRL+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install terminate handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + info!("Signal received, shutting down"); }