Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ services:
- ./databases.json:/config/config.json
#- ./databases.toml:/config/config.toml
- /var/run/docker.sock:/var/run/docker.sock
# - cargo-target:/app/target
# - cargo-target:/app/target
- databases_sqlite-data:/sqlite-data/workspace/data
- ./scripts/sqlite/test-db:/sqlite-data-2/workspace/data
# - /bigdisk:/scratch
# - /bigdisk:/scratch
environment:
APP_ENV: development
LOG: debug
TZ: "Europe/Paris"
# TMPDIR: /scratch
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiYTQyNjQzNTQtZGE3Ni00OWFkLWJkYjctZDVjMjMwYzhjYmViIiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
# TMPDIR: /scratch
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiMjcyMWFiNGQtZDkzNi00NmFkLWE1MWEtOWVhYTk3ZjJhMjAwIiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
Comment thread
RambokDev marked this conversation as resolved.
#CHUNK_SIZE_MB: "1"
#POOLING: 1
#RETRY_ATTEMPTS: 3
Expand All @@ -40,7 +40,7 @@ services:
volumes:
cargo-registry:
cargo-git:
# cargo-target:
# cargo-target:
databases_sqlite-data:
external: true

Expand Down
22 changes: 19 additions & 3 deletions src/domain/mssql/backup.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::format::MssqlFormat;
use crate::services::backup::logger::JobLogger;
use crate::services::config::DatabaseConfig;
use anyhow::{Context, Result};
Expand All @@ -13,7 +14,12 @@ pub async fn run(
logger: Arc<JobLogger>,
) -> Result<PathBuf> {
tokio::task::spawn_blocking(move || -> Result<PathBuf> {
logger.log("info", format!("Starting MSSQL backup for database {}", cfg.name));
let (format, unknown_format) = MssqlFormat::from_config(&cfg);
if let Some(v) = unknown_format {
logger.log("warn", format!("Unknown backup_format '{}' for {}, falling back to 'dacpac'", v, cfg.name));
}

logger.log("info", format!("Starting MSSQL {} backup for database {}", &format.extension()[1..], cfg.name));

let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension));
let connection_string = format!(
Expand All @@ -24,8 +30,18 @@ pub async fn run(
logger.log("info", format!("MSSQL backup: {}:{}/{} → {}", cfg.host, cfg.port, cfg.database, file_path.display()));

let start = Instant::now();
let output = Command::new("sqlpackage")
.arg("/a:Export")
let mut command = Command::new("sqlpackage");
match format {
MssqlFormat::Dacpac => command.args([
"/a:Extract",
"/p:ExtractAllTableData=True",
"/p:ExtractReferencedServerScopedElements=False",
"/p:IgnorePermissions=False",
"/p:IgnoreUserLoginMappings=False",
]),
MssqlFormat::Bacpac => command.arg("/a:Export"),
};
let output = command
.arg(format!("/scs:{}", connection_string))
.arg(format!("/tf:{}", file_path.display()))
.output()
Expand Down
41 changes: 39 additions & 2 deletions src/domain/mssql/connection.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
use crate::services::config::DatabaseConfig;
use anyhow::Result;
use anyhow::{Context, Result};
use tiberius::{AuthMethod, Client, Config};
use tokio::net::TcpStream;
use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};

pub async fn build_client(cfg: &DatabaseConfig) -> Result<Client<Compat<TcpStream>>> {
const DROP_DATABASE_SQL: &str = "DECLARE @q nvarchar(258) = QUOTENAME(@P1);
IF DB_ID(@P1) IS NOT NULL
BEGIN
IF DB_ID(@P1) <= 4
THROW 50000, N'Refusing to restore over a system database; nothing was dropped', 1;
IF ISNULL(HAS_PERMS_BY_NAME(NULL, 'DATABASE', 'CREATE DATABASE'), 0) = 0
THROW 50000, N'Restore needs CREATE DATABASE permission (dbcreator) to recreate the target database; nothing was dropped', 1;
DECLARE @single bit = 0;
IF CAST(SERVERPROPERTY('EngineEdition') AS int) <> 5
BEGIN
EXEC(N'ALTER DATABASE ' + @q + N' SET SINGLE_USER WITH ROLLBACK IMMEDIATE');
SET @single = 1;
END
BEGIN TRY
EXEC(N'DROP DATABASE ' + @q);
END TRY
BEGIN CATCH
IF @single = 1 AND DB_ID(@P1) IS NOT NULL
EXEC(N'ALTER DATABASE ' + @q + N' SET MULTI_USER');
THROW;
END CATCH
END";

pub async fn build_client(cfg: &DatabaseConfig, database: Option<&str>) -> Result<Client<Compat<TcpStream>>> {
let mut config = Config::new();
config.host(&cfg.host);
config.port(cfg.port);
if let Some(database) = database {
config.database(database);
}
config.authentication(AuthMethod::sql_server(&cfg.username, &cfg.password));
config.trust_cert();

Expand All @@ -16,3 +42,14 @@ pub async fn build_client(cfg: &DatabaseConfig) -> Result<Client<Compat<TcpStrea
let client = Client::connect(config, tcp.compat_write()).await?;
Ok(client)
}

pub async fn drop_database(cfg: &DatabaseConfig) -> Result<()> {
let mut client = build_client(cfg, Some("master"))
.await
.with_context(|| format!("Failed to connect to master for {}", cfg.name))?;
client
.execute(DROP_DATABASE_SQL, &[&cfg.database.as_str()])
.await
.with_context(|| format!("Failed to drop MSSQL database {}", cfg.database))?;
Ok(())
}
3 changes: 2 additions & 1 deletion src/domain/mssql/database.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::format::MssqlFormat;
use super::{backup, ping, restore};
use crate::domain::factory::Database;
use crate::services::backup::logger::JobLogger;
Expand All @@ -21,7 +22,7 @@ impl MssqlDatabase {
#[async_trait]
impl Database for MssqlDatabase {
fn file_extension(&self) -> &'static str {
".bacpac"
MssqlFormat::from_config(&self.cfg).0.extension()
}

async fn ping(&self) -> Result<bool> {
Expand Down
33 changes: 33 additions & 0 deletions src/domain/mssql/format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use crate::services::config::DatabaseConfig;
use std::path::Path;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MssqlFormat {
Dacpac,
Bacpac,
}

impl MssqlFormat {
pub fn from_config(cfg: &DatabaseConfig) -> (Self, Option<String>) {
match cfg.options.get("backup_format").and_then(|v| v.as_str()) {
None | Some("dacpac") => (Self::Dacpac, None),
Some("bacpac") => (Self::Bacpac, None),
Some(other) => (Self::Dacpac, Some(other.to_string())),
}
}

pub fn from_path(path: &Path) -> Option<Self> {
match path.extension()?.to_str()?.to_ascii_lowercase().as_str() {
"dacpac" => Some(Self::Dacpac),
"bacpac" => Some(Self::Bacpac),
_ => None,
}
}

pub fn extension(self) -> &'static str {
match self {
Self::Dacpac => ".dacpac",
Self::Bacpac => ".bacpac",
}
}
}
1 change: 1 addition & 0 deletions src/domain/mssql/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod database;
pub(crate) mod format;
mod connection;
mod ping;
mod backup;
Expand Down
2 changes: 1 addition & 1 deletion src/domain/mssql/ping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use tracing::{error, info};
pub async fn run(cfg: DatabaseConfig) -> Result<bool> {
info!("Running ping for MSSQL database {}", cfg.name);

match super::connection::build_client(&cfg).await {
match super::connection::build_client(&cfg, None).await {
Ok(mut client) => match client.simple_query("SELECT 1").await {
Ok(_) => {
info!("MSSQL ping succeeded for {}", cfg.name);
Expand Down
27 changes: 23 additions & 4 deletions src/domain/mssql/restore.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::format::MssqlFormat;
use crate::services::backup::logger::JobLogger;
use crate::services::config::DatabaseConfig;
use anyhow::{Context, Result};
Expand All @@ -7,9 +8,23 @@ use std::sync::Arc;
use std::time::Instant;

pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf, logger: Arc<JobLogger>) -> Result<()> {
tokio::task::spawn_blocking(move || -> Result<()> {
logger.log("debug", format!("Starting MSSQL restore for database {}", cfg.name));
logger.log("debug", format!("Starting MSSQL restore for database {}", cfg.name));

let Some(format) = MssqlFormat::from_path(&restore_file) else {
let message = format!(
"Unsupported MSSQL backup file {}: expected .bacpac or .dacpac",
restore_file.display()
);
logger.log("error", message.clone());
anyhow::bail!(message);
};

logger.log("info", format!("Replacing MSSQL database {} (drop if it exists, then recreate from the backup)", cfg.database));
let start = Instant::now();
super::connection::drop_database(&cfg).await?;
logger.log_command(format!("DROP DATABASE IF EXISTS [{}]", cfg.database), None, Some(0), Some(start.elapsed().as_millis() as f64));
Comment thread
RambokDev marked this conversation as resolved.

tokio::task::spawn_blocking(move || -> Result<()> {
let connection_string = format!(
"Server=tcp:{},{};Database={};User Id={};Password={};TrustServerCertificate=True;Encrypt=False",
cfg.host, cfg.port, cfg.database, cfg.username, cfg.password
Expand All @@ -24,8 +39,12 @@ pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf, logger: Arc<JobLogg
));

let start = Instant::now();
let output = Command::new("sqlpackage")
.arg("/a:Import")
let mut command = Command::new("sqlpackage");
match format {
MssqlFormat::Dacpac => command.args(["/a:Publish", "/p:AllowIncompatiblePlatform=True"]),
MssqlFormat::Bacpac => command.arg("/a:Import"),
};
let output = command
.arg(format!("/tcs:{}", connection_string))
.arg(format!("/sf:{}", restore_file.display()))
.output()
Expand Down
Loading
Loading