use super::guard::Guard;
use super::{bracket, exec_batch, MsClient, MssqlTarget};
use anyhow::{bail, Context, Result};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub struct BackupOptions {
pub server_dir: String, pub compression: bool,
pub stripes: usize,
pub file_name: String, pub to_url: Option<String>,
}
pub struct BackupResult {
pub server_paths: Vec<String>,
pub bytes: u64,
pub seconds: f64,
}
fn join_server_path(dir: &str, name: &str) -> String {
let sep = if dir.contains('\\') && !dir.contains('/') { "\\" } else { "/" };
format!("{}{sep}{name}", dir.trim_end_matches(['/', '\\']))
}
pub async fn backup(client: &mut MsClient, database: &str, o: &BackupOptions) -> Result<BackupResult> {
let t0 = std::time::Instant::now();
if let Some(url) = &o.to_url {
let opts = format!("FORMAT, CHECKSUM, STATS = 10, MAXTRANSFERSIZE = 20971520{}", if o.compression { ", COMPRESSION" } else { "" });
let sql = format!("BACKUP DATABASE {} TO URL = N'{}' WITH {opts}, NAME = N'{} full (duckbridge)'", bracket(database), url.replace('\'', "''"), database.replace('\'', "''"));
exec_batch(client, &sql).await.context("BACKUP DATABASE TO URL failed (SQL Server 2022+ with a matching CREDENTIAL is required)")?;
return Ok(BackupResult { server_paths: vec![url.clone()], bytes: 0, seconds: t0.elapsed().as_secs_f64() });
}
let paths: Vec<String> = if o.stripes <= 1 {
vec![join_server_path(&o.server_dir, &o.file_name)]
} else {
let stem = o.file_name.trim_end_matches(".bak");
(1..=o.stripes).map(|i| join_server_path(&o.server_dir, &format!("{stem}_{i}of{}.bak", o.stripes))).collect()
};
let disks: Vec<String> = paths.iter().map(|p| format!("DISK = N'{}'", p.replace('\'', "''"))).collect();
let opts = format!("FORMAT, INIT, CHECKSUM, STATS = 10{}", if o.compression { ", COMPRESSION" } else { "" });
let sql = format!("BACKUP DATABASE {} TO {} WITH {opts}, NAME = N'{} full (duckbridge)'", bracket(database), disks.join(", "), database.replace('\'', "''"));
exec_batch(client, &sql).await.context("BACKUP DATABASE failed")?;
let rows = client.simple_query(&format!(
"SELECT TOP 1 CAST(backup_size AS BIGINT), CAST(ISNULL(compressed_backup_size, backup_size) AS BIGINT) FROM msdb.dbo.backupset WHERE database_name = N'{}' ORDER BY backup_finish_date DESC",
database.replace('\'', "''"))).await?.into_first_result().await?;
let bytes: u64 = rows.first().and_then(|r| r.get::<i64, _>(1)).unwrap_or(0) as u64;
Ok(BackupResult { server_paths: paths, bytes, seconds: t0.elapsed().as_secs_f64() })
}
#[derive(Debug, Clone)]
pub enum Fetch {
Local,
Docker(String),
None,
}
pub fn fetch(server_paths: &[String], how: &Fetch, local_dir: &Path) -> Result<Vec<PathBuf>> {
std::fs::create_dir_all(local_dir)?;
let mut out = vec![];
for sp in server_paths {
let name = sp.rsplit(['/', '\\']).next().unwrap_or(sp).to_string();
let local = local_dir.join(&name);
match how {
Fetch::None => out.push(PathBuf::from(sp)),
Fetch::Local => {
if Path::new(sp) != local { std::fs::copy(sp, &local).with_context(|| format!("copying {sp} (is the server really local? use --fetch docker:<name> otherwise)"))?; }
out.push(local);
}
Fetch::Docker(name) => {
let st = std::process::Command::new("docker").args(["cp", &format!("{name}:{sp}"), local.to_string_lossy().as_ref()]).status().context("running docker cp")?;
if !st.success() { bail!("docker cp failed for {sp}"); }
out.push(local);
}
}
}
Ok(out)
}
pub async fn verify_restore(target: &MssqlTarget, server_paths: &[String], new_db: &str, keep: bool, guard: &Guard) -> Result<BTreeMap<String, i64>> {
guard.assert_disposable(new_db, "RESTORE DATABASE (verification copy)")?;
let mut c = target.connect("master").await?;
let disks: Vec<String> = server_paths.iter().map(|p| format!("DISK = N'{}'", p.replace('\'', "''"))).collect();
let files = c.simple_query(&format!("RESTORE FILELISTONLY FROM {}", disks.join(", "))).await?.into_first_result().await?;
let data_dir: String = c.simple_query("SELECT CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS NVARCHAR(4000))").await?.into_first_result().await?
.first().and_then(|r| r.get::<&str, _>(0).map(|s| s.to_string())).unwrap_or_else(|| "/var/opt/mssql/data/".into());
let sep = if data_dir.contains('\\') { "\\" } else { "/" };
let data_dir = data_dir.trim_end_matches(['/', '\\']).to_string();
let mut moves = vec![];
for (i, f) in files.iter().enumerate() {
let logical: &str = f.get(0).unwrap_or("");
let ftype: &str = f.get(2).unwrap_or("D");
let ext = if ftype == "L" { "ldf" } else { "mdf" };
moves.push(format!("MOVE N'{}' TO N'{data_dir}{sep}{new_db}_{i}.{ext}'", logical.replace('\'', "''")));
}
guard.drop_database(&mut c, new_db).await?;
exec_batch(&mut c, &format!("RESTORE DATABASE {} FROM {} WITH {}, REPLACE, CHECKSUM", bracket(new_db), disks.join(", "), moves.join(", "))).await.context("RESTORE DATABASE failed")?;
let mut d = target.connect(new_db).await?;
let rows = d.simple_query(
"SELECT s.name, t.name, SUM(p.rows) FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0,1) GROUP BY s.name, t.name"
).await?.into_first_result().await?;
let mut counts = BTreeMap::new();
for r in rows {
let s: &str = r.get(0).unwrap_or("");
let t: &str = r.get(1).unwrap_or("");
let n: i64 = r.get::<i64, _>(2).unwrap_or(0);
counts.insert(format!("{s}.{t}"), n);
}
drop(d);
if !keep { guard.drop_database(&mut c, new_db).await?; }
Ok(counts)
}