From 0f4c3ab61177173d1a7208086a8e5c9c8a8aea83 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 10 Mar 2020 13:35:09 +1000 Subject: [PATCH] add restore media action --- proto/backend.proto | 2 + pylib/anki/rsbackend.py | 4 ++ qt/aqt/mediacheck.py | 23 ++++++++ rslib/ftl/media-check.ftl | 3 + rslib/src/backend.rs | 14 +++++ rslib/src/media/check.rs | 114 ++++++++++++++++++++++++++++++++++---- 6 files changed, 149 insertions(+), 11 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index b4c555e8a..b5a35d0d1 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -43,6 +43,7 @@ message BackendInput { StudiedTodayIn studied_today = 32; CongratsLearnMsgIn congrats_learn_msg = 33; Empty empty_trash = 34; + Empty restore_trash = 35; } } @@ -70,6 +71,7 @@ message BackendOutput { MediaCheckOut check_media = 28; Empty trash_media_files = 29; Empty empty_trash = 34; + Empty restore_trash = 35; BackendError error = 2047; } diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 247f580c1..9bef66dfd 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -363,6 +363,10 @@ class RustBackend: def empty_trash(self): self._run_command(pb.BackendInput(empty_trash=pb.Empty())) + def restore_trash(self): + self._run_command(pb.BackendInput(restore_trash=pb.Empty())) + + def translate_string_in( key: TR, **kwargs: Union[str, int, float] ) -> pb.TranslateStringIn: diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 61bc491e6..941de6bdc 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -100,6 +100,12 @@ class MediaChecker: b.setAutoDefault(False) box.addButton(b, QDialogButtonBox.RejectRole) b.clicked.connect(lambda c: self._on_empty_trash()) # type: ignore + + b = QPushButton(tr(TR.MEDIA_CHECK_RESTORE_TRASH)) + b.setAutoDefault(False) + box.addButton(b, QDialogButtonBox.RejectRole) + b.clicked.connect(lambda c: self._on_restore_trash()) # type: ignore + box.rejected.connect(diag.reject) # type: ignore diag.setMinimumHeight(400) diag.setMinimumWidth(500) @@ -172,3 +178,20 @@ class MediaChecker: tooltip(tr(TR.MEDIA_CHECK_TRASH_EMPTIED)) self.mw.taskman.run_in_background(empty_trash, on_done) + + def _on_restore_trash(self): + self.progress_dialog = self.mw.progress.start() + hooks.bg_thread_progress_callback.append(self._on_progress) + + def restore_trash(): + self.mw.col.backend.restore_trash() + + def on_done(fut: Future): + self.mw.progress.finish() + hooks.bg_thread_progress_callback.remove(self._on_progress) + # check for errors + fut.result() + + tooltip(tr(TR.MEDIA_CHECK_TRASH_RESTORED)) + + self.mw.taskman.run_in_background(restore_trash, on_done) diff --git a/rslib/ftl/media-check.ftl b/rslib/ftl/media-check.ftl index 739d1f3cc..f531a3de4 100644 --- a/rslib/ftl/media-check.ftl +++ b/rslib/ftl/media-check.ftl @@ -48,6 +48,7 @@ media-check-delete-unused-complete = {$count -> } moved to the trash. media-check-trash-emptied = The trash folder is now empty. +media-check-trash-restored = Restored deleted files to the media folder. ## Rendering LaTeX @@ -59,3 +60,5 @@ media-check-delete-unused = Delete Unused media-check-render-latex = Render LaTeX # button to permanently delete media files from the trash folder media-check-empty-trash = Empty Trash +# button to move deleted files from the trash back into the media folder +media-check-restore-trash = Restore Deleted diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs index e074dd960..970768fb5 100644 --- a/rslib/src/backend.rs +++ b/rslib/src/backend.rs @@ -237,6 +237,10 @@ impl Backend { self.empty_trash()?; OValue::EmptyTrash(Empty {}) } + Value::RestoreTrash(_) => { + self.restore_trash()?; + OValue::RestoreTrash(Empty {}) + } }) } @@ -467,6 +471,16 @@ impl Backend { checker.empty_trash() } + + fn restore_trash(&self) -> Result<()> { + let callback = + |progress: usize| self.fire_progress_callback(Progress::MediaCheck(progress as u32)); + + let mgr = MediaManager::new(&self.media_folder, &self.media_db)?; + let mut checker = MediaChecker::new(&mgr, &self.col_path, callback, &self.i18n, &self.log); + + checker.restore_trash() + } } fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { diff --git a/rslib/src/media/check.rs b/rslib/src/media/check.rs index 024bf0408..5e1ff26d4 100644 --- a/rslib/src/media/check.rs +++ b/rslib/src/media/check.rs @@ -18,7 +18,7 @@ use crate::{media::MediaManager, text::extract_media_refs}; use coarsetime::Instant; use std::collections::{HashMap, HashSet}; use std::path::Path; -use std::{borrow::Cow, fs}; +use std::{borrow::Cow, fs, io}; #[derive(Debug, PartialEq)] pub struct MediaCheckOutput { @@ -339,6 +339,43 @@ where Ok(()) } + pub fn restore_trash(&mut self) -> Result<()> { + let trash = trash_folder(&self.mgr.media_folder)?; + + for dentry in trash.read_dir()? { + let dentry = dentry?; + + self.checked += 1; + if self.checked % 10 == 0 { + self.maybe_fire_progress_cb()?; + } + + let orig_path = self.mgr.media_folder.join(dentry.file_name()); + // if the original filename doesn't exist, we can just rename + if let Err(e) = fs::metadata(&orig_path) { + if e.kind() == io::ErrorKind::NotFound { + fs::rename(&dentry.path(), &orig_path)?; + } else { + return Err(e.into()); + } + } else { + // ensure we don't overwrite different data + let fname_os = dentry.file_name(); + let fname = fname_os.to_string_lossy(); + if let Some(data) = data_for_file(&trash, fname.as_ref())? { + let _new_fname = + self.mgr + .add_file(&mut self.mgr.dbctx(), fname.as_ref(), &data)?; + } else { + debug!(self.log, "file disappeared while restoring trash"; "fname"=>fname.as_ref()); + } + fs::remove_file(dentry.path())?; + } + } + + Ok(()) + } + /// Find all media references in notes, fixing as necessary. fn check_media_references( &mut self, @@ -468,12 +505,13 @@ mod test { use crate::log; use crate::log::Logger; use crate::media::check::{MediaCheckOutput, MediaChecker}; + use crate::media::files::trash_folder; use crate::media::MediaManager; - use std::fs; - use std::path::PathBuf; + use std::path::{Path, PathBuf}; + use std::{fs, io}; use tempfile::{tempdir, TempDir}; - fn common_setup() -> Result<(TempDir, MediaManager, PathBuf, Logger)> { + fn common_setup() -> Result<(TempDir, MediaManager, PathBuf, Logger, I18n)> { let dir = tempdir()?; let media_dir = dir.path().join("media"); fs::create_dir(&media_dir)?; @@ -487,12 +525,15 @@ mod test { let mgr = MediaManager::new(&media_dir, media_db)?; let log = log::terminal(); - Ok((dir, mgr, col_path, log)) + + let i18n = I18n::new(&["zz"], "dummy", log.clone()); + + Ok((dir, mgr, col_path, log, i18n)) } #[test] fn media_check() -> Result<()> { - let (_dir, mgr, col_path, log) = common_setup()?; + let (_dir, mgr, col_path, log, i18n) = common_setup()?; // add some test files fs::write(&mgr.media_folder.join("zerobytes"), "")?; @@ -502,8 +543,6 @@ mod test { fs::write(&mgr.media_folder.join("_under.jpg"), "foo")?; fs::write(&mgr.media_folder.join("unused.jpg"), "foo")?; - let i18n = I18n::new(&["zz"], "dummy", log.clone()); - let progress = |_n| true; let mut checker = MediaChecker::new(&mgr, &col_path, progress, &i18n, &log); let mut output = checker.check()?; @@ -551,11 +590,64 @@ Unused: unused.jpg Ok(()) } + fn files_in_dir(dir: &Path) -> Vec { + let mut files = fs::read_dir(dir) + .unwrap() + .map(|dentry| { + let dentry = dentry.unwrap(); + Ok(dentry.file_name().to_string_lossy().to_string()) + }) + .collect::>>() + .unwrap(); + files.sort(); + files + } + + #[test] + fn trash_handling() -> Result<()> { + let (_dir, mgr, col_path, log, i18n) = common_setup()?; + let trash_folder = trash_folder(&mgr.media_folder)?; + fs::write(trash_folder.join("test.jpg"), "test")?; + + let progress = |_n| true; + let mut checker = MediaChecker::new(&mgr, &col_path, progress, &i18n, &log); + + checker.restore_trash()?; + + // file should have been moved to media folder + assert_eq!(files_in_dir(&trash_folder), Vec::::new()); + assert_eq!( + files_in_dir(&mgr.media_folder), + vec!["test.jpg".to_string()] + ); + + // if we repeat the process, restoring should do the same thing if the contents are equal + fs::write(trash_folder.join("test.jpg"), "test")?; + checker.restore_trash()?; + assert_eq!(files_in_dir(&trash_folder), Vec::::new()); + assert_eq!( + files_in_dir(&mgr.media_folder), + vec!["test.jpg".to_string()] + ); + + // but rename if required + fs::write(trash_folder.join("test.jpg"), "test2")?; + checker.restore_trash()?; + assert_eq!(files_in_dir(&trash_folder), Vec::::new()); + assert_eq!( + files_in_dir(&mgr.media_folder), + vec![ + "test-109f4b3c50d7b0df729d299bc6f8e9ef9066971f.jpg".to_string(), + "test.jpg".into() + ] + ); + + Ok(()) + } + #[test] fn unicode_normalization() -> Result<()> { - let (_dir, mgr, col_path, log) = common_setup()?; - - let i18n = I18n::new(&["zz"], "dummy", log.clone()); + let (_dir, mgr, col_path, log, i18n) = common_setup()?; fs::write(&mgr.media_folder.join("ぱぱ.jpg"), "nfd encoding")?;