Skip to content

Commit

Permalink
feat(module): Add preserve mode to copy module
Browse files Browse the repository at this point in the history
This allows to preserve original file mode.

Part of #214
  • Loading branch information
pando85 committed Jan 6, 2022
1 parent 06ff602 commit 3a4e56f
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 28 deletions.
9 changes: 5 additions & 4 deletions rash_book/src/docopt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <argument>]
Expand Down
120 changes: 96 additions & 24 deletions rash_core/src/modules/copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand All @@ -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<String>,
}

#[derive(Debug, PartialEq, Deserialize)]
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[cfg_attr(feature = "docs", derive(EnumString, Display, JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum Input {
Expand Down Expand Up @@ -94,11 +96,11 @@ pub fn copy_file(params: Params, check_mode: bool) -> Result<ModuleResult> {
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)?;
Expand All @@ -114,10 +116,10 @@ pub fn copy_file(params: Params, check_mode: bool) -> Result<ModuleResult> {

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(&params.dest, p)?;
}

Expand All @@ -126,28 +128,52 @@ pub fn copy_file(params: Params, check_mode: bool) -> Result<ModuleResult> {
file.write_all(desired_content.as_bytes())?;
file.set_len(desired_content.len() as u64)?;

if permissions.readonly() {
set_permissions(&params.dest, permissions.clone())?;
if dest_permissions.readonly() {
set_permissions(&params.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(&params.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(&params.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(&params.dest, dest_permissions)?;
}
changed = true;
};
}
changed = true;
None => (),
};

Ok(ModuleResult {
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 3a4e56f

Please sign in to comment.