mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Windows atomic folder move: Handle multiple hardlinks to the same file in a folder
This commit is contained in:
parent
68b5b16625
commit
6946dc5181
@ -81,6 +81,25 @@ class FilesystemTest(BaseTest):
|
|||||||
f.close()
|
f.close()
|
||||||
self.assertNotEqual(cache.field_for('title', 1), 'Moved', 'Title was changed despite file lock')
|
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_library_move(self):
|
def test_library_move(self):
|
||||||
' Test moving of library '
|
' Test moving of library '
|
||||||
from calibre.ptempfile import TemporaryDirectory
|
from calibre.ptempfile import TemporaryDirectory
|
||||||
|
@ -318,6 +318,7 @@ class WindowsAtomicFolderMove(object):
|
|||||||
|
|
||||||
import win32file, winerror
|
import win32file, winerror
|
||||||
from pywintypes import error
|
from pywintypes import error
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
if isbytestring(path):
|
if isbytestring(path):
|
||||||
path = path.decode(filesystem_encoding)
|
path = path.decode(filesystem_encoding)
|
||||||
@ -325,7 +326,13 @@ class WindowsAtomicFolderMove(object):
|
|||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return
|
return
|
||||||
|
|
||||||
for x in os.listdir(path):
|
names = os.listdir(path)
|
||||||
|
name_to_fileid = {x:windows_get_fileid(os.path.join(path, x)) for x in names}
|
||||||
|
fileid_to_names = defaultdict(set)
|
||||||
|
for name, fileid in name_to_fileid.iteritems():
|
||||||
|
fileid_to_names[fileid].add(name)
|
||||||
|
|
||||||
|
for x in names:
|
||||||
f = os.path.normcase(os.path.abspath(os.path.join(path, x)))
|
f = os.path.normcase(os.path.abspath(os.path.join(path, x)))
|
||||||
if not os.path.isfile(f):
|
if not os.path.isfile(f):
|
||||||
continue
|
continue
|
||||||
@ -340,6 +347,20 @@ class WindowsAtomicFolderMove(object):
|
|||||||
win32file.FILE_SHARE_DELETE, None,
|
win32file.FILE_SHARE_DELETE, None,
|
||||||
win32file.OPEN_EXISTING, win32file.FILE_FLAG_SEQUENTIAL_SCAN, 0)
|
win32file.OPEN_EXISTING, win32file.FILE_FLAG_SEQUENTIAL_SCAN, 0)
|
||||||
except error as e:
|
except error as e:
|
||||||
|
if getattr(e, 'winerror', 0) == winerror.ERROR_SHARING_VIOLATION:
|
||||||
|
# The file could be a hardlink to an already opened file,
|
||||||
|
# in which case we use the same handle for both files
|
||||||
|
fileid = name_to_fileid[x]
|
||||||
|
found = False
|
||||||
|
for other in fileid_to_names[fileid]:
|
||||||
|
other = os.path.normcase(os.path.abspath(os.path.join(path, other)))
|
||||||
|
if other in self.handle_map:
|
||||||
|
self.handle_map[f] = self.handle_map[other]
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if found:
|
||||||
|
continue
|
||||||
|
|
||||||
self.close_handles()
|
self.close_handles()
|
||||||
if getattr(e, 'winerror', 0) == winerror.ERROR_SHARING_VIOLATION:
|
if getattr(e, 'winerror', 0) == winerror.ERROR_SHARING_VIOLATION:
|
||||||
err = IOError(errno.EACCES,
|
err = IOError(errno.EACCES,
|
||||||
@ -370,6 +391,8 @@ class WindowsAtomicFolderMove(object):
|
|||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
win32file.SetFilePointer(handle, 0, win32file.FILE_BEGIN)
|
||||||
with lopen(dest, 'wb') as f:
|
with lopen(dest, 'wb') as f:
|
||||||
while True:
|
while True:
|
||||||
hr, raw = win32file.ReadFile(handle, 1024*1024)
|
hr, raw = win32file.ReadFile(handle, 1024*1024)
|
||||||
@ -380,6 +403,7 @@ class WindowsAtomicFolderMove(object):
|
|||||||
f.write(raw)
|
f.write(raw)
|
||||||
|
|
||||||
def release_file(self, path):
|
def release_file(self, path):
|
||||||
|
' Release the lock on the file pointed to by path. Will also release the lock on any hardlinks to path '
|
||||||
key = None
|
key = None
|
||||||
for p, h in self.handle_map.iteritems():
|
for p, h in self.handle_map.iteritems():
|
||||||
if samefile_windows(path, p):
|
if samefile_windows(path, p):
|
||||||
@ -388,7 +412,9 @@ class WindowsAtomicFolderMove(object):
|
|||||||
if key is not None:
|
if key is not None:
|
||||||
import win32file
|
import win32file
|
||||||
win32file.CloseHandle(key[1])
|
win32file.CloseHandle(key[1])
|
||||||
self.handle_map.pop(key[0])
|
remove = [f for f, h in self.handle_map.iteritems() if h is key[1]]
|
||||||
|
for x in remove:
|
||||||
|
self.handle_map.pop(x)
|
||||||
|
|
||||||
def close_handles(self):
|
def close_handles(self):
|
||||||
import win32file
|
import win32file
|
||||||
|
Loading…
x
Reference in New Issue
Block a user