From f4d54562e5c4f7c61faadfe13701286cce1e3d05 Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Tue, 22 Feb 2022 22:19:57 -0600 Subject: refactor(zfs): use builder pattern for FileSystem Refactor FileSystem to use a builder pattern to create a zfs dataset Fix #26 --- zone_zfs/src/error.rs | 5 +- zone_zfs/src/file_system.rs | 171 +++++++++++++++++++++++++++----------------- zone_zfs/src/lib.rs | 11 +-- zone_zfs/src/snapshot.rs | 56 ++++++--------- zone_zfs/src/zfs.rs | 42 +++++++---- 5 files changed, 166 insertions(+), 119 deletions(-) (limited to 'zone_zfs/src') diff --git a/zone_zfs/src/error.rs b/zone_zfs/src/error.rs index bf32f14..872804b 100644 --- a/zone_zfs/src/error.rs +++ b/zone_zfs/src/error.rs @@ -3,7 +3,7 @@ pub type Result = std::result::Result; #[allow(clippy::large_enum_variant)] #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("ZFS error")] + #[error("ZFS error: {0:?}")] ZFS(String), #[error("Snapshot Error: {0:?}")] @@ -20,4 +20,7 @@ pub enum Error { #[error("Config Error: {0:?}")] Config(#[from] figment::Error), + + #[error(transparent)] + Builder(#[from] crate::file_system::FileSystemBuilderError), } diff --git a/zone_zfs/src/file_system.rs b/zone_zfs/src/file_system.rs index c961277..5c747ce 100644 --- a/zone_zfs/src/file_system.rs +++ b/zone_zfs/src/file_system.rs @@ -1,5 +1,6 @@ use crate::{Error, Result, Snapshot}; use bytesize::ByteSize; +use derive_builder::Builder; use std::{ ffi::{OsStr, OsString}, fmt::Display, @@ -7,97 +8,119 @@ use std::{ process::Command, }; -macro_rules! opt [ - ($name:expr, $value:expr) => ({ +macro_rules! concat_opt [ + ($($value:expr),+) => ({ let mut _temp = OsString::new(); - _temp.push($name); - _temp.push("="); - _temp.push($value); + $(_temp.push($value);)+ _temp }) ]; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Builder)] +#[builder( + build_fn(private, name = "build_file_system"), + derive(Debug), + setter(into), + default +)] pub struct FileSystem { - pub(crate) value: OsString, + dataset: PathBuf, mountpoint: PathBuf, quota: ByteSize, } impl Display for FileSystem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.value.to_string_lossy()) + write!(f, "{}", self.dataset.to_string_lossy()) } } -impl TryFrom for FileSystem { - type Error = Error; +impl FileSystemBuilder { + pub fn build(&self) -> Result { + self.build_file_system()?.create() + } + + pub fn build_from(&self, snapshot: &Snapshot) -> Result { + self.build_file_system()?.create_from(snapshot) + } - fn try_from(value: OsString) -> Result { - Ok(FileSystem { - value, - ..FileSystem::default() - }) + pub(crate) fn to_file_system(&self) -> Result { + self.build_file_system().map_err(Error::from) } } -impl TryFrom<&str> for FileSystem { - type Error = Error; +impl FileSystem { + pub fn builder() -> FileSystemBuilder { + FileSystemBuilder::default() + } - fn try_from(value: &str) -> Result { - value.try_into() + /// Get a reference to the file system's dataset. + pub fn dataset(&self) -> &PathBuf { + &self.dataset } -} -impl AsRef for FileSystem { - fn as_ref(&self) -> &OsStr { - self.value.as_ref() + /// Get a reference to the file system's mountpoint. + pub fn mountpoint(&self) -> &PathBuf { + &self.mountpoint } -} -impl From for PathBuf { - fn from(val: FileSystem) -> Self { - PathBuf::from(val.value) + /// Get the file system's quota. + pub fn quota(&self) -> ByteSize { + self.quota } -} -impl From for String { - fn from(val: FileSystem) -> Self { - val.value.to_string_lossy().to_string() + /// Get the value of the file system's `option` from ZFS + pub fn get_opt(&self, option: T) -> Result + where + T: AsRef, + { + Command::new("zfs") + .arg("get") + .arg("-H") + .arg("-o") + .arg("value,source") + .arg(&option) + .arg(&self.dataset) + .output() + .map(|o| String::from_utf8(o.stdout))? + .map(OsString::from) + .map_err(Error::from) } -} -impl FileSystem { - pub(super) fn set_opt(&self, name: T, value: U) -> Result<&Self> + pub fn set_opt(&self, option: T, value: U) -> Result<&Self> where T: AsRef, U: AsRef, { Command::new("zfs") .arg("set") - .arg(opt!(&name, value)) - .arg(&self.value) + .arg(concat_opt!(&option, "=", value)) + .arg(&self.dataset) .status()? .success() .then(|| self) .ok_or_else(|| { - Error::FileSystem(format!("Failed to set {:?}: {:?}", name.as_ref(), self)) + Error::FileSystem(format!("Failed to set {:?}: {:?}", option.as_ref(), self)) }) } - pub(super) fn set_quota(&mut self, quota: ByteSize) -> Result { + pub fn set_quota(&mut self, quota: ByteSize) -> Result { self.set_opt("quota", quota.to_string())?; self.quota = quota; Ok(self.to_owned()) } - pub(super) fn set_mountpoint(&mut self, mountpoint: PathBuf) -> Result { + pub fn set_mountpoint(&mut self, mountpoint: PathBuf) -> Result { self.set_opt("mountpoint", &mountpoint)?; self.mountpoint = mountpoint; Ok(self.to_owned()) } - pub(super) fn get_snapshots(&self) -> Result> { + pub fn get_mountpoint(&self) -> PathBuf { + self.mountpoint.to_owned() + } + + pub fn get_snapshots(&self) -> Result> { Command::new("zfs") .arg("list") .arg("-H") @@ -105,56 +128,76 @@ impl FileSystem { .arg("name") .arg("-t") .arg("snapshot") - .arg(self) + .arg(&self.dataset) .output() .map(|o| String::from_utf8(o.stdout))? .map_err(Error::from)? .split_whitespace() - .map(Snapshot::try_from) + .filter_map(|s| Snapshot::try_from(s).ok()) + .map(Ok) .collect() } - pub(super) fn get_latest_snapshot(&self) -> Result> { - Ok(self - .get_snapshots()? - .into_iter() - .max_by_key(|s| s.timestamp)) + pub fn get_latest_snapshot(&self) -> Result> { + todo!("implement using feature") } - pub fn mount(&self) -> Result<()> { + pub(crate) fn create(&self) -> Result { Command::new("zfs") - .arg("mount") - .arg(&self.value) + .arg("create") + .arg("-o") + .arg(concat_opt!("mountpoint=", &self.mountpoint)) + .arg(concat_opt!("quota=", self.quota.to_string())) .status()? .success() - .then(|| ()) - .ok_or_else(|| Error::FileSystem(format!("Failed to mount: {:?}", self))) + .then(|| self.to_owned()) + .ok_or_else(|| Error::FileSystem(format!("Failed to create file system: {:?}", self))) } - pub fn unmount(&self) -> Result<()> { + pub(crate) fn create_from(&self, snapshot: &Snapshot) -> Result { Command::new("zfs") - .arg("unmount") - .arg(&self.value) + .arg("clone") + .arg(&snapshot.name()) + .arg(&self.dataset) .status()? .success() - .then(|| ()) - .ok_or_else(|| Error::FileSystem(format!("Failed to unmount: {:?}", self))) + .then(|| self.to_owned()) + .ok_or_else(|| Error::Snapshot(format!("Failed to clone snapshot: {:?}", self))) } pub fn destroy(&self, force: bool) -> Result<()> { - let mut args: Vec<&OsStr> = Vec::new(); - let f_arg = &OsString::from("-f"); - if force { - args.push(f_arg); - } - args.push(&self.value); + let args = if force { + vec!["destroy", "-f"] + } else { + vec!["destroy"] + }; Command::new("zfs") - .arg("destroy") .args(args) + .arg(&self.dataset) .status()? .success() .then(|| ()) .ok_or_else(|| Error::FileSystem(format!("Failed to destroy: {:?}", self))) } + + pub fn mount(&self) -> Result<()> { + Command::new("zfs") + .arg("mount") + .arg(&self.dataset) + .status()? + .success() + .then(|| ()) + .ok_or_else(|| Error::FileSystem(format!("Failed to mount: {:?}", self))) + } + + pub fn unmount(&self) -> Result<()> { + Command::new("zfs") + .arg("unmount") + .arg(&self.dataset) + .status()? + .success() + .then(|| ()) + .ok_or_else(|| Error::FileSystem(format!("Failed to unmount: {:?}", self))) + } } diff --git a/zone_zfs/src/lib.rs b/zone_zfs/src/lib.rs index fc23bf8..410f474 100644 --- a/zone_zfs/src/lib.rs +++ b/zone_zfs/src/lib.rs @@ -1,8 +1,9 @@ -pub use config::Config; -pub use error::{Error, Result}; -pub use file_system::FileSystem; -pub use snapshot::Snapshot; -pub use zfs::ZFS; +pub use crate::config::Config; +pub use crate::error::{Error, Result}; +pub use crate::file_system::FileSystem; +pub use crate::file_system::FileSystemBuilder; +pub use crate::snapshot::Snapshot; +pub use crate::zfs::ZFS; mod config; mod error; diff --git a/zone_zfs/src/snapshot.rs b/zone_zfs/src/snapshot.rs index e28182c..2be77a3 100644 --- a/zone_zfs/src/snapshot.rs +++ b/zone_zfs/src/snapshot.rs @@ -1,51 +1,39 @@ use crate::{Error, FileSystem, Result}; -use chrono::{DateTime, Utc}; -use std::{ffi::OsString, path::PathBuf, process::Command}; -use tracing::warn; +use std::{ffi::OsString, path::PathBuf}; #[derive(Debug)] pub struct Snapshot { name: OsString, - file_system: FileSystem, - pub(crate) timestamp: DateTime, + dataset: PathBuf, } impl TryFrom<&str> for Snapshot { type Error = Error; - fn try_from(value: &str) -> Result { - match value.split('@').collect::>()[..] { - [file_system, name] => Ok(Snapshot { - file_system: FileSystem::try_from(file_system)?, - name: file_system.into(), - timestamp: name.parse().unwrap_or_else(|_err| { - warn!( - "Failed to parse timestamp from `{}`, using default value.", - value - ); - chrono::MIN_DATETIME - }), - }), - _ => Err(Error::Snapshot(format!( - "Failed to parse snapshot: {:?}", - value - ))), - } + fn try_from(s: &str) -> Result { + s.split_once('@') + .map(|(d, n)| Snapshot { + name: n.into(), + dataset: d.into(), + }) + .ok_or_else(|| Error::Snapshot(format!("Failed to parse snapshot: {:?}", s))) } } impl Snapshot { - pub fn clone_into_file_system(&self, new_fs: PathBuf) -> Result { - let mut file_system = self.file_system.clone(); - file_system.value.push(&new_fs); + pub fn clone_new(&self, path: PathBuf) -> Result { + FileSystem::builder() + .dataset(&self.dataset.join(path)) + .build_from(self.to_owned()) + } + + /// Get a reference to the snapshot's name. + pub fn name(&self) -> &OsString { + &self.name + } - Command::new("zfs") - .arg("clone") - .arg(&self.name) - .arg(&new_fs) - .status()? - .success() - .then(|| file_system) - .ok_or_else(|| Error::Snapshot(format!("Failed to clone snapshot: {:?}", self))) + /// Get a reference to the snapshot's dataset. + pub fn dataset(&self) -> &PathBuf { + &self.dataset } } diff --git a/zone_zfs/src/zfs.rs b/zone_zfs/src/zfs.rs index c820c1c..0ffba13 100644 --- a/zone_zfs/src/zfs.rs +++ b/zone_zfs/src/zfs.rs @@ -20,15 +20,11 @@ impl ZFS { .map(|config| Self { config }) } - pub fn clone_from_latest(&self, name: PathBuf, parent: PathBuf) -> Result { - Self::get_file_system(parent)? - .get_latest_snapshot()? - .ok_or_else(|| Error::Snapshot("No snapshot found".into()))? - .clone_into_file_system(name)? - .set_quota(self.config.quota) + pub fn clone_from_latest(&self, _name: PathBuf, _parent: PathBuf) -> Result { + todo!("implement using feature") } - pub(super) fn get_file_systems() -> Result> { + pub fn get_file_systems(&self) -> Result> { Command::new("zfs") .arg("list") .arg("-H") @@ -36,16 +32,32 @@ impl ZFS { .arg("name") .output() .map(|o| String::from_utf8(o.stdout))? - .map_err(Error::from)? - .split_whitespace() - .map(FileSystem::try_from) - .collect() + .map_err(Error::from) + .map(|s| { + s.lines() + .into_iter() + .filter_map(|l| l.split_once(" ").map(|s| (s.0.trim(), s.1.trim()))) + .filter_map(|(dataset, mountpoint)| { + let mountpoint = if mountpoint == "none" { + PathBuf::from(mountpoint) + } else { + self.config.mountpoint.to_owned() + }; + + FileSystem::builder() + .mountpoint(mountpoint) + .dataset(dataset) + .to_file_system() + .ok() + }) + .collect() + }) } - fn get_file_system(name: PathBuf) -> Result { - Self::get_file_systems()? + pub fn get_file_system(&self, name: PathBuf) -> Result { + self.get_file_systems()? .into_iter() - .find(|fs| PathBuf::from(&fs.value).ends_with(&name)) + .find(|fs| fs.dataset().ends_with(&name)) .ok_or_else(|| Error::FileSystem("No file system found".into())) } } @@ -55,6 +67,6 @@ mod tests { #[test] fn zfs_list() { use super::*; - assert!(ZFS::get_file_systems().is_ok()); + assert!(ZFS::new().unwrap().get_file_systems().is_ok()); } } -- cgit v1.2.3-70-g09d2