gravityfile_ops/
move_op.rs1use 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 MoveResult {
15 Progress(OperationProgress),
17 Conflict(Conflict),
19 Complete(OperationComplete),
21}
22
23#[derive(Debug, Clone, Default)]
25pub struct MoveOptions {
26 pub conflict_resolution: Option<ConflictResolution>,
28}
29
30pub fn start_move(
34 sources: Vec<PathBuf>,
35 destination: PathBuf,
36 options: MoveOptions,
37) -> mpsc::Receiver<MoveResult> {
38 let (tx, rx) = mpsc::channel(OPERATION_CHANNEL_SIZE);
39
40 if sources.is_empty() {
41 let complete = OperationComplete {
42 operation_type: OperationType::Move,
43 succeeded: 0,
44 failed: 0,
45 bytes_processed: 0,
46 errors: vec![],
47 };
48 tokio::spawn(async move {
49 let _ = tx.send(MoveResult::Complete(complete)).await;
50 });
51 return rx;
52 }
53
54 tokio::spawn(async move {
55 move_impl(sources, destination, options, tx).await;
56 });
57
58 rx
59}
60
61async fn move_impl(
63 sources: Vec<PathBuf>,
64 destination: PathBuf,
65 options: MoveOptions,
66 tx: mpsc::Sender<MoveResult>,
67) {
68 let total_files = sources.len();
69 let mut progress = OperationProgress::new(OperationType::Move, total_files, 0);
70 let global_resolution: Option<ConflictResolution> = options.conflict_resolution;
71 let mut succeeded = 0;
72 let mut failed = 0;
73 let mut moved_pairs: Vec<(PathBuf, PathBuf)> = Vec::new();
74
75 if !destination.exists() {
77 if let Err(e) = fs::create_dir_all(&destination) {
78 progress.add_error(OperationError::new(
79 destination.clone(),
80 format!("Failed to create destination: {}", e),
81 ));
82 let _ = tx
83 .send(MoveResult::Complete(OperationComplete {
84 operation_type: OperationType::Move,
85 succeeded: 0,
86 failed: sources.len(),
87 bytes_processed: 0,
88 errors: progress.errors.clone(),
89 }))
90 .await;
91 return;
92 }
93 }
94
95 for source in sources {
96 let dest_path = destination.join(source.file_name().unwrap_or_default());
97
98 if dest_path.starts_with(&source) {
100 let _ = tx
101 .send(MoveResult::Conflict(Conflict::source_is_ancestor(
102 source.clone(),
103 dest_path.clone(),
104 )))
105 .await;
106 failed += 1;
107 continue;
108 }
109
110 let final_dest = if dest_path.exists() {
112 let conflict_kind = if dest_path.is_dir() {
113 ConflictKind::DirectoryExists
114 } else {
115 ConflictKind::FileExists
116 };
117
118 let resolution = if let Some(res) = global_resolution {
119 res.to_single()
120 } else {
121 let _ = tx
122 .send(MoveResult::Conflict(Conflict::new(
123 source.clone(),
124 dest_path.clone(),
125 conflict_kind,
126 )))
127 .await;
128 ConflictResolution::Skip
129 };
130
131 match resolution {
132 ConflictResolution::Skip | ConflictResolution::SkipAll => {
133 failed += 1;
134 continue;
135 }
136 ConflictResolution::Abort => {
137 let _ = tx
138 .send(MoveResult::Complete(OperationComplete {
139 operation_type: OperationType::Move,
140 succeeded,
141 failed: failed + 1,
142 bytes_processed: progress.bytes_processed,
143 errors: progress.errors.clone(),
144 }))
145 .await;
146 return;
147 }
148 ConflictResolution::AutoRename => auto_rename_path(&dest_path),
149 ConflictResolution::Overwrite | ConflictResolution::OverwriteAll => {
150 let _ = if let Ok(metadata) = fs::symlink_metadata(&dest_path) {
152 if metadata.is_symlink() {
153 fs::remove_file(&dest_path)
154 } else if metadata.is_dir() {
155 fs::remove_dir_all(&dest_path)
156 } else {
157 fs::remove_file(&dest_path)
158 }
159 } else {
160 Ok(())
161 };
162 dest_path.clone()
163 }
164 }
165 } else {
166 dest_path.clone()
167 };
168
169 progress.set_current_file(Some(source.clone()));
171 let _ = tx.send(MoveResult::Progress(progress.clone())).await;
172
173 let source_clone = source.clone();
175 let dest_clone = final_dest.clone();
176
177 let result = tokio::task::spawn_blocking(move || move_item(&source_clone, &dest_clone))
178 .await
179 .map_err(|e| format!("Task failed: {}", e));
180
181 match result {
182 Ok(Ok(bytes)) => {
183 progress.complete_file(bytes);
184 moved_pairs.push((source.clone(), final_dest));
185 succeeded += 1;
186 }
187 Ok(Err(e)) | Err(e) => {
188 progress.add_error(OperationError::new(source.clone(), e));
189 failed += 1;
190 }
191 }
192
193 let _ = tx.send(MoveResult::Progress(progress.clone())).await;
194 }
195
196 let _ = tx
198 .send(MoveResult::Complete(OperationComplete {
199 operation_type: OperationType::Move,
200 succeeded,
201 failed,
202 bytes_processed: progress.bytes_processed,
203 errors: progress.errors,
204 }))
205 .await;
206}
207
208fn move_item(source: &PathBuf, dest: &PathBuf) -> Result<u64, String> {
210 let size = get_size(source);
212
213 if fs::rename(source, dest).is_ok() {
215 return Ok(size);
216 }
217
218 let metadata = fs::symlink_metadata(source).map_err(|e| format!("Failed to read metadata: {}", e))?;
221
222 if metadata.is_symlink() {
223 let target = fs::read_link(source).map_err(|e| format!("Failed to read symlink: {}", e))?;
225 #[cfg(unix)]
226 {
227 std::os::unix::fs::symlink(&target, dest).map_err(|e| format!("Failed to create symlink: {}", e))?;
228 }
229 #[cfg(windows)]
230 {
231 if target.is_dir() {
232 std::os::windows::fs::symlink_dir(&target, dest).map_err(|e| format!("Failed to create symlink: {}", e))?;
233 } else {
234 std::os::windows::fs::symlink_file(&target, dest).map_err(|e| format!("Failed to create symlink: {}", e))?;
235 }
236 }
237 fs::remove_file(source).map_err(|e| format!("Failed to remove source symlink: {}", e))?;
238 } else if metadata.is_dir() {
239 copy_dir_recursive(source, dest)?;
240 fs::remove_dir_all(source).map_err(|e| format!("Failed to remove source: {}", e))?;
241 } else {
242 fs::copy(source, dest).map_err(|e| format!("Failed to copy: {}", e))?;
243 fs::remove_file(source).map_err(|e| format!("Failed to remove source: {}", e))?;
244 }
245
246 Ok(size)
247}
248
249fn get_size(path: &PathBuf) -> u64 {
251 if path.is_dir() {
252 get_dir_size(path)
253 } else {
254 fs::metadata(path).map(|m| m.len()).unwrap_or(0)
255 }
256}
257
258fn get_dir_size(dir: &PathBuf) -> u64 {
260 let mut size = 0u64;
261 if let Ok(entries) = fs::read_dir(dir) {
262 for entry in entries.flatten() {
263 let path = entry.path();
264 if path.is_dir() {
265 size += get_dir_size(&path);
266 } else if let Ok(metadata) = fs::metadata(&path) {
267 size += metadata.len();
268 }
269 }
270 }
271 size
272}
273
274fn copy_dir_recursive(source: &PathBuf, dest: &PathBuf) -> Result<(), String> {
276 fs::create_dir_all(dest).map_err(|e| format!("Failed to create directory: {}", e))?;
277
278 let entries =
279 fs::read_dir(source).map_err(|e| format!("Failed to read directory: {}", e))?;
280
281 for entry in entries {
282 let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
283 let path = entry.path();
284 let dest_path = dest.join(entry.file_name());
285
286 if path.is_dir() {
287 copy_dir_recursive(&path, &dest_path)?;
288 } else {
289 fs::copy(&path, &dest_path).map_err(|e| format!("Failed to copy file: {}", e))?;
290 }
291 }
292
293 Ok(())
294}