use std::path::{Path, PathBuf};
use miette::Result;
#[cfg(target_os = "linux")]
use miette::{Context as _, IntoDiagnostic as _, bail};
#[derive(Debug)]
pub enum Cleanup {
#[cfg(not(target_os = "linux"))]
Nothing,
#[cfg(target_os = "linux")]
Bindfs(PathBuf),
#[cfg(target_os = "linux")]
Copy(PathBuf),
}
pub async fn prepare(source: &Path, backup_type: &str) -> Result<(PathBuf, Cleanup)> {
#[cfg(target_os = "linux")]
{
prepare_linux(source, backup_type).await
}
#[cfg(not(target_os = "linux"))]
{
let _ = backup_type;
Ok((source.to_path_buf(), Cleanup::Nothing))
}
}
pub async fn teardown(cleanup: Cleanup) -> Result<()> {
match cleanup {
#[cfg(not(target_os = "linux"))]
Cleanup::Nothing => Ok(()),
#[cfg(target_os = "linux")]
Cleanup::Bindfs(view) => {
unmount(&view).await;
let _ = tokio::fs::remove_dir_all(&view).await;
Ok(())
}
#[cfg(target_os = "linux")]
Cleanup::Copy(dir) => {
let _ = tokio::process::Command::new("chown")
.args(["-R", "root:root"])
.arg(&dir)
.status()
.await;
tokio::fs::remove_dir_all(&dir)
.await
.into_diagnostic()
.wrap_err_with(|| format!("removing simple backup copy at {}", dir.display()))
}
}
}
#[cfg(target_os = "linux")]
fn view_dir(backup_type: &str) -> PathBuf {
dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/var/cache"))
.join("bestool")
.join("backup-source")
.join(backup_type)
}
#[cfg(target_os = "linux")]
async fn prepare_linux(source: &Path, backup_type: &str) -> Result<(PathBuf, Cleanup)> {
use bestool_kopia::LINUX_KOPIA_USER;
use tokio::process::Command;
let view = view_dir(backup_type);
unmount(&view).await;
if view.exists() {
let _ = tokio::fs::remove_dir_all(&view).await;
}
tokio::fs::create_dir_all(&view)
.await
.into_diagnostic()
.wrap_err_with(|| format!("creating simple backup view dir {}", view.display()))?;
if let Some(parent) = view.parent() {
use std::os::unix::fs::PermissionsExt as _;
tokio::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o755))
.await
.into_diagnostic()
.wrap_err_with(|| format!("making {} traversable", parent.display()))?;
}
match Command::new("bindfs")
.arg("-r")
.arg(format!("--force-user={LINUX_KOPIA_USER}"))
.arg(format!("--force-group={LINUX_KOPIA_USER}"))
.arg("-o")
.arg("allow_other")
.arg(source)
.arg(&view)
.status()
.await
{
Ok(status) if status.success() => {
tracing::info!(source = %source.display(), view = %view.display(), "bindfs view for simple backup");
return Ok((view.clone(), Cleanup::Bindfs(view)));
}
Ok(status) => {
tracing::warn!(%status, "bindfs failed; copying the source instead");
unmount(&view).await;
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
tracing::warn!("bindfs not installed; copying the source instead");
}
Err(err) => return Err(err).into_diagnostic().wrap_err("spawning bindfs"),
}
copy_to_kopia(source, &view).await?;
Ok((view.clone(), Cleanup::Copy(view)))
}
#[cfg(target_os = "linux")]
async fn copy_to_kopia(source: &Path, dest: &Path) -> Result<()> {
use bestool_kopia::LINUX_KOPIA_USER;
use tokio::process::Command;
let status = Command::new("cp")
.arg("-a")
.arg("--reflink=auto")
.arg(format!("{}/.", source.display()))
.arg(dest)
.status()
.await
.into_diagnostic()
.wrap_err("spawning cp for the simple backup copy")?;
if !status.success() {
bail!("copying simple backup source {} failed ({status})", source.display());
}
let status = Command::new("chown")
.arg("-R")
.arg(format!("{LINUX_KOPIA_USER}:{LINUX_KOPIA_USER}"))
.arg(dest)
.status()
.await
.into_diagnostic()
.wrap_err("chowning the simple backup copy to the kopia user")?;
if !status.success() {
bail!("chowning the simple backup copy to {LINUX_KOPIA_USER} failed ({status})");
}
Ok(())
}
#[cfg(target_os = "linux")]
async fn unmount(view: &Path) {
let unmounted = tokio::process::Command::new("fusermount")
.arg("-u")
.arg(view)
.status()
.await
.map(|s| s.success())
.unwrap_or(false);
if !unmounted {
let _ = tokio::process::Command::new("umount")
.arg(view)
.status()
.await;
}
}