add restore media action

This commit is contained in:
Damien Elmes 2020-03-10 13:35:09 +10:00
parent 6ad2a1f9a3
commit 0f4c3ab611
6 changed files with 149 additions and 11 deletions

View file

@ -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;
}

View file

@ -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:

View file

@ -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)

View file

@ -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

View file

@ -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 {

View file

@ -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<String> {
let mut files = fs::read_dir(dir)
.unwrap()
.map(|dentry| {
let dentry = dentry.unwrap();
Ok(dentry.file_name().to_string_lossy().to_string())
})
.collect::<io::Result<Vec<_>>>()
.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::<String>::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::<String>::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::<String>::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")?;