1
0
Fork 0
soft/src/zfs.rs

142 lines
4.4 KiB
Rust

use anyhow::{anyhow, Context, Result};
use duct::cmd;
use std::io::prelude::*;
#[derive(Debug, Default)]
pub struct Dataset {
pub path: String,
pub target_path: Option<String>,
pub mountpoint: Option<String>,
is_in_store: Option<bool>,
}
pub fn get_datasets() -> Result<Vec<Dataset>> {
let mut datasets = Vec::<Dataset>::new();
for line in std::io::BufReader::new(
cmd!(
"zfs",
"get",
"-t",
"filesystem",
"-Hpo",
"name,property,value",
"mountpoint,soft:target,soft:store"
)
.stdin_null()
.reader()
.context("Failed to list ZFS datasets")?,
)
.lines()
{
let l = line.context("Failed to read the list of ZFS datasets")?;
let (n, pv) = l.split_once('\t').ok_or(anyhow!("Weird ZFS output"))?;
let (p, v) = pv.split_once('\t').ok_or(anyhow!("Weird ZFS output"))?;
let (mut last_idx, _) = datasets.len().overflowing_sub(1);
if last_idx == usize::MAX || datasets[last_idx].path != n {
datasets.push(Dataset { path: n.to_owned(), ..Default::default() });
last_idx = datasets.len() - 1;
}
let ds = &mut datasets[last_idx];
match p {
"mountpoint" => ds.mountpoint = Some(v.to_owned()),
"soft:target" => ds.target_path = Some(v.to_owned()),
"soft:store" => ds.is_in_store = Some(v == "true"),
_ => (),
}
}
Ok(datasets)
}
type Store = (Dataset, Vec<Dataset>);
pub fn filter_stores(datasets: Vec<Dataset>) -> Result<Vec<Store>> {
let mut stores = datasets.into_iter().fold(Vec::<Store>::new(), |mut acc, ds| {
if ds.is_in_store == Some(true) {
if acc.last().map(|(l, _)| ds.path.starts_with(&l.path)) == Some(true) {
acc.last_mut().unwrap().1.push(ds);
} else {
acc.push((ds, Vec::new()));
}
}
acc
});
stores.sort_by_cached_key(|(s, _ds)| {
s.mountpoint
.as_ref()
.map(|p| p.chars().filter(|c| *c == '/').count())
.unwrap_or(usize::MAX)
});
Ok(stores)
}
#[derive(Debug)]
pub struct WritableDataset(pub String);
impl WritableDataset {
pub fn from_scratch(under: &Dataset, name: &str, target_path: &str) -> Result<WritableDataset> {
let dspath = format!("{}/{}", under.path, name);
log::trace!("Creating dataset '{}' from scratch", dspath);
#[rustfmt::skip]
cmd!(
"zfs", "create",
"-o", "atime=off",
"-o", "acltype=off",
"-o", "canmount=noauto",
"-o", "devices=off",
"-o", "utf8only=on",
"-o", "xattr=sa",
"-o", "compression=zstd",
"-o", "checksum=sha256",
"-o", "readonly=off",
"-o", format!("soft:target={}", target_path),
&dspath,
)
.run()
.with_context(|| format!("Failed to create dataset '{}'", dspath))?;
Ok(WritableDataset(dspath))
}
pub fn from_snapshot(under: &Dataset, from: &Dataset, name: &str) -> Result<WritableDataset> {
let dspath = format!("{}/{}", under.path, name);
let snpath = format!("{}@S", from.path);
log::trace!("Creating dataset '{}' by cloning '{}'", dspath, snpath);
#[rustfmt::skip]
cmd!(
"zfs", "clone",
"-o", "atime=off",
"-o", "acltype=off",
"-o", "canmount=noauto",
"-o", "devices=off",
"-o", "xattr=sa",
"-o", "compression=zstd",
"-o", "checksum=sha256",
"-o", "readonly=off",
"-o", format!("soft:target={}", from.target_path.as_ref().unwrap()),
&snpath,
&dspath,
)
.run()
.with_context(|| format!("Failed to create dataset '{}' by cloning '{}'", dspath, snpath))?;
Ok(WritableDataset(dspath))
}
fn _commit(&self) -> std::io::Result<()> {
cmd!("zfs", "set", "canmount=on", "readonly=on", &self.0).run()?;
cmd!("zfs", "snapshot", format!("{}@S", &self.0)).run()?;
cmd!("zfs", "mount", &self.0).run()?;
Ok(())
}
pub fn commit(&self) -> Result<()> {
log::trace!("Committing dataset '{}'", self.0);
self._commit()
.with_context(|| format!("Failed to mount dataset '{}'", self.0))
}
}