1use std::fs;
4use std::path::PathBuf;
5
6use tokio::sync::mpsc;
7
8use crate::conflict::{auto_rename_path, Conflict, ConflictKind, ConflictResolution};
9use crate::progress::{OperationComplete, OperationProgress, OperationType};
10use crate::{OperationError, OPERATION_CHANNEL_SIZE};
11
12#[derive(Debug)]
14pub enum CopyResult {
15 Progress(OperationProgress),
17 Conflict(Conflict),
19 Complete(OperationComplete),
21}
22
23#[derive(Debug, Clone, Default)]
25pub struct CopyOptions {
26 pub conflict_resolution: Option<ConflictResolution>,
28 pub preserve_timestamps: bool,
30}
31
32pub fn start_copy(
36 sources: Vec<PathBuf>,
37 destination: PathBuf,
38 options: CopyOptions,
39) -> mpsc::Receiver<CopyResult> {
40 let (tx, rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
41
42 if sources.is_empty() {
43 let complete = OperationComplete {
45 operation_type: OperationType::Copy,
46 succeeded: 0,
47 failed: 0,
48 bytes_processed: 0,
49 errors: vec![],
50 };
51 tokio::spawn(async move {
52 let _ = tx.send(CopyResult::Complete(complete)).await;
53 });
54 return rx;
55 }
56
57 tokio::spawn(async move {
58 copy_impl(sources, destination, options, tx).await;
59 });
60
61 rx
62}
63
64async fn copy_impl(
66 sources: Vec<PathBuf>,
67 destination: PathBuf,
68 options: CopyOptions,
69 tx: mpsc::Sender<CopyResult>,
70) {
71 let (total_files, total_bytes) = calculate_totals(&sources);
73
74 let mut progress = OperationProgress::new(OperationType::Copy, total_files, total_bytes);
75 let global_resolution: Option<ConflictResolution> = options.conflict_resolution;
76 let mut succeeded = 0;
77 let mut failed = 0;
78
79 if !destination.exists() {
81 if let Err(e) = fs::create_dir_all(&destination) {
82 progress.add_error(OperationError::new(
83 destination.clone(),
84 format!("Failed to create destination: {}", e),
85 ));
86 let _ = tx
87 .send(CopyResult::Complete(OperationComplete {
88 operation_type: OperationType::Copy,
89 succeeded: 0,
90 failed: sources.len(),
91 bytes_processed: 0,
92 errors: progress.errors.clone(),
93 }))
94 .await;
95 return;
96 }
97 }
98
99 for source in sources {
100 let dest_path = destination.join(source.file_name().unwrap_or_default());
101
102 if dest_path.exists() {
104 let conflict_kind = if dest_path.is_dir() {
105 ConflictKind::DirectoryExists
106 } else {
107 ConflictKind::FileExists
108 };
109
110 let resolution = if let Some(res) = global_resolution {
111 res.to_single()
112 } else {
113 let _ = tx
116 .send(CopyResult::Conflict(Conflict::new(
117 source.clone(),
118 dest_path.clone(),
119 conflict_kind,
120 )))
121 .await;
122 ConflictResolution::Skip
123 };
124
125 match resolution {
126 ConflictResolution::Skip | ConflictResolution::SkipAll => {
127 failed += 1;
128 continue;
129 }
130 ConflictResolution::Abort => {
131 let _ = tx
132 .send(CopyResult::Complete(OperationComplete {
133 operation_type: OperationType::Copy,
134 succeeded,
135 failed: failed + 1,
136 bytes_processed: progress.bytes_processed,
137 errors: progress.errors.clone(),
138 }))
139 .await;
140 return;
141 }
142 ConflictResolution::AutoRename => {
143 let new_dest = auto_rename_path(&dest_path);
144 if let Err(e) = copy_item(&source, &new_dest, &mut progress, &tx).await {
145 progress.add_error(OperationError::new(source.clone(), e));
146 failed += 1;
147 } else {
148 succeeded += 1;
149 }
150 continue;
151 }
152 ConflictResolution::Overwrite | ConflictResolution::OverwriteAll => {
153 let _ = if let Ok(metadata) = fs::symlink_metadata(&dest_path) {
155 if metadata.is_symlink() {
156 fs::remove_file(&dest_path)
157 } else if metadata.is_dir() {
158 fs::remove_dir_all(&dest_path)
159 } else {
160 fs::remove_file(&dest_path)
161 }
162 } else {
163 Ok(())
164 };
165 }
166 }
167 }
168
169 progress.set_current_file(Some(source.clone()));
171 let _ = tx.send(CopyResult::Progress(progress.clone())).await;
172
173 if let Err(e) = copy_item(&source, &dest_path, &mut progress, &tx).await {
174 progress.add_error(OperationError::new(source.clone(), e));
175 failed += 1;
176 } else {
177 succeeded += 1;
178 }
179 }
180
181 let _ = tx
183 .send(CopyResult::Complete(OperationComplete {
184 operation_type: OperationType::Copy,
185 succeeded,
186 failed,
187 bytes_processed: progress.bytes_processed,
188 errors: progress.errors,
189 }))
190 .await;
191}
192
193async fn copy_item(
195 source: &PathBuf,
196 dest: &PathBuf,
197 progress: &mut OperationProgress,
198 tx: &mpsc::Sender<CopyResult>,
199) -> Result<(), String> {
200 let source = source.clone();
201 let dest = dest.clone();
202
203 let result = tokio::task::spawn_blocking(move || {
204 let metadata = fs::symlink_metadata(&source)
206 .map_err(|e| format!("Failed to read metadata: {}", e))?;
207
208 if metadata.is_symlink() {
209 let target = fs::read_link(&source)
211 .map_err(|e| format!("Failed to read symlink: {}", e))?;
212 #[cfg(unix)]
213 {
214 std::os::unix::fs::symlink(&target, &dest)
215 .map_err(|e| format!("Failed to create symlink: {}", e))?;
216 }
217 #[cfg(windows)]
218 {
219 if target.is_dir() {
220 std::os::windows::fs::symlink_dir(&target, &dest)
221 .map_err(|e| format!("Failed to create symlink: {}", e))?;
222 } else {
223 std::os::windows::fs::symlink_file(&target, &dest)
224 .map_err(|e| format!("Failed to create symlink: {}", e))?;
225 }
226 }
227 Ok(0u64) } else if metadata.is_dir() {
229 copy_dir_recursive(&source, &dest)
230 } else {
231 copy_file(&source, &dest)
232 }
233 })
234 .await
235 .map_err(|e| format!("Task failed: {}", e))?;
236
237 match result {
238 Ok(bytes) => {
239 progress.complete_file(bytes);
240 let _ = tx.send(CopyResult::Progress(progress.clone())).await;
241 Ok(())
242 }
243 Err(e) => Err(e),
244 }
245}
246
247fn copy_file(source: &PathBuf, dest: &PathBuf) -> Result<u64, String> {
249 let metadata = fs::metadata(source).map_err(|e| format!("Failed to read metadata: {}", e))?;
250 let size = metadata.len();
251
252 fs::copy(source, dest).map_err(|e| format!("Failed to copy: {}", e))?;
253
254 Ok(size)
255}
256
257fn copy_dir_recursive(source: &PathBuf, dest: &PathBuf) -> Result<u64, String> {
259 fs::create_dir_all(dest).map_err(|e| format!("Failed to create directory: {}", e))?;
260
261 let mut total_bytes = 0u64;
262
263 let entries =
264 fs::read_dir(source).map_err(|e| format!("Failed to read directory: {}", e))?;
265
266 for entry in entries {
267 let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
268 let path = entry.path();
269 let dest_path = dest.join(entry.file_name());
270
271 if path.is_dir() {
272 total_bytes += copy_dir_recursive(&path, &dest_path)?;
273 } else {
274 total_bytes += copy_file(&path, &dest_path)?;
275 }
276 }
277
278 Ok(total_bytes)
279}
280
281fn calculate_totals(sources: &[PathBuf]) -> (usize, u64) {
283 let mut files = 0;
284 let mut bytes = 0u64;
285
286 for source in sources {
287 if source.is_dir() {
288 let (f, b) = calculate_dir_totals(source);
289 files += f;
290 bytes += b;
291 } else if let Ok(metadata) = fs::metadata(source) {
292 files += 1;
293 bytes += metadata.len();
294 }
295 }
296
297 (files, bytes)
298}
299
300fn calculate_dir_totals(dir: &PathBuf) -> (usize, u64) {
302 let mut files = 0;
303 let mut bytes = 0u64;
304
305 if let Ok(entries) = fs::read_dir(dir) {
306 for entry in entries.flatten() {
307 let path = entry.path();
308 if path.is_dir() {
309 let (f, b) = calculate_dir_totals(&path);
310 files += f;
311 bytes += b;
312 } else if let Ok(metadata) = fs::metadata(&path) {
313 files += 1;
314 bytes += metadata.len();
315 }
316 }
317 }
318
319 (files, bytes)
320}