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 dest_path.is_dir() {
152 fs::remove_dir_all(&dest_path)
153 } else {
154 fs::remove_file(&dest_path)
155 };
156 dest_path.clone()
157 }
158 }
159 } else {
160 dest_path.clone()
161 };
162
163 progress.set_current_file(Some(source.clone()));
165 let _ = tx.send(MoveResult::Progress(progress.clone())).await;
166
167 let source_clone = source.clone();
169 let dest_clone = final_dest.clone();
170
171 let result = tokio::task::spawn_blocking(move || move_item(&source_clone, &dest_clone))
172 .await
173 .map_err(|e| format!("Task failed: {}", e));
174
175 match result {
176 Ok(Ok(bytes)) => {
177 progress.complete_file(bytes);
178 moved_pairs.push((source.clone(), final_dest));
179 succeeded += 1;
180 }
181 Ok(Err(e)) | Err(e) => {
182 progress.add_error(OperationError::new(source.clone(), e));
183 failed += 1;
184 }
185 }
186
187 let _ = tx.send(MoveResult::Progress(progress.clone())).await;
188 }
189
190 let _ = tx
192 .send(MoveResult::Complete(OperationComplete {
193 operation_type: OperationType::Move,
194 succeeded,
195 failed,
196 bytes_processed: progress.bytes_processed,
197 errors: progress.errors,
198 }))
199 .await;
200}
201
202fn move_item(source: &PathBuf, dest: &PathBuf) -> Result<u64, String> {
204 let size = get_size(source);
206
207 if fs::rename(source, dest).is_ok() {
209 return Ok(size);
210 }
211
212 if source.is_dir() {
214 copy_dir_recursive(source, dest)?;
215 fs::remove_dir_all(source).map_err(|e| format!("Failed to remove source: {}", e))?;
216 } else {
217 fs::copy(source, dest).map_err(|e| format!("Failed to copy: {}", e))?;
218 fs::remove_file(source).map_err(|e| format!("Failed to remove source: {}", e))?;
219 }
220
221 Ok(size)
222}
223
224fn get_size(path: &PathBuf) -> u64 {
226 if path.is_dir() {
227 get_dir_size(path)
228 } else {
229 fs::metadata(path).map(|m| m.len()).unwrap_or(0)
230 }
231}
232
233fn get_dir_size(dir: &PathBuf) -> u64 {
235 let mut size = 0u64;
236 if let Ok(entries) = fs::read_dir(dir) {
237 for entry in entries.flatten() {
238 let path = entry.path();
239 if path.is_dir() {
240 size += get_dir_size(&path);
241 } else if let Ok(metadata) = fs::metadata(&path) {
242 size += metadata.len();
243 }
244 }
245 }
246 size
247}
248
249fn copy_dir_recursive(source: &PathBuf, dest: &PathBuf) -> Result<(), String> {
251 fs::create_dir_all(dest).map_err(|e| format!("Failed to create directory: {}", e))?;
252
253 let entries =
254 fs::read_dir(source).map_err(|e| format!("Failed to read directory: {}", e))?;
255
256 for entry in entries {
257 let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
258 let path = entry.path();
259 let dest_path = dest.join(entry.file_name());
260
261 if path.is_dir() {
262 copy_dir_recursive(&path, &dest_path)?;
263 } else {
264 fs::copy(&path, &dest_path).map_err(|e| format!("Failed to copy file: {}", e))?;
265 }
266 }
267
268 Ok(())
269}