221 lines
5.8 KiB
Rust
221 lines
5.8 KiB
Rust
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<Dataset>,
|
|
}
|
|
|
|
impl ZPool {
|
|
pub fn new(arg: &str) -> Result<Self> {
|
|
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<Child> {
|
|
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<Child> {
|
|
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<Dataset> {
|
|
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> {
|
|
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<Vec<Snapshot>> {
|
|
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<Snapshot> {
|
|
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<Vec<Snapshot>> {
|
|
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<Self> {
|
|
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)
|
|
}
|
|
}
|