From a728ba0d13ad6e5bdc19b077202cc78b8703e84c Mon Sep 17 00:00:00 2001 From: Charles GTE Date: Sat, 28 Mar 2026 18:54:24 +0100 Subject: [PATCH 1/2] feat: separate mysql and mariadb --- databases.json | 12 ++- docker-compose.yml | 177 +++++++++++++++++-------------- docker/Dockerfile | 1 + src/domain/factory.rs | 5 +- src/domain/mariadb/backup.rs | 66 ++++++++++++ src/domain/mariadb/connection.rs | 45 ++++++++ src/domain/mariadb/database.rs | 55 ++++++++++ src/domain/mariadb/mod.rs | 5 + src/domain/mariadb/ping.rs | 28 +++++ src/domain/mariadb/restore.rs | 80 ++++++++++++++ src/domain/mod.rs | 1 + src/domain/mysql/backup.rs | 6 ++ src/domain/mysql/ping.rs | 2 +- 13 files changed, 398 insertions(+), 85 deletions(-) create mode 100644 src/domain/mariadb/backup.rs create mode 100644 src/domain/mariadb/connection.rs create mode 100644 src/domain/mariadb/database.rs create mode 100644 src/domain/mariadb/mod.rs create mode 100644 src/domain/mariadb/ping.rs create mode 100644 src/domain/mariadb/restore.rs diff --git a/databases.json b/databases.json index cb701b0..9fbfdc0 100644 --- a/databases.json +++ b/databases.json @@ -13,7 +13,7 @@ { "name": "Test database 2 - MariaDB", "database": "mariadb", - "type": "mysql", + "type": "mariadb", "username": "mariadb", "password": "changeme", "port": 3306, @@ -81,6 +81,16 @@ "username": "default", "host": "db-valkey-auth", "generated_id": "16678561-ff7e-4c97-8c83-0adeff214681" + }, + { + "name": "Test database 12 - Mysql", + "database": "mysqldb", + "type": "mysql", + "username": "mysqldb", + "password": "changeme", + "port": 3306, + "host": "db-mysql", + "generated_id": "16656124-ff7e-4c97-8c83-0adeff214681" } ] } diff --git a/docker-compose.yml b/docker-compose.yml index 7b19a5e..46eedf6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,8 +11,7 @@ services: # - ./databases.toml:/config/config.toml - cargo-registry:/usr/local/cargo/registry - cargo-git:/usr/local/cargo/git - - /var/run/docker.sock:/var/run/docker.sock - +# - /var/run/docker.sock:/var/run/docker.sock # - cargo-target:/app/target # - sqlite-data:/sqlite-data/workspace/data # - ./scripts/sqlite/test-db:/sqlite-data-2/workspace/data @@ -28,35 +27,50 @@ services: networks: - portabase - db-postgres: - container_name: db-postgres - image: postgres:17-alpine +# db-postgres: +# container_name: db-postgres +# image: postgres:17-alpine +# ports: +# - "5436:5432" +# volumes: +# - postgres-data:/var/lib/postgresql/data +# environment: +# - POSTGRES_DB=devdb +# - POSTGRES_USER=devuser +# - POSTGRES_PASSWORD=changeme +# networks: +# - portabase + + db-mariadb: + container_name: db-mariadb + image: mariadb:latest ports: - - "5436:5432" + - "3311:3306" + environment: + - MYSQL_DATABASE=mariadb + - MYSQL_USER=mariadb + - MYSQL_PASSWORD=changeme + - MYSQL_RANDOM_ROOT_PASSWORD=yes volumes: - - postgres-data:/var/lib/postgresql/data + - mariadb-data:/var/lib/mysql + networks: + - portabase + + db-mysql: + container_name: db-mysql + image: mysql:9.5 + ports: + - "3312:3306" environment: - - POSTGRES_DB=devdb - - POSTGRES_USER=devuser - - POSTGRES_PASSWORD=changeme + - MYSQL_DATABASE=mysqldb + - MYSQL_USER=mysqldb + - MYSQL_PASSWORD=changeme + - MYSQL_RANDOM_ROOT_PASSWORD=yes + volumes: + - mysql-data:/var/lib/mysql networks: - portabase # - # db-mariadb: - # container_name: db-mariadb - # image: mariadb:latest - # ports: - # - "3311:3306" - # environment: - # - MYSQL_DATABASE=mariadb - # - MYSQL_USER=mariadb - # - MYSQL_PASSWORD=changeme - # - MYSQL_RANDOM_ROOT_PASSWORD=yes - # volumes: - # - mariadb-data:/var/lib/mysql - # networks: - # - portabase - # # # db-mongodb-auth: # container_name: db-mongodb-auth @@ -105,67 +119,68 @@ services: # stdin_open: true # tty: true - db-redis: - image: redis:latest - container_name: db-redis - ports: - - "6379:6379" - volumes: - - redis-data:/data - command: [ "redis-server", "--appendonly", "yes" ] - networks: - - portabase - - db-redis-auth: - image: redis:latest - container_name: db-redis-auth - ports: - - "6380:6379" - volumes: - - redis-data-auth:/data - environment: - - REDIS_PASSWORD=supersecurepassword - command: [ "redis-server", "--requirepass", "supersecurepassword", "--appendonly", "yes" ] - networks: - - portabase - - db-valkey: - image: valkey/valkey - container_name: db-valkey - environment: - - ALLOW_EMPTY_PASSWORD=yes - ports: - - '6381:6379' - volumes: - - valkey-data:/data - networks: - - portabase - - db-valkey-auth: - image: valkey/valkey - container_name: db-valkey-auth - command: > - --requirepass "supersecurepassword" - ports: - - '6382:6379' - volumes: - - valkey-data-auth:/data - networks: - - portabase +# db-redis: +# image: redis:latest +# container_name: db-redis +# ports: +# - "6379:6379" +# volumes: +# - redis-data:/data +# command: [ "redis-server", "--appendonly", "yes" ] +# networks: +# - portabase +# +# db-redis-auth: +# image: redis:latest +# container_name: db-redis-auth +# ports: +# - "6380:6379" +# volumes: +# - redis-data-auth:/data +# environment: +# - REDIS_PASSWORD=supersecurepassword +# command: [ "redis-server", "--requirepass", "supersecurepassword", "--appendonly", "yes" ] +# networks: +# - portabase +# +# db-valkey: +# image: valkey/valkey +# container_name: db-valkey +# environment: +# - ALLOW_EMPTY_PASSWORD=yes +# ports: +# - '6381:6379' +# volumes: +# - valkey-data:/data +# networks: +# - portabase +# +# db-valkey-auth: +# image: valkey/valkey +# container_name: db-valkey-auth +# command: > +# --requirepass "supersecurepassword" +# ports: +# - '6382:6379' +# volumes: +# - valkey-data-auth:/data +# networks: +# - portabase volumes: cargo-registry: cargo-git: # cargo-target: - postgres-data: - # mariadb-data: - # mongodb-data: - # mongodb-data-auth: - # sqlite-data: - redis-data: - redis-data-auth: - valkey-data: - valkey-data-auth: +# postgres-data: + mariadb-data: + mysql-data: +# mongodb-data: +# mongodb-data-auth: +# sqlite-data: +# redis-data: +# redis-data-auth: +# valkey-data: +# valkey-data-auth: networks: portabase: diff --git a/docker/Dockerfile b/docker/Dockerfile index 0006a89..5ed6453 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -46,6 +46,7 @@ RUN if [ "$TARGETARCH" = "amd64" ]; then \ rm -rf /tmp/pg-x64 /tmp/pg-arm && \ chmod +x /usr/lib/postgresql/*/bin/* + # ========================= # MongoDB client binaries # ========================= diff --git a/src/domain/factory.rs b/src/domain/factory.rs index d3b532a..279f4a0 100644 --- a/src/domain/factory.rs +++ b/src/domain/factory.rs @@ -9,6 +9,7 @@ use crate::services::config::{DatabaseConfig, DbType}; use anyhow::Result; use std::path::{Path, PathBuf}; use std::sync::Arc; +use crate::domain::mariadb::database::MariaDBDatabase; #[async_trait::async_trait] pub trait Database: Send + Sync { @@ -28,7 +29,7 @@ impl DatabaseFactory { Arc::new(PostgresDatabase::new(cfg, format)) } DbType::Mysql => Arc::new(MySQLDatabase::new(cfg)), - DbType::Mariadb => Arc::new(MySQLDatabase::new(cfg)), + DbType::Mariadb => Arc::new(MariaDBDatabase::new(cfg)), DbType::MongoDB => Arc::new(MongoDatabase::new(cfg)), DbType::Sqlite => Arc::new(SqliteDatabase::new(cfg)), DbType::Redis => Arc::new(RedisDatabase::new(cfg)), @@ -43,7 +44,7 @@ impl DatabaseFactory { Arc::new(PostgresDatabase::new(cfg, format)) } DbType::Mysql => Arc::new(MySQLDatabase::new(cfg)), - DbType::Mariadb => Arc::new(MySQLDatabase::new(cfg)), + DbType::Mariadb => Arc::new(MariaDBDatabase::new(cfg)), DbType::MongoDB => Arc::new(MongoDatabase::new(cfg)), DbType::Sqlite => Arc::new(SqliteDatabase::new(cfg)), DbType::Redis => Arc::new(RedisDatabase::new(cfg)), diff --git a/src/domain/mariadb/backup.rs b/src/domain/mariadb/backup.rs new file mode 100644 index 0000000..ff323f1 --- /dev/null +++ b/src/domain/mariadb/backup.rs @@ -0,0 +1,66 @@ +use crate::domain::mariadb::connection::{select_mariadb_path, server_version}; +use crate::services::config::DatabaseConfig; +use anyhow::{Context, Result}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::process::Command; +use tracing::{debug, error, info}; + +pub async fn run( + cfg: DatabaseConfig, + backup_dir: PathBuf, + env: HashMap, + file_extension: &'static str, +) -> Result { + tokio::task::spawn_blocking(move || -> Result { + debug!("Starting backup for database {}", cfg.name); + + let version = match futures::executor::block_on(server_version(&cfg)) { + Ok(v) => { + debug!("Mariadb version detected: {}", v); + v + } + Err(e) => { + error!("Failed to get server version for {}: {:?}", cfg.name, e); + return Err(e.into()); + } + }; + + info!("Mariadb version found: {}", version); + + let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension)); + + let mariadb_dump = select_mariadb_path(&version).join("mariadb-dump"); + info!("Mariadb dump found: {}", mariadb_dump.display()); + + let output = Command::new("mariadb-dump") + .arg("--host").arg(&cfg.host) + .arg("--port").arg(cfg.port.to_string()) + .arg("--user").arg(&cfg.username) + .arg("--routines") + .arg("--events") + .arg("--triggers") + .arg("--single-transaction") + .arg("--quick") + .arg("--skip-lock-tables") + .arg("--add-drop-database") + .arg("--databases").arg(&cfg.database) + .arg("--compress") + .arg("--max-allowed-packet=512M") + .arg("--net-buffer-length=16K") + .arg("--default-character-set=utf8mb4") + .arg("-r").arg(&file_path) + .envs(env) + .output() + .with_context(|| format!("Failed to run mariadb-dump for {}", cfg.name))?; + + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("Mariadb backup failed for {}: {}", cfg.name, stderr); + } + + Ok(file_path) + }) + .await? +} diff --git a/src/domain/mariadb/connection.rs b/src/domain/mariadb/connection.rs new file mode 100644 index 0000000..11015e0 --- /dev/null +++ b/src/domain/mariadb/connection.rs @@ -0,0 +1,45 @@ +use std::path::PathBuf; +use crate::services::config::DatabaseConfig; +use anyhow::Result; +use std::process::Command; + +pub async fn server_version(cfg: &DatabaseConfig) -> Result { + let output = Command::new("mariadb") + .arg("--host") + .arg(&cfg.host) + .arg("--port") + .arg(cfg.port.to_string()) + .arg("--user") + .arg(&cfg.username) + .arg("-e") + .arg("SELECT VERSION();") + .env("MYSQL_PWD", &cfg.password) + .output()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("Version query failed: {}", stderr); + } + + let version = String::from_utf8_lossy(&output.stdout) + .lines() + .nth(1) + .unwrap_or_default() + .trim() + .to_string(); + + Ok(version) +} + + +pub fn select_mariadb_path(version: &str) -> PathBuf { + let mut parts = version.split('.'); + let major = parts.next().and_then(|v| v.parse::().ok()).unwrap_or(10); + let minor = parts.next().and_then(|v| v.parse::().ok()).unwrap_or(0); + + if major < 10 || (major == 10 && minor <= 6) { + "/usr/local/mariadb-10.6/bin".into() + } else { + "/usr/local/mariadb-12.1/bin".into() + } +} \ No newline at end of file diff --git a/src/domain/mariadb/database.rs b/src/domain/mariadb/database.rs new file mode 100644 index 0000000..37d04fc --- /dev/null +++ b/src/domain/mariadb/database.rs @@ -0,0 +1,55 @@ +use super::{backup, ping, restore}; +use crate::domain::factory::Database; +use crate::services::config::DatabaseConfig; +use crate::utils::locks::{DbOpLock, FileLock}; +use anyhow::Result; +use async_trait::async_trait; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +pub struct MariaDBDatabase { + cfg: DatabaseConfig, +} + +impl MariaDBDatabase { + pub fn new(cfg: DatabaseConfig) -> Self { + Self { cfg } + } + + fn build_env(&self) -> HashMap { + let mut envs = std::env::vars().collect::>(); + envs.insert("MYSQL_PWD".to_string(), self.cfg.password.to_string()); + envs + } +} + +#[async_trait] +impl Database for MariaDBDatabase { + fn file_extension(&self) -> &'static str { + ".sql" + } + + async fn ping(&self) -> Result { + ping::run(self.cfg.clone(), self.build_env().clone()).await + } + + async fn backup(&self, dir: &Path) -> Result { + FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?; + let res = backup::run( + self.cfg.clone(), + dir.to_path_buf(), + self.build_env().clone(), + self.file_extension(), + ) + .await; + FileLock::release(&self.cfg.generated_id).await?; + res + } + + async fn restore(&self, file: &Path) -> Result<()> { + FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?; + let res = restore::run(self.cfg.clone(), file.to_path_buf()).await; + FileLock::release(&self.cfg.generated_id).await?; + res + } +} diff --git a/src/domain/mariadb/mod.rs b/src/domain/mariadb/mod.rs new file mode 100644 index 0000000..33dd5f4 --- /dev/null +++ b/src/domain/mariadb/mod.rs @@ -0,0 +1,5 @@ +pub mod backup; +mod connection; +pub mod database; +mod ping; +mod restore; diff --git a/src/domain/mariadb/ping.rs b/src/domain/mariadb/ping.rs new file mode 100644 index 0000000..eec8013 --- /dev/null +++ b/src/domain/mariadb/ping.rs @@ -0,0 +1,28 @@ +use crate::services::config::DatabaseConfig; +use std::collections::HashMap; +use tokio::process::Command; +use tokio::time::{Duration, timeout}; + +pub async fn run(cfg: DatabaseConfig, env: HashMap) -> anyhow::Result { + let mut cmd = Command::new("mysqladmin"); + cmd.arg("--host") + .arg(cfg.host) + .arg("--port") + .arg(cfg.port.to_string()) + .arg("--user") + .arg(cfg.username) + .arg("ping") + .envs(env); + + let result = timeout(Duration::from_secs(10), cmd.output()).await; + + match result { + Ok(output) => { + let output = output?; + Ok(output.status.success()) + } + Err(_) => Ok(false), + } +} + + diff --git a/src/domain/mariadb/restore.rs b/src/domain/mariadb/restore.rs new file mode 100644 index 0000000..8433277 --- /dev/null +++ b/src/domain/mariadb/restore.rs @@ -0,0 +1,80 @@ +use crate::services::config::DatabaseConfig; +use anyhow::{Context, Result}; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::process::Command; +use tracing::{debug, error, info}; + +pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> { + let handle = tokio::task::spawn_blocking(move || -> Result<()> { + debug!("Starting restore for database {}", cfg.name); + + let mut sql_content = String::new(); + let mut file = File::open(&restore_file) + .with_context(|| format!("Failed to open restore file {}", restore_file.display()))?; + file.read_to_string(&mut sql_content) + .with_context(|| format!("Failed to read restore file {}", restore_file.display()))?; + + let drop_create_cmd = format!( + "DROP DATABASE IF EXISTS {0}; CREATE DATABASE {0};", + cfg.database + ); + + let drop_status = Command::new("mariadb") + .arg("--host") + .arg(&cfg.host) + .arg("--port") + .arg(cfg.port.to_string()) + .arg("--user") + .arg(&cfg.username) + .arg("-e") + .arg(&drop_create_cmd) + .env("MYSQL_PWD", &cfg.password) + .status() + .with_context(|| format!("Failed to drop/recreate database {}", cfg.name))?; + + if !drop_status.success() { + error!("Drop/create database failed for {}", cfg.name); + anyhow::bail!("Failed to drop/recreate database {}", cfg.name); + } + info!("Database {} dropped and recreated", cfg.name); + + let mut child = Command::new("mariadb") + .arg("--host") + .arg(&cfg.host) + .arg("--port") + .arg(cfg.port.to_string()) + .arg("--user") + .arg(&cfg.username) + .arg(&cfg.database) + .env("MYSQL_PWD", &cfg.password) + .stdin(std::process::Stdio::piped()) + .spawn() + .with_context(|| format!("Failed to start MariaDB restore for {}", cfg.name))?; + + let mut stdin = child.stdin.take().context("Failed to open child stdin")?; + stdin + .write_all(sql_content.as_bytes()) + .context("Failed to write SQL content to MariaDB stdin")?; + stdin.flush()?; + drop(stdin); + + let output = child + .wait_with_output() + .with_context(|| format!("Failed to complete MariaDB restore for {}", cfg.name))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + error!("MariaDB restore failed for {}: {}", cfg.name, stderr); + anyhow::bail!("MariaDB restore failed for {}", cfg.name); + } + + info!("Restore finished successfully for database {}", cfg.name); + Ok(()) + }); + + handle.await??; + + Ok(()) +} \ No newline at end of file diff --git a/src/domain/mod.rs b/src/domain/mod.rs index 534c955..ab005c3 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -5,3 +5,4 @@ pub mod postgres; mod redis; mod sqlite; mod valkey; +mod mariadb; diff --git a/src/domain/mysql/backup.rs b/src/domain/mysql/backup.rs index 2ec7531..85d2e2f 100644 --- a/src/domain/mysql/backup.rs +++ b/src/domain/mysql/backup.rs @@ -30,6 +30,9 @@ pub async fn run( let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension)); + // let mysql_dump = select_mysql_path(&version).join("mysqldump"); + // info!("MySQL dump found: {}", mysql_dump.display()); + let output = Command::new("mysqldump") .arg("--host") .arg(cfg.host) @@ -54,9 +57,12 @@ pub async fn run( if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); + info!("mysqldump stderr: {}", stderr); anyhow::bail!("MySQL backup failed for {}: {}", cfg.name, stderr); } + info!("Output {}", String::from_utf8_lossy(&output.stdout)); + Ok(file_path) }) .await? diff --git a/src/domain/mysql/ping.rs b/src/domain/mysql/ping.rs index fde0407..3521fee 100644 --- a/src/domain/mysql/ping.rs +++ b/src/domain/mysql/ping.rs @@ -4,7 +4,7 @@ use tokio::process::Command; use tokio::time::{Duration, timeout}; pub async fn run(cfg: DatabaseConfig, env: HashMap) -> anyhow::Result { - let mut cmd = Command::new("mysqladmin"); + let mut cmd = Command::new("mariadb-admin"); cmd.arg("--host") .arg(cfg.host) .arg("--port") From 254253d3a4b934ca86d296ef09d1c4caecba2f25 Mon Sep 17 00:00:00 2001 From: Charles GTE Date: Sun, 29 Mar 2026 09:27:36 +0200 Subject: [PATCH 2/2] feat: separate mysql and mariadb --- src/services/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/config.rs b/src/services/config.rs index d68989c..b1d5a3b 100644 --- a/src/services/config.rs +++ b/src/services/config.rs @@ -27,7 +27,7 @@ impl DbType { pub fn as_str(&self) -> &'static str { match self { DbType::Mysql => "mysql", - DbType::Mariadb => "mysql", + DbType::Mariadb => "mariadb", DbType::Postgresql => "postgresql", DbType::MongoDB => "mongodb", DbType::Sqlite => "sqlite",