add: version 1

This commit is contained in:
timeshifter
2025-07-17 17:53:32 +02:00
commit 6ac5290a28
15 changed files with 603 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
use std::fmt::Write;
use std::{
fmt::Display,
path::{Path, PathBuf},
process::Child,
};
use anyhow::{Result, anyhow, bail};
use crate::cmd::zfs::{
pool_exists, zfs_list_main_datasets, zfs_list_snapshots, zfs_receive, zfs_send_absolute,
zfs_send_incremental, zpool_import,
};
use crate::io::read_vars_from_file;
pub struct Pool {
name: String,
pub datasets: Vec<Dataset>,
}
impl Pool {
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 load_datasets_from_file(&mut self, filename: &str) -> Result<()> {
self.datasets = Dataset::from_file(&PathBuf::from(filename), &self.name)?;
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 import(name: &str) -> Result<Pool> {
zpool_import(name)?;
let result = Pool::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)
}
fn from_file(filename: &Path, pool: &str) -> Result<Vec<Self>> {
let datasets = read_vars_from_file(filename.to_str().unwrap())?;
Ok(datasets
.into_iter()
.map(|ds| {
let mut pb = PathBuf::new();
pb.push(pool);
pb.push(ds);
Self::new(pb)
})
.collect())
}
pub fn find_snapshots_like_tag(&self, snapshot_tag: &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) {
result.push(Snapshot(snapshot_name.to_string()));
}
}
Ok(result)
}
pub fn find_last_snapshot_with_tag(&self, snapshot_tag: &str) -> Result<Snapshot> {
let mut snapshots = self.find_snapshots_like_tag(snapshot_tag)?;
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: &Pool) -> 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)
}
}