1use std::fmt::{Display, Write};
2use std::path::PathBuf;
3use std::sync::mpsc::Sender;
4use std::sync::Arc;
5use std::thread;
6
7use anyhow::{bail, Context, Result};
8use fs_extra;
9use indicatif::{InMemoryTerm, ProgressBar, ProgressDrawTarget, ProgressState, ProgressStyle};
10
11use crate::common::{is_in_path, random_name, NOTIFY_EXECUTABLE};
12use crate::event::FmEvents;
13use crate::io::execute;
14use crate::modes::human_size;
15use crate::{log_info, log_line};
16
17#[derive(Default, Debug, Clone)]
20pub struct DoneCopyMove {
21 pub copy_move: CopyMove,
22 pub from: PathBuf,
23 pub final_to: PathBuf,
24}
25
26impl Display for DoneCopyMove {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(
29 f,
30 "{copy_move} from: {from} to: {to}",
31 copy_move = self.copy_move.preterit(),
32 from = self.from.display(),
33 to = self.final_to.display()
34 )
35 }
36}
37
38fn handle_progress_display(
41 pb: &ProgressBar,
42 process_info: fs_extra::TransitProcess,
43) -> fs_extra::dir::TransitProcessResult {
44 let progress = progress_bar_position(&process_info);
45 pb.set_position(progress);
46 fs_extra::dir::TransitProcessResult::ContinueOrAbort
47}
48
49fn progress_bar_position(process_info: &fs_extra::TransitProcess) -> u64 {
52 if process_info.total_bytes == 0 {
53 return 0;
54 }
55 100 * process_info.copied_bytes / process_info.total_bytes
56}
57
58#[derive(Default, Debug, Clone, Copy)]
60pub enum CopyMove {
61 Copy,
62 #[default]
63 Move,
64}
65
66impl CopyMove {
67 #[inline]
69 pub fn is_copy(&self) -> bool {
70 matches!(self, Self::Copy)
71 }
72
73 fn verb(&self) -> &str {
74 match self {
75 Self::Copy => "copy",
76 Self::Move => "move",
77 }
78 }
79
80 fn preterit(&self) -> &str {
81 match self {
82 Self::Copy => "copied",
83 Self::Move => "moved",
84 }
85 }
86
87 fn copier<P, Q, F>(
88 &self,
89 ) -> for<'a, 'b> fn(
90 &'a [P],
91 Q,
92 &'b fs_extra::dir::CopyOptions,
93 F,
94 ) -> Result<u64, fs_extra::error::Error>
95 where
96 P: AsRef<std::path::Path>,
97 Q: AsRef<std::path::Path>,
98 F: FnMut(fs_extra::TransitProcess) -> fs_extra::dir::TransitProcessResult,
99 {
100 match self {
101 Self::Copy => fs_extra::copy_items_with_progress,
102 Self::Move => fs_extra::move_items_with_progress,
103 }
104 }
105
106 fn log_and_notify(&self, hs_bytes: &str) {
107 let message = format!("{preterit} {hs_bytes} bytes", preterit = self.preterit());
108 let _ = notify(&message);
109 log_info!("{message}");
110 log_line!("{message}");
111 }
112
113 fn setup_progress_bar(
114 &self,
115 width: u16,
116 height: u16,
117 ) -> Result<(InMemoryTerm, ProgressBar, fs_extra::dir::CopyOptions)> {
118 let width = width.saturating_sub(4);
119 let in_mem = InMemoryTerm::new(height, width);
120 let pb = ProgressBar::with_draw_target(
121 Some(100),
122 ProgressDrawTarget::term_like(Box::new(in_mem.clone())),
123 );
124 let action = self.verb().to_owned();
125 pb.set_style(
126 ProgressStyle::with_template(
127 "{spinner} {action} [{elapsed}] [{wide_bar}] {percent}% ({eta})",
128 )?
129 .with_key("eta", |state: &ProgressState, w: &mut dyn Write| {
130 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
131 })
132 .with_key("action", move |_: &ProgressState, w: &mut dyn Write| {
133 write!(w, "{}", &action).unwrap()
134 })
135 .progress_chars("#>-"),
136 );
137 let options = fs_extra::dir::CopyOptions::new();
138 Ok((in_mem, pb, options))
139 }
140}
141
142pub fn copy_move<P>(
161 copy_move: CopyMove,
162 sources: Vec<PathBuf>,
163 dest: P,
164 width: u16,
165 height: u16,
166 fm_sender: Arc<Sender<FmEvents>>,
167) -> Result<InMemoryTerm>
168where
169 P: AsRef<std::path::Path>,
170{
171 let (in_mem, progress_bar, options) = copy_move.setup_progress_bar(width, height)?;
172 let handle_progress = move |process_info: fs_extra::TransitProcess| {
173 handle_progress_display(&progress_bar, process_info)
174 };
175 let mut conflict_handler = ConflictHandler::new(copy_move, &sources, dest)?;
176
177 let _ = thread::spawn(move || {
178 let transfered_bytes = match copy_move.copier()(
179 &sources,
180 &conflict_handler.temp_dest,
181 &options,
182 handle_progress,
183 ) {
184 Ok(transfered_bytes) => transfered_bytes,
185 Err(e) => {
186 log_info!("Error: {e:?}");
187 log_line!("Error: {e:?}");
188 0
189 }
190 };
191
192 fm_sender.send(FmEvents::Refresh).unwrap_or_default();
193
194 match conflict_handler.solve_conflicts() {
195 Ok(done_copy_moves) => fm_sender
196 .send(FmEvents::FileCopied(done_copy_moves))
197 .unwrap_or_default(),
198 Err(error) => log_info!("Conflict Handler error: {error}"),
199 };
200
201 copy_move.log_and_notify(&human_size(transfered_bytes));
202 });
203 Ok(in_mem)
204}
205
206struct ConflictHandler {
208 copy_move: CopyMove,
210 sources: Vec<PathBuf>,
212 temp_dest: PathBuf,
216 has_conflict: bool,
220 final_dest: Option<PathBuf>,
223 done_copy_moves: Vec<DoneCopyMove>,
225}
226
227impl ConflictHandler {
228 fn new<P>(copy_move: CopyMove, sources: &[PathBuf], dest: P) -> Result<Self>
231 where
232 P: AsRef<std::path::Path>,
233 {
234 let has_conflict = ConflictHandler::check_filename_conflict(sources, &dest)?;
235 let temp_dest: PathBuf;
236 let final_dest: Option<PathBuf>;
237 if has_conflict {
238 temp_dest = Self::create_temporary_destination(&dest)?;
239 final_dest = Some(dest.as_ref().to_path_buf());
240 } else {
241 temp_dest = dest.as_ref().to_path_buf();
242 final_dest = None;
243 };
244 let done_copy_moves = vec![];
245 let sources = sources.to_vec();
246
247 Ok(Self {
248 copy_move,
249 sources,
250 temp_dest,
251 has_conflict,
252 final_dest,
253 done_copy_moves,
254 })
255 }
256
257 fn create_temporary_destination<P>(dest: P) -> Result<PathBuf>
260 where
261 P: AsRef<std::path::Path>,
262 {
263 let mut temp_dest = dest.as_ref().to_path_buf();
264 let rand_str = random_name();
265 temp_dest.push(rand_str);
266 std::fs::create_dir(&temp_dest)?;
267 Ok(temp_dest)
268 }
269
270 fn move_copied_files_to_dest(&mut self) -> Result<()> {
274 while let Some(from) = self.sources.pop() {
275 let done_copy_move = self.move_single_file_to_dest(from)?;
276 self.done_copy_moves.push(done_copy_move);
277 }
278
279 if self.temp_dest.read_dir()?.next().is_some() {
280 bail!(
281 "temp_dest {temp_dest} should be empty.",
282 temp_dest = self.temp_dest.display()
283 )
284 }
285 Ok(())
286 }
287
288 fn move_single_file_to_dest(&mut self, from: PathBuf) -> Result<DoneCopyMove> {
292 let filename = from.file_name().context("Should have a filename")?;
293 let mut filename = filename.to_string_lossy().to_string();
294 let mut temp_path = self.temp_dest.clone();
295 temp_path.push(&filename);
296
297 let mut final_to = self
298 .final_dest
299 .clone()
300 .context("Final dest shouldn't be None")?;
301 final_to.push(&filename);
302 while final_to.exists() {
303 final_to.pop();
304 filename.push('_');
305 final_to.push(&filename);
306 }
307 std::fs::rename(temp_path, &final_to)?;
308 Ok(DoneCopyMove {
309 copy_move: self.copy_move,
310 from,
311 final_to,
312 })
313 }
314
315 fn check_filename_conflict<P>(sources: &[PathBuf], dest: P) -> Result<bool>
317 where
318 P: AsRef<std::path::Path>,
319 {
320 for file in sources {
321 let filename = file.file_name().context("Couldn't read filename")?;
322 let mut new_path = dest.as_ref().to_path_buf();
323 new_path.push(filename);
324 if new_path.exists() {
325 return Ok(true);
326 }
327 }
328 Ok(false)
329 }
330
331 fn solve_conflicts(&mut self) -> Result<Vec<DoneCopyMove>> {
335 if self.has_conflict {
336 self.move_copied_files_to_dest()?;
337 self.delete_temp_dest()?;
338 } else {
339 self.build_non_conflict_copy_moves()?;
340 }
341 Ok(std::mem::take(&mut self.done_copy_moves))
342 }
343
344 fn build_non_conflict_copy_moves(&mut self) -> Result<()> {
346 while let Some(from) = self.sources.pop() {
347 let filename = from.file_name().context("Should have a filename")?;
348
349 let mut final_to = self.temp_dest.clone();
350 final_to.push(filename);
351 self.done_copy_moves.push(DoneCopyMove {
352 copy_move: self.copy_move,
353 from,
354 final_to,
355 })
356 }
357 Ok(())
358 }
359
360 fn delete_temp_dest(&self) -> Result<()> {
364 std::fs::remove_dir(&self.temp_dest)?;
365 Ok(())
366 }
367}
368
369impl Drop for ConflictHandler {
370 fn drop(&mut self) {
372 let _ = self.delete_temp_dest();
373 }
374}
375
376fn notify(text: &str) -> Result<()> {
379 if is_in_path(NOTIFY_EXECUTABLE) {
380 execute(NOTIFY_EXECUTABLE, &[text])?;
381 }
382 Ok(())
383}