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 io import BytesIO
from urllib import unquote as urlunquote, quote as urlquote from urllib import unquote as urlunquote, quote as urlquote
from urlparse import urlparse from urlparse import urlparse
from future_builtins import zip
from lxml import etree from lxml import etree
@ -228,6 +229,14 @@ class Container(object): # {{{
data, self.used_encoding = xml_to_unicode(data) data, self.used_encoding = xml_to_unicode(data)
return fix_data(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): def parse_xml(self, data):
data, self.used_encoding = xml_to_unicode( data, self.used_encoding = xml_to_unicode(
data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True) 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]')} for item in self.opf_xpath('//opf:guide/opf:reference[@href and @type]')}
@property @property
def spine_names(self): def spine_iter(self):
manifest_id_map = self.manifest_id_map manifest_id_map = self.manifest_id_map
non_linear = [] non_linear = []
for item in self.opf_xpath('//opf:spine/opf:itemref[@idref]'): for item in self.opf_xpath('//opf:spine/opf:itemref[@idref]'):
idref = item.get('idref') idref = item.get('idref')
@ -319,17 +327,34 @@ class Container(object): # {{{
path = self.name_path_map.get(name, None) path = self.name_path_map.get(name, None)
if path: if path:
if item.get('linear', 'yes') == 'yes': if item.get('linear', 'yes') == 'yes':
yield name, True yield item, name, True
else: else:
non_linear.append(name) non_linear.append((item, name))
for name in non_linear: for item, name in non_linear:
yield name, False yield item, name, False
@property
def spine_names(self):
for item, name, linear in self.spine_iter:
yield name, linear
@property @property
def spine_items(self): def spine_items(self):
for name, linear in self.spine_names: for name, linear in self.spine_names:
yield self.name_path_map[name] 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): def remove_item(self, name):
''' '''
Remove the item identified by name from this container. This removes all 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) ans['container'] = copy.deepcopy(self.container)
return ans 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): def process_encryption(self):
fonts = {} fonts = {}
enc = self.parsed('META-INF/encryption.xml') enc = self.parsed('META-INF/encryption.xml')
@ -630,7 +683,10 @@ class EpubContainer(Container):
alg = em.get('Algorithm') alg = em.get('Algorithm')
if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}: if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
raise DRMError() raise DRMError()
cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0] 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')) name = self.href_to_name(cr.get('URI'))
path = self.name_path_map.get(name, None) path = self.name_path_map.get(name, None)
if path is not None: if path is not None:

View File

@ -8,15 +8,18 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.Qt import ( from PyQt4.Qt import (
QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon, 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.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.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 from calibre.gui2.tweak_book import current_container
TOP_ICON_SIZE = 24 TOP_ICON_SIZE = 24
NAME_ROLE = Qt.UserRole NAME_ROLE = Qt.UserRole
CATEGORY_ROLE = NAME_ROLE + 1
NBSP = '\xa0' NBSP = '\xa0'
class ItemDelegate(QStyledItemDelegate): # {{{ class ItemDelegate(QStyledItemDelegate): # {{{
@ -50,6 +53,8 @@ class ItemDelegate(QStyledItemDelegate): # {{{
class FileList(QTreeWidget): class FileList(QTreeWidget):
delete_requested = pyqtSignal(object, object)
def __init__(self, parent=None): def __init__(self, parent=None):
QTreeWidget.__init__(self, parent) QTreeWidget.__init__(self, parent)
self.delegate = ItemDelegate(self) self.delegate = ItemDelegate(self)
@ -116,7 +121,7 @@ class FileList(QTreeWidget):
for names in container.manifest_type_map.itervalues(): for names in container.manifest_type_map.itervalues():
manifested_names |= set(names) 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): def get_category(mt):
category = 'misc' category = 'misc'
@ -171,8 +176,10 @@ class FileList(QTreeWidget):
icon = self.rendered_emblem_cache[emblems] = canvas icon = self.rendered_emblem_cache[emblems] = canvas
item.setData(0, Qt.DecorationRole, icon) item.setData(0, Qt.DecorationRole, icon)
ok_to_be_unmanifested = container.names_that_need_not_be_manifested
def create_item(name, linear=None): 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) icat = get_category(imt)
category = 'text' if linear is not None else ({'text':'misc'}.get(icat, icat)) 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) item = QTreeWidgetItem(self.categories['text' if linear is not None else category], 1)
@ -182,12 +189,13 @@ class FileList(QTreeWidget):
item.setFlags(flags) item.setFlags(flags)
item.setStatusTip(0, _('Full path: ') + name) item.setStatusTip(0, _('Full path: ') + name)
item.setData(0, NAME_ROLE, name) item.setData(0, NAME_ROLE, name)
item.setData(0, CATEGORY_ROLE, category)
set_display_name(name, item) set_display_name(name, item)
# TODO: Add appropriate tooltips based on the emblems # TODO: Add appropriate tooltips based on the emblems
emblems = [] emblems = []
if name in {cover_page_name, cover_image_name}: if name in {cover_page_name, cover_image_name}:
emblems.append('default_cover.png') 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') emblems.append('dialog_question.png')
if linear is False: if linear is False:
emblems.append('arrow-down.png') emblems.append('arrow-down.png')
@ -205,7 +213,7 @@ class FileList(QTreeWidget):
processed[name] = create_item(name, linear=linear) processed[name] = create_item(name, linear=linear)
all_files = list(container.manifest_type_map.iteritems()) 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: for name in container.name_path_map:
if name in processed: if name in processed:
@ -218,14 +226,54 @@ class FileList(QTreeWidget):
def show_context_menu(self, point): def show_context_menu(self, point):
pass 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): class FileListWidget(QWidget):
delete_requested = pyqtSignal(object, object)
def __init__(self, parent=None): def __init__(self, parent=None):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.setLayout(QGridLayout(self)) self.setLayout(QGridLayout(self))
self.file_list = FileList(self) self.file_list = FileList(self)
self.layout().addWidget(self.file_list) self.layout().addWidget(self.file_list)
self.layout().setContentsMargins(0, 0, 0, 0) 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): def build(self, container):
self.file_list.build(container) self.file_list.build(container)