diff --git a/common/src/config.rs b/common/src/config.rs index c4ee14f0a..cad025ea9 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -540,7 +540,7 @@ impl Default for LFSSshConfig { } } -#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct OauthConfig { pub github_client_id: String, pub github_client_secret: String, @@ -550,6 +550,26 @@ pub struct OauthConfig { pub allowed_cors_origins: Vec, } +impl Default for OauthConfig { + fn default() -> Self { + Self { + github_client_id: String::new(), + github_client_secret: String::new(), + ui_domain: "http://localhost".to_string(), + cookie_domain: "localhost".to_string(), + campsite_api_domain: "http://api.gitmono.test:3001".to_string(), + allowed_cors_origins: vec![ + "http://localhost", + "http://app.gitmega.com", + "http://app.gitmono.test", + ] + .into_iter() + .map(|s| s.to_string()) + .collect(), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/context/src/lib.rs b/context/src/lib.rs index a26011278..1a0a2b900 100644 --- a/context/src/lib.rs +++ b/context/src/lib.rs @@ -1,5 +1,5 @@ -use std::sync::Arc; use jupiter::tests::test_storage; +use std::sync::Arc; /// This is the main application context for the Mono application. /// It holds shared state and configuration for the application. @@ -23,19 +23,12 @@ pub struct AppContext { impl AppContext { /// Creates a new application context with the given configuration. pub async fn new(config: common::config::Config) -> Self { - let config = Arc::new(config); - let storage = jupiter::storage::Storage::new(config.clone()).await; let storage_for_vault = storage.clone(); - let vault = tokio::task::spawn_blocking(move || { - vault::integration::vault_core::VaultCore::new(storage_for_vault) - }) - .await - .expect("VaultCore::new panicked"); - + let vault = vault::integration::vault_core::VaultCore::new(storage_for_vault); #[cfg(feature = "p2p")] let client = gemini::p2p::client::P2PClient::new(storage.clone(), vault.clone()); @@ -49,8 +42,6 @@ impl AppContext { #[cfg(feature = "p2p")] client, } - - } pub fn wrapped_context(&self) -> Arc { @@ -58,22 +49,21 @@ impl AppContext { } pub async fn mock(config: common::config::Config) -> Self { - let config = Arc::new(config); // use Existing test method let storage = test_storage(config.base_dir.clone()).await; - + let storage_for_vault = storage.clone(); let temp_dir = config.base_dir.clone().join("vault"); let key_path = temp_dir.join("core_key.json"); std::fs::create_dir_all(&temp_dir).expect("Mock: Failed to create vault dir"); - + let vault = tokio::task::spawn_blocking(move || { vault::integration::vault_core::VaultCore::config(storage_for_vault, key_path) }) - .await - .expect("VaultCore::config panicked"); + .await + .expect("VaultCore::config panicked"); #[cfg(feature = "p2p")] let client = gemini::p2p::client::P2PClient::new(storage.clone(), vault.clone()); @@ -87,4 +77,3 @@ impl AppContext { } } } - diff --git a/jupiter/Cargo.toml b/jupiter/Cargo.toml index 40f12288b..ebe7e6921 100644 --- a/jupiter/Cargo.toml +++ b/jupiter/Cargo.toml @@ -41,3 +41,4 @@ aws-sdk-s3 = { workspace = true, features = ["rt-tokio"] } anyhow = { workspace = true } tempfile = { workspace = true } indexmap = { workspace = true } +url = { workspace = true } diff --git a/jupiter/src/storage/init.rs b/jupiter/src/storage/init.rs index 16f41f52b..46f91394f 100644 --- a/jupiter/src/storage/init.rs +++ b/jupiter/src/storage/init.rs @@ -1,27 +1,68 @@ use common::config::DbConfig; use common::errors::MegaError; use sea_orm::{ConnectOptions, Database, DatabaseConnection}; -use std::{path::Path, time::Duration}; +use std::{ + net::{SocketAddr, TcpStream}, + path::Path, + time::Duration, +}; use tracing::log; +use url::Url; use crate::migration::apply_migrations; use crate::utils::id_generator; -/// Create a database connection. -/// When postgres is set but not available, it will fall back to sqlite automatically. +/// Create a database connection with failover logic. +/// +/// This function attempts to connect to a database based on the provided configuration: +/// - If PostgreSQL is specified but unavailable, it automatically falls back to SQLite +/// - For local PostgreSQL connections, it first checks port reachability to avoid long timeouts +/// +/// The failover logic works as follows: +/// 1. For PostgreSQL connections: +/// - If the host is local (localhost, 127.0.0.1, etc.), performs a quick port check (100ms timeout) +/// - If port is unreachable, immediately falls back to SQLite without waiting for a full connection timeout +/// - If port is reachable but connection fails, logs the error and falls back to SQLite +/// 2. For non-local PostgreSQL, attempts connection with normal timeouts (3 seconds) +/// - On failure, logs the error and falls back to SQLite +/// 3. For SQLite connections, connects directly without fallback +/// +/// After successful connection, applies any pending database migrations. +/// +/// This optimization helps avoid long waits when local PostgreSQL isn't running. pub async fn database_connection(db_config: &DbConfig) -> DatabaseConnection { id_generator::set_up_options().unwrap(); let conn = if db_config.db_type == "postgres" { - match postgres_connection(db_config).await { - Ok(conn) => conn, - Err(e) => { - log::error!("Failed to connect to postgres: {e}"); - log::info!("Falling back to sqlite"); + if should_check_port_first(&db_config.db_url) { + if !is_port_reachable(&db_config.db_url) { + log::info!("Local postgres port not reachable, falling back to sqlite"); sqlite_connection(db_config) .await .expect("Cannot connect to any database") + } else { + match postgres_connection(db_config).await { + Ok(conn) => conn, + Err(e) => { + log::error!("Failed to connect to postgres: {e}"); + log::info!("Falling back to sqlite"); + sqlite_connection(db_config) + .await + .expect("Cannot connect to any database") + } + } + } + } else { + match postgres_connection(db_config).await { + Ok(conn) => conn, + Err(e) => { + log::error!("Failed to connect to postgres: {e}"); + log::info!("Falling back to sqlite"); + sqlite_connection(db_config) + .await + .expect("Cannot connect to any database") + } } } } else { @@ -34,6 +75,29 @@ pub async fn database_connection(db_config: &DbConfig) -> DatabaseConnection { conn } +fn should_check_port_first(db_url: &str) -> bool { + if let Ok(url) = Url::parse(db_url) { + if let Some(host) = url.host_str() { + return host == "localhost" + || host == "127.0.0.1" + || host == "::1" + || host == "0.0.0.0"; + } + } + false +} + +fn is_port_reachable(db_url: &str) -> bool { + if let Ok(url) = Url::parse(db_url) { + if let (Some(host), Some(port)) = (url.host_str(), url.port()) { + if let Ok(addr) = format!("{host}:{port}").parse::() { + return TcpStream::connect_timeout(&addr, Duration::from_millis(100)).is_ok(); + } + } + } + false +} + async fn postgres_connection(db_config: &DbConfig) -> Result { let db_url = db_config.db_url.to_owned(); log::info!("Connecting to database: {db_url}"); @@ -61,11 +125,59 @@ fn setup_option(db_url: impl Into) -> ConnectOptions { let mut opt = ConnectOptions::new(db_url); opt.max_connections(5) .min_connections(1) - .acquire_timeout(Duration::from_secs(3)) - .connect_timeout(Duration::from_secs(3)) + .acquire_timeout(Duration::from_secs(1)) + .connect_timeout(Duration::from_secs(1)) .idle_timeout(Duration::from_secs(8)) .max_lifetime(Duration::from_secs(8)) .sqlx_logging(true) .sqlx_logging_level(log::LevelFilter::Debug); opt } + +#[cfg(test)] +pub mod test { + use super::*; + + /// Creates a test database connection for unit tests. + pub fn test_local_db_address() { + assert!("postgres://mono:mono@localhost:5432/mono_test" + .parse::() + .is_ok()); + + // Test localhost variants - should return true + assert_eq!( + should_check_port_first("postgres://mono:mono@localhost:5432/mono_test"), + true + ); + assert_eq!( + should_check_port_first("postgres://mono:mono@127.0.0.1:5432/mono_test"), + true + ); + assert_eq!( + should_check_port_first("postgres://mono:mono@::1:5432/mono_test"), + true + ); + assert_eq!( + should_check_port_first("postgres://mono:mono@0.0.0.0:5432/mono_test"), + true + ); + + // Test remote addresses - should return false + assert_eq!( + should_check_port_first("postgres://mono:mono@192.168.1.100:5432/mono_test"), + false + ); + assert_eq!( + should_check_port_first("postgres://mono:mono@example.com:5432/mono_test"), + false + ); + assert_eq!( + should_check_port_first("postgres://mono:mono@10.0.0.1:5432/mono_test"), + false + ); + + // Test invalid URLs - should return false + assert_eq!(should_check_port_first("invalid_url"), false); + assert_eq!(should_check_port_first(""), false); + } +} diff --git a/mega/src/cli.rs b/mega/src/cli.rs index 4e0482d50..13481bbea 100644 --- a/mega/src/cli.rs +++ b/mega/src/cli.rs @@ -29,17 +29,20 @@ pub fn parse(args: Option>) -> MegaResult { None => cli().try_get_matches().unwrap_or_else(|e| e.exit()), }; - // Get the current directory + // Load configuration from the config file or default location let current_dir = env::current_dir()?; - // Get the path to the config file in the current directory + let base_dir = common::config::mega_base(); let config_path = current_dir.join("config.toml"); + let config_path_alt = base_dir.join("etc/config.toml"); let config = if let Some(path) = matches.get_one::("config").cloned() { Config::new(path.to_str().unwrap()).unwrap() } else if config_path.exists() { Config::new(config_path.to_str().unwrap()).unwrap() + } else if config_path_alt.exists() { + Config::new(config_path_alt.to_str().unwrap()).unwrap() } else { - eprintln!("can't find config.toml under {:?}, you can manually set config.toml path with --config parameter", env::current_dir().unwrap()); + eprintln!("can't find config.toml under {:?} or {:?}, you can manually set config.toml path with --config parameter", env::current_dir().unwrap(), base_dir); Config::default() }; diff --git a/mono/src/cli.rs b/mono/src/cli.rs index c22c2e323..3bd12c0b6 100644 --- a/mono/src/cli.rs +++ b/mono/src/cli.rs @@ -29,17 +29,20 @@ pub fn parse(args: Option>) -> MegaResult { None => cli().try_get_matches().unwrap_or_else(|e| e.exit()), }; - // Get the current directory + // Load configuration from the config file or default location let current_dir = env::current_dir()?; - // Get the path to the config file in the current directory - let config_path = current_dir.join("../../config/config.toml"); + let base_dir = common::config::mega_base(); + let config_path = current_dir.join("config.toml"); + let config_path_alt = base_dir.join("etc/config.toml"); let config = if let Some(path) = matches.get_one::("config").cloned() { Config::new(path.to_str().unwrap()).unwrap() } else if config_path.exists() { Config::new(config_path.to_str().unwrap()).unwrap() + } else if config_path_alt.exists() { + Config::new(config_path_alt.to_str().unwrap()).unwrap() } else { - eprintln!("can't find config.toml under {:?}, you can manually set config.toml path with --config parameter", env::current_dir().unwrap()); + eprintln!("can't find config.toml under {:?} or {:?}, you can manually set config.toml path with --config parameter", env::current_dir().unwrap(), base_dir); Config::default() }; diff --git a/mono/src/server/https_server.rs b/mono/src/server/https_server.rs index 3980b15c2..b54f63ea7 100644 --- a/mono/src/server/https_server.rs +++ b/mono/src/server/https_server.rs @@ -94,7 +94,7 @@ pub async fn app(storage: Storage, host: String, port: u16) -> Router { }; let config = storage.config(); - let oauth_config = config.oauth.clone().unwrap(); + let oauth_config = config.oauth.clone().unwrap_or_default(); let api_state = MonoApiServiceState { storage: storage.clone(), oauth_client: Some(oauth_client(oauth_config.clone()).unwrap()), diff --git a/vault/src/integration/vault_core.rs b/vault/src/integration/vault_core.rs index d2a381b18..2b6b5073b 100644 --- a/vault/src/integration/vault_core.rs +++ b/vault/src/integration/vault_core.rs @@ -1,4 +1,3 @@ - use crate::integration::jupiter_backend::JupiterBackend; use common::errors::MegaError; use jupiter::storage::Storage; @@ -7,7 +6,6 @@ use std::{ sync::{Arc, RwLock}, }; - use rusty_vault::{ core::Core, logical::{Operation, Request, Response}, @@ -19,8 +17,6 @@ use tracing::log; const CORE_KEY_FILE: &str = "core_key.json"; // where the core key is stored, like `root_token` - - #[derive(Debug, Clone, Serialize, Deserialize)] struct CoreKey { secret_shares: Vec>, @@ -57,7 +53,6 @@ impl VaultCore { tracing::info!("{key_path:?}"); std::fs::create_dir_all(&dir).expect("Failed to create vault directory"); Self::config(ctx.clone(), key_path) - } pub fn config(ctx: Storage, key_path: PathBuf) -> Self { @@ -75,7 +70,6 @@ impl VaultCore { }; let core = Arc::new(RwLock::new(core)); - let key = { let mut managed_core = core.write().unwrap(); managed_core @@ -83,8 +77,6 @@ impl VaultCore { .expect("Failed to configure vault core"); let core_key = if !key_path.exists() { - - let result = managed_core .init(&seal_config) .expect("Failed to initialize vault"); @@ -211,8 +203,7 @@ mod tests { "Vault core should be initialized" ); } - - + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_vault_api() { let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory");