diff options
Diffstat (limited to 'crates/common/tedge_utils/src/paths.rs')
-rw-r--r-- | crates/common/tedge_utils/src/paths.rs | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/crates/common/tedge_utils/src/paths.rs b/crates/common/tedge_utils/src/paths.rs new file mode 100644 index 00000000..495a8e46 --- /dev/null +++ b/crates/common/tedge_utils/src/paths.rs @@ -0,0 +1,200 @@ +use std::{ + ffi::OsString, + fs::File, + path::{Path, PathBuf}, +}; + +use std::io::Write; +use tempfile::{NamedTempFile, PersistError}; + +#[derive(thiserror::Error, Debug)] +pub enum PathsError { + #[error("Directory Error. Check permissions for {1}.")] + DirCreationFailed(#[source] std::io::Error, PathBuf), + + #[error("File Error. Check permissions for {1}.")] + FileCreationFailed(#[source] PersistError, PathBuf), + + #[error("User's Home Directory not found.")] + HomeDirNotFound, + + #[error(transparent)] + IoError(#[from] std::io::Error), + + #[error("Path conversion to String failed: {path:?}.")] + PathToStringFailed { path: OsString }, + + #[error("Couldn't write configuration file, check permissions.")] + PersistError(#[from] PersistError), + + #[error("Directory: {path:?} not found")] + DirNotFound { path: OsString }, + + #[error("Parent directory for the path: {path:?} not found")] + ParentDirNotFound { path: OsString }, + + #[error("Relative path: {path:?} is not permitted. Provide an absolute path instead.")] + RelativePathNotPermitted { path: OsString }, +} + +pub fn pathbuf_to_string(pathbuf: PathBuf) -> Result<String, PathsError> { + pathbuf + .into_os_string() + .into_string() + .map_err(|os_string| PathsError::PathToStringFailed { path: os_string }) +} + +pub fn create_directories(dir_path: impl AsRef<Path>) -> Result<(), PathsError> { + let dir_path = dir_path.as_ref(); + std::fs::create_dir_all(dir_path) + .map_err(|error| PathsError::DirCreationFailed(error, dir_path.into())) +} + +pub fn persist_tempfile(file: NamedTempFile, path_to: impl AsRef<Path>) -> Result<(), PathsError> { + let path_to = path_to.as_ref(); + let _ = file + .persist(path_to) + .map_err(|error| PathsError::FileCreationFailed(error, path_to.into()))?; + + Ok(()) +} + +pub fn ok_if_not_found(err: std::io::Error) -> std::io::Result<()> { + match err.kind() { + std::io::ErrorKind::NotFound => Ok(()), + _ => Err(err), + } +} + +/// A DraftFile is a temporary file +/// that can be populated using the `Write` trait +/// then finally and atomically persisted to a target file. +pub struct DraftFile { + file: NamedTempFile, + target: PathBuf, +} + +impl DraftFile { + /// Create a draft for a file + pub fn new(target: impl AsRef<Path>) -> Result<DraftFile, PathsError> { + let target = target.as_ref(); + + // Since the persist method will rename the temp file into the target, + // one has to create the temp file in the same file system as the target. + let dir = target + .parent() + .ok_or_else(|| PathsError::ParentDirNotFound { + path: target.as_os_str().into(), + })?; + let file = NamedTempFile::new_in(dir)?; + let target = target.to_path_buf(); + + Ok(DraftFile { file, target }) + } + + /// Atomically persist the file into its target path + pub fn persist(self) -> Result<(), PathsError> { + let target = &self.target; + let _ = self + .file + .persist(target) + .map_err(|error| PathsError::FileCreationFailed(error, target.into()))?; + + Ok(()) + } +} + +impl Write for DraftFile { + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { + self.file.write(buf) + } + #[inline] + fn flush(&mut self) -> std::io::Result<()> { + self.file.flush() + } +} + +// This isn't complete way to retrieve HOME dir from the user. +// We could parse passwd file to get actual home path if we can get user name. +// I suppose rust provides some way to do it or allows through c bindings... But this implies unsafe code. +// Another alternative is to use deprecated env::home_dir() -1 +// https://github.com/rust-lang/rust/issues/71684 +pub fn home_dir() -> Option<PathBuf> { + std::env::var_os("HOME") + .and_then(|home| if home.is_empty() { None } else { Some(home) }) + .map(PathBuf::from) +} + +/// Set the permission modes of a Unix file. +#[cfg(not(windows))] +pub fn set_permission(file: &File, mode: u32) -> Result<(), std::io::Error> { + use std::os::unix::fs::PermissionsExt; + let mut perm = file.metadata()?.permissions(); + perm.set_mode(mode); + file.set_permissions(perm) +} + +/// On windows, no file permission modes are changed. +/// +/// So Windows might be used for dev even if not supported. +#[cfg(windows)] +pub fn set_permission(_file: &File, _mode: u32) -> Result<(), std::io::Error> { + Ok(()) +} + +pub fn validate_parent_dir_exists(path: impl AsRef<Path>) -> Result<(), PathsError> { + let path = path.as_ref(); + if path.is_relative() { + Err(PathsError::RelativePathNotPermitted { path: path.into() }) + } else { + match path.parent() { + None => Err(PathsError::ParentDirNotFound { path: path.into() }), + Some(parent) => { + if !parent.exists() { + Err(PathsError::DirNotFound { + path: parent.into(), + }) + } else { + Ok(()) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + + #[test] + fn pathbuf_to_string_ok() { + let pathbuf: PathBuf = "test".into(); + let expected: String = "test".into(); + let result = pathbuf_to_string(pathbuf).unwrap(); + assert_eq!(result, expected); + } + + #[test] + #[cfg(unix)] // On windows the error is unexpectedly RelativePathNotPermitted + fn validate_path_non_existent() { + let result = validate_parent_dir_exists(Path::new("/non/existent/path")); + assert_matches!(result.unwrap_err(), PathsError::DirNotFound { .. }); + } + + #[test] + #[cfg(unix)] // On windows the error is unexpectedly RelativePathNotPermitted + fn validate_parent_dir_non_existent() { + let result = validate_parent_dir_exists(Path::new("/")); + assert_matches!(result.unwrap_err(), PathsError::ParentDirNotFound { .. }); + } + + #[test] + fn validate_parent_dir_relative_path() { + let result = validate_parent_dir_exists(Path::new("test.txt")); + assert_matches!( + result.unwrap_err(), + PathsError::RelativePathNotPermitted { .. } + ); + } +} |