From 3c3e7c15299e3ab81c2d11dc87fdd8e9cc45ce1b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 12 Nov 2025 18:00:39 +0100 Subject: [PATCH 1/7] CI: enable windows testing --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58674b6751..d34dac328d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -501,7 +501,7 @@ 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 From 24c2f8048bdd8d6be97d593eb851c5a8b84d6d9a Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 12 Nov 2025 18:01:16 +0100 Subject: [PATCH 2/7] temp: start windows Ci immediately not after other tests. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d34dac328d..d3fcf131a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -504,7 +504,7 @@ jobs: 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 From abfda16a17cdf4726b00045b46d9638caf26779b Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 12 Nov 2025 18:07:20 +0100 Subject: [PATCH 3/7] temp: disable native and vm tests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3fcf131a1..2dce9d8cf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,6 +122,7 @@ jobs: native_tests: + if: false # 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 From cb94dc7271ba2ba48d2ca99fd71834cc77d75418 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 14 Nov 2025 00:06:33 +0100 Subject: [PATCH 4/7] parseformat: add support for Windows drive paths and improve POSIX path handling --- src/borg/helpers/parseformat.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 193023df51..08ec5c8ade 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -493,7 +493,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 +541,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 +569,15 @@ 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("/"): + self.path = 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 From 6fbeb8a44385cfc6b5aa992a45434b27a6e626ec Mon Sep 17 00:00:00 2001 From: valtron Date: Thu, 13 Nov 2025 22:03:53 -0700 Subject: [PATCH 5/7] file url fixes --- src/borg/legacyrepository.py | 6 ++++-- src/borg/repository.py | 19 +++---------------- src/borg/testsuite/archiver/lock_cmds_test.py | 3 ++- src/borg/testsuite/storelocking_test.py | 3 ++- 4 files changed, 11 insertions(+), 20 deletions(-) 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 From 5f486af024ef98d2aa8d6ac97149981e54868d9c Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 14 Nov 2025 17:19:41 +0100 Subject: [PATCH 6/7] parseformat: normalize POSIX paths to handle '.' and '..' components --- src/borg/helpers/parseformat.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 08ec5c8ade..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 @@ -573,7 +574,9 @@ def _parse(self, text): # 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("/"): - self.path = p + # 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. From 744001ae3179ac5a1b6c3078c78ef4bb309f8b82 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 14 Nov 2025 17:21:04 +0100 Subject: [PATCH 7/7] temp: re-enable linux and mypy tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dce9d8cf3..6f89487ccc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,7 @@ jobs: native_tests: - if: false # can be used to temporarily disable the build + if: true # can be used to temporarily disable the build needs: [lint, security] permissions: contents: read