Fix #7207 ("Library Check" to also clean up and delete files/folders.)

This commit is contained in:
Kovid Goyal 2010-10-18 09:53:55 -06:00
commit a9b64899aa
3 changed files with 80 additions and 45 deletions

View File

@ -3,11 +3,16 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
import os
from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \
QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \
QLineEdit
QLineEdit, Qt
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.library.check_library import CheckLibrary, CHECKS
from calibre.library.database2 import delete_file
from calibre import prints
class Item(QTreeWidgetItem):
pass
@ -24,23 +29,28 @@ class CheckLibraryDialog(QDialog):
self.setLayout(self._layout)
self.log = QTreeWidget(self)
self.log.itemChanged.connect(self.item_changed)
self._layout.addWidget(self.log)
self.check = QPushButton(_('Run the check'))
self.check = QPushButton(_('&Run the check'))
self.check.setDefault(False)
self.check.clicked.connect(self.run_the_check)
self.copy = QPushButton(_('Copy to clipboard'))
self.copy = QPushButton(_('Copy &to clipboard'))
self.copy.setDefault(False)
self.copy.clicked.connect(self.copy_to_clipboard)
self.ok = QPushButton('&Done')
self.ok.setDefault(True)
self.ok.clicked.connect(self.accept)
self.delete = QPushButton('Delete &marked')
self.delete.setDefault(False)
self.delete.clicked.connect(self.delete_marked)
self.cancel = QPushButton('&Cancel')
self.cancel.setDefault(False)
self.cancel.clicked.connect(self.reject)
self.bbox = QDialogButtonBox(self)
self.bbox.addButton(self.copy, QDialogButtonBox.ActionRole)
self.bbox.addButton(self.check, QDialogButtonBox.ActionRole)
self.bbox.addButton(self.delete, QDialogButtonBox.ActionRole)
self.bbox.addButton(self.cancel, QDialogButtonBox.RejectRole)
self.bbox.addButton(self.ok, QDialogButtonBox.AcceptRole)
@ -83,35 +93,66 @@ class CheckLibraryDialog(QDialog):
plaintext = []
def builder(tree, checker, check):
attr = check[0]
attr, h, checkable = check
list = getattr(checker, attr, None)
if list is None:
return
h = check[1]
tl = Item([h])
for problem in list:
it = Item()
if checkable:
it.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
it.setCheckState(1, False)
else:
it.setFlags(Qt.ItemIsEnabled)
it.setText(0, problem[0])
it.setText(1, problem[1])
p = ', '.join(problem[2])
it.setText(2, p)
tl.addChild(it)
plaintext.append(','.join([h, problem[0], problem[1], p]))
self.all_items.append(it)
plaintext.append(','.join([h, problem[0], problem[1]]))
tree.addTopLevelItem(tl)
t = self.log
t.clear()
t.setColumnCount(3);
t.setHeaderLabels([_('Name'), _('Path from library'), _('Additional Information')])
t.setColumnCount(2);
t.setHeaderLabels([_('Name'), _('Path from library')])
self.all_items = []
for check in CHECKS:
builder(t, checker, check)
t.setColumnWidth(0, 200)
t.setColumnWidth(1, 400)
self.delete.setEnabled(False)
self.text_results = '\n'.join(plaintext)
def item_changed(self, item, column):
for it in self.all_items:
if it.checkState(1):
self.delete.setEnabled(True)
return
def delete_marked(self):
if not confirm('<p>'+_('The marked files and folders will be '
'<b>permanently deleted</b>. Are you sure?')
+'</p>', 'check_library_editor_delete', self):
return
# Sort the paths in reverse length order so that we can be sure that
# if an item is in another item, the sub-item will be deleted first.
items = sorted(self.all_items,
key=lambda x: len(x.text(1)),
reverse=True)
for it in items:
if it.checkState(1):
try:
delete_file(os.path.join(self.db.library_path, unicode(it.text(1))))
except:
prints('failed to delete',
os.path.join(self.db.library_path,
unicode(it.text(1))))
self.run_the_check()
def copy_to_clipboard(self):
QApplication.clipboard().setText(self.text_results)

View File

