Skip to content

Commit 83b87cf

Browse files
authored
Add cp -r support to cp-utility tool (#285)
*Issue #, if available:* ADOT SDK instrumentation Docker Images only support `cp -a` copy command so that the [CWAgentOperator can extract](https://github.com/aws/amazon-cloudwatch-agent-operator/blob/main/pkg/instrumentation/nodejs.go#L65) the instrumentation SDK via `cp -a`, but upstream OTel Operator has [changed a few months ago](open-telemetry/opentelemetry-operator@4cd6dcb) to use `cp -r` instead of `-a` to copy files from the OTel SDK Instrumentation Images. Today, OTel Operator is not compatible with some of the ADOT SDKs, and in the future, CWAgent Operator may also change to use `cp -r` as well *Description of changes:* - Copy changes from Java's PR to add `cp -r` support - aws-observability/aws-otel-java-instrumentation#843 - Modify the above `cp -r` support with bug fix implemented in Python's `cp -a` command - #214 (comment) *Testing:* - Tested on PetClinic Sample App's Python Services with latest OTel Operator + Collector ![image](https://github.com/user-attachments/assets/c08aec2b-01ce-46c4-abfd-27c5b4f16c6b) By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 042edf1 commit 83b87cf

File tree

1 file changed

+115
-5
lines changed

1 file changed

+115
-5
lines changed

tools/cp-utility/src/main.rs

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ enum CopyType {
2828
SingleFile,
2929
/// equivalent to cp -a <source> <dest>
3030
Archive,
31+
/// equivalent to cp -r <source> <dest>
32+
Recursive,
3133
}
3234

3335
/// Encapsulate a copy operation
@@ -42,18 +44,22 @@ struct CopyOperation {
4244

4345
/// Parse command line arguments and transform into `CopyOperation`
4446
fn parse_args(args: Vec<&str>) -> io::Result<CopyOperation> {
45-
if !(args.len() == 3 || args.len() == 4 && args[1].eq("-a")) {
47+
if !(args.len() == 3 || (args.len() == 4 && (args[1] == "-a" || args[1] == "-r"))) {
4648
return Err(io::Error::new(
4749
io::ErrorKind::InvalidInput,
48-
"Invalid parameters. Expected cp [-a] <source> <destination>",
50+
"Invalid parameters. Expected cp [-a | -r] <source> <destination>",
4951
));
5052
}
5153

5254
if args.len() == 4 {
5355
return Ok(CopyOperation {
5456
source: PathBuf::from(args[2]),
5557
destination: PathBuf::from(args[3]),
56-
copy_type: CopyType::Archive,
58+
copy_type: match args[1] {
59+
"-a" => CopyType::Archive,
60+
"-r" => CopyType::Recursive,
61+
_ => panic!("Invalid option. Expected -a or -r"),
62+
},
5763
});
5864
}
5965

@@ -69,10 +75,40 @@ fn do_copy(operation: CopyOperation) -> io::Result<()> {
6975
match operation.copy_type {
7076
CopyType::Archive => copy_archive(&operation.source, &operation.destination)?,
7177
CopyType::SingleFile => fs::copy(&operation.source, &operation.destination).map(|_| ())?,
78+
CopyType::Recursive => copy_recursive(&operation.source, &operation.destination)?,
7279
};
7380
Ok(())
7481
}
7582

83+
fn copy_recursive(source: &Path, dest: &Path) -> io::Result<()> {
84+
let mut stack = VecDeque::new();
85+
stack.push_back((source.to_path_buf(), dest.to_path_buf()));
86+
while let Some((current_source, current_dest)) = stack.pop_back() {
87+
if current_source.is_dir() {
88+
if !current_dest.exists() {
89+
fs::create_dir(&current_dest)?;
90+
}
91+
for entry in fs::read_dir(current_source)? {
92+
let next_source = entry?.path();
93+
let next_dest =
94+
current_dest
95+
.clone()
96+
.join(next_source.file_name().ok_or(io::Error::new(
97+
io::ErrorKind::InvalidInput,
98+
"Invalid source file",
99+
))?);
100+
stack.push_back((next_source, next_dest));
101+
}
102+
} else if current_source.is_symlink() {
103+
// Follow symbolic links as regular files
104+
fs::copy(current_source, current_dest)?;
105+
} else if current_source.is_file() {
106+
fs::copy(current_source, current_dest)?;
107+
}
108+
}
109+
Ok(())
110+
}
111+
76112
// Execute the recursive type of copy operation
77113
fn copy_archive(source: &Path, dest: &Path) -> io::Result<()> {
78114
let mut stack = VecDeque::new();
@@ -100,7 +136,6 @@ fn copy_archive(source: &Path, dest: &Path) -> io::Result<()> {
100136
fs::copy(current_source, current_dest)?;
101137
}
102138
}
103-
104139
Ok(())
105140
}
106141

@@ -163,7 +198,7 @@ mod tests {
163198
fn parser_failure() {
164199
// prepare
165200
let inputs = vec![
166-
vec!["cp", "-r", "foo.txt", "bar.txt"],
201+
vec!["cp", "-r", "foo.txt", "bar.txt", "foo1.txt"],
167202
vec!["cp", "-a", "param1", "param2", "param3"],
168203
vec!["cp", "param1", "param2", "param3"],
169204
];
@@ -177,6 +212,24 @@ mod tests {
177212
}
178213
}
179214

215+
#[test]
216+
fn parser_correct() {
217+
// prepare
218+
let inputs = vec![
219+
vec!["cp", "-r", "foo.txt", "bar.txt"],
220+
vec!["cp", "-a", "param1", "param2"],
221+
vec!["cp", "param1", "param2"],
222+
];
223+
224+
for input in inputs.into_iter() {
225+
// act
226+
let result = parse_args(input.clone());
227+
228+
// assert
229+
assert!(result.is_ok(), "input should fail {:?}", input);
230+
}
231+
}
232+
180233
#[test]
181234
fn test_copy_single() {
182235
// prepare
@@ -220,6 +273,51 @@ mod tests {
220273
assert!(result.is_err());
221274
}
222275

276+
#[test]
277+
fn test_copy_recursive() {
278+
// prepare
279+
let tempdir = tempfile::tempdir().unwrap();
280+
let test_base = tempdir.path().to_path_buf();
281+
["foo", "foo/foo0", "foo/foo1", "foo/bar"]
282+
.iter()
283+
.for_each(|x| create_dir(&test_base, x));
284+
let files = [
285+
"foo/file1.txt",
286+
"foo/file2.txt",
287+
"foo/foo1/file3.txt",
288+
"foo/bar/file4.txt",
289+
];
290+
files.iter().for_each(|x| create_file(&test_base, x));
291+
[("foo/symlink1.txt", "./file1.txt")]
292+
.iter()
293+
.for_each(|(x, y)| create_symlink(&test_base, x, y));
294+
295+
// act
296+
let recursive_copy = CopyOperation {
297+
copy_type: CopyType::Recursive,
298+
source: test_base.join("foo"),
299+
destination: test_base.join("bar"),
300+
};
301+
do_copy(recursive_copy).unwrap();
302+
303+
// assert
304+
files.iter().for_each(|x| {
305+
assert_same_file(
306+
&test_base.join(x),
307+
&test_base.join(x.replace("foo/", "bar/")),
308+
)
309+
});
310+
assert_same_file(
311+
&test_base.join("foo/symlink1.txt"),
312+
&test_base.join("bar/symlink1.txt"),
313+
);
314+
// recursive copy will treat symlink as a file
315+
assert_recursive_same_link(
316+
&test_base.join("foo/symlink1.txt"),
317+
&test_base.join("bar/symlink1.txt"),
318+
)
319+
}
320+
223321
#[test]
224322
fn test_copy_archive() {
225323
// prepare
@@ -342,4 +440,16 @@ mod tests {
342440

343441
assert_eq!(fs::read_link(source).unwrap(), fs::read_link(dest).unwrap());
344442
}
443+
444+
fn assert_recursive_same_link(source: &Path, dest: &Path) {
445+
assert!(source.exists());
446+
assert!(dest.exists());
447+
assert!(source.is_symlink());
448+
assert!(dest.is_file());
449+
450+
assert_eq!(
451+
fs::read_to_string(source).unwrap(),
452+
fs::read_to_string(dest).unwrap()
453+
);
454+
}
345455
}

0 commit comments

Comments
 (0)