From 3a4e56f0f595e72f43d8dfcd53b7674fc47d9963 Mon Sep 17 00:00:00 2001 From: Alexander Gil Date: Thu, 6 Jan 2022 22:00:36 +0100 Subject: [PATCH] feat(module): Add preserve mode to copy module This allows to preserve original file mode. Part of #214 --- rash_book/src/docopt.md | 9 +-- rash_core/src/modules/copy.rs | 120 +++++++++++++++++++++++++++------- 2 files changed, 101 insertions(+), 28 deletions(-) diff --git a/rash_book/src/docopt.md b/rash_book/src/docopt.md index d90def26..09d96331 100644 --- a/rash_book/src/docopt.md +++ b/rash_book/src/docopt.md @@ -8,8 +8,8 @@ weight: 7000 `rash` has an integrated command-line parser based in the documentation of your script. This is an ad-hoc implementation based in [Docopt](http://docopt.org/). The main idea -behind it is that you just write the documentation of your script and Rash -automatically parses arguments based on it. +behind is to write the documentation and `rash` automatically parses arguments based on it. + E.g.: ```yaml @@ -53,8 +53,9 @@ sequence of characters delimited by either whitespace, one of `[]()|` characters ### [optional elements] Elements (arguments, commands) enclosed with square brackets `[]` are marked to be -optional. It does not matter if elements are enclosed in the same or different pairs of brackets, -e.g.: +optional. It does not matter if elements are enclosed in the same or different pairs of brackets. + +E.g.: ``` Usage: my_program [command ] diff --git a/rash_core/src/modules/copy.rs b/rash_core/src/modules/copy.rs index d6fd7cc5..9c443b58 100644 --- a/rash_core/src/modules/copy.rs +++ b/rash_core/src/modules/copy.rs @@ -20,7 +20,7 @@ /// mode: "0400" /// ``` /// ANCHOR_END: examples -use crate::error::Result; +use crate::error::{Error, ErrorKind, Result}; use crate::logger::diff_files; use crate::modules::{parse_params, ModuleResult}; use crate::utils::parse_octal; @@ -29,7 +29,7 @@ use crate::vars::Vars; #[cfg(feature = "docs")] use rash_derive::DocJsonSchema; -use std::fs::{set_permissions, File, OpenOptions}; +use std::fs::{metadata, set_permissions, File, OpenOptions}; use std::io::prelude::*; use std::io::SeekFrom; use std::io::{BufReader, Write}; @@ -52,10 +52,12 @@ pub struct Params { /// The absolute path where the file should be copied to. pub dest: String, /// Permissions of the destination file or directory. + /// The mode may also be the special string `preserve`. + /// `preserve` means that the file will be given the same permissions as the source file. pub mode: Option, } -#[derive(Debug, PartialEq, Deserialize)] +#[derive(Clone, Debug, PartialEq, Deserialize)] #[cfg_attr(feature = "docs", derive(EnumString, Display, JsonSchema))] #[serde(rename_all = "lowercase")] pub enum Input { @@ -94,11 +96,11 @@ pub fn copy_file(params: Params, check_mode: bool) -> Result { let mut buf_reader = BufReader::new(&read_file); let mut content = String::new(); buf_reader.read_to_string(&mut content)?; - let metadata = read_file.metadata()?; - let mut permissions = metadata.permissions(); + let dest_metadata = read_file.metadata()?; + let mut dest_permissions = dest_metadata.permissions(); let mut changed = false; - let desired_content = match params.input { + let desired_content = match params.input.clone() { Input::Content(s) => s, Input::Src(src) => { let file = File::open(&src)?; @@ -114,10 +116,10 @@ pub fn copy_file(params: Params, check_mode: bool) -> Result { if !check_mode { trace!("changing content: {:?}", &desired_content); - if permissions.readonly() { - let mut p = permissions.clone(); + if dest_permissions.readonly() { + let mut p = dest_permissions.clone(); // enable write - p.set_mode(permissions.mode() | 0o200); + p.set_mode(dest_permissions.mode() | 0o200); set_permissions(¶ms.dest, p)?; } @@ -126,28 +128,52 @@ pub fn copy_file(params: Params, check_mode: bool) -> Result { file.write_all(desired_content.as_bytes())?; file.set_len(desired_content.len() as u64)?; - if permissions.readonly() { - set_permissions(¶ms.dest, permissions.clone())?; + if dest_permissions.readonly() { + set_permissions(¶ms.dest, dest_permissions.clone())?; } } changed = true; }; - let mode = match params.mode { - Some(s) => parse_octal(&s)?, - None => parse_octal("0644")?, - }; - - // & 0o7777 to remove lead 100: 100644 -> 644 - let original_mode = permissions.mode() & 0o7777; - if original_mode != mode { - if !check_mode { - trace!("changing mode: {:o}", &mode); - permissions.set_mode(mode); - set_permissions(¶ms.dest, permissions)?; + match params.mode.as_deref() { + Some("preserve") => match params.input { + Input::Src(src) => { + let src_metadata = metadata(&src)?; + let src_permissions = src_metadata.permissions(); + let mode = src_permissions.mode(); + if dest_permissions.mode() != mode { + if !check_mode { + trace!("changing mode: {:o}", &mode); + dest_permissions.set_mode(mode); + set_permissions(¶ms.dest, dest_permissions)?; + } + changed = true; + }; + } + // errordest_permissions + _ => { + return Err(Error::new( + ErrorKind::InvalidData, + "preserve cannot be used in with content", + )) + } + }, + Some(s) => { + let mode = parse_octal(s)?; + + // & 0o7777 to remove lead 100: 100644 -> 644 + let original_mode = dest_permissions.mode() & 0o7777; + if original_mode != mode { + if !check_mode { + trace!("changing mode: {:o}", &mode); + dest_permissions.set_mode(mode); + set_permissions(¶ms.dest, dest_permissions)?; + } + changed = true; + }; } - changed = true; + None => (), }; Ok(ModuleResult { @@ -346,6 +372,52 @@ mod tests { ); } + #[test] + fn test_copy_file_preserve() { + let src_dir = tempdir().unwrap(); + let dest_dir = tempdir().unwrap(); + + let file_src_path = src_dir.path().join("preserve.txt"); + let file_dest_path = dest_dir.path().join("preserve.txt"); + let mut file = File::create(file_src_path.clone()).unwrap(); + writeln!(file, "test").unwrap(); + + let mut permissions = file.metadata().unwrap().permissions(); + permissions.set_mode(0o604); + set_permissions(&file_src_path, permissions).unwrap(); + + let output = copy_file( + Params { + input: Input::Src(file_src_path.to_str().unwrap().to_string()), + dest: file_dest_path.to_str().unwrap().to_string(), + mode: Some("preserve".to_string()), + }, + false, + ) + .unwrap(); + + let mut file = File::open(&file_dest_path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + assert_eq!(contents, "test\n"); + + let metadata = file.metadata().unwrap(); + let permissions = metadata.permissions(); + assert_eq!( + format!("{:o}", permissions.mode() & 0o7777), + format!("{:o}", 0o604) + ); + + assert_eq!( + output, + ModuleResult { + changed: true, + output: Some(file_dest_path.to_str().unwrap().to_string()), + extra: None, + } + ); + } + #[test] fn test_copy_file_change() { let dir = tempdir().unwrap();