use std::path::{Path, PathBuf};
use miette::{Result, miette};
use tracing::{info, warn};
use super::{resolve::ResolvedCluster, sys};
const TOPLEVEL_MOUNT_DIR: &str = "/var/lib/bestool";
const TOPLEVEL_MOUNT_PREFIX: &str = "/var/lib/bestool/bestool-btrfs-toplevel";
const SNAPSHOT_INFIX: &str = "bestool-kopia-";
#[derive(Debug)]
pub struct Mounts {
toplevel_mount: PathBuf,
snapshot_path: PathBuf,
kopia_mount: PathBuf,
}
fn stable_kopia_mount(backup_type: &str) -> PathBuf {
super::stable_source_dir(backup_type)
}
fn snapshot_name(token: &str) -> String {
format!("{SNAPSHOT_INFIX}{token}")
}
fn toplevel_mount(token: &str) -> PathBuf {
PathBuf::from(format!("{TOPLEVEL_MOUNT_PREFIX}.{token}"))
}
pub async fn prepare(resolved: &ResolvedCluster, backup_type: &str) -> Result<(PathBuf, Mounts)> {
let token = sys::run_token();
let base_mount = sys::findmnt_target(&resolved.data_dir).await?;
let rel = sys::relative_data_path(&resolved.data_dir, &base_mount)?;
let fsdev = format!(
"/dev/disk/by-uuid/{}",
sys::findmnt_field("UUID", &resolved.data_dir).await?
);
let map = sys::postgres_to_kopia_idmap().await?;
let kopia_mount = stable_kopia_mount(backup_type);
reap_stale(&fsdev, &kopia_mount).await;
let toplevel_mount = toplevel_mount(&token);
let snapshot_name = snapshot_name(&token);
let snapshot_path = toplevel_mount.join(&snapshot_name);
let mut mounts = Mounts {
toplevel_mount: toplevel_mount.clone(),
snapshot_path: PathBuf::new(),
kopia_mount: PathBuf::new(),
};
sys::mkdir(&toplevel_mount).await?;
sys::run_ok(
"mount",
&["-o", "subvolid=5", &fsdev, sys::path(&toplevel_mount)],
)
.await?;
info!(snapshot = %snapshot_path.display(), "creating read-only btrfs snapshot");
sys::run_ok(
"btrfs",
&[
"subvolume",
"snapshot",
"-r",
sys::path(&base_mount),
sys::path(&snapshot_path),
],
)
.await?;
mounts.snapshot_path = snapshot_path.clone();
sys::mkdir(&kopia_mount).await?;
if let Some(parent) = kopia_mount.parent() {
sys::make_traversable(parent).await?;
}
sys::run_ok(
"mount",
&[
&fsdev,
sys::path(&kopia_mount),
"-o",
&format!("subvol={snapshot_name},X-mount.idmap={map}"),
],
)
.await?;
mounts.kopia_mount = kopia_mount.clone();
Ok((kopia_mount.join(rel), mounts))
}
pub async fn teardown(mounts: Mounts) -> Result<()> {
if !mounts.kopia_mount.as_os_str().is_empty() {
sys::umount(&mounts.kopia_mount).await;
sys::rmdir(&mounts.kopia_mount).await;
}
if !mounts.snapshot_path.as_os_str().is_empty() {
let _ = sys::run_ok(
"btrfs",
&["subvolume", "delete", sys::path(&mounts.snapshot_path)],
)
.await
.map_err(|err| miette!("deleting snapshot subvolume: {err}"))
.inspect_err(|err| warn!("{err}"));
}
if !mounts.toplevel_mount.as_os_str().is_empty() {
sys::umount(&mounts.toplevel_mount).await;
sys::rmdir(&mounts.toplevel_mount).await;
}
Ok(())
}
async fn reap_stale(fsdev: &str, kopia_mount: &Path) {
sys::umount(kopia_mount).await;
if let Ok(entries) = sys::glob_prefix(TOPLEVEL_MOUNT_DIR, "bestool-btrfs-toplevel.") {
for stale in entries {
sys::umount(&stale).await;
sys::rmdir(&stale).await;
}
}
let reap_mount = PathBuf::from(format!("{TOPLEVEL_MOUNT_PREFIX}.reap-{}", sys::run_token()));
if sys::mkdir(&reap_mount).await.is_ok()
&& sys::run_ok("mount", &["-o", "subvolid=5", fsdev, sys::path(&reap_mount)])
.await
.is_ok()
{
if let Ok(subs) = sys::glob_prefix(sys::path(&reap_mount), SNAPSHOT_INFIX) {
for sub in subs {
let _ = sys::run_ok("btrfs", &["subvolume", "delete", sys::path(&sub)]).await;
}
}
sys::umount(&reap_mount).await;
}
sys::rmdir(&reap_mount).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapshot_name_carries_reaper_infix() {
let name = snapshot_name("deadbeef");
assert_eq!(name, "bestool-kopia-deadbeef");
assert!(name.starts_with(SNAPSHOT_INFIX));
}
#[test]
fn stable_mount_is_per_type_and_fixed() {
assert_eq!(
stable_kopia_mount("tamanu-postgres"),
super::super::stable_source_dir("tamanu-postgres")
);
}
#[test]
fn toplevel_mount_path() {
assert_eq!(
toplevel_mount("cafef00d"),
PathBuf::from("/var/lib/bestool/bestool-btrfs-toplevel.cafef00d")
);
}
#[test]
fn reaper_glob_matches_toplevel_and_reap_mounts() {
let file = |p: &PathBuf| p.file_name().unwrap().to_string_lossy().into_owned();
assert!(file(&toplevel_mount("abc")).starts_with("bestool-btrfs-toplevel."));
let reap = PathBuf::from(format!("{TOPLEVEL_MOUNT_PREFIX}.reap-abc"));
assert!(file(&reap).starts_with("bestool-btrfs-toplevel."));
}
}