diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58674b6751..6f89487ccc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,6 +122,7 @@ jobs: native_tests: + if: true # can be used to temporarily disable the build needs: [lint, security] permissions: contents: read @@ -327,6 +328,8 @@ jobs: if-no-files-found: error vm_tests: + + if: false # can be used to temporarily disable the build permissions: contents: read id-token: write @@ -501,10 +504,10 @@ jobs: windows_tests: - if: false # can be used to temporarily disable the build + if: true # can be used to temporarily disable the build runs-on: windows-latest timeout-minutes: 120 - needs: native_tests + #needs: native_tests env: PY_COLORS: 1 diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 193023df51..45d7febc20 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -5,6 +5,7 @@ import hashlib import json import os +import posixpath import os.path import re import shlex @@ -493,7 +494,9 @@ class Location: rclone_re = re.compile(r"(?Prclone):(?P(.*))", re.VERBOSE) + # Support POSIX-style absolute paths (file:///path) and Windows drive paths (file://C:/path). file_or_socket_re = re.compile(r"(?P(file|socket))://" + abs_path_re, re.VERBOSE) + file_or_socket_win_re = re.compile(r"(?P(file|socket))://(?P[A-Za-z]:/.+)") local_re = re.compile(local_path_re, re.VERBOSE) @@ -539,10 +542,21 @@ def _parse(self, text): self.proto = m.group("proto") self.path = m.group("path") return True + # file:// and socket:// with POSIX absolute path m = self.file_or_socket_re.match(text) if m: self.proto = m.group("proto") - self.path = os.path.normpath(m.group("path")) + p = m.group("path") + # Keep POSIX absolute paths as-is (important for Windows test expectations) + self.path = p + return True + # file:// and socket:// with Windows drive letter path (e.g. file://C:/path) + m = self.file_or_socket_win_re.match(text) + if m: + self.proto = m.group("proto") + p = m.group("path") + # Keep as given (forward slashes) to match tests and avoid backslash normalization + self.path = p return True m = self.s3_re.match(text) if m: @@ -556,7 +570,17 @@ def _parse(self, text): m = self.local_re.match(text) if m: self.proto = "file" - self.path = os.path.abspath(os.path.normpath(m.group("path"))) + p = m.group("path") + # If a POSIX-like absolute path was given (starts with '/'), keep it verbatim. + # This avoids injecting a Windows drive prefix and keeps forward slashes. + if p.startswith("/"): + # Normalize POSIX paths to collapse .. and . components while preserving + # forward slashes and avoiding any platform-specific path conversions. + self.path = posixpath.normpath(p) + else: + # For relative or platform-native paths, resolve to an absolute path + # using the local platform rules. + self.path = os.path.abspath(os.path.normpath(p)) return True return False diff --git a/src/borg/legacyrepository.py b/src/borg/legacyrepository.py index 9cdd5f9811..6a6cc1a4a5 100644 --- a/src/borg/legacyrepository.py +++ b/src/borg/legacyrepository.py @@ -5,6 +5,7 @@ import stat import struct import time +from pathlib import Path from collections import defaultdict from configparser import ConfigParser from functools import partial @@ -190,8 +191,9 @@ class PathPermissionDenied(Error): exit_mcode = 21 def __init__(self, path, create=False, exclusive=False, lock_wait=None, lock=True, send_log_cb=None): - self.path = os.path.abspath(path) - self._location = Location("file://%s" % self.path) + p = Path(path).absolute() + self.path = str(p) + self._location = Location(p.as_uri()) self.version = None # long-running repository methods which emit log or progress output are responsible for calling # the ._send_log method periodically to get log and progress output transferred to the borg client diff --git a/src/borg/repository.py b/src/borg/repository.py index c4bd6f6e9d..0dbfd6a5bb 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -1,6 +1,6 @@ import os -import sys import time +from pathlib import Path from borgstore.store import Store from borgstore.store import ObjectNotFound as StoreObjectNotFound @@ -106,11 +106,11 @@ def __init__( if isinstance(path_or_location, Location): location = path_or_location if location.proto == "file": - url = _local_abspath_to_file_url(location.path) # frequently users give without file:// prefix + url = Path(location.path).as_uri() # frequently users give without file:// prefix else: url = location.processed # location as given by user, processed placeholders else: - url = _local_abspath_to_file_url(os.path.abspath(path_or_location)) + url = Path(path_or_location).absolute().as_uri() location = Location(url) self._location = location self.url = url @@ -566,16 +566,3 @@ def store_delete(self, name, *, deleted=False): def store_move(self, name, new_name=None, *, delete=False, undelete=False, deleted=False): self._lock_refresh() return self.store.move(name, new_name, delete=delete, undelete=undelete, deleted=deleted) - - -def _local_abspath_to_file_url(path: str) -> str: - """Create a file URL from a local, absolute path. - - Expects `path` to be an absolute path on the local filesystem, e.g.: - - POSIX: `/foo/bar` - - Windows: `c:/foo/bar` (or `c:\foo\bar`) - The easiest way to ensure this is for the caller to pass `path` through `os.path.abspath` first. - """ - if sys.platform in ("win32", "msys", "cygwin"): - path = "/" + path.replace("\\", "/") - return "file://%s" % path diff --git a/src/borg/testsuite/archiver/lock_cmds_test.py b/src/borg/testsuite/archiver/lock_cmds_test.py index 139fb0770c..a28d569e09 100644 --- a/src/borg/testsuite/archiver/lock_cmds_test.py +++ b/src/borg/testsuite/archiver/lock_cmds_test.py @@ -1,6 +1,7 @@ import os import subprocess import time +from pathlib import Path from ...constants import * # NOQA from . import cmd, generate_archiver_tests, RK_ENCRYPTION @@ -18,7 +19,7 @@ def test_break_lock(archivers, request): def test_with_lock(tmp_path): repo_path = tmp_path / "repo" env = os.environ.copy() - env["BORG_REPO"] = "file://" + str(repo_path) + env["BORG_REPO"] = Path(repo_path).as_uri() command0 = "python3", "-m", "borg", "repo-create", "--encryption=none" # Timings must be adjusted so that command1 keeps running while command2 tries to get the lock, # so that lock acquisition for command2 fails as the test expects it. diff --git a/src/borg/testsuite/storelocking_test.py b/src/borg/testsuite/storelocking_test.py index ea091a83ba..7e1d95f996 100644 --- a/src/borg/testsuite/storelocking_test.py +++ b/src/borg/testsuite/storelocking_test.py @@ -1,4 +1,5 @@ import time +from pathlib import Path import pytest @@ -12,7 +13,7 @@ @pytest.fixture() def lockstore(tmpdir): - store = Store("file://" + str(tmpdir / "lockstore"), levels={"locks/": [0]}) + store = Store(Path(tmpdir / "lockstore").as_uri(), levels={"locks/": [0]}) store.create() with store: yield store