diff options
-rw-r--r-- | Cargo.lock | 76 | ||||
-rw-r--r-- | zone_core/src/lib.rs | 4 | ||||
-rw-r--r-- | zone_zfs/Cargo.toml | 2 | ||||
-rw-r--r-- | zone_zfs/src/error.rs | 5 | ||||
-rw-r--r-- | zone_zfs/src/file_system.rs | 171 | ||||
-rw-r--r-- | zone_zfs/src/lib.rs | 11 | ||||
-rw-r--r-- | zone_zfs/src/snapshot.rs | 56 | ||||
-rw-r--r-- | zone_zfs/src/zfs.rs | 42 |
8 files changed, 240 insertions, 127 deletions
@@ -219,12 +219,36 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core 0.12.4", + "darling_macro 0.12.4", +] + +[[package]] +name = "darling" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.1", + "darling_macro 0.13.1", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", ] [[package]] @@ -243,16 +267,58 @@ dependencies = [ [[package]] name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core 0.12.4", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" dependencies = [ - "darling_core", + "darling_core 0.13.1", "quote", "syn", ] [[package]] +name = "derive_builder" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" +dependencies = [ + "darling 0.12.4", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] name = "devise" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1287,7 +1353,7 @@ version = "0.8.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc114779fc27afb78179233e966f469e47fd7a98dc15181cff2574cdddb65612" dependencies = [ - "darling", + "darling 0.13.1", "proc-macro2", "quote", "rocket_http", @@ -2182,7 +2248,7 @@ name = "zone_zfs" version = "0.1.0" dependencies = [ "bytesize", - "chrono", + "derive_builder", "figment", "serde", "thiserror", diff --git a/zone_core/src/lib.rs b/zone_core/src/lib.rs index 14f6755..db99cfa 100644 --- a/zone_core/src/lib.rs +++ b/zone_core/src/lib.rs @@ -92,7 +92,7 @@ impl TryFrom<FileSystem> for Container { type Error = zone_zfs::Error; fn try_from(file_system: FileSystem) -> Result<Self, Self::Error> { - let path_buf = PathBuf::from(&file_system) + let path_buf = PathBuf::from(file_system.dataset()) .file_name() .ok_or_else(|| { Self::Error::FileSystem(format!("Invalid FileSystem path: {:?}", file_system)) @@ -108,7 +108,7 @@ impl TryFrom<FileSystem> for Container { Self::Error::FileSystem(format!("Failed to parse container ID: {:?}", err)) })?; - let template = PathBuf::from(&file_system) + let template = PathBuf::from(file_system.dataset()) .parent() .ok_or_else(|| { Self::Error::FileSystem(format!("Invalid path for filesystem: {:?}", &file_system)) diff --git a/zone_zfs/Cargo.toml b/zone_zfs/Cargo.toml index 41ef655..0346d48 100644 --- a/zone_zfs/Cargo.toml +++ b/zone_zfs/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] bytesize = { version = "1.1.0", features = ["serde"] } -chrono = "0.4.19" +derive_builder = "0.10.2" figment = "0.10.6" serde = "1.0.136" thiserror = "1.0.30" 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<T> = std::result::Result<T, Error>; #[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<OsString> for FileSystem { - type Error = Error; +impl FileSystemBuilder { + pub fn build(&self) -> Result<FileSystem> { + self.build_file_system()?.create() + } + + pub fn build_from(&self, snapshot: &Snapshot) -> Result<FileSystem> { + self.build_file_system()?.create_from(snapshot) + } - fn try_from(value: OsString) -> Result<Self> { - Ok(FileSystem { - value, - ..FileSystem::default() - }) + pub(crate) fn to_file_system(&self) -> Result<FileSystem> { + 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<Self> { - value.try_into() + /// Get a reference to the file system's dataset. + pub fn dataset(&self) -> &PathBuf { + &self.dataset } -} -impl AsRef<OsStr> 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<FileSystem> 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<FileSystem> 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<T>(&self, option: T) -> Result<OsString> + where + T: AsRef<OsStr>, + { + 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<T, U>(&self, name: T, value: U) -> Result<&Self> + pub fn set_opt<T, U>(&self, option: T, value: U) -> Result<&Self> where T: AsRef<OsStr>, U: AsRef<OsStr>, { 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<Self> { + pub fn set_quota(&mut self, quota: ByteSize) -> Result<Self> { self.set_opt("quota", quota.to_string())?; self.quota = quota; Ok(self.to_owned()) } - pub(super) fn set_mountpoint(&mut self, mountpoint: PathBuf) -> Result<Self> { + pub fn set_mountpoint(&mut self, mountpoint: PathBuf) -> Result<Self> { self.set_opt("mountpoint", &mountpoint)?; self.mountpoint = mountpoint; Ok(self.to_owned()) } - pub(super) fn get_snapshots(&self) -> Result<Vec<Snapshot>> { + pub fn get_mountpoint(&self) -> PathBuf { + self.mountpoint.to_owned() + } + + pub fn get_snapshots(&self) -> Result<Vec<Snapshot>> { 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<Option<Snapshot>> { - Ok(self - .get_snapshots()? - .into_iter() - .max_by_key(|s| s.timestamp)) + pub fn get_latest_snapshot(&self) -> Result<Option<Snapshot>> { + todo!("implement using feature") } - pub fn mount(&self) -> Result<()> { + pub(crate) fn create(&self) -> Result<Self> { 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<Self> { 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<Utc>, + dataset: PathBuf, } impl TryFrom<&str> for Snapshot { type Error = Error; - fn try_from(value: &str) -> Result<Self> { - match value.split('@').collect::<Vec<&str>>()[..] { - [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<Self> { + 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<FileSystem> { - let mut file_system = self.file_system.clone(); - file_system.value.push(&new_fs); + pub fn clone_new(&self, path: PathBuf) -> Result<FileSystem> { + 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<FileSystem> { - 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<FileSystem> { + todo!("implement using feature") } - pub(super) fn get_file_systems() -> Result<Vec<FileSystem>> { + pub fn get_file_systems(&self) -> Result<Vec<FileSystem>> { 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<FileSystem> { - Self::get_file_systems()? + pub fn get_file_system(&self, name: PathBuf) -> Result<FileSystem> { + 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()); } } |