Skip to content
Merged
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 @@ -1362,6 +1362,10 @@ interacts with the repository (e.g., via ``borg repo-list``), depending on the v
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
27 changes: 26 additions & 1 deletion src/borg/security.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import shutil
from pathlib import Path

Expand Down Expand Up @@ -130,7 +131,11 @@ def assert_location_matches(self):
# produced in different normalization forms (NFC when typed, NFD when derived from the filesystem),
# which would otherwise be mistaken for a repository relocation (see issue #2913). Normalizing is
# idempotent and applied symmetrically, so it never creates a spurious mismatch for remote repos.
if previous_location and normalize_local_path(previous_location) != normalize_local_path(repository_location):
if (
previous_location
and normalize_local_path(previous_location) != normalize_local_path(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
Expand All @@ -150,6 +155,26 @@ def assert_location_matches(self):
with SaveFile(self.location_file) as fd:
fd.write(repository_location)

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_key_type(self, key):
# Make sure an encrypted repository has not been swapped for an unencrypted repository
if self.known() and not self.key_matches(key):
Expand Down
29 changes: 28 additions & 1 deletion src/borg/testsuite/archiver/checks_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ...helpers import Location, get_security_dir, bin_to_hex
from ...helpers import EXIT_ERROR
from ...repository import Repository
from .. import changedir
from .. import changedir, are_symlinks_supported
from . import cmd, _extract_repository_id, create_test_files
from . import create_regular_file, assert_creates_file, generate_archiver_tests, RK_ENCRYPTION
from . import set_empty_passphrase
Expand Down Expand Up @@ -172,6 +172,33 @@ def test_repository_move(archivers, request, monkeypatch):
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(archiver, monkeypatch):
# accessing the same repository directory via a different path (here: a symlink) is not a relocation.
cmd(archiver, "repo-create", RK_ENCRYPTION)
security_dir = get_security_directory(archiver.repository_path)
repo_location = archiver.repository_location
symlink_location = archiver.repository_path + "_symlink"
os.symlink(archiver.repository_path, symlink_location)
monkeypatch.setenv("BORG_RELOCATED_REPO_ACCESS_IS_OK", "no")
for location in (repo_location, symlink_location, repo_location):
archiver.repository_location = location
cmd(archiver, "repo-info")
# the stored location is not updated.
with open(os.path.join(security_dir, "location")) as fd:
assert fd.read() == Location(repo_location).canonical_path()


def test_repository_previously_remote(archiver):
# a repository previously accessed via a URL, now accessed via a local path, is a relocation.
cmd(archiver, "repo-create", RK_ENCRYPTION)
security_dir = get_security_directory(archiver.repository_path)
with open(os.path.join(security_dir, "location"), "w") as fd:
fd.write(Location("ssh://__testsuite__/" + archiver.repository_path).canonical_path())
with pytest.raises(Cache.RepositoryAccessAborted):
cmd(archiver, "repo-info")


def test_unknown_unencrypted(archivers, request, monkeypatch):
archiver = request.getfixturevalue(archivers)
cmd(archiver, "repo-create", "--encryption=authenticated-sha256")
Expand Down
Loading