mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
use media.trash for unused media deletion as well
This commit is contained in:
parent
4fc898ec1e
commit
4c0f216df2
6 changed files with 68 additions and 45 deletions
|
@ -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<string,string> renamed = 5;
|
||||
}
|
||||
|
||||
message TrashMediaFilesIn {
|
||||
repeated string fnames = 1;
|
||||
}
|
|
@ -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
|
||||
##########################################################################
|
||||
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<dyn Fn(Vec<u8>) -> bool + Send>;
|
|||
pub struct Backend {
|
||||
#[allow(dead_code)]
|
||||
col_path: PathBuf,
|
||||
media_folder: String,
|
||||
media_folder: PathBuf,
|
||||
media_db: String,
|
||||
progress_callback: Option<ProtoProgressCallback>,
|
||||
}
|
||||
|
@ -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<u16>) -> Vec<u32> {
|
||||
|
|
|
@ -288,7 +288,7 @@ pub(super) fn mtime_as_i64<P: AsRef<Path>>(path: P) -> io::Result<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
|
||||
S: AsRef<str> + std::fmt::Debug,
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue