From 1a0405faf39623d4011de100217455e8f32ffd07 Mon Sep 17 00:00:00 2001 From: Bastien Dejean Date: Sat, 25 Jul 2020 12:09:12 +0200 Subject: [PATCH] Add support for removing documents Fixes #138. --- .gitignore | 1 + src/library.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++ src/settings/mod.rs | 2 + src/view/home/mod.rs | 66 +++++++++++++++++++++++++++++++ src/view/mod.rs | 2 + 5 files changed, 164 insertions(+) diff --git a/.gitignore b/.gitignore index e3bec276..e36704cc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /.metadata.json /.reading-states /.fat32-epoch +/.trash /css/*-user.css /hyphenation-patterns /dictionaries diff --git a/src/library.rs b/src/library.rs index 35d6c7b2..9842988a 100644 --- a/src/library.rs +++ b/src/library.rs @@ -8,6 +8,7 @@ use indexmap::IndexMap; use fxhash::{FxHashMap, FxHashSet, FxBuildHasher}; use chrono::{Local, TimeZone}; use filetime::{FileTime, set_file_handle_times}; +use anyhow::{Error, format_err}; use crate::metadata::{Info, ReaderInfo, FileInfo, SimpleStatus, SortMethod}; use crate::metadata::{sort, sorter, extract_metadata_from_epub}; use crate::settings::{LibraryMode, ImportSettings}; @@ -320,6 +321,98 @@ impl Library { self.has_db_changed = true; } + pub fn remove>(&mut self, path: P) -> Result<(), Error> { + let full_path = self.home.join(path.as_ref()); + + let fp = self.paths.get(path.as_ref()).cloned().or_else(|| { + full_path.metadata().ok() + .and_then(|md| md.fingerprint(self.fat32_epoch).ok()) + }).ok_or_else(|| format_err!("Can't get fingerprint of {}.", path.as_ref().display()))?; + + if full_path.exists() { + fs::remove_file(&full_path)?; + if let Some(parent) = full_path.parent() { + if parent != self.home { + fs::remove_dir(parent).ok(); + } + } + } + + let rsp = self.reading_state_path(fp); + if rsp.exists() { + fs::remove_file(rsp)?; + } + + if self.mode == LibraryMode::Database { + self.paths.remove(path.as_ref()); + if self.db.shift_remove(&fp).is_some() { + self.has_db_changed = true; + } + } else { + self.reading_states.remove(&fp); + } + + self.modified_reading_states.remove(&fp); + + Ok(()) + } + + pub fn move_to>(&mut self, path: P, other: &mut Library) -> Result<(), Error> { + if !self.home.join(path.as_ref()).exists() { + return Err(format_err!("Can't move non-existing file {}.", path.as_ref().display())); + } + + let fp = self.paths.get(path.as_ref()).cloned().or_else(|| { + self.home.join(path.as_ref()) + .metadata().ok() + .and_then(|md| md.fingerprint(self.fat32_epoch).ok()) + }).ok_or_else(|| format_err!("Can't get fingerprint of {}.", path.as_ref().display()))?; + + let src = self.home.join(path.as_ref()); + let mut dest = other.home.join(path.as_ref()); + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent)?; + } + + if dest.exists() { + let prefix = Local::now().format("%Y%m%d_%H%M%S "); + let name = dest.file_name().and_then(|name| name.to_str()) + .map(|name| prefix.to_string() + name) + .ok_or_else(|| format_err!("Can't compute new name for {}.", dest.display()))?; + dest.set_file_name(name); + } + + fs::rename(&src, &dest)?; + + let rsp_src = self.reading_state_path(fp); + if rsp_src.exists() { + let rsp_dest = other.reading_state_path(fp); + fs::rename(&rsp_src, &rsp_dest)?; + } + + if self.mode == LibraryMode::Database { + if let Some(mut info) = self.db.shift_remove(&fp) { + let dest_path = dest.strip_prefix(&other.home)?; + info.file.path = dest_path.to_path_buf(); + other.db.insert(fp, info); + self.paths.remove(path.as_ref()); + other.paths.insert(dest_path.to_path_buf(), fp); + self.has_db_changed = true; + other.has_db_changed = true; + } + } else { + if let Some(reader_info) = self.reading_states.remove(&fp) { + other.reading_states.insert(fp, reader_info); + } + } + + if self.modified_reading_states.remove(&fp) { + other.modified_reading_states.insert(fp); + } + + Ok(()) + } + pub fn clean_up(&mut self) { if self.mode == LibraryMode::Database { let home = &self.home; diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 3430d2fc..2f97be92 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -251,6 +251,7 @@ pub struct HomeSettings { pub address_bar: bool, pub navigation_bar: bool, pub max_levels: usize, + pub max_trash_size: u64, } @@ -303,6 +304,7 @@ impl Default for HomeSettings { address_bar: false, navigation_bar: true, max_levels: 3, + max_trash_size: 32 * (1 << 20), } } } diff --git a/src/view/home/mod.rs b/src/view/home/mod.rs index 9f8efbb5..1e8f4191 100644 --- a/src/view/home/mod.rs +++ b/src/view/home/mod.rs @@ -7,6 +7,7 @@ mod shelf; mod book; mod bottom_bar; +use std::fs; use std::mem; use std::thread; use std::path::{Path, PathBuf}; @@ -48,6 +49,8 @@ use crate::color::BLACK; use crate::font::Fonts; use crate::app::Context; +pub const TRASH_DIRNAME: &str = ".trash"; + #[derive(Debug)] pub struct Home { id: Id, @@ -855,6 +858,23 @@ impl Home { entries.push(EntryKind::SubMenu("Set As".to_string(), submenu)) } + entries.push(EntryKind::Separator); + let selected_library = context.settings.selected_library; + let libraries = context.settings.libraries.iter().enumerate() + .filter(|(index, _)| *index != selected_library) + .map(|(index, lib)| { + EntryKind::Command(lib.name.clone(), + EntryId::MoveTo(path.clone(), index)) + }).collect::>(); + if !libraries.is_empty() { + entries.push(EntryKind::SubMenu("Move To".to_string(), libraries)); + + } + + entries.push(EntryKind::Command("Remove".to_string(), + EntryId::Remove(path.clone()))); + + let book_menu = Menu::new(rect, ViewId::BookMenu, MenuKind::Contextual, entries, context); rq.add(RenderData::new(book_menu.id(), *book_menu.rect(), UpdateMode::Gui)); self.children.push(Box::new(book_menu) as Box); @@ -956,6 +976,40 @@ impl Home { self.refresh_visibles(true, false, rq, context); } + fn remove(&mut self, path: &Path, rq: &mut RenderQueue, context: &mut Context) -> Result<(), Error> { + let trash_path = context.library.home.join(TRASH_DIRNAME); + if !trash_path.is_dir() { + fs::create_dir_all(&trash_path)?; + } + let mut trash = Library::new(trash_path, LibraryMode::Database); + context.library.move_to(path, &mut trash)?; + let (mut files, _) = trash.list(&trash.home, None, false); + let mut size = files.iter().map(|info| info.file.size).sum::(); + if size > context.settings.home.max_trash_size { + sort(&mut files, SortMethod::Added, true); + while size > context.settings.home.max_trash_size { + let info = files.pop().unwrap(); + if let Err(e) = trash.remove(&info.file.path) { + eprintln!("{}", e); + break; + } + size -= info.file.size; + } + } + trash.flush(); + self.refresh_visibles(true, false, rq, context); + Ok(()) + } + + fn move_to(&mut self, path: &Path, index: usize, rq: &mut RenderQueue, context: &mut Context) -> Result<(), Error> { + let library_settings = &context.settings.libraries[index]; + let mut library = Library::new(&library_settings.path, library_settings.mode); + context.library.move_to(path, &mut library)?; + library.flush(); + self.refresh_visibles(true, false, rq, context); + Ok(()) + } + fn set_reverse_order(&mut self, value: bool, rq: &mut RenderQueue, context: &mut Context) { self.reverse_order = value; self.current_page = 0; @@ -1374,6 +1428,18 @@ impl View for Home { } true }, + Event::Select(EntryId::Remove(ref path)) => { + self.remove(path, rq, context) + .map_err(|e| eprintln!("{}", e)) + .ok(); + true + }, + Event::Select(EntryId::MoveTo(ref path, index)) => { + self.move_to(path, index, rq, context) + .map_err(|e| eprintln!("{}", e)) + .ok(); + true + }, Event::Select(EntryId::ToggleShowHidden) => { context.library.show_hidden = !context.library.show_hidden; self.refresh_visibles(true, false, rq, context); diff --git a/src/view/mod.rs b/src/view/mod.rs index 9cd4b61e..e5a3c595 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -491,6 +491,8 @@ pub enum EntryId { CleanUp, Sort(SortMethod), ReverseOrder, + Remove(PathBuf), + MoveTo(PathBuf, usize), AddDirectory(PathBuf), SelectDirectory(PathBuf), ToggleSelectDirectory(PathBuf),