mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -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;
|
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;
|
||||||
}
|
}
|
|
@ -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
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
)
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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,
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue