diff options
author | qkzk <qu3nt1n@gmail.com> | 2023-10-31 23:20:47 +0100 |
---|---|---|
committer | qkzk <qu3nt1n@gmail.com> | 2023-10-31 23:20:47 +0100 |
commit | 76a25089935f35e8a24c735b975c1896fcddbb07 (patch) | |
tree | bef421ca2866e4806394b3b23b49732974bd10e8 | |
parent | ef6b7a1e3ca1ee232b4fe7a679f4684cf193941a (diff) |
trash refactor
-rw-r--r-- | src/constant_strings_paths.rs | 2 | ||||
-rw-r--r-- | src/event_exec.rs | 2 | ||||
-rw-r--r-- | src/term_manager.rs | 23 | ||||
-rw-r--r-- | src/trash.rs | 249 |
4 files changed, 166 insertions, 110 deletions
diff --git a/src/constant_strings_paths.rs b/src/constant_strings_paths.rs index 3bb1631..b8469ac 100644 --- a/src/constant_strings_paths.rs +++ b/src/constant_strings_paths.rs @@ -14,6 +14,8 @@ pub const ACTION_LOG_PATH: &str = "~/.config/fm/log/action_logger.log"; pub const TRASH_FOLDER_FILES: &str = "~/.local/share/Trash/files"; /// Path to the trash folder info file pub const TRASH_FOLDER_INFO: &str = "~/.local/share/Trash/info"; +/// Trash info files extension. Watchout it includes the final '.' +pub const TRASH_INFO_EXTENSION: &str = ".trashinfo"; /// Log file path. Rotating file logs are created in the same directeroy pub const LOG_PATH: &str = "~/.config/fm/fm{}"; /// File where marks are stored. diff --git a/src/event_exec.rs b/src/event_exec.rs index 3b7e08d..d29b4f9 100644 --- a/src/event_exec.rs +++ b/src/event_exec.rs @@ -863,7 +863,7 @@ impl EventAction { } let trash_mount_point = opt_mount_point(disk_used_by_path( status.system_info.disks(), - &std::path::PathBuf::from(&status.trash.trash_folder_files), + std::path::Path::new(&status.trash.trash_folder_files), )); for flagged in status.flagged.content.iter() { diff --git a/src/term_manager.rs b/src/term_manager.rs index 4a427b8..4b097d4 100644 --- a/src/term_manager.rs +++ b/src/term_manager.rs @@ -894,7 +894,7 @@ impl<'a> WinSecondary<'a> { } Ok(()) } - /// Display a list of edited (deleted, copied, moved) files for confirmation + /// Display a list of edited (deleted, copied, moved, trashed) files for confirmation fn confirm( &self, status: &Status, @@ -905,13 +905,22 @@ impl<'a> WinSecondary<'a> { info!("confirmed action: {:?}", confirmed_mode); match confirmed_mode { NeedConfirmation::EmptyTrash => { - for (row, trashinfo) in status.trash.content.iter().enumerate() { - canvas.print_with_attr( - row + ContentWindow::WINDOW_MARGIN_TOP + 2, + if status.trash.is_empty() { + let _ = canvas.print_with_attr( + ContentWindow::WINDOW_MARGIN_TOP + 2, 4, - &format!("{trashinfo}"), - Attr::default(), - )?; + "Trash is empty", + ATTR_YELLOW_BOLD, + ); + } else { + for (row, trashinfo) in status.trash.content().iter().enumerate() { + canvas.print_with_attr( + row + ContentWindow::WINDOW_MARGIN_TOP + 2, + 4, + &format!("{trashinfo}"), + Attr::default(), + )?; + } } } _ => { diff --git a/src/trash.rs b/src/trash.rs index 75aa28d..d607bda 100644 --- a/src/trash.rs +++ b/src/trash.rs @@ -8,7 +8,7 @@ use log::info; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; -use crate::constant_strings_paths::{TRASH_FOLDER_FILES, TRASH_FOLDER_INFO}; +use crate::constant_strings_paths::{TRASH_FOLDER_FILES, TRASH_FOLDER_INFO, TRASH_INFO_EXTENSION}; use crate::impl_selectable_content; use crate::log::write_log_line; use crate::utils::read_lines; @@ -72,73 +72,95 @@ DeletionDate={} /// Reads a .trashinfo file and parse it into a new instance. /// - /// Let say `Document.trashinfo` contains : + /// Let say `Documents.trashinfo` contains : /// + /// ```not_rust /// [TrashInfo] /// Path=/home/quentin/Documents /// DeletionDate=2022-12-31T22:45:55 + /// ``` /// /// It will be parsed into + /// ```rust /// TrashInfo { PathBuf::from("/home/quentin/Documents"), "Documents", "2022-12-31T22:45:55" } + /// ``` pub fn from_trash_info_file(trash_info_file: &Path) -> Result<Self> { - let mut option_path: Option<PathBuf> = None; - let mut option_deleted_time: Option<String> = None; - let mut found_trash_info_line: bool = false; - if let Some(dest_name) = trash_info_file.file_name() { - let dest_name = Self::remove_extension(dest_name.to_str().unwrap().to_owned())?; - if let Ok(lines) = read_lines(trash_info_file) { - for (index, line_result) in lines.enumerate() { - let Ok(line) = line_result.as_ref() else { - continue; - }; - if line.starts_with("[Trash Info]") { - if index == 0 { - found_trash_info_line = true; - continue; - } else { - return trashinfo_error("[TrashInfo] was found after first line"); - } - } - if line.starts_with("Path=") && option_path.is_none() { - if !found_trash_info_line { - return trashinfo_error("Found Path line before TrashInfo"); - } - let path_part = &line[5..]; - let cow_path_str = url_escape::decode(path_part); - let path_str = cow_path_str.as_ref(); - option_path = Some(PathBuf::from(path_str)); - } else if line.starts_with("DeletionDate=") && option_deleted_time.is_none() { - if !found_trash_info_line { - return trashinfo_error("Found DeletionDate line before TrashInfo"); - } - let deletion_date_str = &line[13..]; - match parsed_date_from_path_info(deletion_date_str) { - Ok(()) => (), - Err(e) => return Err(e), - } - option_deleted_time = Some(deletion_date_str.to_owned()) - } - } - } - match (option_path, option_deleted_time) { - (Some(origin), Some(deletion_date)) => Ok(Self { + let (option_path, option_deleted_time) = Self::parse_trash_info_file(trash_info_file)?; + + match (option_path, option_deleted_time) { + (Some(origin), Some(deletion_date)) => { + let dest_name = Self::get_dest_name(trash_info_file)?; + Ok(Self { dest_name, deletion_date, origin, - }), - _ => trashinfo_error("Couldn't parse the trash info file"), + }) } + _ => Err(anyhow!("Couldn't parse the trash info file")), + } + } + + fn get_dest_name(trash_info_file: &Path) -> Result<String> { + if let Some(dest_name) = trash_info_file.file_name() { + let dest_name = Self::remove_extension(dest_name.to_str().unwrap().to_owned())?; + Ok(dest_name) } else { - trashinfo_error("Couldn't parse the trash info filename") + Err(anyhow!("Couldn't parse the trash info filename")) + } + } + + fn parse_trash_info_file(trash_info_file: &Path) -> Result<(Option<PathBuf>, Option<String>)> { + let mut option_path: Option<PathBuf> = None; + let mut option_deleted_time: Option<String> = None; + + if let Ok(mut lines) = read_lines(trash_info_file) { + let Some(Ok(first_line)) = lines.next() else { + return Err(anyhow!("Unreadable TrashInfo file")); + }; + if !first_line.starts_with("[Trash Info]") { + return Err(anyhow!("First line should start with [TrashInfo]")); + } + + for line in lines { + let Ok(line) = line else { + continue; + }; + if option_path.is_none() && line.starts_with("Path=") { + option_path = Some(Self::parse_option_path(&line)); + continue; + } + if option_deleted_time.is_none() && line.starts_with("DeletionDate=") { + option_deleted_time = Some(Self::parse_deletion_date(&line)?); + } + } + } + + Ok((option_path, option_deleted_time)) + } + + fn parse_option_path(line: &str) -> PathBuf { + let path_part = &line[5..]; + let cow_path_str = url_escape::decode(path_part); + let path_str = cow_path_str.as_ref(); + PathBuf::from(path_str) + } + + fn parse_deletion_date(line: &str) -> Result<String> { + let deletion_date_str = &line[13..]; + match parsed_date_from_path_info(deletion_date_str) { + Ok(()) => Ok(deletion_date_str.to_owned()), + Err(e) => Err(e), } } fn remove_extension(mut destname: String) -> Result<String> { - if destname.ends_with(".trashinfo") { + if destname.ends_with(TRASH_INFO_EXTENSION) { destname.truncate(destname.len() - 10); Ok(destname) } else { - Err(anyhow!("trahsinfo: filename doesn't contain .trashfino")) + Err(anyhow!( + "trahsinfo: filename doesn't contain {TRASH_INFO_EXTENSION}" + )) } } } @@ -159,7 +181,7 @@ impl std::fmt::Display for TrashInfo { #[derive(Clone)] pub struct Trash { /// Trashed files info. - pub content: Vec<TrashInfo>, + content: Vec<TrashInfo>, index: usize, /// The path to the trashed files pub trash_folder_files: String, @@ -171,7 +193,7 @@ impl Trash { if let Some(file_name) = origin.file_name() { let mut dest = file_name .to_str() - .context("pick_dest_name :Couldn't parse the origin filename into a string")? + .context("pick_dest_name: Couldn't parse the origin filename into a string")? .to_owned(); let mut dest_path = PathBuf::from(&self.trash_folder_files); dest_path.push(&dest); @@ -242,50 +264,67 @@ impl Trash { } let dest_file_name = self.pick_dest_name(origin)?; - let trash_info = TrashInfo::new(origin, &dest_file_name); - let mut trashfile_filename = PathBuf::from(&self.trash_folder_files); - trashfile_filename.push(&dest_file_name); - let mut dest_trashinfo_name = dest_file_name.clone(); - dest_trashinfo_name.push_str(".trashinfo"); - let mut trashinfo_filename = PathBuf::from(&self.trash_folder_info); - trashinfo_filename.push(dest_trashinfo_name); - trash_info.write_trash_info(&trashinfo_filename)?; + self.execute_trash(TrashInfo::new(origin, &dest_file_name), &dest_file_name) + } + + fn concat_path(root: &str, filename: &str) -> PathBuf { + let mut concatened_path = PathBuf::from(root); + concatened_path.push(filename); + concatened_path + } + + fn trashfile_path(&self, dest_file_name: &str) -> PathBuf { + Self::concat_path(&self.trash_folder_files, dest_file_name) + } + fn trashinfo_path(&self, dest_trashinfo_name: &str) -> PathBuf { + let mut dest_trashinfo_name = dest_trashinfo_name.to_owned(); + dest_trashinfo_name.push_str(TRASH_INFO_EXTENSION); + Self::concat_path(&self.trash_folder_info, &dest_trashinfo_name) + } + + fn execute_trash(&mut self, trash_info: TrashInfo, dest_file_name: &str) -> Result<()> { + let trashfile_filename = &self.trashfile_path(dest_file_name); + std::fs::rename(&trash_info.origin, trashfile_filename)?; + Self::log_trash_add(&trash_info.origin, dest_file_name); + trash_info.write_trash_info(&self.trashinfo_path(dest_file_name))?; self.content.push(trash_info); - std::fs::rename(origin, &trashfile_filename)?; + Ok(()) + } + fn log_trash_add(origin: &Path, dest_file_name: &str) { info!("moved to trash {:?} -> {:?}", origin, dest_file_name); let log_line = format!("moved to trash {:?} -> {:?}", origin, dest_file_name); write_log_line(log_line); - Ok(()) } /// Empty the trash, removing all the files and the trashinfo. /// This action requires a confirmation. /// Watchout, it may delete files that weren't parsed. pub fn empty_trash(&mut self) -> Result<()> { - remove_dir_all(&self.trash_folder_files)?; - create_dir(&self.trash_folder_files)?; - - remove_dir_all(&self.trash_folder_info)?; - create_dir(&self.trash_folder_info)?; + self.empty_trash_dirs()?; let number_of_elements = self.content.len(); - self.content = vec![]; + Self::log_trash_empty(number_of_elements); + Ok(()) + } - let log_line = format!( - "Emptied the trash: {} files permanently deleted", - number_of_elements - ); - write_log_line(log_line); - info!( - "Emptied the trash: {} files permanently deleted", - number_of_elements - ); + fn empty_trash_dirs(&self) -> Result<(), std::io::Error> { + Self::empty_dir(&self.trash_folder_files)?; + Self::empty_dir(&self.trash_folder_files) + } - Ok(()) + fn empty_dir(dir: &str) -> Result<(), std::io::Error> { + remove_dir_all(dir)?; + create_dir(dir) + } + + fn log_trash_empty(number_of_elements: usize) { + let log_line = format!("Emptied the trash: {number_of_elements} files permanently deleted"); + write_log_line(log_line); + info!("Emptied the trash: {number_of_elements} files permanently deleted"); } fn remove_selected_file(&mut self) -> Result<(PathBuf, PathBuf, PathBuf)> { @@ -294,37 +333,33 @@ impl Trash { "remove selected file: Can't restore from an empty trash", )); } - let trashinfo = self.content[self.index].to_owned(); - - let origin = trashinfo.origin; - let dest_name = trashinfo.dest_name; - let parent = find_parent(&origin)?; + let trashinfo = &self.content[self.index]; + let origin = trashinfo.origin.to_owned(); - let mut info_name = dest_name.clone(); - info_name.push_str(".trashinfo"); + let parent = find_parent(&trashinfo.origin)?; - let mut trashed_file_content = PathBuf::from(&self.trash_folder_files); - trashed_file_content.push(&dest_name); - - let mut trashed_file_info = PathBuf::from(&self.trash_folder_info); - trashed_file_info.push(&info_name); + let trashed_file_content = self.trashfile_path(&trashinfo.dest_name); + let trashed_file_info = self.trashinfo_path(&trashinfo.dest_name); if !trashed_file_content.exists() { return Err(anyhow!("trash restore: Couldn't find the trashed file",)); } if !trashed_file_info.exists() { - return Err(anyhow!( - "trash restore: Couldn't find the trashed file info", - )); + return Err(anyhow!("trash restore: Couldn't find the trashed info",)); } - self.content.remove(self.index); - std::fs::remove_file(&trashed_file_info)?; + self.remove_from_content_and_delete_trashinfo(&trashed_file_info)?; Ok((origin, trashed_file_content, parent)) } + fn remove_from_content_and_delete_trashinfo(&mut self, trashed_file_info: &Path) -> Result<()> { + self.content.remove(self.index); + std::fs::remove_file(trashed_file_info)?; + Ok(()) + } + /// Restores a file from the trash to its previous directory. /// If the parent (or ancestor) folder were deleted, it is recreated. pub fn restore(&mut self) -> Result<()> { @@ -332,13 +367,22 @@ impl Trash { return Ok(()); } let (origin, trashed_file_content, parent) = self.remove_selected_file()?; + Self::execute_restore(&origin, &trashed_file_content, &parent)?; + Self::log_trash_restore(&origin); + Ok(()) + } + + fn execute_restore(origin: &Path, trashed_file_content: &Path, parent: &Path) -> Result<()> { if !parent.exists() { - std::fs::create_dir_all(&parent)? + std::fs::create_dir_all(parent)? } - std::fs::rename(trashed_file_content, &origin)?; + std::fs::rename(trashed_file_content, origin)?; + Ok(()) + } + + fn log_trash_restore(origin: &Path) { let log_line = format!("Trash restored: {origin}", origin = origin.display()); write_log_line(log_line); - Ok(()) } /// Deletes a file permanently from the trash. @@ -350,15 +394,20 @@ impl Trash { let (_, trashed_file_content, _) = self.remove_selected_file()?; std::fs::remove_file(&trashed_file_content)?; + Self::log_trash_remove(&trashed_file_content); + if self.index > 0 { self.index -= 1 } + Ok(()) + } + + fn log_trash_remove(trashed_file_content: &Path) { let log_line = format!( - "Trash removed {trashed_file_content}", + "Trash removed: {trashed_file_content}", trashed_file_content = trashed_file_content.display() ); write_log_line(log_line); - Ok(()) } } @@ -382,10 +431,6 @@ fn rand_string() -> String { .collect() } -fn trashinfo_error(msg: &str) -> Result<TrashInfo> { - Err(anyhow!("trash {}", msg)) -} - fn find_parent(path: &Path) -> Result<PathBuf> { Ok(path .parent() |