From 4c0f216df2dd0fae32a566190c095bdec949240a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 11 Feb 2020 17:30:10 +1000 Subject: [PATCH] use media.trash for unused media deletion as well --- proto/backend.proto | 6 +++ pylib/anki/media.py | 4 ++ pylib/anki/rsbackend.py | 5 +++ qt/aqt/mediacheck.py | 85 ++++++++++++++++++++-------------------- rslib/src/backend.rs | 11 +++++- rslib/src/media/files.rs | 2 +- 6 files changed, 68 insertions(+), 45 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index 8a157850f..6b9a4016b 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -28,6 +28,7 @@ message BackendInput { AddFileToMediaFolderIn add_file_to_media_folder = 26; SyncMediaIn sync_media = 27; Empty check_media = 28; + TrashMediaFilesIn trash_media_files = 29; } } @@ -46,6 +47,7 @@ message BackendOutput { string add_file_to_media_folder = 26; Empty sync_media = 27; MediaCheckOut check_media = 28; + Empty trash_media_files = 29; BackendError error = 2047; } @@ -270,4 +272,8 @@ message MediaCheckOut { repeated string dirs = 3; repeated string oversize = 4; map renamed = 5; +} + +message TrashMediaFilesIn { + repeated string fnames = 1; } \ No newline at end of file diff --git a/pylib/anki/media.py b/pylib/anki/media.py index 291a27f9f..c19a9831b 100644 --- a/pylib/anki/media.py +++ b/pylib/anki/media.py @@ -111,6 +111,10 @@ class MediaManager: def have(self, fname: str) -> bool: return os.path.exists(os.path.join(self.dir(), fname)) + def trash_files(self, fnames: List[str]) -> None: + "Move provided files to the trash." + self.col.backend.trash_media_files(fnames) + # String manipulation ########################################################################## diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 8625e9857..53a11dbda 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -312,3 +312,8 @@ class RustBackend: return self._run_command( pb.BackendInput(check_media=pb.Empty()), release_gil=True, ).check_media + + def trash_media_files(self, fnames: List[str]) -> None: + self._run_command( + pb.BackendInput(trash_media_files=pb.TrashMediaFilesIn(fnames=fnames)) + ) diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index eb596d06d..0d78a03c6 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -3,11 +3,10 @@ from __future__ import annotations +import itertools import time from concurrent.futures import Future -from typing import Optional - -from send2trash import send2trash +from typing import Iterable, List, Optional, TypeVar import aqt from anki import hooks @@ -16,6 +15,17 @@ from anki.rsbackend import Interrupted, MediaCheckOutput, Progress, ProgressKind from aqt.qt import * from aqt.utils import askUser, restoreGeom, saveGeom, showText, tooltip +T = TypeVar("T") + + +def chunked_list(l: Iterable[T], n: int) -> Iterable[List[T]]: + l = iter(l) + while True: + res = list(itertools.islice(l, n)) + if not res: + return + yield res + def check_media_db(mw: aqt.AnkiQt) -> None: c = MediaChecker(mw) @@ -74,11 +84,12 @@ class MediaChecker: layout.addWidget(text) box = QDialogButtonBox(QDialogButtonBox.Close) layout.addWidget(box) + if output.unused: b = QPushButton(_("Delete Unused Files")) b.setAutoDefault(False) - box.addButton(b, QDialogButtonBox.ActionRole) - b.clicked.connect(lambda c, u=output.unused, d=diag: deleteUnused(self.mw, u, d)) # type: ignore + box.addButton(b, QDialogButtonBox.RejectRole) + b.clicked.connect(lambda c: self._on_trash_files(output.unused)) # type: ignore if output.missing: if any(map(lambda x: x.startswith("latex-"), output.missing)): @@ -120,6 +131,32 @@ class MediaChecker: self.mw.progress.update(_("Checked {}...").format(count)) return True + def _on_trash_files(self, fnames: List[str]): + if not askUser(_("Delete unused media?")): + return + + self.progress_dialog = self.mw.progress.start() + + last_progress = time.time() + remaining = len(fnames) + try: + for chunk in chunked_list(fnames, 25): + self.mw.col.media.trash_files(chunk) + remaining -= len(chunk) + if time.time() - last_progress >= 0.3: + label = ( + ngettext( + "%d file remaining...", "%d files remaining...", remaining, + ) + % remaining + ) + self.mw.progress.update(label) + finally: + self.mw.progress.finish() + self.progress_dialog = None + + tooltip(_("Files moved to trash.")) + def describe_output(output: MediaCheckOutput) -> str: buf = [] @@ -172,41 +209,3 @@ def describe_output(output: MediaCheckOutput) -> str: buf.append("") return "\n".join(buf) - - -def deleteUnused(self, unused, diag): - if not askUser(_("Delete unused media?")): - return - - mdir = self.col.media.dir() - self.progress.start(immediate=True) - try: - lastProgress = 0 - for c, f in enumerate(unused): - path = os.path.join(mdir, f) - if os.path.exists(path): - send2trash(path) - - now = time.time() - if now - lastProgress >= 0.3: - numberOfRemainingFilesToBeDeleted = len(unused) - c - lastProgress = now - label = ( - ngettext( - "%d file remaining...", - "%d files remaining...", - numberOfRemainingFilesToBeDeleted, - ) - % numberOfRemainingFilesToBeDeleted - ) - self.progress.update(label) - finally: - self.progress.finish() - # caller must not pass in empty list - # pylint: disable=undefined-loop-variable - numberOfFilesDeleted = c + 1 - tooltip( - ngettext("Deleted %d file.", "Deleted %d files.", numberOfFilesDeleted) - % numberOfFilesDeleted - ) - diag.close() diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs index 8f9d72552..203d7227f 100644 --- a/rslib/src/backend.rs +++ b/rslib/src/backend.rs @@ -7,6 +7,7 @@ use crate::backend_proto::{Empty, RenderedTemplateReplacement, SyncMediaIn}; use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind}; use crate::latex::{extract_latex, ExtractedLatex}; use crate::media::check::MediaChecker; +use crate::media::files::remove_files; use crate::media::sync::MediaSyncProgress; use crate::media::MediaManager; use crate::sched::{local_minutes_west_for_stamp, sched_timing_today}; @@ -25,7 +26,7 @@ pub type ProtoProgressCallback = Box) -> bool + Send>; pub struct Backend { #[allow(dead_code)] col_path: PathBuf, - media_folder: String, + media_folder: PathBuf, media_db: String, progress_callback: Option, } @@ -187,6 +188,10 @@ impl Backend { OValue::SyncMedia(Empty {}) } Value::CheckMedia(_) => OValue::CheckMedia(self.check_media()?), + Value::TrashMediaFiles(input) => { + self.remove_media_files(&input.fnames)?; + OValue::TrashMediaFiles(Empty {}) + } }) } @@ -363,6 +368,10 @@ impl Backend { oversize: output.oversize, }) } + + fn remove_media_files(&self, fnames: &[String]) -> Result<()> { + remove_files(&self.media_folder, fnames) + } } fn ords_hash_to_set(ords: HashSet) -> Vec { diff --git a/rslib/src/media/files.rs b/rslib/src/media/files.rs index a07b458f4..ec4bb0a2d 100644 --- a/rslib/src/media/files.rs +++ b/rslib/src/media/files.rs @@ -288,7 +288,7 @@ pub(super) fn mtime_as_i64>(path: P) -> io::Result { .as_secs() as i64) } -pub(super) fn remove_files(media_folder: &Path, files: &[S]) -> Result<()> +pub fn remove_files(media_folder: &Path, files: &[S]) -> Result<()> where S: AsRef + std::fmt::Debug, {