diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index 7d4d4fb04d..bcf02f9704 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -27,7 +27,8 @@ from calibre.utils.icu import sort_key from calibre.utils.config import to_json, from_json, prefs, tweaks from calibre.utils.date import utcfromtimestamp, parse_date from calibre.utils.filenames import ( - is_case_sensitive, samefile, hardlink_file, ascii_filename, WindowsAtomicFolderMove, atomic_rename) + is_case_sensitive, samefile, hardlink_file, ascii_filename, + WindowsAtomicFolderMove, atomic_rename, remove_dir_if_empty) from calibre.utils.magick.draw import save_cover_data_to from calibre.utils.formatter_functions import load_user_template_functions from calibre.db.tables import (OneToOneTable, ManyToOneTable, ManyToManyTable, @@ -1184,8 +1185,7 @@ class DB(object): Read all data from the db into the python in-memory tables ''' - with self.conn: # Use a single transaction, to ensure nothing modifies - # the db while we are reading + with self.conn: # Use a single transaction, to ensure nothing modifies the db while we are reading for table in self.tables.itervalues(): try: table.read(self) @@ -1538,11 +1538,7 @@ class DB(object): if permanent: for path in paths: self.rmtree(path) - try: - os.rmdir(os.path.dirname(path)) - except OSError as e: - if e.errno != errno.ENOTEMPTY: - raise + remove_dir_if_empty(os.path.dirname(path), ignore_metadata_caches=True) else: delete_service().delete_books(paths, self.library_path) @@ -1666,6 +1662,4 @@ class DB(object): self.execute('UPDATE books SET path=? WHERE id=?', (path.replace(os.sep, '/'), book_id)) vals = [(book_id, fmt, size, name) for fmt, size, name in formats] self.executemany('INSERT INTO data (book,format,uncompressed_size,name) VALUES (?,?,?,?)', vals) - # }}} - - + # }}} diff --git a/src/calibre/db/delete_service.py b/src/calibre/db/delete_service.py index 6d3e41ebbb..beda2ae954 100644 --- a/src/calibre/db/delete_service.py +++ b/src/calibre/db/delete_service.py @@ -11,6 +11,7 @@ from threading import Thread from Queue import Queue from calibre.ptempfile import remove_dir +from calibre.utils.filenames import remove_dir_if_empty from calibre.utils.recycle_bin import delete_tree, delete_file class DeleteService(Thread): @@ -93,10 +94,10 @@ class DeleteService(Thread): time.sleep(1) shutil.move(path, dest) if delete_empty_parent: - self.remove_dir_if_empty(os.path.dirname(path)) + remove_dir_if_empty(os.path.dirname(path), ignore_metadata_caches=True) requests.append(dest) if not requests: - self.remove_dir_if_empty(tdir) + remove_dir_if_empty(tdir) else: self.requests.put(tdir) diff --git a/src/calibre/db/tests/add_remove.py b/src/calibre/db/tests/add_remove.py index 0c5443cfed..6fc81bba47 100644 --- a/src/calibre/db/tests/add_remove.py +++ b/src/calibre/db/tests/add_remove.py @@ -231,6 +231,8 @@ class AddRemoveTest(BaseTest): fmtpath = cache.format_abspath(1, 'FMT1') bookpath = os.path.dirname(fmtpath) authorpath = os.path.dirname(bookpath) + os.mkdir(os.path.join(authorpath, '.DS_Store')) + open(os.path.join(authorpath, 'Thumbs.db'), 'wb').close() item_id = {v:k for k, v in cache.fields['#series'].table.id_map.iteritems()}['My Series Two'] cache.remove_books((1,), permanent=True) for x in (fmtpath, bookpath, authorpath): diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py index 4bf1cffee3..e9a589fe8a 100644 --- a/src/calibre/utils/filenames.py +++ b/src/calibre/utils/filenames.py @@ -459,3 +459,34 @@ def atomic_rename(oldpath, newpath): time.sleep(1) else: os.rename(oldpath, newpath) + +def remove_dir_if_empty(path, ignore_metadata_caches=False): + ''' Remove a directory if it is empty or contains only the folder metadata + caches from different OSes. To delete the folder if it contains only + metadata caches, set ignore_metadata_caches to True.''' + try: + os.rmdir(path) + except OSError as e: + if e.errno == errno.ENOTEMPTY or len(os.listdir(path)) > 0: + # Some linux systems appear to raise an EPERM instead of an + # ENOTEMPTY, see https://bugs.launchpad.net/bugs/1240797 + if ignore_metadata_caches: + try: + found = False + for x in os.listdir(path): + if x.lower() in {'.ds_store', 'thumbs.db'}: + found = True + x = os.path.join(path, x) + if os.path.isdir(x): + import shutil + shutil.rmtree(x) + else: + os.remove(x) + except Exception: # We could get an error, if, for example, windows has locked Thumbs.db + found = False + if found: + remove_dir_if_empty(path) + return + raise + +