@@ -28,6 +28,8 @@ enum CopyType {
28
28
SingleFile ,
29
29
/// equivalent to cp -a <source> <dest>
30
30
Archive ,
31
+ /// equivalent to cp -r <source> <dest>
32
+ Recursive ,
31
33
}
32
34
33
35
/// Encapsulate a copy operation
@@ -42,18 +44,22 @@ struct CopyOperation {
42
44
43
45
/// Parse command line arguments and transform into `CopyOperation`
44
46
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" ) ) ) {
46
48
return Err ( io:: Error :: new (
47
49
io:: ErrorKind :: InvalidInput ,
48
- "Invalid parameters. Expected cp [-a] <source> <destination>" ,
50
+ "Invalid parameters. Expected cp [-a | -r ] <source> <destination>" ,
49
51
) ) ;
50
52
}
51
53
52
54
if args. len ( ) == 4 {
53
55
return Ok ( CopyOperation {
54
56
source : PathBuf :: from ( args[ 2 ] ) ,
55
57
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
+ } ,
57
63
} ) ;
58
64
}
59
65
@@ -69,7 +75,48 @@ fn do_copy(operation: CopyOperation) -> io::Result<()> {
69
75
match operation. copy_type {
70
76
CopyType :: Archive => copy_archive ( & operation. source , & operation. destination ) ?,
71
77
CopyType :: SingleFile => fs:: copy ( & operation. source , & operation. destination ) . map ( |_| ( ) ) ?,
78
+ CopyType :: Recursive => copy_recursive ( & operation. source , & operation. destination ) ?,
79
+ } ;
80
+ Ok ( ( ) )
81
+ }
82
+
83
+ fn copy_recursive ( source : & Path , dest : & Path ) -> io:: Result < ( ) > {
84
+ // This will cover the case in which the destination exists
85
+ let sanitized_dest: PathBuf = if dest. exists ( ) {
86
+ // If the path is a normal file, this is the file name. If it’s the path of a directory, this is the directory name.s
87
+ dest. to_path_buf ( )
88
+ . join ( source. file_name ( ) . ok_or ( io:: Error :: new (
89
+ io:: ErrorKind :: InvalidInput ,
90
+ "Invalid source file" ,
91
+ ) ) ?)
92
+ } else {
93
+ dest. to_path_buf ( )
72
94
} ;
95
+
96
+ let mut stack = VecDeque :: new ( ) ;
97
+ stack. push_back ( ( source. to_path_buf ( ) , sanitized_dest) ) ;
98
+
99
+ while let Some ( ( current_source, current_dest) ) = stack. pop_back ( ) {
100
+ if current_source. is_dir ( ) {
101
+ fs:: create_dir ( & current_dest) ?;
102
+ for entry in fs:: read_dir ( current_source) ? {
103
+ let next_source = entry?. path ( ) ;
104
+ let next_dest =
105
+ current_dest
106
+ . clone ( )
107
+ . join ( next_source. file_name ( ) . ok_or ( io:: Error :: new (
108
+ io:: ErrorKind :: InvalidInput ,
109
+ "Invalid source file" ,
110
+ ) ) ?) ;
111
+ stack. push_back ( ( next_source, next_dest) ) ;
112
+ }
113
+ } else if current_source. is_symlink ( ) {
114
+ // Follow symbolic links as regular files
115
+ fs:: copy ( current_source, current_dest) ?;
116
+ } else if current_source. is_file ( ) {
117
+ fs:: copy ( current_source, current_dest) ?;
118
+ }
119
+ }
73
120
Ok ( ( ) )
74
121
}
75
122
@@ -172,7 +219,7 @@ mod tests {
172
219
fn parser_failure ( ) {
173
220
// prepare
174
221
let inputs = vec ! [
175
- vec![ "cp" , "-r" , "foo.txt" , "bar.txt" ] ,
222
+ vec![ "cp" , "-r" , "foo.txt" , "bar.txt" , "foo1.txt" ] ,
176
223
vec![ "cp" , "-a" , "param1" , "param2" , "param3" ] ,
177
224
vec![ "cp" , "param1" , "param2" , "param3" ] ,
178
225
] ;
@@ -186,6 +233,24 @@ mod tests {
186
233
}
187
234
}
188
235
236
+ #[ test]
237
+ fn parser_correct ( ) {
238
+ // prepare
239
+ let inputs = vec ! [
240
+ vec![ "cp" , "-r" , "foo.txt" , "bar.txt" ] ,
241
+ vec![ "cp" , "-a" , "param1" , "param2" ] ,
242
+ vec![ "cp" , "param1" , "param2" ] ,
243
+ ] ;
244
+
245
+ for input in inputs. into_iter ( ) {
246
+ // act
247
+ let result = parse_args ( input. clone ( ) ) ;
248
+
249
+ // assert
250
+ assert ! ( result. is_ok( ) , "input should fail {:?}" , input) ;
251
+ }
252
+ }
253
+
189
254
#[ test]
190
255
fn test_copy_single ( ) {
191
256
// prepare
@@ -229,6 +294,51 @@ mod tests {
229
294
assert ! ( result. is_err( ) ) ;
230
295
}
231
296
297
+ #[ test]
298
+ fn test_copy_recursive ( ) {
299
+ // prepare
300
+ let tempdir = tempfile:: tempdir ( ) . unwrap ( ) ;
301
+ let test_base = tempdir. path ( ) . to_path_buf ( ) ;
302
+ [ "foo" , "foo/foo0" , "foo/foo1" , "foo/bar" ]
303
+ . iter ( )
304
+ . for_each ( |x| create_dir ( & test_base, x) ) ;
305
+ let files = [
306
+ "foo/file1.txt" ,
307
+ "foo/file2.txt" ,
308
+ "foo/foo1/file3.txt" ,
309
+ "foo/bar/file4.txt" ,
310
+ ] ;
311
+ files. iter ( ) . for_each ( |x| create_file ( & test_base, x) ) ;
312
+ [ ( "foo/symlink1.txt" , "./file1.txt" ) ]
313
+ . iter ( )
314
+ . for_each ( |( x, y) | create_symlink ( & test_base, x, y) ) ;
315
+
316
+ // act
317
+ let recursive_copy = CopyOperation {
318
+ copy_type : CopyType :: Recursive ,
319
+ source : test_base. join ( "foo" ) ,
320
+ destination : test_base. join ( "bar" ) ,
321
+ } ;
322
+ do_copy ( recursive_copy) . unwrap ( ) ;
323
+
324
+ // assert
325
+ files. iter ( ) . for_each ( |x| {
326
+ assert_same_file (
327
+ & test_base. join ( x) ,
328
+ & test_base. join ( x. replace ( "foo/" , "bar/" ) ) ,
329
+ )
330
+ } ) ;
331
+ assert_same_file (
332
+ & test_base. join ( "foo/symlink1.txt" ) ,
333
+ & test_base. join ( "bar/symlink1.txt" ) ,
334
+ ) ;
335
+ // recursive copy will treat symlink as a file
336
+ assert_recursive_same_link (
337
+ & test_base. join ( "foo/symlink1.txt" ) ,
338
+ & test_base. join ( "bar/symlink1.txt" ) ,
339
+ )
340
+ }
341
+
232
342
#[ test]
233
343
fn test_copy_archive ( ) {
234
344
// prepare
@@ -351,4 +461,16 @@ mod tests {
351
461
352
462
assert_eq ! ( fs:: read_link( source) . unwrap( ) , fs:: read_link( dest) . unwrap( ) ) ;
353
463
}
464
+
465
+ fn assert_recursive_same_link ( source : & Path , dest : & Path ) {
466
+ assert ! ( source. exists( ) ) ;
467
+ assert ! ( dest. exists( ) ) ;
468
+ assert ! ( source. is_symlink( ) ) ;
469
+ assert ! ( dest. is_file( ) ) ;
470
+
471
+ assert_eq ! (
472
+ fs:: read_to_string( source) . unwrap( ) ,
473
+ fs:: read_to_string( dest) . unwrap( )
474
+ ) ;
475
+ }
354
476
}
0 commit comments