1use std::fmt::Write;
2use std::path::PathBuf;
3use std::sync::mpsc::Sender;
4use std::sync::Arc;
5use std::thread;
6
7use anyhow::{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
17fn handle_progress_display(
20 pb: &ProgressBar,
21 process_info: fs_extra::TransitProcess,
22) -> fs_extra::dir::TransitProcessResult {
23 let progress = progress_bar_position(&process_info);
24 pb.set_position(progress);
25 fs_extra::dir::TransitProcessResult::ContinueOrAbort
26}
27
28fn progress_bar_position(process_info: &fs_extra::TransitProcess) -> u64 {
31 if process_info.total_bytes == 0 {
32 return 0;
33 }
34 100 * process_info.copied_bytes / process_info.total_bytes
35}
36
37#[derive(Debug)]
39pub enum CopyMove {
40 Copy,
41 Move,
42}
43
44impl CopyMove {
45 #[inline]
47 pub fn is_copy(&self) -> bool {
48 matches!(self, Self::Copy)
49 }
50
51 fn verb(&self) -> &str {
52 match self {
53 Self::Copy => "copy",
54 Self::Move => "move",
55 }
56 }
57
58 fn preterit(&self) -> &str {
59 match self {
60 Self::Copy => "copied",
61 Self::Move => "moved",
62 }
63 }
64
65 fn copier<P, Q, F>(
66 &self,
67 ) -> for<'a, 'b> fn(
68 &'a [P],
69 Q,
70 &'b fs_extra::dir::CopyOptions,
71 F,
72 ) -> Result<u64, fs_extra::error::Error>
73 where
74 P: AsRef<std::path::Path>,
75 Q: AsRef<std::path::Path>,
76 F: FnMut(fs_extra::TransitProcess) -> fs_extra::dir::TransitProcessResult,
77 {
78 match self {
79 Self::Copy => fs_extra::copy_items_with_progress,
80 Self::Move => fs_extra::move_items_with_progress,
81 }
82 }
83
84 fn log_and_notify(&self, hs_bytes: &str) {
85 let message = format!("{preterit} {hs_bytes} bytes", preterit = self.preterit());
86 let _ = notify(&message);
87 log_info!("{message}");
88 log_line!("{message}");
89 }
90
91 fn setup_progress_bar(
92 &self,
93 width: u16,
94 height: u16,
95 ) -> Result<(InMemoryTerm, ProgressBar, fs_extra::dir::CopyOptions)> {
96 let width = width.saturating_sub(4);
97 let in_mem = InMemoryTerm::new(height, width);
98 let pb = ProgressBar::with_draw_target(
99 Some(100),
100 ProgressDrawTarget::term_like(Box::new(in_mem.clone())),
101 );
102 let action = self.verb().to_owned();
103 pb.set_style(
104 ProgressStyle::with_template(
105 "{spinner} {action} [{elapsed}] [{wide_bar}] {percent}% ({eta})",
106 )?
107 .with_key("eta", |state: &ProgressState, w: &mut dyn Write| {
108 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
109 })
110 .with_key("action", move |_: &ProgressState, w: &mut dyn Write| {
111 write!(w, "{}", &action).unwrap()
112 })
113 .progress_chars("#>-"),
114 );
115 let options = fs_extra::dir::CopyOptions::new();
116 Ok((in_mem, pb, options))
117 }
118}
119
120pub fn copy_move<P>(
139 copy_or_move: CopyMove,
140 sources: Vec<PathBuf>,
141 dest: P,
142 width: u16,
143 height: u16,
144 fm_sender: Arc<Sender<FmEvents>>,
145) -> Result<InMemoryTerm>
146where
147 P: AsRef<std::path::Path>,
148{
149 let (in_mem, progress_bar, options) = copy_or_move.setup_progress_bar(width, height)?;
150 let handle_progress = move |process_info: fs_extra::TransitProcess| {
151 handle_progress_display(&progress_bar, process_info)
152 };
153 let conflict_handler = ConflictHandler::new(dest, &sources)?;
154
155 let _ = thread::spawn(move || {
156 let transfered_bytes = match copy_or_move.copier()(
157 &sources,
158 &conflict_handler.temp_dest,
159 &options,
160 handle_progress,
161 ) {
162 Ok(transfered_bytes) => transfered_bytes,
163 Err(e) => {
164 log_info!("Error: {e:?}");
165 log_line!("Error: {e:?}");
166 0
167 }
168 };
169
170 fm_sender.send(FmEvents::Refresh).unwrap_or_default();
171
172 if let Err(e) = conflict_handler.solve_conflicts() {
173 log_info!("Conflict Handler error: {e}");
174 }
175
176 copy_or_move.log_and_notify(&human_size(transfered_bytes));
177 if matches!(copy_or_move, CopyMove::Copy) {
178 fm_sender.send(FmEvents::FileCopied).unwrap_or_default();
179 }
180 });
181 Ok(in_mem)
182}
183
184struct ConflictHandler {
186 temp_dest: PathBuf,
190 has_conflict: bool,
194 final_dest: Option<PathBuf>,
197}
198
199impl ConflictHandler {
200 fn new<P>(dest: P, sources: &[PathBuf]) -> Result<Self>
203 where
204 P: AsRef<std::path::Path>,
205 {
206 let has_conflict = ConflictHandler::check_filename_conflict(sources, &dest)?;
207 let temp_dest: PathBuf;
208 let final_dest: Option<PathBuf>;
209 if has_conflict {
210 temp_dest = Self::create_temporary_destination(&dest)?;
211 final_dest = Some(dest.as_ref().to_path_buf());
212 } else {
213 temp_dest = dest.as_ref().to_path_buf();
214 final_dest = None;
215 };
216
217 Ok(Self {
218 temp_dest,
219 has_conflict,
220 final_dest,
221 })
222 }
223
224 fn create_temporary_destination<P>(dest: P) -> Result<PathBuf>
227 where
228 P: AsRef<std::path::Path>,
229 {
230 let mut temp_dest = dest.as_ref().to_path_buf();
231 let rand_str = random_name();
232 temp_dest.push(rand_str);
233 std::fs::create_dir(&temp_dest)?;
234 Ok(temp_dest)
235 }
236
237 fn move_copied_files_to_dest(&self) -> Result<()> {
241 for file in std::fs::read_dir(&self.temp_dest).context("Unreachable folder")? {
242 let file = file.context("File don't exist")?;
243 self.move_single_file_to_dest(file)?;
244 }
245 Ok(())
246 }
247
248 fn delete_temp_dest(&self) -> Result<()> {
252 std::fs::remove_dir(&self.temp_dest)?;
253 Ok(())
254 }
255
256 fn move_single_file_to_dest(&self, file: std::fs::DirEntry) -> Result<()> {
260 let mut file_name = file
261 .file_name()
262 .to_str()
263 .context("Couldn't cast the filename")?
264 .to_owned();
265
266 let mut final_dest = self
267 .final_dest
268 .clone()
269 .context("Final dest shouldn't be None")?;
270 final_dest.push(&file_name);
271 while final_dest.exists() {
272 final_dest.pop();
273 file_name.push('_');
274 final_dest.push(&file_name);
275 }
276 std::fs::rename(file.path(), final_dest)?;
277 Ok(())
278 }
279
280 fn check_filename_conflict<P>(sources: &[PathBuf], dest: P) -> Result<bool>
282 where
283 P: AsRef<std::path::Path>,
284 {
285 for file in sources {
286 let filename = file.file_name().context("Couldn't read filename")?;
287 let mut new_path = dest.as_ref().to_path_buf();
288 new_path.push(filename);
289 if new_path.exists() {
290 return Ok(true);
291 }
292 }
293 Ok(false)
294 }
295
296 fn solve_conflicts(&self) -> Result<()> {
300 if self.has_conflict {
301 self.move_copied_files_to_dest()?;
302 self.delete_temp_dest()?;
303 }
304 Ok(())
305 }
306}
307
308impl Drop for ConflictHandler {
309 fn drop(&mut self) {
310 let _ = self.delete_temp_dest();
311 }
312}
313
314fn notify(text: &str) -> Result<()> {
317 if is_in_path(NOTIFY_EXECUTABLE) {
318 execute(NOTIFY_EXECUTABLE, &[text])?;
319 }
320 Ok(())
321}