diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index ac15e31c67..43cf1feb6e 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -1933,6 +1933,22 @@ class DB: with src: yield relpath, src, stat_result + def rename_extra_file(self, relpath, newrelpath, book_path, replace=True): + bookdir = os.path.join(self.library_path, book_path) + src = os.path.abspath(os.path.join(bookdir, relpath)) + dest = os.path.abspath(os.path.join(bookdir, newrelpath)) + src, dest = make_long_path_useable(src), make_long_path_useable(dest) + if src == dest or not os.path.exists(src): + return False + if not replace and os.path.exists(dest) and not os.path.samefile(src, dest): + return False + try: + os.replace(src, dest) + except FileNotFoundError: + os.makedirs(os.path.dirname(dest), exist_ok=True) + os.replace(src, dest) + return True + def add_extra_file(self, relpath, stream, book_path, replace=True, auto_rename=False): bookdir = os.path.join(self.library_path, book_path) dest = os.path.abspath(os.path.join(bookdir, relpath)) diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 2ccf6181d4..878a5d8dca 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -3093,6 +3093,17 @@ class Cache: self._clear_extra_files_cache(book_id) return added + @write_api + def rename_extra_files(self, book_id, map_of_relpath_to_new_relpath, replace=False): + ' Rename extra data files ' + path = self._field_for('path', book_id).replace('/', os.sep) + renamed = set() + for relpath, newrelpath in map_of_relpath_to_new_relpath.items(): + if self.backend.rename_extra_file(relpath, newrelpath, path, replace): + renamed.add(relpath) + self._clear_extra_files_cache(book_id) + return renamed + @write_api def merge_extra_files(self, dest_id, src_ids, replace=False): ' Merge the extra files from src_ids into dest_id. Conflicting files are auto-renamed unless replace=True in which case they are replaced. ' diff --git a/src/calibre/db/tests/filesystem.py b/src/calibre/db/tests/filesystem.py index e7e851da50..9e2bce4a6a 100644 --- a/src/calibre/db/tests/filesystem.py +++ b/src/calibre/db/tests/filesystem.py @@ -137,6 +137,23 @@ class FilesystemTest(BaseTest): if x.is_dir(): self.assertTrue(os.listdir(x.path)) + def test_rename_of_extra_files(self): + cl = self.cloned_library + cache = self.init_cache(cl) + cache.add_extra_files(1, {'a': BytesIO(b'aaa'), 'b': BytesIO(b'bbb')}) + + def relpaths(): + return {e.relpath for e in cache.list_extra_files(1)} + + self.assertEqual(relpaths(), {'a', 'b'}) + self.assertEqual(cache.rename_extra_files(1, {'a': 'data/c'}), {'a'}) + self.assertEqual(relpaths(), {'data/c', 'b'}) + self.assertEqual(cache.rename_extra_files(1, {'b': 'B'}), {'b'}) + self.assertEqual(relpaths(), {'data/c', 'B'}) + self.assertEqual(cache.rename_extra_files(1, {'B': 'data/c'}), set()) + self.assertEqual(cache.rename_extra_files(1, {'B': 'data/c'}, replace=True), {'B'}) + + @unittest.skipUnless(iswindows, 'Windows only') def test_windows_atomic_move(self): 'Test book file open in another process when changing metadata'