use std::{
path::{Path, PathBuf},
process::Stdio,
};
use miette::{Context as _, IntoDiagnostic as _, Result, bail, miette};
pub(super) fn path(p: &Path) -> &str {
p.to_str().unwrap_or_default()
}
pub(super) async fn run_ok(program: &str, args: &[&str]) -> Result<()> {
let output = tokio::process::Command::new(program)
.args(args)
.stdin(Stdio::null())
.output()
.await
.into_diagnostic()
.wrap_err_with(|| format!("spawning {program}"))?;
if !output.status.success() {
bail!(
"{program} {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(())
}
pub(super) async fn capture(program: &str, args: &[&str]) -> Result<String> {
let output = tokio::process::Command::new(program)
.args(args)
.stdin(Stdio::null())
.output()
.await
.into_diagnostic()
.wrap_err_with(|| format!("spawning {program}"))?;
if !output.status.success() {
bail!(
"{program} {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&output.stderr).trim()
);
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}
pub(super) async fn findmnt_target(data_dir: &Path) -> Result<PathBuf> {
Ok(PathBuf::from(findmnt_field("TARGET", data_dir).await?))
}
pub(super) async fn findmnt_field(field: &str, data_dir: &Path) -> Result<String> {
capture("findmnt", &["-no", field, "--target", path(data_dir)]).await
}
pub(super) async fn uid_of(user: &str) -> Result<u32> {
parse_id(&capture("id", &["-u", user]).await?, user)
}
pub(super) async fn gid_of(user: &str) -> Result<u32> {
parse_id(&capture("id", &["-g", user]).await?, user)
}
fn parse_id(out: &str, user: &str) -> Result<u32> {
out.trim()
.parse()
.into_diagnostic()
.wrap_err_with(|| format!("parsing id for {user}: {out:?}"))
}
pub(super) async fn mkdir(dir: &Path) -> Result<()> {
tokio::fs::create_dir_all(dir)
.await
.into_diagnostic()
.wrap_err_with(|| format!("creating {}", dir.display()))
}
pub(super) async fn umount(dir: &Path) {
if is_mountpoint(dir).await {
let _ = run_ok("umount", &[path(dir)]).await;
}
}
pub(super) async fn rmdir(dir: &Path) {
let _ = tokio::fs::remove_dir(dir).await;
}
pub(super) async fn is_mountpoint(dir: &Path) -> bool {
tokio::process::Command::new("mountpoint")
.arg("-q")
.arg(dir)
.stdin(Stdio::null())
.status()
.await
.map(|s| s.success())
.unwrap_or(false)
}
pub(super) fn glob_prefix(dir: impl AsRef<Path>, prefix: &str) -> Result<Vec<PathBuf>> {
let mut out = Vec::new();
for entry in std::fs::read_dir(dir.as_ref()).into_diagnostic()? {
let entry = entry.into_diagnostic()?;
if entry.file_name().to_string_lossy().starts_with(prefix) {
out.push(entry.path());
}
}
Ok(out)
}
pub(super) fn idmap(postgres_uid: u32, kopia_uid: u32, postgres_gid: u32, kopia_gid: u32) -> String {
format!("u:{postgres_uid}:{kopia_uid}:1 g:{postgres_gid}:{kopia_gid}:1")
}
pub(super) async fn postgres_to_kopia_idmap() -> Result<String> {
Ok(idmap(
uid_of("postgres").await?,
uid_of("kopia").await?,
gid_of("postgres").await?,
gid_of("kopia").await?,
))
}
pub(super) fn relative_data_path(data_dir: &Path, base_mount: &Path) -> Result<PathBuf> {
data_dir
.strip_prefix(base_mount)
.map(Path::to_path_buf)
.map_err(|_| {
miette!(
"data dir {} is not under its mountpoint {}",
data_dir.display(),
base_mount.display()
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn idmap_format() {
assert_eq!(idmap(114, 997, 120, 995), "u:114:997:1 g:120:995:1");
}
#[test]
fn relative_data_path_strips_mountpoint() {
let rel = relative_data_path(
Path::new("/var/lib/postgresql/16/main"),
Path::new("/var/lib/postgresql"),
)
.unwrap();
assert_eq!(rel, PathBuf::from("16/main"));
}
#[test]
fn relative_data_path_rejects_outside() {
assert!(relative_data_path(Path::new("/srv/pg"), Path::new("/var/lib/postgresql")).is_err());
}
}