diff --git a/Cargo.lock b/Cargo.lock index d9dce72..5145b1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,9 +104,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" dependencies = [ "shlex", ] @@ -495,6 +495,7 @@ dependencies = [ "serde", "serde_json", "serde_with", + "toml", ] [[package]] @@ -574,6 +575,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_with" version = "3.12.0" @@ -704,6 +714,40 @@ dependencies = [ "time-core", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.7.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.14" @@ -957,3 +1001,12 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index 327bc3a..fd19fd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,13 @@ repository = "https://github.com/Clivern/Pentheus" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = "4.5.2" -home = "0.5.9" -inquire = "0.7.0" -serde = "1.0.195" -serde_json = "1.0.114" -serde_with = "3.6.0" +clap = "4.5.23" +home = "0.5.11" +inquire = "0.7.5" +serde = { version = "1.0.217", features = ["derive"] } +serde_json = "1.0.134" +serde_with = "3.12.0" +toml = "0.8.19" [profile.release] opt-level = "z" # Optimize for size. diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..f1101b8 --- /dev/null +++ b/config.toml @@ -0,0 +1,48 @@ +# Database backups +[database] + [database.sqlite_db_01] + driver = "sqlite" + path = "/opt/backup/app1-db.sqlite" + + [database.my_mysql_db_01] + driver = "mysql" + host = "mysql_host" + port = 3306 + user = "mysql_user" + password = "mysql_password" + database = "my_mysql_db" + + [database.my_postgres_db_01] + driver = "postgres" + host = "postgres_host" + port = 5432 + user = "postgres" + password = "postgres_password" + database = "my_postgres_db" + +# Backup storage +[storage] + [storage.local_store] + driver = "local" + path = "/path/to/local/backup" + compress = "zip" + + [storage.s3_store] + driver = "s3" + bucket = "my-backup-bucket" + region = "us-east-1" + access_key = "my_access_key" + secret_key = "my_secret_key" + compress = "none" + +# Cron Jobs +[cron] + [cron.sqlite_db_01_cron] + schedule = "5 4 * * *" + database = "sqlite_db_01" + storage = "local_store" + +# Backups state file +[state] +storage = "s3_store" +path = "/state.json" diff --git a/src/config/local.rs b/src/config/local.rs index 2156e19..f060726 100644 --- a/src/config/local.rs +++ b/src/config/local.rs @@ -1,3 +1,243 @@ // Copyright 2025 Clivern. All rights reserved. // Use of this source code is governed by the MIT // license that can be found in the LICENSE file. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use toml; + +/// Represents the top-level configuration structure. +#[derive(Debug, Serialize, Deserialize)] +pub struct Configs { + /// A mapping of database configurations. + pub database: HashMap, + /// A mapping of storage configurations. + pub storage: HashMap, + /// A mapping of cron job configurations. + pub cron: HashMap, + /// State configuration for backups. + pub state: StateConfig, +} + +/// Represents a single database configuration. +#[derive(Debug, Serialize, Deserialize)] +pub struct DatabaseConfig { + /// The type of database driver (e.g., "sqlite", "mysql"). + pub driver: String, + /// The hostname or IP address of the database server. + pub host: Option, + /// The port number for connecting to the database. + pub port: Option, + /// The username for database authentication. + pub user: Option, + /// The password for database authentication. + pub password: Option, + /// The name of the database to connect to. + pub database: Option, + /// The file path for SQLite databases. + pub path: Option, +} + +/// Represents a single storage configuration. +#[derive(Debug, Serialize, Deserialize)] +pub struct StorageConfig { + /// The type of storage driver (e.g., "local", "s3"). + pub driver: String, + /// The file path for local storage. + pub path: Option, + /// The name of the S3 bucket (if applicable). + pub bucket: Option, + /// The AWS region for S3 storage (if applicable). + pub region: Option, + /// The access key for S3 authentication (if applicable). + pub access_key: Option, + /// The secret key for S3 authentication (if applicable). + pub secret_key: Option, + /// Compression method for stored files (e.g., "zip"). + pub compress: Option, +} + +/// Represents a single cron job configuration. +#[derive(Debug, Serialize, Deserialize)] +pub struct CronConfig { + /// The schedule for the cron job (in cron format). + pub schedule: String, + /// The name of the database to be used by the cron job. + pub database: String, + /// The storage type to be used by the cron job. + pub storage: String, +} + +/// Represents the state configuration for backups. +#[derive(Debug, Serialize, Deserialize)] +pub struct StateConfig { + /// The type of storage used for state management. + pub storage: String, + /// The file path to the state file. + pub path: String, +} + +/// Error returned when a configuration file is not found. +#[derive(Debug)] +struct ConfigNotFoundError { + message: String, +} + +impl std::error::Error for ConfigNotFoundError {} + +impl std::fmt::Display for ConfigNotFoundError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +/// Error returned when a configuration file is invalid. +#[derive(Debug)] +struct ConfigsInvalidError { + message: String, +} + +impl std::error::Error for ConfigsInvalidError {} + +impl std::fmt::Display for ConfigsInvalidError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +/// Loads configurations from a specified TOML file. +/// +/// # Arguments +/// +/// * `path` - A string slice that holds the path to the TOML configuration file. +/// +/// # Returns +/// +/// This function returns a `Result` which is: +/// - `Ok(Configs)` if loading and parsing are successful. +/// - `Err(Box)` if an error occurs (e.g., file not found or invalid format). +pub fn load_configs(path: &str) -> Result> { + let contents = fs::read_to_string(path).map_err(|_| { + Box::new(ConfigNotFoundError { + message: format!("Config file {} not found", path), + }) + })?; + + let configs = toml::from_str(&contents).map_err(|_| { + Box::new(ConfigsInvalidError { + message: format!("Config file {} is invalid", path), + }) + })?; + + Ok(configs) +} + +/// Tests loading valid configurations from a TOML file. +/// Tests loading valid configurations from a TOML file. +#[test] +fn test_load_configs_valid() { + match load_configs("config.toml") { + Ok(config) => { + // Check state configuration + assert_eq!(&config.state.storage, "s3_store"); + assert_eq!(&config.state.path, "/state.json"); + + // Check database configurations + assert!(config.database.contains_key("sqlite_db_01")); + let sqlite_db = config.database.get("sqlite_db_01").unwrap(); + assert_eq!(&sqlite_db.driver, "sqlite"); + assert_eq!( + sqlite_db.path, + Some("/opt/backup/app1-db.sqlite".to_string()) + ); + assert!(sqlite_db.host.is_none()); + assert!(sqlite_db.port.is_none()); + assert!(sqlite_db.user.is_none()); + assert!(sqlite_db.password.is_none()); + assert!(sqlite_db.database.is_none()); + + assert!(config.database.contains_key("my_mysql_db_01")); + let mysql_db = config.database.get("my_mysql_db_01").unwrap(); + assert_eq!(&mysql_db.driver, "mysql"); + assert_eq!(mysql_db.host, Some("mysql_host".to_string())); + assert_eq!(mysql_db.port, Some(3306)); + assert_eq!(mysql_db.user, Some("mysql_user".to_string())); + assert_eq!(mysql_db.password, Some("mysql_password".to_string())); + assert_eq!(mysql_db.database, Some("my_mysql_db".to_string())); + assert!(mysql_db.path.is_none()); + + assert!(config.database.contains_key("my_postgres_db_01")); + let postgres_db = config.database.get("my_postgres_db_01").unwrap(); + assert_eq!(&postgres_db.driver, "postgres"); + assert_eq!(postgres_db.host, Some("postgres_host".to_string())); + assert_eq!(postgres_db.port, Some(5432)); + assert_eq!(postgres_db.user, Some("postgres".to_string())); + assert_eq!(postgres_db.password, Some("postgres_password".to_string())); + assert_eq!(postgres_db.database, Some("my_postgres_db".to_string())); + assert!(postgres_db.path.is_none()); + + // Check storage configurations + assert!(config.storage.contains_key("local_store")); + let local_store = config.storage.get("local_store").unwrap(); + assert_eq!(&local_store.driver, "local"); + assert_eq!(local_store.path, Some("/path/to/local/backup".to_string())); + assert_eq!(local_store.compress, Some("zip".to_string())); + assert!(local_store.bucket.is_none()); + assert!(local_store.region.is_none()); + assert!(local_store.access_key.is_none()); + assert!(local_store.secret_key.is_none()); + + assert!(config.storage.contains_key("s3_store")); + let s3_store = config.storage.get("s3_store").unwrap(); + assert_eq!(&s3_store.driver, "s3"); + assert_eq!(s3_store.bucket, Some("my-backup-bucket".to_string())); + assert_eq!(s3_store.region, Some("us-east-1".to_string())); + assert_eq!(s3_store.access_key, Some("my_access_key".to_string())); + assert_eq!(s3_store.secret_key, Some("my_secret_key".to_string())); + assert_eq!(s3_store.compress, Some("none".to_string())); + + // Check cron job configurations + assert!(config.cron.contains_key("sqlite_db_01_cron")); + let cron_job = config.cron.get("sqlite_db_01_cron").unwrap(); + assert_eq!(&cron_job.schedule, "5 4 * * *"); + assert_eq!(&cron_job.database, "sqlite_db_01"); + assert_eq!(&cron_job.storage, "local_store"); + } + Err(_) => { + panic!("Failed to load configurations!"); + } + } +} + +/// Tests loading configurations from a non-existent TOML file. +#[test] +fn test_load_configs_not_found() { + match load_configs("config.tom") { + Ok(_) => { + assert_eq!(true, false); + } + Err(e) => { + assert_eq!( + e.to_string(), + "Config file config.tom not found".to_string() + ); + } + } +} + +/// Tests loading configurations from an invalid TOML file. +#[test] +fn test_load_configs_invalid() { + match load_configs("Cargo.toml") { + Ok(_) => { + assert_eq!(true, false); + } + Err(e) => { + assert_eq!( + e.to_string(), + "Config file Cargo.toml is invalid".to_string() + ); + } + } +} diff --git a/src/main.rs b/src/main.rs index 5281f1b..04c7364 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ // license that can be found in the LICENSE file. mod cmd; +mod config; use clap::Command; use cmd::version::get_version;