Skip to content

Commit

Permalink
Parse user input for what attributes to preserve
Browse files Browse the repository at this point in the history
  • Loading branch information
wykurz committed Apr 19, 2024
1 parent ad07bb5 commit fc7c978
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 36 deletions.
61 changes: 48 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,29 +45,64 @@ USAGE:
rcp [FLAGS] [OPTIONS] [paths]...
FLAGS:
-L, --dereference Always follow symbolic links in source
-e, --fail-early Exit on first error
-h, --help Prints help information
-o, --overwrite Overwrite existing files/directories
-p, --preserve Preserve additional file attributes: file owner, group, setuid, setgid, mtime and atime
--progress Show progress
-q, --quiet Quiet mode, don't report errors
--summary Print summary at the end
-V, --version Prints version information
-v, --verbose Verbose level (implies "summary"): -v INFO / -vv DEBUG / -vvv TRACE (default: ERROR))
-L, --dereference
Always follow symbolic links in source
-e, --fail-early
Exit on first error
-h, --help
Prints help information
-o, --overwrite
Overwrite existing files/directories
-p, --preserve
Preserve additional file attributes: file owner, group, setuid, setgid, mtime and atime
--progress
Show progress
-q, --quiet
Quiet mode, don't report errors
--summary
Print summary at the end
-V, --version
Prints version information
-v, --verbose
Verbose level (implies "summary"): -v INFO / -vv DEBUG / -vvv TRACE (default: ERROR))
OPTIONS:
--max-blocking-threads <max-blocking-threads>
Number of blocking worker threads, 0 means Tokio runtime default (512) [default: 0]
--max-workers <max-workers> Number of worker threads, 0 means number of cores [default: 0]
--max-workers <max-workers>
Number of worker threads, 0 means number of cores [default: 0]
--overwrite-compare <overwrite-compare>
Comma separated list of file attributes to compare when when deciding if files are "identical", used with
--overwrite flag. Options are: uid, gid, size, mtime, ctime [default: size,mtime]
--read-buffer <read-buffer> File copy read buffer size [default: 128KiB]
--preserve-settings <preserve-settings>
Specify exactly what attributes to preserve.
If specified, the "preserve" flag is ignored.
The format is: "<type1>:<attributes1> <type2>:<attributes2> ..." Where <type> is one of: "f" (file), "d"
(directory), "l" (symlink) And <attributes> is a comma separated list of: "uid", "gid", "time", <mode mask>
Where <mode mask> is a 4 digit octal number
Example: "f:uid,gid,time,0777 d:uid,gid,time,0777 l:uid,gid,time"
--read-buffer <read-buffer>
File copy read buffer size [default: 128KiB]
ARGS:
<paths>... Source path(s) and destination path
<paths>...
Source path(s) and destination path
```

rrm
Expand Down
62 changes: 62 additions & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#[macro_use]
extern crate lazy_static;

use anyhow::Context;
use anyhow::{anyhow, Result};
use std::future::Future;
use tracing::{event, instrument, Level};
Expand Down Expand Up @@ -99,6 +100,67 @@ pub fn parse_metadata_cmp_settings(settings: &str) -> Result<filecmp::MetadataCm
Ok(metadata_cmp_settings)
}

fn parse_type_settings(
settings: &str,
) -> Result<(preserve::UserAndTimeSettings, Option<preserve::ModeMask>)> {
let mut user_and_time = preserve::UserAndTimeSettings::default();
let mut mode_mask = None;
for setting in settings.split(',') {
match setting {
"uid" => user_and_time.uid = true,
"gid" => user_and_time.gid = true,
"time" => user_and_time.time = true,
_ => {
if let Ok(mask) = u32::from_str_radix(setting, 8) {
mode_mask = Some(mask);
} else {
return Err(anyhow!("Unknown preserve attribute specified: {}", setting));
}
}
}
}
Ok((user_and_time, mode_mask))
}

pub fn parse_preserve_settings(settings: &str) -> Result<preserve::PreserveSettings> {
let mut preserve_settings = preserve::PreserveSettings::default();
for type_settings in settings.split(' ') {
if let Some((file_type, settings)) = type_settings.split_once(':') {
let (user_and_time_settings, mode_opt) =
parse_type_settings(settings).context(format!(
"parsing preserve settings: {}, type: {}",
settings, file_type
))?;
match file_type {
"f" | "file" => {
preserve_settings.file = preserve::FileSettings::default();
preserve_settings.file.user_and_time = user_and_time_settings;
if let Some(mode) = mode_opt {
preserve_settings.file.mode_mask = mode;
};
}
"d" | "dir" | "directory" => {
preserve_settings.dir = preserve::DirSettings::default();
preserve_settings.dir.user_and_time = user_and_time_settings;
if let Some(mode) = mode_opt {
preserve_settings.dir.mode_mask = mode;
};
}
"l" | "link" | "symlink" => {
preserve_settings.symlink = preserve::SymlinkSettings::default();
preserve_settings.symlink.user_and_time = user_and_time_settings;
}
_ => {
return Err(anyhow!("Unknown file type: {}", file_type));
}
}
} else {
return Err(anyhow!("Invalid preserve settings: {}", settings));
}
}
Ok(preserve_settings)
}

pub async fn copy(
src: &std::path::Path,
dst: &std::path::Path,
Expand Down
46 changes: 25 additions & 21 deletions common/src/preserve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,36 @@ impl UserAndTimeSettings {
}
}

#[derive(Copy, Clone, Debug, Default)]
pub type ModeMask = u32;

#[derive(Copy, Clone, Debug)]
pub struct FileSettings {
pub user_and_time: UserAndTimeSettings,
pub mode_mask: u32,
pub mode_mask: ModeMask,
}

#[derive(Copy, Clone, Debug, Default)]
impl Default for FileSettings {
fn default() -> Self {
Self {
user_and_time: UserAndTimeSettings::default(),
mode_mask: 0o0777, // remove sticky bit, setuid and setgid to mimic "cp" tool
}
}
}

#[derive(Copy, Clone, Debug)]
pub struct DirSettings {
pub user_and_time: UserAndTimeSettings,
pub mode_mask: u32,
pub mode_mask: ModeMask,
}

impl Default for DirSettings {
fn default() -> Self {
Self {
user_and_time: UserAndTimeSettings::default(),
mode_mask: 0o0777,
}
}
}

#[derive(Copy, Clone, Debug, Default)]
Expand Down Expand Up @@ -171,21 +191,5 @@ pub fn preserve_all() -> PreserveSettings {
}

pub fn preserve_default() -> PreserveSettings {
let user_and_time = UserAndTimeSettings {
uid: false,
gid: false,
time: false,
};

PreserveSettings {
file: FileSettings {
user_and_time,
mode_mask: 0o0777, // remove sticky bit, setuid and setgid to mimic "cp" tool
},
dir: DirSettings {
user_and_time,
mode_mask: 0o0777,
},
symlink: SymlinkSettings { user_and_time },
}
PreserveSettings::default()
}
28 changes: 26 additions & 2 deletions rcp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ struct Args {
#[structopt(short, long)]
overwrite: bool,

/// Comma separated list of file attributes to compare when when deciding if files are "identical", used with --overwrite flag. Options are: uid, gid, size, mtime, ctime
/// Comma separated list of file attributes to compare when when deciding if files are "identical", used with --overwrite flag.
/// Options are: uid, gid, size, mtime, ctime
#[structopt(long, default_value = "size,mtime")]
overwrite_compare: String,

Expand All @@ -26,6 +27,19 @@ struct Args {
#[structopt(short, long)]
preserve: bool,

/// Specify exactly what attributes to preserve.
///
/// If specified, the "preserve" flag is ignored.
///
/// The format is: "<type1>:<attributes1> <type2>:<attributes2> ..."
/// Where <type> is one of: "f" (file), "d" (directory), "l" (symlink)
/// And <attributes> is a comma separated list of: "uid", "gid", "time", <mode mask>
/// Where <mode mask> is a 4 digit octal number
///
/// Example: "f:uid,gid,time,0777 d:uid,gid,time,0777 l:uid,gid,time"
#[structopt(long)]
preserve_settings: Option<String>,

/// Always follow symbolic links in source
#[structopt(short = "-L", long)]
dereference: bool,
Expand Down Expand Up @@ -115,11 +129,21 @@ async fn async_main(args: Args) -> Result<CopySummary> {
overwrite: args.overwrite,
overwrite_compare: common::parse_metadata_cmp_settings(&args.overwrite_compare)?,
};
let preserve = if args.preserve {
event!(Level::DEBUG, "copy settings: {:?}", &settings);
if args.preserve_settings.is_some() && args.preserve {
event!(
Level::WARN,
"The --preserve flag is ignored when --preserve-settings is specified!"
);
}
let preserve = if let Some(preserve_settings) = args.preserve_settings {
common::parse_preserve_settings(&preserve_settings)?
} else if args.preserve {
common::preserve_all()
} else {
common::preserve_default()
};
event!(Level::DEBUG, "preserve settings: {:?}", &preserve);
for (src_path, dst_path) in src_dst {
let do_copy =
|| async move { common::copy(&src_path, &dst_path, &settings, &preserve).await };
Expand Down

0 comments on commit fc7c978

Please sign in to comment.