use std::fmt::Write; use std::{fmt::Display, path::PathBuf, process::Child}; use anyhow::{Result, anyhow, bail}; use crate::cmd::zfs::*; /// A ZFS pool. It holds the name of the pool and the list of datasets belonging to it. pub struct ZPool { pub name: String, pub datasets: Vec, } impl ZPool { pub fn new(arg: &str) -> Result { let result = Self { name: arg.to_string(), datasets: Vec::new(), }; if !result.exists() { bail!("pool does not exist") } Ok(result) } fn exists(&self) -> bool { pool_exists(&self.name) } pub fn send_absolute(&self, dataset_to_send: &Dataset, snapshot: &Snapshot) -> Result { assert!(self.datasets.contains(dataset_to_send)); let mut qualified_name = dataset_to_send.0.to_str().unwrap().to_string(); qualified_name.push('@'); qualified_name.push_str(&snapshot.0); let handle = zfs_send_absolute(&qualified_name)?; Ok(handle) } pub fn send_incremental( &self, dataset_to_send: &Dataset, common_snapshot: &Snapshot, recent_snapshot: &Snapshot, ) -> Result { let mut from = String::new(); let mut to = String::new(); write!(from, "@{common_snapshot}")?; write!(to, "{dataset_to_send}@{recent_snapshot}")?; zfs_send_incremental(&from, &to) } pub fn receive(&self, handle: Child) -> Result<()> { zfs_receive(&self.name, handle)?; Ok(()) } pub fn get_datasets_from_system(&mut self) -> Result<()> { let datasets = zfs_list_main_datasets(&self.name)?; self.datasets = datasets .iter() .map(PathBuf::from) .map(Dataset::new) .skip(1) .collect(); // first one is the Pool Ok(()) } pub fn contains(&self, dataset: &Dataset) -> bool { self.datasets.contains(dataset) } pub fn get_matching_dataset(&self, dataset_from_other_pool: &Dataset) -> Option { let mut required_name = PathBuf::new(); required_name.push(&self.name); dataset_from_other_pool .0 .iter() .skip(1) .for_each(|element| required_name.push(element)); let matching_dataset = Dataset::new(required_name); if self.contains(&matching_dataset) { Some(matching_dataset) } else { None } } pub fn export(self) -> Result<()> { zpool_export(&self.name) } pub fn scrub(&self) -> Result<()> { zpool_scrub(&self.name) } pub fn import(name: &str) -> Result { zpool_import(name)?; let result = ZPool::new(name)?; Ok(result) } } /// Full path of a ZFS dataset. /// /// E.g. if the name of the pool is `tank`, with the elements `ROOT` and `default` below it, this /// struct would contain `tank/ROOT/default`. #[derive(Debug, PartialEq, Clone)] pub struct Dataset(PathBuf); impl Dataset { fn new(path: PathBuf) -> Self { Self(path) } pub fn find_snapshots_like_tag( &self, snapshot_tag: &str, snapshot_interval: &str, ) -> Result> { let snapshots = zfs_list_snapshots(self.0.to_str().unwrap())?; let mut result = vec![]; for item in snapshots { let snapshot_name = item.split('@').next_back().unwrap(); if snapshot_name.contains(snapshot_tag) && snapshot_name.contains(snapshot_interval) { result.push(Snapshot(snapshot_name.to_string())); } } Ok(result) } pub fn find_last_snapshot_with_tag( &self, snapshot_tag: &str, snapshot_interval: &str, ) -> Result { let mut snapshots = self.find_snapshots_like_tag(snapshot_tag, snapshot_interval)?; snapshots.sort_unstable(); let result = snapshots .last() .cloned() .ok_or(anyhow!("could not find any snapshots"))?; Ok(result) } fn get_snapshots(&self) -> Result> { let snapshots_string = zfs_list_snapshots(self.0.to_str().unwrap())?; let snapshots = snapshots_string .into_iter() // TODO should snapshot just take a string? .map(|s| Snapshot::new(&s)) .collect(); Ok(snapshots) } pub fn change_pool_name(&self, new_pool: &ZPool) -> Self { let new_pool = new_pool.name.as_str(); let mut pathbuf = PathBuf::new(); pathbuf.push(new_pool); self.0 .iter() .skip(1) .for_each(|element| pathbuf.push(element)); Self(pathbuf) } } impl Display for Dataset { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0.to_str().unwrap()) } } #[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)] /// A ZFS snapshot. /// /// If the absolute ZFS path were "zpool/dataset@snapshot", this would contain "snapshot". pub struct Snapshot(String); impl Snapshot { fn new(name: &str) -> Self { Self(name.to_string()) } pub fn find_last_common_snapshot_with_tag( ds1: &Dataset, ds2: &Dataset, tag: &str, ) -> Option { let sn1 = ds1.get_snapshots().ok()?; let sn2 = ds2.get_snapshots().ok()?; let mut common = vec![]; for snapshot in sn1 { if !snapshot.0.contains(tag) { continue; } if sn2.contains(&snapshot) { common.push(snapshot); } } common.sort(); common.last().cloned() } } impl Display for Snapshot { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } }