gravityfile_ops/
copy.rs

1//! Async copy operation with progress reporting.
2
3use 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/// Result sent through the channel during copy operations.
13#[derive(Debug)]
14pub enum CopyResult {
15    /// Progress update.
16    Progress(OperationProgress),
17    /// A conflict was detected that needs resolution.
18    Conflict(Conflict),
19    /// The operation completed.
20    Complete(OperationComplete),
21}
22
23/// Options for copy operations.
24#[derive(Debug, Clone, Default)]
25pub struct CopyOptions {
26    /// How to handle conflicts (None means ask for each).
27    pub conflict_resolution: Option<ConflictResolution>,
28    /// Whether to preserve timestamps.
29    pub preserve_timestamps: bool,
30}
31
32/// Start an async copy operation.
33///
34/// Returns a receiver for progress updates and results.
35pub 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        // Send immediate completion for empty sources
44        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
64/// Internal implementation of copy operation.
65async fn copy_impl(
66    sources: Vec<PathBuf>,
67    destination: PathBuf,
68    options: CopyOptions,
69    tx: mpsc::Sender<CopyResult>,
70) {
71    // First, calculate total size and file count
72    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    // Ensure destination exists and is a directory
80    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        // Check for conflicts
103        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                // Send conflict and wait (in real impl, would need response channel)
114                // For now, default to skip
115                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                    // Remove existing before copy - use symlink_metadata to avoid following symlinks
154                    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        // Perform the copy
170        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    // Send completion
182    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
193/// Copy a single item (file, directory, or symlink).
194async 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        // Use symlink_metadata to avoid following symlinks
205        let metadata = fs::symlink_metadata(&source)
206            .map_err(|e| format!("Failed to read metadata: {}", e))?;
207
208        if metadata.is_symlink() {
209            // For symlinks, read the target and recreate at destination
210            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) // Symlinks have no real size
228        } 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
247/// Copy a single file.
248fn 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
257/// Recursively copy a directory.
258fn 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
281/// Calculate total files and bytes for a list of sources.
282fn 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
300/// Calculate totals for a directory recursively.
301fn 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}