101 lines
2.9 KiB
Rust
101 lines
2.9 KiB
Rust
//! 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<String> {
|
|
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<Vec<String>> {
|
|
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<Vec<String>> {
|
|
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<Option<&'static str>> {
|
|
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)
|
|
}
|