@ -14,14 +14,14 @@ from calibre.ebooks import BOOK_EXTENSIONS
EBOOK_EXTENSIONS = frozenset(BOOK_EXTENSIONS)
NORMALS = frozenset(['metadata.opf', 'cover.jpg'])
CHECKS = [('invalid_titles', _('Invalid titles')),
('extra_titles', _('Extra titles')),
('invalid_authors', _('Invalid authors')),
('extra_authors', _('Extra authors')),
('missing_formats', _('Missing book formats')),
('extra_formats', _('Extra book formats')),
('extra_files', _('Unknown files in books')),
('failed_folders', _('Folders raising exception'))
CHECKS = [('invalid_titles', _('Invalid titles'), True),
('extra_titles', _('Extra titles'), True),
('invalid_authors', _('Invalid authors'), True),
('extra_authors', _('Extra authors'), True),
('missing_formats', _('Missing book formats'), False),
('extra_formats', _('Extra book formats'), True),
('extra_files', _('Unknown files in books'), True),
('failed_folders', _('Folders raising exception'), False)
]
@ -41,7 +41,6 @@ class CheckLibrary(object):
self.all_lc_dbpaths = frozenset([f.lower() for f in self.all_dbpaths])
self.db_id_regexp = re.compile(r'^.* \((\d+)\)$')
self.bad_ext_pat = re.compile(r'[^a-z0-9]+')
self.dirs = []
self.book_dirs = []
@ -78,7 +77,7 @@ class CheckLibrary(object):
auth_path = os.path.join(lib, auth_dir)
# First check: author must be a directory
if not os.path.isdir(auth_path):
self.invalid_authors.append((auth_dir, auth_dir, []))
self.invalid_authors.append((auth_dir, auth_dir))
continue
self.potential_authors[auth_dir] = {}
@ -93,7 +92,7 @@ class CheckLibrary(object):
m = self.db_id_regexp.search(title_dir)
# Second check: title must have an ID and must be a directory
if m is None or not os.path.isdir(title_path):
self.invalid_titles.append((auth_dir, db_path, [title_dir]))
self.invalid_titles.append((auth_dir, db_path))
continue
id = m.group(1)
@ -101,12 +100,12 @@ class CheckLibrary(object):
if self.is_case_sensitive:
if int(id) not in self.all_ids or \
db_path not in self.all_dbpaths:
self.extra_titles.append((title_dir, db_path, []))
self.extra_titles.append((title_dir, db_path))
continue
else:
if int(id) not in self.all_ids or \
db_path.lower() not in self.all_lc_dbpaths:
self.extra_titles.append((title_dir, db_path, []))
self.extra_titles.append((title_dir, db_path))
continue
# Record the book to check its formats
@ -115,7 +114,7 @@ class CheckLibrary(object):
# Fourth check: author directories that contain no titles
if not found_titles:
self.extra_authors.append((auth_dir, auth_dir, []))
self.extra_authors.append((auth_dir, auth_dir))
for x in self.book_dirs:
try:
@ -132,9 +131,7 @@ class CheckLibrary(object):
ext = ext[1:].lower()
if ext in EBOOK_EXTENSIONS:
return True
if self.bad_ext_pat.search(ext) is not None:
return False
return True
return False
def process_book(self, lib, book_info):
(db_path, title_dir, book_id) = book_info
@ -148,18 +145,18 @@ class CheckLibrary(object):
if self.is_case_sensitive:
unknowns = frozenset(filenames-formats-NORMALS)
# Check: any books that aren't formats or normally there?
if unknowns:
self.extra_files.append((title_dir, db_path, unknowns))
for u in unknowns:
self.extra_files.append((title_dir, os.path.join(db_path, u)))
# Check: any book formats that should be there?
missing = book_formats - formats
if missing:
self.missing_formats.append((title_dir, db_path, missing))
for m in missing:
self.missing_formats.append((title_dir, os.path.join(db_path, m)))
# Check: any book formats that shouldn't be there?
extra = formats - book_formats - NORMALS
if extra:
self.extra_formats.append((title_dir, db_path, extra))
for e in extra:
self.extra_formats.append((title_dir, os.path.join(db_path, e)))
else:
def lc_map(fnames, fset):
m = {}
@ -171,19 +168,16 @@ class CheckLibrary(object):
formats_lc = frozenset([f.lower() for f in formats])
unknowns = frozenset(filenames_lc-formats_lc-NORMALS)
# Check: any books that aren't formats or normally there?
if unknowns:
self.extra_files.append((title_dir, db_path,
lc_map(filenames, unknowns)))
for f in lc_map(filenames, unknowns):
self.extra_files.append((title_dir, os.path.join(db_path, f)))
book_formats_lc = frozenset([f.lower() for f in book_formats])
# Check: any book formats that should be there?
missing = book_formats_lc - formats_lc
if missing:
self.missing_formats.append((title_dir, db_path,
lc_map(book_formats, missing)))
for m in lc_map(book_formats, missing):
self.missing_formats.append((title_dir, os.path.join(db_path, m)))
# Check: any book formats that shouldn't be there?
extra = formats_lc - book_formats_lc - NORMALS
if extra:
self.extra_formats.append((title_dir, db_path,
lc_map(formats, extra)))
for e in lc_map(formats, extra):
self.extra_formats.append((title_dir, os.path.join(db_path, e)))

View File

@ -943,11 +943,11 @@ def command_check_library(args, dbpath):
return
if opts.csv:
for i in list:
print check[1] + ',' + i[0] + ',' + i[1] + ',' + '|'.join(i[2])
print check[1] + ',' + i[0] + ',' + i[1]
else:
print check[1]
for i in list:
print ' %-30.30s - %-30.30s - %s'%(i[0], i[1], ', '.join(i[2]))
print ' %-40.40s - %-40.40s'%(i[0], i[1])
db = LibraryDatabase2(dbpath)
checker = CheckLibrary(dbpath, db)