Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,10 @@ interacts with the repository (i.e, via ``borg list``), depending on the value
of ``BORG_RELOCATED_REPO_ACCESS_IS_OK``, borg may warn you that the repository
has been moved. You will be given a prompt to confirm you are OK with this.

Accessing the same local repository directory via a different path (e.g., via a
symlink, a bind mount or a different letter case on a case-insensitive filesystem)
is not considered a relocation, as long as the previous path still exists.

If ``BORG_RELOCATED_REPO_ACCESS_IS_OK`` is unset, borg will interactively ask for
each repository whether it's OK.

Expand Down
23 changes: 22 additions & 1 deletion src/borg/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ def assert_location_matches(self, cache_config=None):
logger.debug('security: using previous_location of cache: %r', previous_location)

repository_location = self.repository._location.canonical_path()
if previous_location and previous_location != repository_location:
if (previous_location and previous_location != repository_location and
not self._is_same_local_directory(previous_location, repository_location)):
msg = ("Warning: The repository at location {} was previously located at {}\n".format(
repository_location, previous_location) +
"Do you want to continue? [yN] ")
Expand All @@ -133,6 +134,26 @@ def assert_location_matches(self, cache_config=None):
if cache_config:
cache_config.save()

def _is_same_local_directory(self, previous_location, repository_location):
"""Return True if both locations are local paths that refer to the same existing directory.

The same local repository directory can be reached via different paths, e.g. through a symlink,
a bind mount or a different letter case on a case-insensitive filesystem (see issue #8188).
This is not a relocation, so no warning is needed. The stored location is not updated.
"""
if self.repository._location.proto != 'file':
return False
if '://' in previous_location or not os.path.isabs(previous_location):
return False
try:
previous_st = os.stat(previous_location)
current_st = os.stat(repository_location)
except OSError:
# the previous path does not exist anymore (e.g. the repository was moved or renamed).
return False
# some filesystems report st_ino == 0 for everything, which would make any two paths look the same.
return previous_st.st_ino != 0 and os.path.samestat(previous_st, current_st)

def assert_no_manifest_replay(self, manifest, key, cache_config=None):
try:
with open(self.manifest_ts_file) as fd:
Expand Down
31 changes: 31 additions & 0 deletions src/borg/testsuite/archiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,37 @@ def test_repository_move(self):
for file in ('location', 'key-type', 'manifest-timestamp'):
assert os.path.exists(os.path.join(security_dir, file))

@pytest.mark.skipif(not are_symlinks_supported(), reason='symlinks not supported')
def test_repository_access_via_symlink(self):
# accessing the same repository directory via a different path (here: a symlink) is not a relocation.
if self.prefix:
pytest.skip('only local repositories are affected')
self.cmd('init', '--encryption=repokey', self.repository_location)
security_dir = self.get_security_dir()
symlink_location = self.repository_location + '_symlink'
os.symlink(self.repository_path, self.repository_path + '_symlink')
with environment_variable(BORG_RELOCATED_REPO_ACCESS_IS_OK='no'):
for location in (self.repository_location, symlink_location, self.repository_location):
self.cmd('info', location)
# the stored location is not updated.
with open(os.path.join(security_dir, 'location')) as fd:
assert fd.read() == Location(self.repository_location).canonical_path()

def test_repository_previously_remote(self):
# a repository previously accessed via a URL, now accessed via a local path, is a relocation.
if self.prefix:
pytest.skip('only local repositories are affected')
self.cmd('init', '--encryption=repokey', self.repository_location)
with open(os.path.join(self.get_security_dir(), 'location'), 'w') as fd:
fd.write(Location('ssh://__testsuite__' + self.repository_path).canonical_path())
shutil.rmtree(self.cache_path)
with environment_variable(BORG_RELOCATED_REPO_ACCESS_IS_OK='no'):
if self.FORK_DEFAULT:
self.cmd('info', self.repository_location, exit_code=EXIT_ERROR)
else:
with pytest.raises(Cache.RepositoryAccessAborted):
self.cmd('info', self.repository_location)

def test_security_dir_compat(self):
self.cmd('init', '--encryption=repokey', self.repository_location)
with open(os.path.join(self.get_security_dir(), 'location'), 'w') as fd:
Expand Down
Loading