use media.trash for unused media deletion as well

This commit is contained in:
Damien Elmes 2020-02-11 17:30:10 +10:00
parent 4fc898ec1e
commit 4c0f216df2
6 changed files with 68 additions and 45 deletions

View file

@ -28,6 +28,7 @@ message BackendInput {
AddFileToMediaFolderIn add_file_to_media_folder = 26; AddFileToMediaFolderIn add_file_to_media_folder = 26;
SyncMediaIn sync_media = 27; SyncMediaIn sync_media = 27;
Empty check_media = 28; Empty check_media = 28;
TrashMediaFilesIn trash_media_files = 29;
} }
} }
@ -46,6 +47,7 @@ message BackendOutput {
string add_file_to_media_folder = 26; string add_file_to_media_folder = 26;
Empty sync_media = 27; Empty sync_media = 27;
MediaCheckOut check_media = 28; MediaCheckOut check_media = 28;
Empty trash_media_files = 29;
BackendError error = 2047; BackendError error = 2047;
} }
@ -270,4 +272,8 @@ message MediaCheckOut {
repeated string dirs = 3; repeated string dirs = 3;
repeated string oversize = 4; repeated string oversize = 4;
map<string,string> renamed = 5; map<string,string> renamed = 5;
}
message TrashMediaFilesIn {
repeated string fnames = 1;
} }

View file

@ -111,6 +111,10 @@ class MediaManager:
def have(self, fname: str) -> bool: def have(self, fname: str) -> bool:
return os.path.exists(os.path.join(self.dir(), fname)) 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 # String manipulation
########################################################################## ##########################################################################

View file

@ -312,3 +312,8 @@ class RustBackend:
return self._run_command( return self._run_command(
pb.BackendInput(check_media=pb.Empty()), release_gil=True, pb.BackendInput(check_media=pb.Empty()), release_gil=True,
).check_media ).check_media
def trash_media_files(self, fnames: List[str]) -> None:
self._run_command(
pb.BackendInput(trash_media_files=pb.TrashMediaFilesIn(fnames=fnames))
)

View file

@ -3,11 +3,10 @@
from __future__ import annotations from __future__ import annotations
import itertools
import time import time
from concurrent.futures import Future from concurrent.futures import Future
from typing import Optional from typing import Iterable, List, Optional, TypeVar
from send2trash import send2trash
import aqt import aqt
from anki import hooks from anki import hooks
@ -16,6 +15,17 @@ from anki.rsbackend import Interrupted, MediaCheckOutput, Progress, ProgressKind
from aqt.qt import * from aqt.qt import *
from aqt.utils import askUser, restoreGeom, saveGeom, showText, tooltip 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: def check_media_db(mw: aqt.AnkiQt) -> None:
c = MediaChecker(mw) c = MediaChecker(mw)
@ -74,11 +84,12 @@ class MediaChecker:
layout.addWidget(text) layout.addWidget(text)
box = QDialogButtonBox(QDialogButtonBox.Close) box = QDialogButtonBox(QDialogButtonBox.Close)
layout.addWidget(box) layout.addWidget(box)
if output.unused: if output.unused:
b = QPushButton(_("Delete Unused Files")) b = QPushButton(_("Delete Unused Files"))
b.setAutoDefault(False) b.setAutoDefault(False)
box.addButton(b, QDialogButtonBox.ActionRole) box.addButton(b, QDialogButtonBox.RejectRole)
b.clicked.connect(lambda c, u=output.unused, d=diag: deleteUnused(self.mw, u, d)) # type: ignore b.clicked.connect(lambda c: self._on_trash_files(output.unused)) # type: ignore
if output.missing: if output.missing:
if any(map(lambda x: x.startswith("latex-"), 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)) self.mw.progress.update(_("Checked {}...").format(count))
return True 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: def describe_output(output: MediaCheckOutput) -> str:
buf = [] buf = []
@ -172,41 +209,3 @@ def describe_output(output: MediaCheckOutput) -> str:
buf.append("") buf.append("")
return "\n".join(buf) 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()

View file

@ -7,6 +7,7 @@ use crate::backend_proto::{Empty, RenderedTemplateReplacement, SyncMediaIn};
use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind}; use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind};
use crate::latex::{extract_latex, ExtractedLatex}; use crate::latex::{extract_latex, ExtractedLatex};
use crate::media::check::MediaChecker; use crate::media::check::MediaChecker;
use crate::media::files::remove_files;
use crate::media::sync::MediaSyncProgress; use crate::media::sync::MediaSyncProgress;
use crate::media::MediaManager; use crate::media::MediaManager;
use crate::sched::{local_minutes_west_for_stamp, sched_timing_today}; use crate::sched::{local_minutes_west_for_stamp, sched_timing_today};
@ -25,7 +26,7 @@ pub type ProtoProgressCallback = Box<dyn Fn(Vec<u8>) -> bool + Send>;
pub struct Backend { pub struct Backend {
#[allow(dead_code)] #[allow(dead_code)]
col_path: PathBuf, col_path: PathBuf,
media_folder: String, media_folder: PathBuf,
media_db: String, media_db: String,
progress_callback: Option<ProtoProgressCallback>, progress_callback: Option<ProtoProgressCallback>,
} }
@ -187,6 +188,10 @@ impl Backend {
OValue::SyncMedia(Empty {}) OValue::SyncMedia(Empty {})
} }
Value::CheckMedia(_) => OValue::CheckMedia(self.check_media()?), 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, oversize: output.oversize,
}) })
} }
fn remove_media_files(&self, fnames: &[String]) -> Result<()> {
remove_files(&self.media_folder, fnames)
}
} }
fn ords_hash_to_set(ords: HashSet<u16>) -> Vec<u32> { fn ords_hash_to_set(ords: HashSet<u16>) -> Vec<u32> {

View file

@ -288,7 +288,7 @@ pub(super) fn mtime_as_i64<P: AsRef<Path>>(path: P) -> io::Result<i64> {
.as_secs() as i64) .as_secs() as i64)
} }
pub(super) fn remove_files<S>(media_folder: &Path, files: &[S]) -> Result<()> pub fn remove_files<S>(media_folder: &Path, files: &[S]) -> Result<()>
where where
S: AsRef<str> + std::fmt::Debug, S: AsRef<str> + std::fmt::Debug,
{ {