command_stream/commands/
cp.rs1use crate::commands::CommandContext;
4use crate::utils::{trace_lazy, CommandResult, VirtualUtils};
5use std::fs;
6use std::path::Path;
7
8pub async fn cp(ctx: CommandContext) -> CommandResult {
12 if ctx.args.len() < 2 {
13 return VirtualUtils::invalid_argument_error("cp", "missing file operand");
14 }
15
16 let mut recursive = false;
18 let mut paths = Vec::new();
19
20 for arg in &ctx.args {
21 if arg == "-r" || arg == "-R" || arg == "--recursive" {
22 recursive = true;
23 } else if arg.starts_with('-') {
24 if arg.contains('r') || arg.contains('R') {
25 recursive = true;
26 }
27 } else {
28 paths.push(arg.clone());
29 }
30 }
31
32 if paths.len() < 2 {
33 return VirtualUtils::invalid_argument_error("cp", "missing destination file operand");
34 }
35
36 let cwd = ctx.get_cwd();
37 let dest = paths.pop().unwrap();
38 let dest_path = VirtualUtils::resolve_path(&dest, Some(&cwd));
39
40 let dest_is_dir = dest_path.is_dir();
42 let multiple_sources = paths.len() > 1;
43
44 if multiple_sources && !dest_is_dir {
45 return CommandResult::error(format!("cp: target '{}' is not a directory\n", dest));
46 }
47
48 for source in paths {
49 let source_path = VirtualUtils::resolve_path(&source, Some(&cwd));
50
51 trace_lazy("VirtualCommand", || {
52 format!("cp: copying {:?} to {:?}", source_path, dest_path)
53 });
54
55 if !source_path.exists() {
56 return CommandResult::error(format!(
57 "cp: cannot stat '{}': No such file or directory\n",
58 source
59 ));
60 }
61
62 let final_dest = if dest_is_dir {
63 dest_path.join(source_path.file_name().unwrap_or_default())
64 } else {
65 dest_path.clone()
66 };
67
68 if source_path.is_dir() {
69 if !recursive {
70 return CommandResult::error(format!(
71 "cp: -r not specified; omitting directory '{}'\n",
72 source
73 ));
74 }
75
76 if let Err(e) = copy_dir_recursive(&source_path, &final_dest) {
77 return CommandResult::error(format!("cp: cannot copy '{}': {}\n", source, e));
78 }
79 } else {
80 if let Some(parent) = final_dest.parent() {
81 if !parent.exists() {
82 if let Err(e) = fs::create_dir_all(parent) {
83 return CommandResult::error(format!(
84 "cp: cannot create directory '{}': {}\n",
85 parent.display(),
86 e
87 ));
88 }
89 }
90 }
91
92 if let Err(e) = fs::copy(&source_path, &final_dest) {
93 return CommandResult::error(format!("cp: cannot copy '{}': {}\n", source, e));
94 }
95 }
96 }
97
98 CommandResult::success_empty()
99}
100
101fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
102 fs::create_dir_all(dst)?;
103
104 for entry in fs::read_dir(src)? {
105 let entry = entry?;
106 let entry_path = entry.path();
107 let dest_path = dst.join(entry.file_name());
108
109 if entry_path.is_dir() {
110 copy_dir_recursive(&entry_path, &dest_path)?;
111 } else {
112 fs::copy(&entry_path, &dest_path)?;
113 }
114 }
115
116 Ok(())
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use tempfile::tempdir;
123
124 #[tokio::test]
125 async fn test_cp_file() {
126 let temp = tempdir().unwrap();
127 let src = temp.path().join("source.txt");
128 let dst = temp.path().join("dest.txt");
129 fs::write(&src, "test content").unwrap();
130
131 let ctx = CommandContext::new(vec![
132 src.to_string_lossy().to_string(),
133 dst.to_string_lossy().to_string(),
134 ]);
135 let result = cp(ctx).await;
136
137 assert!(result.is_success());
138 assert!(dst.exists());
139 assert_eq!(fs::read_to_string(&dst).unwrap(), "test content");
140 }
141
142 #[tokio::test]
143 async fn test_cp_directory_recursive() {
144 let temp = tempdir().unwrap();
145 let src_dir = temp.path().join("src_dir");
146 let dst_dir = temp.path().join("dst_dir");
147
148 fs::create_dir(&src_dir).unwrap();
149 fs::write(src_dir.join("file.txt"), "test").unwrap();
150
151 let ctx = CommandContext::new(vec![
152 "-r".to_string(),
153 src_dir.to_string_lossy().to_string(),
154 dst_dir.to_string_lossy().to_string(),
155 ]);
156 let result = cp(ctx).await;
157
158 assert!(result.is_success());
159 assert!(dst_dir.join("file.txt").exists());
160 }
161
162 #[tokio::test]
163 async fn test_cp_directory_without_recursive() {
164 let temp = tempdir().unwrap();
165 let src_dir = temp.path().join("src_dir");
166 let dst_dir = temp.path().join("dst_dir");
167
168 fs::create_dir(&src_dir).unwrap();
169
170 let ctx = CommandContext::new(vec![
171 src_dir.to_string_lossy().to_string(),
172 dst_dir.to_string_lossy().to_string(),
173 ]);
174 let result = cp(ctx).await;
175
176 assert!(!result.is_success());
177 assert!(result.stderr.contains("-r not specified"));
178 }
179}