//! Manage ZFS datasets and snapshots using the `zfs` executable on `PATH`. //! //! ```no_run //! for snapshot in zfs::list_snapshots("pool/home")? { //! println!("{snapshot}"); //! } //! # Ok::<(), anyhow::Error>(()) //! ``` use std::process::Command; use anyhow::{Context, Result, bail}; const ZFS_EXECUTABLE: &str = "zfs"; fn zfs_command(args: &[&str]) -> Result { let output = Command::new(ZFS_EXECUTABLE) .args(args) .env("LC_ALL", "C") .output() .context("Failed to execute zfs")?; if !output.status.success() { let message = if output.stderr.is_empty() { &output.stdout } else { &output.stderr }; bail!( "zfs {} failed ({}): {}", args.join(" "), output.status, String::from_utf8_lossy(message).trim() ); } String::from_utf8(output.stdout).context("zfs returned invalid UTF-8") } /// List filesystem datasets, excluding pool roots. /// /// # Errors /// /// Returns an error if executing ZFS or decoding its output fails. pub fn list_datasets() -> Result> { let output = zfs_command(&["list", "-Hp", "-t", "filesystem", "-o", "name"])?; Ok(output .lines() .filter(|name| name.contains('/')) .map(String::from) .collect()) } /// List snapshot names belonging directly to `dataset`, excluding child datasets. /// /// Returned names omit the `dataset@` prefix. /// /// # Errors /// /// Returns an error if executing ZFS or decoding its output fails. pub fn list_snapshots(dataset: &str) -> Result> { let output = zfs_command(&[ "list", "-Hp", "-t", "snapshot", "-o", "name", "-d", "1", dataset, ])?; Ok(output .lines() .filter_map(|name| { let (owner, snapshot) = name.split_once('@')?; (owner == dataset).then(|| snapshot.to_owned()) }) .collect()) } /// Create `dataset@snapshot`. /// /// # Errors /// /// Returns an error if executing ZFS or decoding its output fails. pub fn create_snapshot(dataset: &str, snapshot: &str) -> Result<()> { zfs_command(&["snapshot", &format!("{dataset}@{snapshot}")])?; Ok(()) } /// Delete `dataset@snapshot`. /// /// Returns `None` on success, or `Some("snapshot has dependent clones")` when /// dependent clones prevent deletion. /// /// # Errors /// /// Returns an error for other ZFS failures, execution failures, or invalid UTF-8 output. pub fn delete_snapshot(dataset: &str, snapshot: &str) -> Result> { if let Err(error) = zfs_command(&["destroy", &format!("{dataset}@{snapshot}")]) { if error.to_string().contains(&format!( "cannot destroy '{dataset}@{snapshot}': snapshot has dependent clones" )) { return Ok(Some("snapshot has dependent clones")); } return Err(error); } Ok(None) }