use std::{path::Path, process::Command};
use miette::{Result, bail};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Strategy {
Btrfs,
ThinLvm,
Vss,
BaseBackup,
}
impl Strategy {
fn parse(name: &str) -> Result<Self> {
Ok(match name {
"btrfs" => Strategy::Btrfs,
"thin-lvm" | "thinlvm" | "lvm" => Strategy::ThinLvm,
"vss" => Strategy::Vss,
"basebackup" | "base-backup" | "pg_basebackup" => Strategy::BaseBackup,
other => bail!("unknown backup strategy override '{other}'"),
})
}
}
fn segtype_is_thin(segtype: &str) -> bool {
segtype.trim() == "thin"
}
pub fn detect(strategy_override: Option<&str>, data_dir: &Path) -> Result<Strategy> {
if let Some(name) = strategy_override {
return Strategy::parse(name);
}
if cfg!(windows) {
return Ok(Strategy::Vss);
}
if findmnt_field("FSTYPE", data_dir)? == "btrfs" {
return Ok(Strategy::Btrfs);
}
if is_thin_lvm(data_dir) {
return Ok(Strategy::ThinLvm);
}
Ok(Strategy::BaseBackup)
}
fn is_thin_lvm(data_dir: &Path) -> bool {
let Ok(source) = findmnt_field("SOURCE", data_dir) else {
return false;
};
let Ok(output) = Command::new("lvs")
.args(["--noheadings", "-o", "segtype", &source])
.output()
else {
return false;
};
output.status.success() && segtype_is_thin(&String::from_utf8_lossy(&output.stdout))
}
fn findmnt_field(field: &str, path: &Path) -> Result<String> {
let output = Command::new("findmnt")
.args(["-no", field, "--target"])
.arg(path)
.output()
.map_err(|e| miette::miette!("running findmnt for {}: {e}", path.display()))?;
if !output.status.success() {
bail!(
"findmnt {field} failed for {}: {}",
path.display(),
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thin_segtype_detection() {
assert!(segtype_is_thin("thin"));
assert!(segtype_is_thin(" thin "));
assert!(!segtype_is_thin("linear"));
assert!(!segtype_is_thin("striped"));
}
#[test]
fn override_parsing() {
assert_eq!(Strategy::parse("btrfs").unwrap(), Strategy::Btrfs);
assert_eq!(Strategy::parse("basebackup").unwrap(), Strategy::BaseBackup);
assert!(Strategy::parse("nonsense").is_err());
}
#[test]
fn override_takes_precedence() {
assert_eq!(
detect(Some("btrfs"), Path::new("/nonexistent")).unwrap(),
Strategy::Btrfs
);
}
}