Start work on deleting files

This commit is contained in:
Kovid Goyal 2013-10-10 21:13:52 +05:30
parent 8938d40298
commit 8b607437c4
2 changed files with 117 additions and 13 deletions

View File

@ -12,6 +12,7 @@ from collections import defaultdict
from io import BytesIO
from urllib import unquote as urlunquote, quote as urlquote
from urlparse import urlparse
from future_builtins import zip
from lxml import etree
@ -228,6 +229,14 @@ class Container(object): # {{{
data, self.used_encoding = xml_to_unicode(data)
return fix_data(data)
@property
def names_that_need_not_be_manifested(self):
return {self.opf_name}
@property
def names_that_must_not_be_removed(self):
return {self.opf_name}
def parse_xml(self, data):
data, self.used_encoding = xml_to_unicode(
data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True)
@ -309,9 +318,8 @@ class Container(object): # {{{
for item in self.opf_xpath('//opf:guide/opf:reference[@href and @type]')}
@property
def spine_names(self):
def spine_iter(self):
manifest_id_map = self.manifest_id_map
non_linear = []
for item in self.opf_xpath('//opf:spine/opf:itemref[@idref]'):
idref = item.get('idref')
@ -319,17 +327,34 @@ class Container(object): # {{{
path = self.name_path_map.get(name, None)
if path:
if item.get('linear', 'yes') == 'yes':
yield name, True
yield item, name, True
else:
non_linear.append(name)
for name in non_linear:
yield name, False
non_linear.append((item, name))
for item, name in non_linear:
yield item, name, False
@property
def spine_names(self):
for item, name, linear in self.spine_iter:
yield name, linear
@property
def spine_items(self):
for name, linear in self.spine_names:
yield self.name_path_map[name]
def remove_from_spine(self, spine_items, remove_if_no_longer_in_spine=True):
nixed = set()
for (name, remove), (item, xname, linear) in zip(spine_items, self.spine_iter):
if remove and name == xname:
self.remove_from_xml(item)
nixed.add(name)
if remove_if_no_longer_in_spine:
# Remove from the book if no longer in spine
nixed -= {name for name, linear in self.spine_names}
for name in nixed:
self.remove_item(name)
def remove_item(self, name):
'''
Remove the item identified by name from this container. This removes all
@ -623,6 +648,34 @@ class EpubContainer(Container):
ans['container'] = copy.deepcopy(self.container)
return ans
@property
def names_that_need_not_be_manifested(self):
return super(EpubContainer, self).names_that_need_not_be_manifested | {'META-INF/' + x for x in self.META_INF}
@property
def names_that_must_not_be_removed(self):
return super(EpubContainer, self).names_that_must_not_be_removed | {'META-INF/container.xml'}
def remove_item(self, name):
# Handle removal of obfuscated fonts
if name == 'META-INF/encryption.xml':
self.obfuscated_fonts.clear()
if name in self.obfuscated_fonts:
self.obfuscated_fonts.pop(name, None)
enc = self.parsed('META-INF/encryption.xml')
for em in enc.xpath('//*[local-name()="EncryptionMethod" and @Algorithm]'):
alg = em.get('Algorithm')
if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
continue
try:
cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
except (IndexError, ValueError, KeyError):
continue
if name == self.href_to_name(cr.get('URI')):
self.remove_from_xml(em.getparent())
self.dirty('META-INF/encryption.xml')
super(EpubContainer, self).remove_item(name)
def process_encryption(self):
fonts = {}
enc = self.parsed('META-INF/encryption.xml')
@ -630,7 +683,10 @@ class EpubContainer(Container):
alg = em.get('Algorithm')
if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
raise DRMError()
try:
cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
except (IndexError, ValueError, KeyError):
continue
name = self.href_to_name(cr.get('URI'))
path = self.name_path_map.get(name, None)
if path is not None:

View File

@ -8,15 +8,18 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.Qt import (
QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon,
QStyledItemDelegate, QStyle, QPixmap, QPainter)
QStyledItemDelegate, QStyle, QPixmap, QPainter, pyqtSignal)
from calibre import guess_type, human_readable
from calibre import human_readable
from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS
from calibre.ebooks.oeb.polish.container import guess_type
from calibre.ebooks.oeb.polish.cover import get_cover_page_name, get_raster_cover_name
from calibre.gui2 import error_dialog
from calibre.gui2.tweak_book import current_container
TOP_ICON_SIZE = 24
NAME_ROLE = Qt.UserRole
CATEGORY_ROLE = NAME_ROLE + 1
NBSP = '\xa0'
class ItemDelegate(QStyledItemDelegate): # {{{
@ -50,6 +53,8 @@ class ItemDelegate(QStyledItemDelegate): # {{{
class FileList(QTreeWidget):
delete_requested = pyqtSignal(object, object)
def __init__(self, parent=None):
QTreeWidget.__init__(self, parent)
self.delegate = ItemDelegate(self)
@ -116,7 +121,7 @@ class FileList(QTreeWidget):
for names in container.manifest_type_map.itervalues():
manifested_names |= set(names)
font_types = {guess_type('a.'+x)[0] for x in ('ttf', 'otf', 'woff')}
font_types = {guess_type('a.'+x) for x in ('ttf', 'otf', 'woff')}
def get_category(mt):
category = 'misc'
@ -171,8 +176,10 @@ class FileList(QTreeWidget):
icon = self.rendered_emblem_cache[emblems] = canvas
item.setData(0, Qt.DecorationRole, icon)
ok_to_be_unmanifested = container.names_that_need_not_be_manifested
def create_item(name, linear=None):
imt = container.mime_map.get(name, guess_type(name)[0])
imt = container.mime_map.get(name, guess_type(name))
icat = get_category(imt)
category = 'text' if linear is not None else ({'text':'misc'}.get(icat, icat))
item = QTreeWidgetItem(self.categories['text' if linear is not None else category], 1)
@ -182,12 +189,13 @@ class FileList(QTreeWidget):
item.setFlags(flags)
item.setStatusTip(0, _('Full path: ') + name)
item.setData(0, NAME_ROLE, name)
item.setData(0, CATEGORY_ROLE, category)
set_display_name(name, item)
# TODO: Add appropriate tooltips based on the emblems
emblems = []
if name in {cover_page_name, cover_image_name}:
emblems.append('default_cover.png')
if name not in manifested_names and name not in {container.opf_name, 'META-INF/container.xml', 'META-INF/encryption.xml'}:
if name not in manifested_names and name not in ok_to_be_unmanifested:
emblems.append('dialog_question.png')
if linear is False:
emblems.append('arrow-down.png')
@ -205,7 +213,7 @@ class FileList(QTreeWidget):
processed[name] = create_item(name, linear=linear)
all_files = list(container.manifest_type_map.iteritems())
all_files.append((guess_type('a.opf')[0], [container.opf_name]))
all_files.append((guess_type('a.opf'), [container.opf_name]))
for name in container.name_path_map:
if name in processed:
@ -218,14 +226,54 @@ class FileList(QTreeWidget):
def show_context_menu(self, point):
pass
def keyPressEvent(self, ev):
if ev.key() in (Qt.Key_Delete, Qt.Key_Backspace):
ev.accept()
self.request_delete()
else:
return QTreeWidget.keyPressEvent(self, ev)
def request_delete(self):
names = {unicode(item.data(0, NAME_ROLE).toString()) for item in self.selectedItems()}
bad = names & current_container().names_that_must_not_be_removed
if bad:
return error_dialog(self, _('Cannot delete'),
_('The file(s) %s cannot be deleted.') % ('<b>%s</b>' % ', '.join(bad)), show=True)
text = self.categories['text']
children = (text.child(i) for i in xrange(text.childCount()))
spine_removals = [(unicode(item.data(0, NAME_ROLE).toString()), item.isSelected()) for item in children]
other_removals = {unicode(item.data(0, NAME_ROLE).toString()) for item in self.selectedItems()
if unicode(item.data(0, CATEGORY_ROLE).toString()) != 'text'}
self.delete_requested.emit(spine_removals, other_removals)
def delete_done(self, spine_removals, other_removals):
removals = []
for i, (name, remove) in enumerate(spine_removals):
if remove:
removals.append(self.categories['text'].child(i))
for category, parent in self.categories.iteritems():
if category != 'text':
for i in xrange(parent.childCount()):
child = parent.child(i)
if unicode(child.data(0, NAME_ROLE).toString()) in other_removals:
removals.append(child)
for c in removals:
c.parent().removeChild(c.parent().indexOfChild(c))
class FileListWidget(QWidget):
delete_requested = pyqtSignal(object, object)
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setLayout(QGridLayout(self))
self.file_list = FileList(self)
self.layout().addWidget(self.file_list)
self.layout().setContentsMargins(0, 0, 0, 0)
for x in ('delete_requested',):
getattr(self.file_list, x).connect(getattr(self, x))
def build(self, container):
self.file_list.build(container)