fm/modes/menu/
copy_move.rs1use 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 fn verb(&self) -> &str {
46 match self {
47 Self::Copy => "copy",
48 Self::Move => "move",
49 }
50 }
51
52 fn preterit(&self) -> &str {
53 match self {
54 Self::Copy => "copied",
55 Self::Move => "moved",
56 }
57 }
58
59 fn copier<P, Q, F>(
60 &self,
61 ) -> for<'a, 'b> fn(
62 &'a [P],
63 Q,
64 &'b fs_extra::dir::CopyOptions,
65 F,
66 ) -> Result<u64, fs_extra::error::Error>
67 where
68 P: AsRef<std::path::Path>,
69 Q: AsRef<std::path::Path>,
70 F: FnMut(fs_extra::TransitProcess) -> fs_extra::dir::TransitProcessResult,
71 {
72 match self {
73 Self::Copy => fs_extra::copy_items_with_progress,
74 Self::Move => fs_extra::move_items_with_progress,
75 }
76 }
77
78 fn log_and_notify(&self, hs_bytes: &str) {
79 let message = format!("{preterit} {hs_bytes} bytes", preterit = self.preterit());
80 let _ = notify(&message);
81 log_info!("{message}");
82 log_line!("{message}");
83 }
84
85 fn setup_progress_bar(
86 &self,
87 width: u16,
88 height: u16,
89 ) -> Result<(InMemoryTerm, ProgressBar, fs_extra::dir::CopyOptions)> {
90 let width = width.saturating_sub(4);
91 let in_mem = InMemoryTerm::new(height, width);
92 let pb = ProgressBar::with_draw_target(
93 Some(100),
94 ProgressDrawTarget::term_like(Box::new(in_mem.clone())),
95 );
96 let action = self.verb().to_owned();
97 pb.set_style(
98 ProgressStyle::with_template(
99 "{spinner} {action} [{elapsed}] [{wide_bar}] {percent}% ({eta})",
100 )?
101 .with_key("eta", |state: &ProgressState, w: &mut dyn Write| {
102 write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
103 })
104 .with_key("action", move |_: &ProgressState, w: &mut dyn Write| {
105 write!(w, "{}", &action).unwrap()
106 })
107 .progress_chars("#>-"),
108 );
109 let options = fs_extra::dir::CopyOptions::new();
110 Ok((in_mem, pb, options))
111 }
112}
113
114pub fn copy_move<P>(
133 copy_or_move: CopyMove,
134 sources: Vec<PathBuf>,
135 dest: P,
136 width: u16,
137 height: u16,
138 fm_sender: Arc<Sender<FmEvents>>,
139) -> Result<InMemoryTerm>
140where
141 P: AsRef<std::path::Path>,
142{
143 let (in_mem, progress_bar, options) = copy_or_move.setup_progress_bar(width, height)?;
144 let handle_progress = move |process_info: fs_extra::TransitProcess| {
145 handle_progress_display(&progress_bar, process_info)
146 };
147 let conflict_handler = ConflictHandler::new(dest, &sources)?;
148
149 let _ = thread::spawn(move || {
150 let transfered_bytes = match copy_or_move.copier()(
151 &sources,
152 &conflict_handler.temp_dest,
153 &options,
154 handle_progress,
155 ) {
156 Ok(transfered_bytes) => transfered_bytes,
157 Err(e) => {
158 log_info!("Error: {e:?}");
159 log_line!("Error: {e:?}");
160 0
161 }
162 };
163
164 fm_sender.send(FmEvents::Refresh).unwrap_or_default();
165
166 if let Err(e) = conflict_handler.solve_conflicts() {
167 log_info!("Conflict Handler error: {e}");
168 }
169
170 copy_or_move.log_and_notify(&human_size(transfered_bytes));
171 if matches!(copy_or_move, CopyMove::Copy) {
172 fm_sender.send(FmEvents::FileCopied).unwrap_or_default();
173 }
174 });
175 Ok(in_mem)
176}
177
178struct ConflictHandler {
180 temp_dest: PathBuf,
184 has_conflict: bool,
188 final_dest: Option<PathBuf>,
191}
192
193impl ConflictHandler {
194 fn new<P>(dest: P, sources: &[PathBuf]) -> Result<Self>
197 where
198 P: AsRef<std::path::Path>,
199 {
200 let has_conflict = ConflictHandler::check_filename_conflict(sources, &dest)?;
201 let temp_dest: PathBuf;
202 let final_dest: Option<PathBuf>;
203 if has_conflict {
204 temp_dest = Self::create_temporary_destination(&dest)?;
205 final_dest = Some(dest.as_ref().to_path_buf());
206 } else {
207 temp_dest = dest.as_ref().to_path_buf();
208 final_dest = None;
209 };
210
211 Ok(Self {
212 temp_dest,
213 has_conflict,
214 final_dest,
215 })
216 }
217
218 fn create_temporary_destination<P>(dest: P) -> Result<PathBuf>
221 where
222 P: AsRef<std::path::Path>,
223 {
224 let mut temp_dest = dest.as_ref().to_path_buf();
225 let rand_str = random_name();
226 temp_dest.push(rand_str);
227 std::fs::create_dir(&temp_dest)?;
228 Ok(temp_dest)
229 }
230
231 fn move_copied_files_to_dest(&self) -> Result<()> {
235 for file in std::fs::read_dir(&self.temp_dest).context("Unreachable folder")? {
236 let file = file.context("File don't exist")?;
237 self.move_single_file_to_dest(file)?;
238 }
239 Ok(())
240 }
241
242 fn delete_temp_dest(&self) -> Result<()> {
246 std::fs::remove_dir(&self.temp_dest)?;
247 Ok(())
248 }
249
250 fn move_single_file_to_dest(&self, file: std::fs::DirEntry) -> Result<()> {
254 let mut file_name = file
255 .file_name()
256 .to_str()
257 .context("Couldn't cast the filename")?
258 .to_owned();
259
260 let mut final_dest = self
261 .final_dest
262 .clone()
263 .context("Final dest shouldn't be None")?;
264 final_dest.push(&file_name);
265 while final_dest.exists() {
266 final_dest.pop();
267 file_name.push('_');
268 final_dest.push(&file_name);
269 }
270 std::fs::rename(file.path(), final_dest)?;
271 Ok(())
272 }
273
274 fn check_filename_conflict<P>(sources: &[PathBuf], dest: P) -> Result<bool>
276 where
277 P: AsRef<std::path::Path>,
278 {
279 for file in sources {
280 let filename = file.file_name().context("Couldn't read filename")?;
281 let mut new_path = dest.as_ref().to_path_buf();
282 new_path.push(filename);
283 if new_path.exists() {
284 return Ok(true);
285 }
286 }
287 Ok(false)
288 }
289
290 fn solve_conflicts(&self) -> Result<()> {
294 if self.has_conflict {
295 self.move_copied_files_to_dest()?;
296 self.delete_temp_dest()?;
297 }
298 Ok(())
299 }
300}
301
302impl Drop for ConflictHandler {
303 fn drop(&mut self) {
304 let _ = self.delete_temp_dest();
305 }
306}
307
308fn notify(text: &str) -> Result<()> {
311 if is_in_path(NOTIFY_EXECUTABLE) {
312 execute(NOTIFY_EXECUTABLE, &[text])?;
313 }
314 Ok(())
315}