summaryrefslogtreecommitdiffstats
path: root/crates/common/tedge_utils/src/file.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/common/tedge_utils/src/file.rs')
-rw-r--r--crates/common/tedge_utils/src/file.rs360
1 files changed, 360 insertions, 0 deletions
diff --git a/crates/common/tedge_utils/src/file.rs b/crates/common/tedge_utils/src/file.rs
new file mode 100644
index 00000000..f3659d56
--- /dev/null
+++ b/crates/common/tedge_utils/src/file.rs
@@ -0,0 +1,360 @@
+use nix::unistd::*;
+use std::io::Write;
+use std::os::linux::fs::MetadataExt;
+use std::os::unix::fs::PermissionsExt;
+use std::path::{Path, PathBuf};
+use std::{fs, io};
+use users::{get_group_by_name, get_user_by_name};
+
+#[derive(thiserror::Error, Debug)]
+pub enum FileError {
+ #[error("Creating the directory failed: {dir:?}.")]
+ DirectoryCreateFailed { dir: String, from: std::io::Error },
+
+ #[error("Creating the file failed: {file:?}.")]
+ FileCreateFailed { file: String, from: std::io::Error },
+
+ #[error("Failed to change owner: {name:?}.")]
+ MetaDataError { name: String, from: std::io::Error },
+
+ #[error("User not found: {user:?}.")]
+ UserNotFound { user: String },
+
+ #[error("Group not found: {group:?}.")]
+ GroupNotFound { group: String },
+
+ #[error(transparent)]
+ Errno(#[from] nix::errno::Errno),
+
+ #[error("The path is not accessible. {path:?}")]
+ PathNotAccessible { path: PathBuf },
+
+ #[error("Writing the content to the file failed: {file:?}.")]
+ WriteContentFailed { file: String, from: std::io::Error },
+}
+
+pub fn create_directory_with_user_group(
+ dir: impl AsRef<Path>,
+ user: &str,
+ group: &str,
+ mode: u32,
+) -> Result<(), FileError> {
+ let perm_entry = PermissionEntry::new(Some(user.into()), Some(group.into()), Some(mode));
+ perm_entry.create_directory(dir.as_ref())
+}
+
+pub fn create_directory_with_mode(dir: impl AsRef<Path>, mode: u32) -> Result<(), FileError> {
+ let perm_entry = PermissionEntry::new(None, None, Some(mode));
+ perm_entry.create_directory(dir.as_ref())
+}
+
+pub fn create_file_with_user_group(
+ file: impl AsRef<Path>,
+ user: &str,
+ group: &str,
+ mode: u32,
+ default_content: Option<&str>,
+) -> Result<(), FileError> {
+ let perm_entry = PermissionEntry::new(Some(user.into()), Some(group.into()), Some(mode));
+ perm_entry.create_file(file.as_ref(), default_content)
+}
+
+#[derive(Debug, PartialEq, Eq, Default, Clone)]
+pub struct PermissionEntry {
+ pub user: Option<String>,
+ pub group: Option<String>,
+ pub mode: Option<u32>,
+}
+
+impl PermissionEntry {
+ pub fn new(user: Option<String>, group: Option<String>, mode: Option<u32>) -> Self {
+ Self { user, group, mode }
+ }
+
+ pub fn apply(&self, path: &Path) -> Result<(), FileError> {
+ match (&self.user, &self.group) {
+ (Some(user), Some(group)) => {
+ change_user_and_group(path, user, group)?;
+ }
+ (Some(user), None) => {
+ change_user(path, user)?;
+ }
+ (None, Some(group)) => {
+ change_group(path, group)?;
+ }
+ (None, None) => {}
+ }
+
+ if let Some(mode) = &self.mode {
+ change_mode(path, *mode)?;
+ }
+
+ Ok(())
+ }
+
+ fn create_directory(&self, dir: &Path) -> Result<(), FileError> {
+ match fs::create_dir(dir) {
+ Ok(_) => {
+ self.apply(dir)?;
+ Ok(())
+ }
+ Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()),
+ Err(e) => Err(FileError::DirectoryCreateFailed {
+ dir: dir.display().to_string(),
+ from: e,
+ }),
+ }
+ }
+
+ /// This function creates a file with a given path, specific access privileges and with the given content.
+ /// If the file already exists, then it will not be re-created and it will not overwrite/append the contents of the file.
+ /// This method returns
+ /// Ok() when file is created and the content is written successfully into the file.
+ /// Ok() when the file aleady exists
+ /// Err(_) When it can not create the file with the appropriate owner and access permissions.
+ fn create_file(&self, file: &Path, default_content: Option<&str>) -> Result<(), FileError> {
+ match fs::OpenOptions::new()
+ .create_new(true)
+ .write(true)
+ .open(file)
+ {
+ Ok(mut f) => {
+ self.apply(file)?;
+ if let Some(default_content) = default_content {
+ f.write(default_content.as_bytes()).map_err(|e| {
+ FileError::WriteContentFailed {
+ file: file.display().to_string(),
+ from: e,
+ }
+ })?;
+ }
+ Ok(())
+ }
+
+ Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()),
+ Err(e) => Err(FileError::FileCreateFailed {
+ file: file.display().to_string(),
+ from: e,
+ }),
+ }
+ }
+}
+
+pub fn change_user_and_group(file: &Path, user: &str, group: &str) -> Result<(), FileError> {
+ let ud = get_user_by_name(user)
+ .map(|u| u.uid())
+ .ok_or_else(|| FileError::UserNotFound { user: user.into() })?;
+
+ let uid = get_metadata(Path::new(file))?.st_uid();
+
+ let gd = get_group_by_name(group)
+ .map(|g| g.gid())
+ .ok_or_else(|| FileError::GroupNotFound {
+ group: group.into(),
+ })?;
+
+ let gid = get_metadata(Path::new(file))?.st_gid();
+
+ // if user and group are same as existing, then do not change
+ if (ud != uid) && (gd != gid) {
+ chown(file, Some(Uid::from_raw(ud)), Some(Gid::from_raw(gd)))?;
+ }
+
+ Ok(())
+}
+
+fn change_user(file: &Path, user: &str) -> Result<(), FileError> {
+ let ud = get_user_by_name(user)
+ .map(|u| u.uid())
+ .ok_or_else(|| FileError::UserNotFound { user: user.into() })?;
+
+ let uid = get_metadata(Path::new(file))?.st_uid();
+
+ // if user is same as existing, then do not change
+ if ud != uid {
+ chown(file, Some(Uid::from_raw(ud)), None)?;
+ }
+
+ Ok(())
+}
+
+fn change_group(file: &Path, group: &str) -> Result<(), FileError> {
+ let gd = get_group_by_name(group)
+ .map(|g| g.gid())
+ .ok_or_else(|| FileError::GroupNotFound {
+ group: group.into(),
+ })?;
+
+ let gid = get_metadata(Path::new(file))?.st_gid();
+
+ // if group is same as existing, then do not change
+ if gd != gid {
+ chown(file, None, Some(Gid::from_raw(gd)))?;
+ }
+
+ Ok(())
+}
+
+fn change_mode(file: &Path, mode: u32) -> Result<(), FileError> {
+ let mut perm = get_metadata(Path::new(file))?.permissions();
+ perm.set_mode(mode);
+
+ fs::set_permissions(file, perm).map_err(|e| FileError::MetaDataError {
+ name: file.display().to_string(),
+ from: e,
+ })?;
+
+ Ok(())
+}
+
+/// Return metadata when the given path exists and accessible by user
+pub fn get_metadata(path: &Path) -> Result<fs::Metadata, FileError> {
+ fs::metadata(&path).map_err(|_| FileError::PathNotAccessible {
+ path: path.to_path_buf(),
+ })
+}
+
+/// Return filename if the given path contains a filename
+pub fn get_filename(path: PathBuf) -> Option<String> {
+ let filename = path.file_name()?.to_str()?.to_string();
+ Some(filename)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::os::unix::fs::PermissionsExt;
+ use std::path::Path;
+ use tempfile::TempDir;
+
+ #[test]
+ fn create_file_correct_user_group() {
+ let temp_dir = TempDir::new().unwrap();
+ let file_path = temp_dir.path().join("file").display().to_string();
+
+ let user = whoami::username();
+ let _ = create_file_with_user_group(&file_path, &user, &user, 0o644, None).unwrap();
+ assert!(Path::new(file_path.as_str()).exists());
+ let meta = std::fs::metadata(file_path.as_str()).unwrap();
+ let perm = meta.permissions();
+ println!("{:o}", perm.mode());
+ assert!(format!("{:o}", perm.mode()).contains("644"));
+ }
+
+ #[test]
+ fn create_file_with_default_content() {
+ let temp_dir = TempDir::new().unwrap();
+ let file_path = temp_dir.path().join("file").display().to_string();
+ let user = whoami::username();
+
+ let example_config = r#"# Add the configurations to be managed by c8y-configuration-plugin
+ files = [
+ # { path = '/etc/tedge/tedge.toml' },
+ ]"#;
+
+ // Create a new file with default content
+ create_file_with_user_group(
+ &file_path,
+ &user,
+ &user,
+ 0o775,
+ Some(&example_config.to_string()),
+ )
+ .unwrap();
+
+ let content = fs::read(file_path).unwrap();
+ assert_eq!(example_config.as_bytes(), content);
+ }
+
+ #[test]
+ fn create_file_wrong_user() {
+ let temp_dir = TempDir::new().unwrap();
+ let file_path = temp_dir.path().join("file").display().to_string();
+
+ let user = whoami::username();
+ let err = create_file_with_user_group(&file_path, "test", &user, 0o775, None).unwrap_err();
+
+ assert!(err.to_string().contains("User not found"));
+ }
+
+ #[test]
+ fn create_file_wrong_group() {
+ let temp_dir = TempDir::new().unwrap();
+ let file_path = temp_dir.path().join("file").display().to_string();
+
+ let user = whoami::username();
+ let err = create_file_with_user_group(&file_path, &user, "test", 0o775, None).unwrap_err();
+
+ assert!(err.to_string().contains("Group not found"));
+ fs::remove_file(file_path.as_str()).unwrap();
+ }
+
+ #[test]
+ fn create_directory_with_correct_user_group() {
+ let temp_dir = TempDir::new().unwrap();
+ let dir_path = temp_dir.path().join("dir").display().to_string();
+
+ let user = whoami::username();
+ let _ = create_directory_with_user_group(&dir_path, &user, &user, 0o775).unwrap();
+
+ assert!(Path::new(dir_path.as_str()).exists());
+ let meta = fs::metadata(dir_path.as_str()).unwrap();
+ let perm = meta.permissions();
+ println!("{:o}", perm.mode());
+ assert!(format!("{:o}", perm.mode()).contains("775"));
+ }
+
+ #[test]
+ fn create_directory_with_wrong_user() {
+ let temp_dir = TempDir::new().unwrap();
+ let dir_path = temp_dir.path().join("dir").display().to_string();
+
+ let user = whoami::username();
+
+ let err = create_directory_with_user_group(dir_path, "test", &user, 0o775).unwrap_err();
+
+ assert!(err.to_string().contains("User not found"));
+ }
+
+ #[test]
+ fn create_directory_with_wrong_group() {
+ let temp_dir = TempDir::new().unwrap();
+ let dir_path = temp_dir.path().join("dir").display().to_string();
+
+ let user = whoami::username();
+
+ let err = create_directory_with_user_group(dir_path, &user, "test", 0o775).unwrap_err();
+
+ assert!(err.to_string().contains("Group not found"));
+ }
+
+ #[test]
+ fn change_file_permissions() {
+ let temp_dir = TempDir::new().unwrap();
+ let file_path = temp_dir.path().join("file").display().to_string();
+
+ let user = whoami::username();
+ let _ = create_file_with_user_group(&file_path, &user, &user, 0o644, None).unwrap();
+ assert!(Path::new(file_path.as_str()).exists());
+
+ let meta = fs::metadata(file_path.as_str()).unwrap();
+ let perm = meta.permissions();
+ assert!(format!("{:o}", perm.mode()).contains("644"));
+
+ let permission_set = PermissionEntry::new(None, None, Some(0o444));
+ permission_set.apply(Path::new(file_path.as_str())).unwrap();
+
+ let meta = fs::metadata(file_path.as_str()).unwrap();
+ let perm = meta.permissions();
+ assert!(format!("{:o}", perm.mode()).contains("444"));
+ }
+
+ #[test]
+ fn verify_get_file_name() {
+ assert_eq!(
+ get_filename(PathBuf::from("/it/is/file.txt")),
+ Some("file.txt".to_string())
+ );
+ assert_eq!(get_filename(PathBuf::from("/")), None);
+ }
+}