mirror of https://github.com/borgbackup/borg.git
Merge pull request #1603 from ThomasWaldmann/fix-fuse-hardlinks
Fix FUSE hardlinks
This commit is contained in:
commit
765b8f38d6
|
@ -15,7 +15,7 @@ from .logger import create_logger
|
||||||
logger = create_logger()
|
logger = create_logger()
|
||||||
|
|
||||||
from .archive import Archive
|
from .archive import Archive
|
||||||
from .helpers import daemonize, safe_encode
|
from .helpers import daemonize
|
||||||
from .item import Item
|
from .item import Item
|
||||||
from .lrucache import LRUCache
|
from .lrucache import LRUCache
|
||||||
|
|
||||||
|
@ -43,7 +43,10 @@ class ItemCache:
|
||||||
return pos + self.offset
|
return pos + self.offset
|
||||||
|
|
||||||
def get(self, inode):
|
def get(self, inode):
|
||||||
self.fd.seek(inode - self.offset, io.SEEK_SET)
|
offset = inode - self.offset
|
||||||
|
if offset < 0:
|
||||||
|
raise ValueError('ItemCache.get() called with an invalid inode number')
|
||||||
|
self.fd.seek(offset, io.SEEK_SET)
|
||||||
item = next(msgpack.Unpacker(self.fd, read_size=1024))
|
item = next(msgpack.Unpacker(self.fd, read_size=1024))
|
||||||
return Item(internal_dict=item)
|
return Item(internal_dict=item)
|
||||||
|
|
||||||
|
@ -133,46 +136,66 @@ class FuseOperations(llfuse.Operations):
|
||||||
def process_archive(self, archive, prefix=[]):
|
def process_archive(self, archive, prefix=[]):
|
||||||
"""Build fuse inode hierarchy from archive metadata
|
"""Build fuse inode hierarchy from archive metadata
|
||||||
"""
|
"""
|
||||||
|
self.file_versions = {} # for versions mode: original path -> version
|
||||||
unpacker = msgpack.Unpacker()
|
unpacker = msgpack.Unpacker()
|
||||||
for key, chunk in zip(archive.metadata.items, self.repository.get_many(archive.metadata.items)):
|
for key, chunk in zip(archive.metadata.items, self.repository.get_many(archive.metadata.items)):
|
||||||
_, data = self.key.decrypt(key, chunk)
|
_, data = self.key.decrypt(key, chunk)
|
||||||
unpacker.feed(data)
|
unpacker.feed(data)
|
||||||
for item in unpacker:
|
for item in unpacker:
|
||||||
item = Item(internal_dict=item)
|
item = Item(internal_dict=item)
|
||||||
|
path = os.fsencode(os.path.normpath(item.path))
|
||||||
is_dir = stat.S_ISDIR(item.mode)
|
is_dir = stat.S_ISDIR(item.mode)
|
||||||
if is_dir:
|
if is_dir:
|
||||||
try:
|
try:
|
||||||
# This can happen if an archive was created with a command line like
|
# This can happen if an archive was created with a command line like
|
||||||
# $ borg create ... dir1/file dir1
|
# $ borg create ... dir1/file dir1
|
||||||
# In this case the code below will have created a default_dir inode for dir1 already.
|
# In this case the code below will have created a default_dir inode for dir1 already.
|
||||||
inode = self._find_inode(item.path, prefix)
|
inode = self._find_inode(path, prefix)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
self.items[inode] = item
|
self.items[inode] = item
|
||||||
continue
|
continue
|
||||||
segments = prefix + os.fsencode(os.path.normpath(item.path)).split(b'/')
|
segments = prefix + path.split(b'/')
|
||||||
del item.path
|
|
||||||
parent = 1
|
parent = 1
|
||||||
for segment in segments[:-1]:
|
for segment in segments[:-1]:
|
||||||
parent = self.process_inner(segment, parent)
|
parent = self.process_inner(segment, parent)
|
||||||
self.process_leaf(segments[-1], item, parent, prefix, is_dir)
|
self.process_leaf(segments[-1], item, parent, prefix, is_dir)
|
||||||
|
|
||||||
def process_leaf(self, name, item, parent, prefix, is_dir):
|
def process_leaf(self, name, item, parent, prefix, is_dir):
|
||||||
def version_name(name, item):
|
def file_version(item):
|
||||||
if 'chunks' in item:
|
if 'chunks' in item:
|
||||||
ident = 0
|
ident = 0
|
||||||
for chunkid, _, _ in item.chunks:
|
for chunkid, _, _ in item.chunks:
|
||||||
ident = adler32(chunkid, ident)
|
ident = adler32(chunkid, ident)
|
||||||
name = name + safe_encode('.%08x' % ident)
|
return ident
|
||||||
return name
|
|
||||||
|
def make_versioned_name(name, version, add_dir=False):
|
||||||
|
if add_dir:
|
||||||
|
# add intermediate directory with same name as filename
|
||||||
|
path_fname = name.rsplit(b'/', 1)
|
||||||
|
name += b'/' + path_fname[-1]
|
||||||
|
return name + os.fsencode('.%08x' % version)
|
||||||
|
|
||||||
if self.versions and not is_dir:
|
if self.versions and not is_dir:
|
||||||
parent = self.process_inner(name, parent)
|
parent = self.process_inner(name, parent)
|
||||||
name = version_name(name, item)
|
version = file_version(item)
|
||||||
|
if version is not None:
|
||||||
|
# regular file, with contents - maybe a hardlink master
|
||||||
|
name = make_versioned_name(name, version)
|
||||||
|
path = os.fsencode(os.path.normpath(item.path))
|
||||||
|
self.file_versions[path] = version
|
||||||
|
|
||||||
|
del item.path # safe some space
|
||||||
if 'source' in item and stat.S_ISREG(item.mode):
|
if 'source' in item and stat.S_ISREG(item.mode):
|
||||||
inode = self._find_inode(item.source, prefix)
|
# a hardlink, no contents, <source> is the hardlink master
|
||||||
|
source = os.fsencode(os.path.normpath(item.source))
|
||||||
|
if self.versions:
|
||||||
|
# adjust source name with version
|
||||||
|
version = self.file_versions[source]
|
||||||
|
source = make_versioned_name(source, version, add_dir=True)
|
||||||
|
name = make_versioned_name(name, version)
|
||||||
|
inode = self._find_inode(source, prefix)
|
||||||
item = self.cache.get(inode)
|
item = self.cache.get(inode)
|
||||||
item.nlink = item.get('nlink', 1) + 1
|
item.nlink = item.get('nlink', 1) + 1
|
||||||
self.items[inode] = item
|
self.items[inode] = item
|
||||||
|
@ -215,7 +238,7 @@ class FuseOperations(llfuse.Operations):
|
||||||
return self.cache.get(inode)
|
return self.cache.get(inode)
|
||||||
|
|
||||||
def _find_inode(self, path, prefix=[]):
|
def _find_inode(self, path, prefix=[]):
|
||||||
segments = prefix + os.fsencode(os.path.normpath(path)).split(b'/')
|
segments = prefix + path.split(b'/')
|
||||||
inode = 1
|
inode = 1
|
||||||
for segment in segments:
|
for segment in segments:
|
||||||
inode = self.contents[inode][segment]
|
inode = self.contents[inode][segment]
|
||||||
|
|
|
@ -1453,6 +1453,9 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
def test_fuse_versions_view(self):
|
def test_fuse_versions_view(self):
|
||||||
self.cmd('init', self.repository_location)
|
self.cmd('init', self.repository_location)
|
||||||
self.create_regular_file('test', contents=b'first')
|
self.create_regular_file('test', contents=b'first')
|
||||||
|
if are_hardlinks_supported():
|
||||||
|
self.create_regular_file('hardlink1', contents=b'')
|
||||||
|
os.link('input/hardlink1', 'input/hardlink2')
|
||||||
self.cmd('create', self.repository_location + '::archive1', 'input')
|
self.cmd('create', self.repository_location + '::archive1', 'input')
|
||||||
self.create_regular_file('test', contents=b'second')
|
self.create_regular_file('test', contents=b'second')
|
||||||
self.cmd('create', self.repository_location + '::archive2', 'input')
|
self.cmd('create', self.repository_location + '::archive2', 'input')
|
||||||
|
@ -1463,6 +1466,10 @@ class ArchiverTestCase(ArchiverTestCaseBase):
|
||||||
files = os.listdir(path)
|
files = os.listdir(path)
|
||||||
assert all(f.startswith('test.') for f in files) # ... with files test.xxxxxxxx in there
|
assert all(f.startswith('test.') for f in files) # ... with files test.xxxxxxxx in there
|
||||||
assert {b'first', b'second'} == {open(os.path.join(path, f), 'rb').read() for f in files}
|
assert {b'first', b'second'} == {open(os.path.join(path, f), 'rb').read() for f in files}
|
||||||
|
if are_hardlinks_supported():
|
||||||
|
st1 = os.stat(os.path.join(mountpoint, 'input', 'hardlink1', 'hardlink1.00000000'))
|
||||||
|
st2 = os.stat(os.path.join(mountpoint, 'input', 'hardlink2', 'hardlink2.00000000'))
|
||||||
|
assert st1.st_ino == st2.st_ino
|
||||||
|
|
||||||
@unittest.skipUnless(has_llfuse, 'llfuse not installed')
|
@unittest.skipUnless(has_llfuse, 'llfuse not installed')
|
||||||
def test_fuse_allow_damaged_files(self):
|
def test_fuse_allow_damaged_files(self):
|
||||||
|
|
Loading…
Reference in New Issue