use std::path::{Path, PathBuf};
use miette::{Context as _, IntoDiagnostic as _, Result, bail};
use tracing::{info, warn};
use super::resolve::ResolvedCluster;
const ALIAS: &str = "bestoolpg";
fn backup_root(data_dir: &Path) -> &Path {
match data_dir.parent() {
Some(parent) if parent.join("bin").is_dir() => parent,
_ => data_dir,
}
}
#[derive(Debug)]
pub struct Shadow {
expose_target: String,
metadata: PathBuf,
}
fn volume_of(path: &str) -> Result<&str> {
let bytes = path.as_bytes();
if bytes.len() >= 2 && bytes[1] == b':' {
Ok(&path[..2])
} else {
bail!("cannot determine the volume of Windows path {path:?}")
}
}
fn relative_to_volume<'a>(data_dir: &'a str, volume: &str) -> &'a str {
data_dir
.strip_prefix(volume)
.unwrap_or(data_dir)
.trim_start_matches(['\\', '/'])
}
fn expose_target_dir(volume: &str, backup_type: &str) -> String {
format!("{volume}\\bestool-backup-shadow\\{backup_type}")
}
fn kopia_source(expose_target: &str, rel: &str) -> String {
format!("{expose_target}\\{rel}")
}
fn create_script(volume: &str, expose_target: &str, metadata: &str) -> String {
format!(
"set context persistent\n\
set metadata \"{metadata}\"\n\
set verbose on\n\
begin backup\n\
add volume {volume} alias {ALIAS}\n\
create\n\
expose %{ALIAS}% \"{expose_target}\"\n\
end backup\n"
)
}
fn delete_script(expose_target: &str) -> String {
format!("delete shadows exposed \"{expose_target}\"\n")
}
pub async fn prepare(resolved: &ResolvedCluster, backup_type: &str) -> Result<(PathBuf, Shadow)> {
let root = backup_root(&resolved.data_dir).to_string_lossy().into_owned();
let volume = volume_of(&root)?.to_owned();
let rel = relative_to_volume(&root, &volume).to_owned();
let expose_target = expose_target_dir(&volume, backup_type);
let metadata = std::env::temp_dir().join(format!("bestool-vss-{}.cab", std::process::id()));
reap_stale(&expose_target).await;
info!(volume = %volume, expose_target = %expose_target, "creating VSS shadow copy");
run_diskshadow(&create_script(&volume, &expose_target, &metadata.to_string_lossy()))
.await
.wrap_err("creating VSS shadow copy")?;
let source = PathBuf::from(kopia_source(&expose_target, &rel));
Ok((
source,
Shadow {
expose_target,
metadata,
},
))
}
pub async fn teardown(shadow: Shadow) -> Result<()> {
if let Err(err) = run_diskshadow(&delete_script(&shadow.expose_target)).await {
warn!("deleting VSS shadow copy failed: {err}");
}
let _ = tokio::fs::remove_file(&shadow.metadata).await;
Ok(())
}
async fn reap_stale(expose_target: &str) {
let _ = run_diskshadow(&delete_script(expose_target)).await;
}
async fn run_diskshadow(script: &str) -> Result<()> {
let script_file =
std::env::temp_dir().join(format!("bestool-diskshadow-{}.txt", std::process::id()));
tokio::fs::write(&script_file, script)
.await
.into_diagnostic()
.wrap_err("writing diskshadow script")?;
let output = tokio::process::Command::new("diskshadow")
.arg("/s")
.arg(&script_file)
.stdin(std::process::Stdio::null())
.output()
.await
.into_diagnostic()
.wrap_err("spawning diskshadow");
let _ = tokio::fs::remove_file(&script_file).await;
let output = output?;
if !output.status.success() {
bail!(
"diskshadow failed ({}): {}",
output.status,
String::from_utf8_lossy(&output.stdout).trim()
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn volume_and_relative_path() {
assert_eq!(volume_of("C:\\Tamanu\\data").unwrap(), "C:");
assert_eq!(volume_of("D:\\pg").unwrap(), "D:");
assert!(volume_of("\\\\server\\share").is_err());
assert_eq!(relative_to_volume("C:\\Tamanu\\data", "C:"), "Tamanu\\data");
assert_eq!(relative_to_volume("C:\\pg", "C:"), "pg");
}
#[test]
fn stable_expose_target_and_source() {
let target = expose_target_dir("C:", "tamanu-postgres");
assert_eq!(target, "C:\\bestool-backup-shadow\\tamanu-postgres");
assert_eq!(
kopia_source(&target, "Tamanu\\data"),
"C:\\bestool-backup-shadow\\tamanu-postgres\\Tamanu\\data"
);
}
#[test]
fn create_script_exposes_a_persistent_shadow() {
let script = create_script("C:", "C:\\shadow\\pg", "C:\\meta.cab");
assert!(script.contains("set context persistent"));
assert!(script.contains("add volume C: alias bestoolpg"));
assert!(script.contains("expose %bestoolpg% \"C:\\shadow\\pg\""));
assert!(script.contains("set metadata \"C:\\meta.cab\""));
}
#[test]
fn backup_root_is_the_install_dir_on_the_edb_layout() {
let tmp = tempfile::tempdir().unwrap();
let install = tmp.path().join("18");
let data = install.join("data");
std::fs::create_dir_all(install.join("bin")).unwrap();
std::fs::create_dir_all(&data).unwrap();
assert_eq!(backup_root(&data), install);
let bare = tmp.path().join("bare").join("data");
std::fs::create_dir_all(&bare).unwrap();
assert_eq!(backup_root(&bare), bare);
}
#[test]
fn delete_script_targets_the_exposed_path() {
assert_eq!(
delete_script("C:\\shadow\\pg"),
"delete shadows exposed \"C:\\shadow\\pg\"\n"
);
}
}