feat(zfs): add reusable dataset and snapshot operations

This commit is contained in:
Richie committed 2026-10-11 20:11:21 -04:00
1 parent df045d4c63
commit 704999b94c
4 files changed
+115 -8

No files matched your search

Generated
+1 -8
View File
@@ -1179,6 +1179,7 @@ dependencies = [
"signal",
"tempfile",
"toml",
"zfs",
]
[[package]]
@@ -1839,14 +1840,6 @@ name = "zfs"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"env_logger",
"log",
"serde",
"signal",
"tempfile",
"toml",
]
[[package]]
+1
View File
@@ -12,6 +12,7 @@ log = "0.4"
serde = "1.0"
serde_json = "1.0"
signal = { path = "rust/signal" }
zfs = { path = "rust/zfs" }
[workspace.lints.clippy]
all = { level = "deny", priority = -1 }
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "zfs"
version = "0.1.0"
edition = "2024"
description = "List ZFS datasets and manage snapshots through the zfs executable"
license = "MIT"
publish = false
[dependencies]
anyhow.workspace = true
[lints]
workspace = true
+100
View File
@@ -0,0 +1,100 @@
//! 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)
}