def test_windows_atomic_move(self): 'Test book file open in another process when changing metadata' cl = self.cloned_library cache = self.init_cache(cl) fpath = cache.format_abspath(1, 'FMT1') f = open(fpath, 'rb') with self.assertRaises(IOError): cache.set_field('title', {1:'Moved'}) with self.assertRaises(IOError): cache.remove_books({1}) f.close() self.assertNotEqual(cache.field_for('title', 1), 'Moved', 'Title was changed despite file lock') # Test on folder with hardlinks from calibre.ptempfile import TemporaryDirectory from calibre.utils.filenames import hardlink_file, WindowsAtomicFolderMove raw = b'xxx' with TemporaryDirectory() as tdir1, TemporaryDirectory() as tdir2: a, b = os.path.join(tdir1, 'a'), os.path.join(tdir1, 'b') a = os.path.join(tdir1, 'a') with open(a, 'wb') as f: f.write(raw) hardlink_file(a, b) wam = WindowsAtomicFolderMove(tdir1) wam.copy_path_to(a, os.path.join(tdir2, 'a')) wam.copy_path_to(b, os.path.join(tdir2, 'b')) wam.delete_originals() self.assertEqual([], os.listdir(tdir1)) self.assertEqual({'a', 'b'}, set(os.listdir(tdir2))) self.assertEqual(raw, open(os.path.join(tdir2, 'a'), 'rb').read()) self.assertEqual(raw, open(os.path.join(tdir2, 'b'), 'rb').read())
def test_windows_atomic_move(self): "Test book file open in another process when changing metadata" cl = self.cloned_library cache = self.init_cache(cl) fpath = cache.format_abspath(1, "FMT1") f = open(fpath, "rb") with self.assertRaises(IOError): cache.set_field("title", {1: "Moved"}) with self.assertRaises(IOError): cache.remove_books({1}) f.close() self.assertNotEqual(cache.field_for("title", 1), "Moved", "Title was changed despite file lock") # Test on folder with hardlinks from calibre.ptempfile import TemporaryDirectory from calibre.utils.filenames import hardlink_file, WindowsAtomicFolderMove raw = b"xxx" with TemporaryDirectory() as tdir1, TemporaryDirectory() as tdir2: a, b = os.path.join(tdir1, "a"), os.path.join(tdir1, "b") a = os.path.join(tdir1, "a") with open(a, "wb") as f: f.write(raw) hardlink_file(a, b) wam = WindowsAtomicFolderMove(tdir1) wam.copy_path_to(a, os.path.join(tdir2, "a")) wam.copy_path_to(b, os.path.join(tdir2, "b")) wam.delete_originals() self.assertEqual([], os.listdir(tdir1)) self.assertEqual({"a", "b"}, set(os.listdir(tdir2))) self.assertEqual(raw, open(os.path.join(tdir2, "a"), "rb").read()) self.assertEqual(raw, open(os.path.join(tdir2, "b"), "rb").read())
def windows_check_if_files_in_use(self, paths): ''' Raises an EACCES IOError if any of the files in the folder of book_id are opened in another program on windows. ''' if iswindows: for path in paths: spath = os.path.join(self.library_path, *path.split('/')) wam = None if os.path.exists(spath): try: wam = WindowsAtomicFolderMove(spath) finally: if wam is not None: wam.close_handles()
def update_path(self, book_id, title, author, path_field, formats_field): path = self.construct_path_name(book_id, title, author) current_path = path_field.for_book(book_id) formats = formats_field.for_book(book_id, default_value=()) fname = self.construct_file_name(book_id, title, author) # Check if the metadata used to construct paths has changed changed = False for fmt in formats: name = formats_field.format_fname(book_id, fmt) if name and name != fname: changed = True break if path == current_path and not changed: return spath = os.path.join(self.library_path, *current_path.split('/')) tpath = os.path.join(self.library_path, *path.split('/')) source_ok = current_path and os.path.exists(spath) wam = WindowsAtomicFolderMove( spath) if iswindows and source_ok else None try: if not os.path.exists(tpath): os.makedirs(tpath) if source_ok: # Migrate existing files dest = os.path.join(tpath, 'cover.jpg') self.copy_cover_to(current_path, dest, windows_atomic_move=wam, use_hardlink=True) for fmt in formats: dest = os.path.join(tpath, fname + '.' + fmt.lower()) self.copy_format_to(book_id, fmt, formats_field.format_fname( book_id, fmt), current_path, dest, windows_atomic_move=wam, use_hardlink=True) # Update db to reflect new file locations for fmt in formats: formats_field.table.set_fname(book_id, fmt, fname, self) path_field.table.set_path(book_id, path, self) # Delete not needed directories if source_ok: if os.path.exists(spath) and not samefile(spath, tpath): if wam is not None: wam.delete_originals() self.rmtree(spath, permanent=True) parent = os.path.dirname(spath) if len(os.listdir(parent)) == 0: self.rmtree(parent, permanent=True) finally: if wam is not None: wam.close_handles() curpath = self.library_path c1, c2 = current_path.split('/'), path.split('/') if not self.is_case_sensitive and len(c1) == len(c2): # On case-insensitive systems, title and author renames that only # change case don't cause any changes to the directories in the file # system. This can lead to having the directory names not match the # title/author, which leads to trouble when libraries are copied to # a case-sensitive system. The following code attempts to fix this # by checking each segment. If they are different because of case, # then rename the segment. Note that the code above correctly # handles files in the directories, so no need to do them here. for oldseg, newseg in zip(c1, c2): if oldseg.lower() == newseg.lower() and oldseg != newseg: try: os.rename(os.path.join(curpath, oldseg), os.path.join(curpath, newseg)) except: break # Fail silently since nothing catastrophic has happened curpath = os.path.join(curpath, newseg)
def update_path(self, book_id, title, author, path_field, formats_field): path = self.construct_path_name(book_id, title, author) current_path = path_field.for_book(book_id) formats = formats_field.for_book(book_id, default_value=()) fname = self.construct_file_name(book_id, title, author) # Check if the metadata used to construct paths has changed changed = False for fmt in formats: name = formats_field.format_fname(book_id, fmt) if name and name != fname: changed = True break if path == current_path and not changed: return spath = os.path.join(self.library_path, *current_path.split('/')) tpath = os.path.join(self.library_path, *path.split('/')) source_ok = current_path and os.path.exists(spath) wam = WindowsAtomicFolderMove(spath) if iswindows and source_ok else None try: if not os.path.exists(tpath): os.makedirs(tpath) if source_ok: # Migrate existing files dest = os.path.join(tpath, 'cover.jpg') self.copy_cover_to(current_path, dest, windows_atomic_move=wam, use_hardlink=True) for fmt in formats: dest = os.path.join(tpath, fname+'.'+fmt.lower()) self.copy_format_to(book_id, fmt, formats_field.format_fname(book_id, fmt), current_path, dest, windows_atomic_move=wam, use_hardlink=True) # Update db to reflect new file locations for fmt in formats: formats_field.table.set_fname(book_id, fmt, fname, self) path_field.table.set_path(book_id, path, self) # Delete not needed directories if source_ok: if os.path.exists(spath) and not samefile(spath, tpath): if wam is not None: wam.delete_originals() self.rmtree(spath, permanent=True) parent = os.path.dirname(spath) if len(os.listdir(parent)) == 0: self.rmtree(parent, permanent=True) finally: if wam is not None: wam.close_handles() curpath = self.library_path c1, c2 = current_path.split('/'), path.split('/') if not self.is_case_sensitive and len(c1) == len(c2): # On case-insensitive systems, title and author renames that only # change case don't cause any changes to the directories in the file # system. This can lead to having the directory names not match the # title/author, which leads to trouble when libraries are copied to # a case-sensitive system. The following code attempts to fix this # by checking each segment. If they are different because of case, # then rename the segment. Note that the code above correctly # handles files in the directories, so no need to do them here. for oldseg, newseg in zip(c1, c2): if oldseg.lower() == newseg.lower() and oldseg != newseg: try: os.rename(os.path.join(curpath, oldseg), os.path.join(curpath, newseg)) except: break # Fail silently since nothing catastrophic has happened curpath = os.path.join(curpath, newseg)