From e444b169ea643ad94e3b981bb816c5d951622c53 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 5 Feb 2009 22:13:14 -0800 Subject: [PATCH] IGN:Make database a little more robust when deleting files and when detecting case sensitivity --- src/calibre/ebooks/metadata/opf2.py | 357 +++++++++++++++------------- src/calibre/library/database2.py | 34 +-- 2 files changed, 215 insertions(+), 176 deletions(-) diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index 1ebd2d5aaa..3f3e3581a6 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -17,21 +17,21 @@ from calibre.ebooks.chardet import xml_to_unicode from calibre import relpath from calibre.constants import __appname__, __version__ from calibre.ebooks.metadata.toc import TOC -from calibre.ebooks.metadata import MetaInformation, get_parser +from calibre.ebooks.metadata import MetaInformation class Resource(object): ''' - Represents a resource (usually a file on the filesystem or a URL pointing + Represents a resource (usually a file on the filesystem or a URL pointing to the web. Such resources are commonly referred to in OPF files. - + They have the interface: - + :member:`path` :member:`mime_type` :method:`href` ''' - + def __init__(self, href_or_path, basedir=os.getcwd(), is_path=True): self.orig = href_or_path self._href = None @@ -63,13 +63,13 @@ class Resource(object): pc = pc.decode('utf-8') self.path = os.path.abspath(os.path.join(basedir, pc.replace('/', os.sep))) self.fragment = url[-1] - - + + def href(self, basedir=None): ''' Return a URL pointing to this resource. If it is a file on the filesystem the URL is relative to `basedir`. - + `basedir`: If None, the basedir of this resource is used (see :method:`set_basedir`). If this resource has no basedir, then the current working directory is used as the basedir. ''' @@ -91,54 +91,54 @@ class Resource(object): if isinstance(rpath, unicode): rpath = rpath.encode('utf-8') return rpath.replace(os.sep, '/')+frag - + def set_basedir(self, path): self._basedir = path - + def basedir(self): return self._basedir - + def __repr__(self): return 'Resource(%s, %s)'%(repr(self.path), repr(self.href())) - - + + class ResourceCollection(object): - + def __init__(self): self._resources = [] - + def __iter__(self): for r in self._resources: yield r - + def __len__(self): return len(self._resources) - + def __getitem__(self, index): return self._resources[index] - + def __bool__(self): return len(self._resources) > 0 - + def __str__(self): resources = map(repr, self) return '[%s]'%', '.join(resources) - + def __repr__(self): return str(self) - + def append(self, resource): if not isinstance(resource, Resource): raise ValueError('Can only append objects of type Resource') self._resources.append(resource) - + def remove(self, resource): self._resources.remove(resource) - + def replace(self, start, end, items): 'Same as list[start:end] = items' self._resources[start:end] = items - + @staticmethod def from_directory_contents(top, topdown=True): collection = ResourceCollection() @@ -148,16 +148,16 @@ class ResourceCollection(object): res.set_basedir(top) collection.append(res) return collection - + def set_basedir(self, path): for res in self: res.set_basedir(path) - + class ManifestItem(Resource): - + @staticmethod def from_opf_manifest_item(item, basedir): href = item.get('href', None) @@ -167,7 +167,7 @@ class ManifestItem(Resource): if mt: res.mime_type = mt return res - + @apply def media_type(): def fget(self): @@ -175,18 +175,18 @@ class ManifestItem(Resource): def fset(self, val): self.mime_type = val return property(fget=fget, fset=fset) - - + + def __unicode__(self): return u''%(self.id, self.href(), self.media_type) - + def __str__(self): return unicode(self).encode('utf-8') - + def __repr__(self): return unicode(self) - - + + def __getitem__(self, index): if index == 0: return self.href() @@ -196,7 +196,7 @@ class ManifestItem(Resource): class Manifest(ResourceCollection): - + @staticmethod def from_opf_manifest_element(items, dir): m = Manifest() @@ -211,7 +211,7 @@ class Manifest(ResourceCollection): except ValueError: continue return m - + @staticmethod def from_paths(entries): ''' @@ -226,7 +226,7 @@ class Manifest(ResourceCollection): m.next_id += 1 m.append(mi) return m - + def add_item(self, path, mime_type=None): mi = ManifestItem(path, is_path=True) if mime_type: @@ -235,37 +235,37 @@ class Manifest(ResourceCollection): self.next_id += 1 self.append(mi) return mi.id - + def __init__(self): ResourceCollection.__init__(self) self.next_id = 1 - - + + def item(self, id): for i in self: if i.id == id: return i - + def id_for_path(self, path): path = os.path.normpath(os.path.abspath(path)) for i in self: if i.path and os.path.normpath(i.path) == path: - return i.id - + return i.id + def path_for_id(self, id): for i in self: if i.id == id: return i.path class Spine(ResourceCollection): - + class Item(Resource): - + def __init__(self, idfunc, *args, **kwargs): Resource.__init__(self, *args, **kwargs) self.is_linear = True self.id = idfunc(self.path) - + @staticmethod def from_opf_spine_element(itemrefs, manifest): s = Spine(manifest) @@ -278,7 +278,7 @@ class Spine(ResourceCollection): r.is_linear = itemref.get('linear', 'yes') == 'yes' s.append(r) return s - + @staticmethod def from_paths(paths, manifest): s = Spine(manifest) @@ -288,14 +288,14 @@ class Spine(ResourceCollection): except: continue return s - - - + + + def __init__(self, manifest): ResourceCollection.__init__(self) self.manifest = manifest - - + + def replace(self, start, end, ids): ''' Replace the items between start (inclusive) and end (not inclusive) with @@ -308,7 +308,7 @@ class Spine(ResourceCollection): raise ValueError('id %s not in manifest') items.append(Spine.Item(lambda x: id, path, is_path=True)) ResourceCollection.replace(start, end, items) - + def linear_items(self): for r in self: if r.is_linear: @@ -318,15 +318,15 @@ class Spine(ResourceCollection): for r in self: if not r.is_linear: yield r.path - + def items(self): for i in self: yield i.path - + class Guide(ResourceCollection): - + class Reference(Resource): - + @staticmethod def from_opf_resource_item(ref, basedir): title, href, type = ref.get('title', ''), ref.get('href'), ref.get('type') @@ -334,14 +334,14 @@ class Guide(ResourceCollection): res.title = title res.type = type return res - + def __repr__(self): ans = '' - - + + @staticmethod def from_opf_guide(references, base_dir=os.getcwdu()): coll = Guide() @@ -352,7 +352,7 @@ class Guide(ResourceCollection): except: continue return coll - + def set_cover(self, path): map(self.remove, [i for i in self if 'cover' in i.type.lower()]) for type in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'): @@ -362,13 +362,13 @@ class Guide(ResourceCollection): class MetadataField(object): - + def __init__(self, name, is_dc=True, formatter=None, none_is=None): self.name = name self.is_dc = is_dc self.formatter = formatter self.none_is = none_is - + def __real_get__(self, obj, type=None): ans = obj.get_metadata_element(self.name) if ans is None: @@ -382,13 +382,13 @@ class MetadataField(object): except: return None return ans - + def __get__(self, obj, type=None): ans = self.__real_get__(obj, type) if ans is None: ans = self.none_is return ans - + def __set__(self, obj, val): elem = obj.get_metadata_element(self.name) if elem is None: @@ -410,10 +410,11 @@ class OPF(object): XPath = functools.partial(etree.XPath, namespaces=xpn) CONTENT = XPath('self::*[re:match(name(), "meta$", "i")]/@content') TEXT = XPath('string()') - - + + metadata_path = XPath('descendant::*[re:match(name(), "metadata", "i")]') metadata_elem_path = XPath('descendant::*[re:match(name(), concat($name, "$"), "i") or (re:match(name(), "meta$", "i") and re:match(@name, concat("^calibre:", $name, "$"), "i"))]') + title_path = XPath('descendant::*[re:match(name(), "title", "i")]') authors_path = XPath('descendant::*[re:match(name(), "creator", "i") and (@role="aut" or @opf:role="aut" or (not(@role) and not(@opf:role)))]') bkp_path = XPath('descendant::*[re:match(name(), "contributor", "i") and (@role="bkp" or @opf:role="bkp")]') tags_path = XPath('descendant::*[re:match(name(), "subject", "i")]') @@ -423,10 +424,10 @@ class OPF(object): application_id_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+ '(re:match(@opf:scheme, "calibre|libprs500", "i") or re:match(@scheme, "calibre|libprs500", "i"))]') manifest_path = XPath('descendant::*[re:match(name(), "manifest", "i")]/*[re:match(name(), "item", "i")]') - manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]') + manifest_ppath = XPath('descendant::*[re:match(name(), "manifest", "i")]') spine_path = XPath('descendant::*[re:match(name(), "spine", "i")]/*[re:match(name(), "itemref", "i")]') guide_path = XPath('descendant::*[re:match(name(), "guide", "i")]/*[re:match(name(), "reference", "i")]') - + title = MetadataField('title') publisher = MetadataField('publisher') language = MetadataField('language') @@ -435,8 +436,8 @@ class OPF(object): series = MetadataField('series', is_dc=False) series_index = MetadataField('series_index', is_dc=False, formatter=int, none_is=1) rating = MetadataField('rating', is_dc=False, formatter=int) - - + + def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True): if not hasattr(stream, 'read'): stream = open(stream, 'rb') @@ -463,7 +464,7 @@ class OPF(object): self.guide = Guide.from_opf_guide(guide, basedir) if guide else None self.cover_data = (None, None) self.find_toc() - + def find_toc(self): self.toc = None try: @@ -480,7 +481,7 @@ class OPF(object): for item in self.manifest: if 'toc' in item.href().lower(): toc = item.path - + if toc is None: return self.toc = TOC(base_path=self.base_dir) if toc.lower() in ('ncx', 'ncxtoc'): @@ -494,10 +495,10 @@ class OPF(object): else: self.toc.read_html_toc(toc) except: - pass - - - + pass + + + def get_text(self, elem): return u''.join(self.CONTENT(elem) or self.TEXT(elem)) @@ -506,10 +507,10 @@ class OPF(object): elem.attrib['content'] = content else: elem.text = content - + def itermanifest(self): return self.manifest_path(self.root) - + def create_manifest_item(self, href, media_type): ids = [i.get('id', None) for i in self.itermanifest()] id = None @@ -519,11 +520,11 @@ class OPF(object): break if not media_type: media_type = 'application/xhtml+xml' - ans = etree.Element('{%s}item'%self.NAMESPACES['opf'], + ans = etree.Element('{%s}item'%self.NAMESPACES['opf'], attrib={'id':id, 'href':href, 'media-type':media_type}) ans.tail = '\n\t\t' return ans - + def replace_manifest_item(self, item, items): items = [self.create_manifest_item(*i) for i in items] for i, item2 in enumerate(items): @@ -532,7 +533,7 @@ class OPF(object): index = manifest.index(item) manifest[index:index+1] = items return [i.get('id') for i in items] - + def add_path_to_manifest(self, path, media_type): has_path = False path = os.path.abspath(path) @@ -546,22 +547,22 @@ class OPF(object): item = self.create_manifest_item(href, media_type) manifest = self.manifest_ppath(self.root)[0] manifest.append(item) - + def iterspine(self): return self.spine_path(self.root) - + def spine_items(self): for item in self.iterspine(): idref = item.get('idref', '') for x in self.itermanifest(): if x.get('id', None) == idref: yield x.get('href', '') - + def create_spine_item(self, idref): ans = etree.Element('{%s}itemref'%self.NAMESPACES['opf'], idref=idref) ans.tail = '\n\t\t' return ans - + def replace_spine_items_by_idref(self, idref, new_idrefs): items = list(map(self.create_spine_item, new_idrefs)) spine = self.XPath('/opf:package/*[re:match(name(), "spine", "i")]')(self.root)[0] @@ -569,31 +570,31 @@ class OPF(object): for x in old: i = spine.index(x) spine[i:i+1] = items - + def create_guide_element(self): e = etree.SubElement(self.root, '{%s}guide'%self.NAMESPACES['opf']) e.text = '\n ' e.tail = '\n' return e - + def remove_guide(self): self.guide = None for g in self.root.xpath('./*[re:match(name(), "guide", "i")]', namespaces={'re':'http://exslt.org/regular-expressions'}): self.root.remove(g) - + def create_guide_item(self, type, title, href): - e = etree.Element('{%s}reference'%self.NAMESPACES['opf'], + e = etree.Element('{%s}reference'%self.NAMESPACES['opf'], type=type, title=title, href=href) e.tail='\n' return e - + def add_guide_item(self, type, title, href): g = self.root.xpath('./*[re:match(name(), "guide", "i")]', namespaces={'re':'http://exslt.org/regular-expressions'})[0] g.append(self.create_guide_item(type, title, href)) - + def iterguide(self): return self.guide_path(self.root) - + def unquote_urls(self): def get_href(item): raw = unquote(item.get('href', '')) @@ -604,16 +605,16 @@ class OPF(object): item.set('href', get_href(item)) for item in self.iterguide(): item.set('href', get_href(item)) - + @apply def authors(): - + def fget(self): ans = [] for elem in self.authors_path(self.metadata): ans.extend([x.strip() for x in self.get_text(elem).split(',')]) return ans - + def fset(self, val): remove = list(self.authors_path(self.metadata)) for elem in remove: @@ -622,12 +623,12 @@ class OPF(object): attrib = {'{%s}role'%self.NAMESPACES['opf']: 'aut'} elem = self.create_metadata_element('creator', attrib=attrib) self.set_text(elem, author) - + return property(fget=fget, fset=fset) - + @apply def author_sort(): - + def fget(self): matches = self.authors_path(self.metadata) if matches: @@ -637,39 +638,59 @@ class OPF(object): ans = match.get('file-as', None) if ans: return ans - + def fset(self, val): matches = self.authors_path(self.metadata) if matches: matches[0].set('file-as', unicode(val)) - + return property(fget=fget, fset=fset) - + + @apply + def title_sort(): + + def fget(self): + matches = self.title_path(self.metadata) + if matches: + for match in matches: + ans = match.get('{%s}file-as'%self.NAMESPACES['opf'], None) + if not ans: + ans = match.get('file-as', None) + if ans: + return ans + + def fset(self, val): + matches = self.title_path(self.metadata) + if matches: + matches[0].set('file-as', unicode(val)) + + return property(fget=fget, fset=fset) + @apply def tags(): - + def fget(self): ans = [] for tag in self.tags_path(self.metadata): ans.append(self.get_text(tag)) return ans - + def fset(self, val): for tag in list(self.tags_path(self.metadata)): self.metadata.remove(tag) for tag in val: elem = self.create_metadata_element('subject') self.set_text(elem, unicode(tag)) - + return property(fget=fget, fset=fset) - + @apply def isbn(): - + def fget(self): for match in self.isbn_path(self.metadata): return self.get_text(match) or None - + def fset(self, val): matches = self.isbn_path(self.metadata) if not matches: @@ -682,11 +703,11 @@ class OPF(object): @apply def application_id(): - + def fget(self): for match in self.application_id_path(self.metadata): return self.get_text(match) or None - + def fset(self, val): matches = self.application_id_path(self.metadata) if not matches: @@ -696,14 +717,14 @@ class OPF(object): self.set_text(matches[0], unicode(val)) return property(fget=fget, fset=fset) - + @apply def book_producer(): - + def fget(self): for match in self.bkp_path(self.metadata): return self.get_text(match) or None - + def fset(self, val): matches = self.bkp_path(self.metadata) if not matches: @@ -712,8 +733,8 @@ class OPF(object): attrib=attrib)] self.set_text(matches[0], unicode(val)) return property(fget=fget, fset=fset) - - + + def guess_cover(self): ''' Try to guess a cover. Needed for some old/badly formed OPF files. @@ -733,11 +754,11 @@ class OPF(object): cpath = os.access(os.path.join(self.base_dir, prefix+suffix), os.R_OK) if os.access(os.path.join(self.base_dir, prefix+suffix), os.R_OK): return cpath - - + + @apply def cover(): - + def fget(self): if self.guide is not None: for t in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'): @@ -748,19 +769,19 @@ class OPF(object): return self.guess_cover() except: pass - + def fset(self, path): if self.guide is not None: self.guide.set_cover(path) for item in list(self.iterguide()): if 'cover' in item.get('type', ''): item.getparent().remove(item) - + else: g = self.create_guide_element() self.guide = Guide() self.guide.set_cover(path) - etree.SubElement(g, 'opf:reference', nsmap=self.NAMESPACES, + etree.SubElement(g, 'opf:reference', nsmap=self.NAMESPACES, attrib={'type':'cover', 'href':self.guide[-1].href()}) id = self.manifest.id_for_path(self.cover) if id is None: @@ -768,14 +789,14 @@ class OPF(object): for item in self.guide: if item.type.lower() == t: self.create_manifest_item(item.href(), mimetypes.guess_type(path)[0]) - - return property(fget=fget, fset=fset) - + + return property(fget=fget, fset=fset) + def get_metadata_element(self, name): matches = self.metadata_elem_path(self.metadata, name=name) if matches: return matches[-1] - + def create_metadata_element(self, name, attrib=None, is_dc=True): if is_dc: name = '{%s}%s' % (self.NAMESPACES['dc'], name) @@ -787,24 +808,25 @@ class OPF(object): nsmap=self.NAMESPACES) elem.tail = '\n' return elem - + def render(self, encoding='utf-8'): raw = etree.tostring(self.root, encoding=encoding, pretty_print=True) if not raw.lstrip().startswith('\n'%encoding.upper()+raw return raw - + def smart_update(self, mi): - for attr in ('author_sort', 'title_sort', 'comments', 'category', + for attr in ('title', 'authors', 'author_sort', 'title_sort', 'publisher', 'series', 'series_index', 'rating', - 'isbn', 'language', 'tags', 'title', 'authors'): + 'isbn', 'language', 'tags', 'category', 'comments'): val = getattr(mi, attr, None) if val is not None and val != [] and val != (None, None): setattr(self, attr, val) - - + print self.render() + + class OPFCreator(MetaInformation): - + def __init__(self, base_path, *args, **kwargs): ''' Initialize. @@ -824,63 +846,63 @@ class OPFCreator(MetaInformation): self.guide = Guide() if self.cover: self.guide.set_cover(self.cover) - - + + def create_manifest(self, entries): ''' Create - + `entries`: List of (path, mime-type) If mime-type is None it is autodetected ''' - entries = map(lambda x: x if os.path.isabs(x[0]) else + entries = map(lambda x: x if os.path.isabs(x[0]) else (os.path.abspath(os.path.join(self.base_path, x[0])), x[1]), entries) self.manifest = Manifest.from_paths(entries) self.manifest.set_basedir(self.base_path) - + def create_manifest_from_files_in(self, files_and_dirs): entries = [] - + def dodir(dir): for spec in os.walk(dir): root, files = spec[0], spec[-1] for name in files: path = os.path.join(root, name) if os.path.isfile(path): - entries.append((path, None)) - + entries.append((path, None)) + for i in files_and_dirs: if os.path.isdir(i): dodir(i) else: entries.append((i, None)) - - self.create_manifest(entries) - + + self.create_manifest(entries) + def create_spine(self, entries): ''' Create the element. Must first call :method:`create_manifest`. - + `entries`: List of paths ''' - entries = map(lambda x: x if os.path.isabs(x) else + entries = map(lambda x: x if os.path.isabs(x) else os.path.abspath(os.path.join(self.base_path, x)), entries) self.spine = Spine.from_paths(entries, self.manifest) - + def set_toc(self, toc): ''' Set the toc. You must call :method:`create_spine` before calling this method. - + :param toc: A :class:`TOC` object ''' self.toc = toc - + def create_guide(self, guide_element): self.guide = Guide.from_opf_guide(guide_element, self.base_path) self.guide.set_basedir(self.base_path) - - def render(self, opf_stream=sys.stdout, ncx_stream=None, + + def render(self, opf_stream=sys.stdout, ncx_stream=None, ncx_manifest_entry=None): from calibre.resources import opf_template from calibre.utils.genshi.template import MarkupTemplate @@ -914,14 +936,14 @@ class OPFCreator(MetaInformation): class OPFTest(unittest.TestCase): - + def setUp(self): self.stream = cStringIO.StringIO( '''\ - A Cool & © ß Title + A Cool & © ß Title Monkey Kitchen, Next OneTwo 123456789 @@ -936,33 +958,48 @@ class OPFTest(unittest.TestCase): ''' ) self.opf = OPF(self.stream, os.getcwd()) - - def testReading(self): - opf = self.opf + + def testReading(self, opf=None): + if opf is None: + opf = self.opf self.assertEqual(opf.title, u'A Cool & \xa9 \xdf Title') self.assertEqual(opf.authors, u'Monkey Kitchen,Next'.split(',')) self.assertEqual(opf.author_sort, 'Monkey') + self.assertEqual(opf.title_sort, 'Wow') self.assertEqual(opf.tags, ['One', 'Two']) self.assertEqual(opf.isbn, '123456789') self.assertEqual(opf.series, 'A one book series') self.assertEqual(opf.series_index, None) self.assertEqual(list(opf.itermanifest())[0].get('href'), 'a ~ b') - + def testWriting(self): for test in [('title', 'New & Title'), ('authors', ['One', 'Two']), ('author_sort', "Kitchen"), ('tags', ['Three']), - ('isbn', 'a'), ('rating', 3), ('series_index', 1)]: + ('isbn', 'a'), ('rating', 3), ('series_index', 1), + ('title_sort', 'ts')]: setattr(self.opf, *test) self.assertEqual(getattr(self.opf, test[0]), test[1]) - + self.opf.render() + def testCreator(self): + opf = OPFCreator(os.getcwd(), self.opf) + buf = cStringIO.StringIO() + opf.render(buf) + raw = buf.getvalue() + self.testReading(opf=OPF(cStringIO.StringIO(raw), os.getcwd())) + + def testSmartUpdate(self): + self.opf.smart_update(self.opf) + self.testReading() + def suite(): return unittest.TestLoader().loadTestsFromTestCase(OPFTest) - + def test(): unittest.TextTestRunner(verbosity=2).run(suite()) + def option_parser(): parser = get_parser('opf') parser.add_option('--language', default=None, help=_('Set the dc:language field')) @@ -1010,7 +1047,7 @@ def main(args=sys.argv): print MetaInformation(OPF(open(opfpath, 'rb'), basedir)) return 0 - + if __name__ == '__main__': sys.exit(main()) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 5f29c34bcb..209f700820 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -15,7 +15,7 @@ from PyQt4.QtCore import QCoreApplication, QThread, QReadWriteLock from PyQt4.QtGui import QApplication, QPixmap, QImage __app = None -from calibre.library import title_sort +from calibre.ebooks.metadata import title_sort from calibre.library.database import LibraryDatabase from calibre.library.sqlite import connect, IntegrityError from calibre.utils.search_query_parser import SearchQueryParser @@ -27,15 +27,6 @@ from calibre.customize.ui import run_plugins_on_import from calibre import sanitize_file_name copyfile = os.link if hasattr(os, 'link') else shutil.copyfile -iscaseinsensitive = iswindows or isosx - -def normpath(x): - # The builtin os.path.normcase doesn't work on OS X - x = os.path.abspath(x) - if iscaseinsensitive: - x = x.lower() - return x - FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5, 'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10, @@ -355,7 +346,8 @@ class LibraryDatabase2(LibraryDatabase): if isinstance(self.dbpath, unicode): self.dbpath = self.dbpath.encode(filesystem_encoding) self.connect() - self.is_case_sensitive = not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) + self.is_case_sensitive = not iswindows and not isosx and \ + not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) # Upgrade database while True: meth = getattr(self, 'upgrade_version_%d'%self.user_version, None) @@ -489,6 +481,16 @@ class LibraryDatabase2(LibraryDatabase): name = title + ' - ' + author return name + def rmtree(self, path): + if not self.normpath(self.library_path).startswith(self.normpath(path)): + shutil.rmtree(path) + + def normpath(self, path): + path = os.path.abspath(os.path.realpath(path)) + if not self.is_case_sensitive: + path = path.lower() + return path + def set_path(self, index, index_is_id=False): ''' Set the path to the directory containing this books files based on its @@ -532,11 +534,11 @@ class LibraryDatabase2(LibraryDatabase): self.data.set(id, FIELD_MAP['path'], path, row_is_id=True) # Delete not needed directories if current_path and os.path.exists(spath): - if normpath(spath) != normpath(tpath): - shutil.rmtree(spath) + if self.normpath(spath) != self.normpath(tpath): + self.rmtree(spath) parent = os.path.dirname(spath) if len(os.listdir(parent)) == 0: - shutil.rmtree(parent) + self.rmtree(parent) def add_listener(self, listener): ''' @@ -699,10 +701,10 @@ class LibraryDatabase2(LibraryDatabase): path = os.path.join(self.library_path, self.path(id, index_is_id=True)) self.data.remove(id) if os.path.exists(path): - shutil.rmtree(path) + self.rmtree(path) parent = os.path.dirname(path) if len(os.listdir(parent)) == 0: - shutil.rmtree(parent) + self.rmtree(parent) self.conn.execute('DELETE FROM books WHERE id=?', (id,)) self.conn.commit() self.clean()