1use std::collections::HashSet;
4use std::fs;
5use std::path::PathBuf;
6
7use tokio::sync::mpsc;
8use tokio_util::sync::CancellationToken;
9
10use crate::conflict::{Conflict, ConflictKind, ConflictResolution, auto_rename_path};
11use crate::progress::{OperationComplete, OperationProgress, OperationType};
12use crate::{OPERATION_CHANNEL_SIZE, OperationError};
13
14#[derive(Debug)]
16pub enum MoveResult {
17 Progress(OperationProgress),
19 Conflict(Conflict),
21 Complete(MoveComplete),
23}
24
25#[derive(Debug)]
28pub struct MoveComplete {
29 pub inner: OperationComplete,
31 pub moved_pairs: Vec<(PathBuf, PathBuf)>,
33}
34
35#[derive(Debug, Clone, Default)]
37pub struct MoveOptions {
38 pub conflict_resolution: Option<ConflictResolution>,
40}
41
42pub fn start_move(
46 sources: Vec<PathBuf>,
47 destination: PathBuf,
48 options: MoveOptions,
49 token: CancellationToken,
50) -> mpsc::Receiver<MoveResult> {
51 let (tx, rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
52
53 if sources.is_empty() {
54 let complete = MoveComplete {
55 inner: OperationComplete {
56 operation_type: OperationType::Move,
57 succeeded: 0,
58 failed: 0,
59 bytes_processed: 0,
60 errors: vec![],
61 },
62 moved_pairs: vec![],
63 };
64 tokio::spawn(async move {
65 let _ = tx.send(MoveResult::Complete(complete)).await;
66 });
67 return rx;
68 }
69
70 tokio::spawn(async move {
71 move_impl(sources, destination, options, token, tx).await;
72 });
73
74 rx
75}
76
77async fn move_impl(
79 sources: Vec<PathBuf>,
80 destination: PathBuf,
81 options: MoveOptions,
82 token: CancellationToken,
83 tx: mpsc::Sender<MoveResult>,
84) {
85 let total_files = sources.len();
86 let mut progress = OperationProgress::new(OperationType::Move, total_files, 0);
87 let global_resolution: Option<ConflictResolution> = options.conflict_resolution;
88 let mut succeeded = 0;
89 let mut failed = 0;
90 let mut moved_pairs: Vec<(PathBuf, PathBuf)> = Vec::new();
91
92 if !destination.exists()
94 && let Err(e) = fs::create_dir_all(&destination)
95 {
96 progress.add_error(OperationError::new(
97 destination.clone(),
98 format!("Failed to create destination: {}", e),
99 ));
100 let _ = tx
101 .send(MoveResult::Complete(MoveComplete {
102 inner: OperationComplete {
103 operation_type: OperationType::Move,
104 succeeded: 0,
105 failed: sources.len(),
106 bytes_processed: 0,
107 errors: progress.errors.clone(),
108 },
109 moved_pairs: vec![],
110 }))
111 .await;
112 return;
113 }
114
115 for source in sources {
116 if token.is_cancelled() {
118 break;
119 }
120
121 let file_name = match source.file_name() {
123 Some(n) => n.to_owned(),
124 None => {
125 progress.add_error(OperationError::new(
126 source.clone(),
127 "Source path has no filename component".to_string(),
128 ));
129 failed += 1;
130 continue;
131 }
132 };
133 let dest_path = destination.join(&file_name);
134
135 if dest_path.starts_with(&source) {
137 let _ = tx
138 .send(MoveResult::Conflict(Conflict::source_is_ancestor(
139 source.clone(),
140 dest_path.clone(),
141 )))
142 .await;
143 failed += 1;
144 continue;
145 }
146
147 let dest_meta = fs::symlink_metadata(&dest_path).ok();
149 let final_dest = if dest_meta.is_some() {
150 let conflict_kind = if dest_meta.as_ref().is_some_and(|m| m.is_dir()) {
151 ConflictKind::DirectoryExists
152 } else {
153 ConflictKind::FileExists
154 };
155
156 let resolution = if let Some(res) = global_resolution {
157 res.to_single()
158 } else {
159 let _ = tx
160 .send(MoveResult::Conflict(Conflict::new(
161 source.clone(),
162 dest_path.clone(),
163 conflict_kind,
164 )))
165 .await;
166 ConflictResolution::Skip
167 };
168
169 match resolution {
170 ConflictResolution::Skip | ConflictResolution::SkipAll => {
171 failed += 1;
172 continue;
173 }
174 ConflictResolution::Abort => {
175 let _ = tx
176 .send(MoveResult::Complete(MoveComplete {
177 inner: OperationComplete {
178 operation_type: OperationType::Move,
179 succeeded,
180 failed: failed + 1,
181 bytes_processed: progress.bytes_processed,
182 errors: progress.errors.clone(),
183 },
184 moved_pairs,
185 }))
186 .await;
187 return;
188 }
189 ConflictResolution::AutoRename => auto_rename_path(&dest_path),
190 ConflictResolution::Overwrite | ConflictResolution::OverwriteAll => {
191 let remove_result = if let Some(ref m) = dest_meta {
193 if m.is_symlink() || !m.is_dir() {
194 fs::remove_file(&dest_path)
195 } else {
196 fs::remove_dir_all(&dest_path)
197 }
198 } else {
199 Ok(())
200 };
201 if let Err(e) = remove_result {
202 progress.add_error(OperationError::new(
203 dest_path.clone(),
204 format!("Failed to remove existing destination: {}", e),
205 ));
206 failed += 1;
207 continue;
208 }
209 dest_path.clone()
210 }
211 }
212 } else {
213 dest_path.clone()
214 };
215
216 let source_clone = source.clone();
218 let dest_clone = final_dest.clone();
219
220 let result = tokio::task::spawn_blocking(move || move_item(&source_clone, &dest_clone))
221 .await
222 .map_err(|e| format!("Task failed: {}", e));
223
224 match result {
225 Ok(Ok(bytes)) => {
226 progress.set_current_file(Some(source.clone()));
227 progress.complete_file(bytes);
228 moved_pairs.push((source.clone(), final_dest));
229 succeeded += 1;
230 let _ = tx.send(MoveResult::Progress(progress.clone())).await;
231 }
232 Ok(Err(e)) | Err(e) => {
233 progress.add_error(OperationError::new(source.clone(), e));
234 failed += 1;
235 }
236 }
237 }
238
239 let _ = tx
241 .send(MoveResult::Complete(MoveComplete {
242 inner: OperationComplete {
243 operation_type: OperationType::Move,
244 succeeded,
245 failed,
246 bytes_processed: progress.bytes_processed,
247 errors: progress.errors,
248 },
249 moved_pairs,
250 }))
251 .await;
252}
253
254fn move_item(source: &PathBuf, dest: &PathBuf) -> Result<u64, String> {
256 let size = get_size(source);
258
259 if fs::rename(source, dest).is_ok() {
261 return Ok(size);
262 }
263
264 let metadata =
266 fs::symlink_metadata(source).map_err(|e| format!("Failed to read metadata: {}", e))?;
267
268 if metadata.is_symlink() {
269 let target = fs::read_link(source).map_err(|e| format!("Failed to read symlink: {}", e))?;
271 #[cfg(unix)]
272 {
273 std::os::unix::fs::symlink(&target, dest)
274 .map_err(|e| format!("Failed to create symlink: {}", e))?;
275 }
276 #[cfg(windows)]
277 {
278 if target.is_dir() {
279 std::os::windows::fs::symlink_dir(&target, dest)
280 .map_err(|e| format!("Failed to create symlink: {}", e))?;
281 } else {
282 std::os::windows::fs::symlink_file(&target, dest)
283 .map_err(|e| format!("Failed to create symlink: {}", e))?;
284 }
285 }
286 fs::remove_file(source).map_err(|e| format!("Failed to remove source symlink: {}", e))?;
287 } else if metadata.is_dir() {
288 if let Err(e) = copy_dir_recursive(source, dest, &mut HashSet::new()) {
289 let _ = fs::remove_dir_all(dest);
291 return Err(e);
292 }
293 if let Err(e) = fs::remove_dir_all(source) {
294 tracing::warn!(
297 "Source removal failed after cross-fs move; cleaning up destination: {}",
298 e
299 );
300 let _ = fs::remove_dir_all(dest);
301 return Err(format!("Failed to remove source after copy: {}", e));
302 }
303 } else {
304 fs::copy(source, dest).map_err(|e| format!("Failed to copy: {}", e))?;
305 fs::remove_file(source).map_err(|e| format!("Failed to remove source: {}", e))?;
306 }
307
308 Ok(size)
309}
310
311fn get_size(path: &PathBuf) -> u64 {
315 match fs::symlink_metadata(path) {
316 Ok(m) if m.is_symlink() => 0,
317 Ok(m) if m.is_dir() => get_dir_size(path),
318 Ok(m) => m.len(),
319 Err(_) => 0,
320 }
321}
322
323fn get_dir_size(dir: &PathBuf) -> u64 {
327 let mut size = 0u64;
328 if let Ok(entries) = fs::read_dir(dir) {
329 for entry in entries.flatten() {
330 let Ok(ft) = entry.file_type() else { continue };
331 if ft.is_symlink() {
332 continue;
334 }
335 let path = entry.path();
336 if ft.is_dir() {
337 size += get_dir_size(&path);
338 } else if let Ok(metadata) = fs::symlink_metadata(&path) {
339 size += metadata.len();
340 }
341 }
342 }
343 size
344}
345
346pub fn copy_dir_recursive_pub(
349 source: &PathBuf,
350 dest: &PathBuf,
351 visited: &mut HashSet<u64>,
352) -> Result<(), String> {
353 copy_dir_recursive(source, dest, visited)
354}
355
356fn copy_dir_recursive(
361 source: &PathBuf,
362 dest: &PathBuf,
363 visited: &mut HashSet<u64>,
364) -> Result<(), String> {
365 #[cfg(unix)]
367 {
368 use std::os::unix::fs::MetadataExt;
369 if let Ok(meta) = fs::symlink_metadata(source) {
370 let inode = meta.ino();
371 if !visited.insert(inode) {
372 return Ok(()); }
374 }
375 }
376
377 fs::create_dir_all(dest).map_err(|e| format!("Failed to create directory: {}", e))?;
378
379 let entries = fs::read_dir(source).map_err(|e| format!("Failed to read directory: {}", e))?;
380
381 for entry in entries {
382 let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
383 let file_type = entry
385 .file_type()
386 .map_err(|e| format!("Failed to read file type: {}", e))?;
387 let path = entry.path();
388 let dest_path = dest.join(entry.file_name());
389
390 if file_type.is_symlink() {
391 let target =
393 fs::read_link(&path).map_err(|e| format!("Failed to read symlink: {}", e))?;
394 #[cfg(unix)]
395 {
396 std::os::unix::fs::symlink(&target, &dest_path)
397 .map_err(|e| format!("Failed to create symlink: {}", e))?;
398 }
399 #[cfg(windows)]
400 {
401 let result = if path.is_dir() {
405 std::os::windows::fs::symlink_dir(&target, &dest_path)
406 } else {
407 std::os::windows::fs::symlink_file(&target, &dest_path)
408 };
409 if let Err(e) = result {
410 tracing::warn!(
411 "Failed to create symlink {} -> {}: {}",
412 dest_path.display(),
413 target.display(),
414 e
415 );
416 }
417 }
418 } else if file_type.is_dir() {
419 copy_dir_recursive(&path, &dest_path, visited)?;
420 } else {
421 fs::copy(&path, &dest_path).map_err(|e| format!("Failed to copy file: {}", e))?;
422 }
423 }
424
425 Ok(())
426}