Repository: borgbackup/borg

Issue:
borg2: use platformdirs python package?
@RayyanAnsari suggested to use the `platformdirs` python lib ( https://github.com/platformdirs/platformdirs ) and I had a closer look now:

- the code quality seems decent, from an experienced developer
- the amount of code is not much, but also not trivial. it might be also interesting for us that it supports android also (but not sure it is relevant for termux users).
- some dists have it already packaged, some do not (looked at debian,ubuntu,fedora), so there might be some work for packagers.
- there is 1 open ticket in their tracker with some discussion whether they use some (macOS) directory correctly or not. https://github.com/platformdirs/platformdirs/issues/98
- so, i guess we could add it as a python dependency for borg if we can get rid of some of the corresponding code we have.
- we would need to be careful, so we do not break "borg transfer" when it reads borg1 archives/repos (config/cache access the borg1 way).
- for borg2 config/cache, we could be incompatible as that would be all-new anyway.
- it seems to not yet autocreate not-yet existing directories, but guess the maintainer there is open for a PR with that. https://github.com/platformdirs/platformdirs/issues/120


Discussion:
ThomasWaldmann: Looks like MYPY cancels it all.

About the tests:

Maybe rather test legacy paths == not-legacy paths and also with/without misc. env vars set, so we can be sure nothing breaks for accessing borg1 stuff.
ThomasWaldmann: shall I help with some commits so this advances faster?
RayyanAnsari: > Looks like MYPY cancels it all.

This was because I forgot to add `platformdirs` to `setup.cfg`. It should work now.

> shall I help with some commits so this advances faster?

That would be great - I think all that's left is some small details in the tests. I should be able to get round to this in a few days.


codecov-commenter: # [Codecov](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) Report
> Merging [#7300](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) (0fdb35e) into [master](https://codecov.io/gh/borgbackup/borg/commit/b2654bc17de472162ad9892eb4b0668232789424?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) (b2654bc) will **decrease** coverage by `0.24%`.
> The diff coverage is `68.42%`.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup)

```diff
@@            Coverage Diff             @@
##           master    #7300      +/-   ##
==========================================
- Coverage   83.37%   83.13%   -0.24%     
==========================================
  Files          67       67              
  Lines       11651    11628      -23     
  Branches     2123     1846     -277     
==========================================
- Hits         9714     9667      -47     
- Misses       1367     1384      +17     
- Partials      570      577       +7     
```


| [Impacted Files](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) | Coverage Δ | |
|---|---|---|
| [src/borg/archive.py](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvYXJjaGl2ZS5weQ==) | `82.16% <ø> (-0.25%)` | :arrow_down: |
| [src/borg/archiver/\_common.py](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvYXJjaGl2ZXIvX2NvbW1vbi5weQ==) | `89.78% <ø> (ø)` | |
| [src/borg/archiver/check\_cmd.py](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvYXJjaGl2ZXIvY2hlY2tfY21kLnB5) | `73.80% <ø> (ø)` | |
| [src/borg/manifest.py](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvbWFuaWZlc3QucHk=) | `93.90% <ø> (ø)` | |
| [src/borg/helpers/fs.py](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvaGVscGVycy9mcy5weQ==) | `78.94% <68.42%> (-1.78%)` | :arrow_down: |
| [src/borg/fuse\_impl.py](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvZnVzZV9pbXBsLnB5) | `30.76% <0.00%> (-23.08%)` | :arrow_down: |
| [src/borg/helpers/nanorst.py](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvaGVscGVycy9uYW5vcnN0LnB5) | `84.81% <0.00%> (-2.69%)` | :arrow_down: |
| [src/borg/locking.py](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvbG9ja2luZy5weQ==) | `84.12% <0.00%> (-1.45%)` | :arrow_down: |
| [src/borg/archiver/create\_cmd.py](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvYXJjaGl2ZXIvY3JlYXRlX2NtZC5weQ==) | `77.77% <0.00%> (-0.64%)` | :arrow_down: |
| [src/borg/platform/xattr.py](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup#diff-c3JjL2JvcmcvcGxhdGZvcm0veGF0dHIucHk=) | `81.96% <0.00%> (-0.58%)` | :arrow_down: |
| ... and [14 more](https://codecov.io/gh/borgbackup/borg/pull/7300?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=borgbackup)

ThomasWaldmann: Looks like with `legacy=False`, using `BORG_BASE_DIR` will be unsupported.

Guess that would make reading stuff from existing borg1 setups fail (e.g. for `borg transfer`).

Guess more tests are needed, esp. comparing legacy to non-legacy dirs.

ThomasWaldmann: I'll do a rebase on master / force push soon...


Patch:
diff --git a/.coafile b/.coafile
index 625090a1..b6300b66 100644
--- a/.coafile
+++ b/.coafile
@@ -13,7 +13,8 @@ file_naming_convention = snake
 
 [all.python]
 files = src/borg/**/*.py
-bears = PyDocStyleBear, PyLintBear
+bears = PEP8Bear, PyDocStyleBear, PyLintBear
+pep_ignore = E123,E125,E126,E127,E128,E226,E301,E309,E402,F401,F405,F811,W690
 pylint_disable = C0103, C0111, C0112, C0122, C0123, C0301, C0302, C0325, C0330, C0411, C0412, C0413, C1801,
                  I1101,
                  W0102, W0104, W0106, W0108, W0120, W0201, W0212, W0221, W0231, W0401, W0404,
diff --git a/docs/changes.rst b/docs/changes.rst
index 11b182c0..bb9c5f86 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -83,7 +83,6 @@ Compatibility notes:
   - removed --nobsdflags (use --noflags)
   - removed --noatime (default now, see also --atime)
   - removed --save-space option (does not change behaviour)
-- using --list together with --progress is now disallowed, #7219
 - the --glob-archives option was renamed to --match-archives (the short option
   name -a is unchanged) and extended to support different pattern styles:
 
@@ -99,74 +98,34 @@ Compatibility notes:
 
 New features:
 
-- info: add used storage quota, #7121
-- transfer: support --progress
-- create/recreate/import-tar: add --checkpoint-volume option
-- support date-based matching for archive selection,
-  add --newer/--older/--newest/--oldest options, #7062 #7296
+- adding used storage quota to borg info, #7121
 
 Fixes:
 
 - disallow --list with --progress, #7219
 - create: fix --list --dry-run output for directories, #7209
 - do no assume hardlink_master=True if not present, #7175
-- fix item_ptrs orphaned chunks of checkpoint archives
-- avoid orphan content chunks on BackupOSError, #6709
-- transfer: fix bug in obfuscated data upgrade code
-- fs.py: fix bug in f-string (thanks mypy!)
-- recreate: when --target is given, do not detect "nothing to do", #7254.
-- locking (win32): deal with os.rmdir/listdir PermissionErrors
-- locking: thread id must be parsed as hex from lock file name
-- extract: fix mtime when ResourceFork xattr is set (macOS specific), #7234
 
 Other changes:
 
-- use local time / local timezone to output timestamps, #7283
+- switch archive and file timestamps to UTC, also output tzoffset
 - update development.lock.txt, including a setuptools security fix, #7227
 - remove --save-space option (does not change behaviour)
-- remove part files from final archive
-- remove --consider-part-files, related stats code, update docs
-- transfer: drop part files
-- check: show id of orphaned chunks
-- ArchiveItem.cmdline list-of-str -> .command_line str, #7246
-- Item: symlinks: rename .source to .target, #7245
-- Item: make user/group/uid/gid optional
-- create: do not store user/group for stdin data by default, #7249
-- extract: chown only if we have u/g info in archived item, #7249
-- export-tar: for items w/o uid/gid, default to 0/0, #7249
-- fix some uid/gid lookup code / tests for win32
-- cache.py: be less verbose during cache sync
-- update bash completion script commands and options, #7273
 - validation / placeholders / JSON:
 
-  - implement (text|binary)_to_json: key (text), key_b64 (base64(binary))
-  - remove bpath, barchive, bcomment placeholders / JSON keys
-  - archive metadata: make sure hostname and username have no surrogate escapes
   - text attributes (like archive name, comment): validate more strictly, #2290
   - transfer: validate archive names and comment before transfer
-  - json output: use text_to_json (path, target), #6151
+  - remove bpath, barchive, bcomment placeholders / JSON keys
 - docs:
 
-  - docs and comments consistency, readability and spelling fixes
+  - docs and comments consistency and readability improvement
   - fix --progress display description, #7180
-  - document how borg deals with non-unicode bytes in JSON output
-  - document another way to get UTF-8 encoding on stdin/stdout/stderr, #2273
-  - pruning interprets timestamps in the local timezone where borg prune runs
-  - shellpattern: add license, use copyright/license markup
-  - key change-passphrase: fix --encryption value in examples
-  - remove BORG_LIBB2_PREFIX (not used any more)
 - tests:
 
-  - fix archiver tests on Windows, add running the tests to Windows CI
+  - fix archiver tests on Windows
   - fix tox4 passenv issue, #7199
   - github actions updates (fix deprecation warnings)
   - add tests for borg transfer/upgrade
-  - fix test hanging reading FIFO when `borg create` failed
-  - mypy inspired fixes / updates
-  - fix prune tests, prune in localtime
-  - do not look up uid 0 / gid 0, but current process uid/gid
-  - safe_unlink tests: use os.link to support win32 also
-  - fix test_size_on_disk_accurate for large st_blksize, #7250
 
 
 Version 2.0.0b4 (2022-11-27)
diff --git a/scripts/msys2-install-deps b/scripts/msys2-install-deps
index 39fae4c7..227f505d 100644
--- a/scripts/msys2-install-deps
+++ b/scripts/msys2-install-deps
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-pacman -S --needed --noconfirm git mingw-w64-ucrt-x86_64-{toolchain,pkgconf,zstd,lz4,xxhash,openssl,python,cython,python-setuptools,python-wheel,python-pkgconfig,python-packaging,python-msgpack,python-argon2_cffi,python-pip}
+pacman -S --needed --noconfirm git mingw-w64-ucrt-x86_64-{toolchain,pkgconf,zstd,lz4,xxhash,openssl,python-msgpack,python-argon2_cffi,python-platformdirs,python,cython,python-setuptools,python-wheel,python-pkgconfig,python-packaging,python-pip}
 pip install pyinstaller
 
 if [ "$1" = "development" ]; then
diff --git a/setup.cfg b/setup.cfg
index 127e8635..47cf3119 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,6 +41,7 @@ setup_requires =
 install_requires =
   msgpack >=1.0.3, <=1.0.4
   packaging
+  platformdirs >=3.0.0, <4.0.0
   argon2-cffi
 tests_require =
   pytest
diff --git a/src/borg/helpers/__init__.py b/src/borg/helpers/__init__.py
index 82cd027a..e42d7841 100644
--- a/src/borg/helpers/__init__.py
+++ b/src/borg/helpers/__init__.py
@@ -11,7 +11,7 @@
 from .checks import check_extension_modules, check_python
 from .datastruct import StableDict, Buffer, EfficientCollectionQueue
 from .errors import Error, ErrorWithTraceback, IntegrityError, DecompressionError
-from .fs import ensure_dir, get_security_dir, get_keys_dir, get_base_dir, get_cache_dir, get_config_dir
+from .fs import ensure_dir, get_security_dir, get_keys_dir, get_base_dir, join_base_dir, get_cache_dir, get_config_dir
 from .fs import dir_is_tagged, dir_is_cachedir, make_path_safe, scandir_inorder
 from .fs import secure_erase, safe_unlink, dash_open, os_open, os_stat, umount
 from .fs import O_, flags_root, flags_dir, flags_special_follow, flags_special, flags_base, flags_normal, flags_noatime
diff --git a/src/borg/helpers/fs.py b/src/borg/helpers/fs.py
index d69a8871..80574655 100644
--- a/src/borg/helpers/fs.py
+++ b/src/borg/helpers/fs.py
@@ -8,6 +8,8 @@
 import sys
 import textwrap
 
+import platformdirs
+
 from .errors import Error
 
 from .process import prepare_subprocess_env
@@ -40,7 +42,7 @@ def ensure_dir(path, mode=stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO, pretty_dea
             raise
 
 
-def get_base_dir():
+def get_base_dir(*, legacy=False):
     """Get home directory / base directory for borg:
 
     - BORG_BASE_DIR, if set
@@ -48,47 +50,65 @@ def get_base_dir():
     - ~$USER, if USER is set
     - ~
     """
-    base_dir = os.environ.get("BORG_BASE_DIR") or os.environ.get("HOME")
-    # os.path.expanduser() behaves differently for '~' and '~someuser' as
-    # parameters: when called with an explicit username, the possibly set
-    # environment variable HOME is no longer respected. So we have to check if
-    # it is set and only expand the user's home directory if HOME is unset.
-    if not base_dir:
-        base_dir = os.path.expanduser("~%s" % os.environ.get("USER", ""))
+    if legacy:
+        base_dir = os.environ.get("BORG_BASE_DIR") or os.environ.get("HOME")
+        # os.path.expanduser() behaves differently for '~' and '~someuser' as
+        # parameters: when called with an explicit username, the possibly set
+        # environment variable HOME is no longer respected. So we have to check if
+        # it is set and only expand the user's home directory if HOME is unset.
+        if not base_dir:
+            base_dir = os.path.expanduser("~%s" % os.environ.get("USER", ""))
+    else:
+        # we only care for BORG_BASE_DIR here, as it can be used to override the base dir
+        # and not use any more or less platform specific way to determine the base dir.
+        base_dir = os.environ.get("BORG_BASE_DIR")
     return base_dir
 
 
-def get_keys_dir():
+def join_base_dir(*paths, **kw):
+    legacy = kw.get("legacy", True)
+    base_dir = get_base_dir(legacy=legacy)
+    return None if base_dir is None else os.path.join(base_dir, *paths)
+
+
+def get_keys_dir(*, legacy=False):
     """Determine where to repository keys and cache"""
     keys_dir = os.environ.get("BORG_KEYS_DIR")
     if keys_dir is None:
         # note: do not just give this as default to the environment.get(), see issue #5979.
-        keys_dir = os.path.join(get_config_dir(), "keys")
+        keys_dir = os.path.join(get_config_dir(legacy=legacy), "keys")
     ensure_dir(keys_dir)
     return keys_dir
 
 
-def get_security_dir(repository_id=None):
+def get_security_dir(repository_id=None, *, legacy=False):
     """Determine where to store local security information."""
     security_dir = os.environ.get("BORG_SECURITY_DIR")
     if security_dir is None:
         # note: do not just give this as default to the environment.get(), see issue #5979.
-        security_dir = os.path.join(get_config_dir(), "security")
+        security_dir = os.path.join(get_config_dir(legacy=legacy), "security")
     if repository_id:
         security_dir = os.path.join(security_dir, repository_id)
     ensure_dir(security_dir)
     return security_dir
 
 
-def get_cache_dir():
+def get_cache_dir(*, legacy=False):
     """Determine where to repository keys and cache"""
-    # Get cache home path
-    cache_home = os.path.join(get_base_dir(), ".cache")
-    # Try to use XDG_CACHE_HOME instead if BORG_BASE_DIR isn't explicitly set
-    if not os.environ.get("BORG_BASE_DIR"):
-        cache_home = os.environ.get("XDG_CACHE_HOME", cache_home)
-    # Use BORG_CACHE_DIR if set, otherwise assemble final path from cache home path
-    cache_dir = os.environ.get("BORG_CACHE_DIR", os.path.join(cache_home, "borg"))
+
+    if legacy:
+        # Get cache home path
+        cache_home = join_base_dir(".cache", legacy=legacy)
+        # Try to use XDG_CACHE_HOME instead if BORG_BASE_DIR isn't explicitly set
+        if not os.environ.get("BORG_BASE_DIR"):
+            cache_home = os.environ.get("XDG_CACHE_HOME", cache_home)
+        # Use BORG_CACHE_DIR if set, otherwise assemble final path from cache home path
+        cache_dir = os.environ.get("BORG_CACHE_DIR", os.path.join(cache_home, "borg"))
+    else:
+        cache_dir = os.environ.get(
+            "BORG_CACHE_DIR", join_base_dir(".cache", legacy=legacy) or platformdirs.user_cache_dir("borg")
+        )
+
     # Create path if it doesn't exist yet
     ensure_dir(cache_dir)
     cache_tag_fn = os.path.join(cache_dir, CACHE_TAG_NAME)
@@ -110,15 +130,22 @@ def get_cache_dir():
     return cache_dir
 
 
-def get_config_dir():
+def get_config_dir(*, legacy=False):
     """Determine where to store whole config"""
+
     # Get config home path
-    config_home = os.path.join(get_base_dir(), ".config")
-    # Try to use XDG_CONFIG_HOME instead if BORG_BASE_DIR isn't explicitly set
-    if not os.environ.get("BORG_BASE_DIR"):
-        config_home = os.environ.get("XDG_CONFIG_HOME", config_home)
-    # Use BORG_CONFIG_DIR if set, otherwise assemble final path from config home path
-    config_dir = os.environ.get("BORG_CONFIG_DIR", os.path.join(config_home, "borg"))
+    if legacy:
+        config_home = join_base_dir(".config", legacy=legacy)
+        # Try to use XDG_CONFIG_HOME instead if BORG_BASE_DIR isn't explicitly set
+        if not os.environ.get("BORG_BASE_DIR"):
+            config_home = os.environ.get("XDG_CONFIG_HOME", config_home)
+        # Use BORG_CONFIG_DIR if set, otherwise assemble final path from config home path
+        config_dir = os.environ.get("BORG_CONFIG_DIR", os.path.join(config_home, "borg"))
+    else:
+        config_dir = os.environ.get(
+            "BORG_CONFIG_DIR", join_base_dir(".config", legacy=legacy) or platformdirs.user_config_dir("borg")
+        )
+
     # Create path if it doesn't exist yet
     ensure_dir(config_dir)
     return config_dir


Test patch:
diff --git a/conftest.py b/conftest.py
index 1c08cbbd..362ee0de 100644
--- a/conftest.py
+++ b/conftest.py
@@ -20,13 +20,12 @@
 
 @pytest.fixture(autouse=True)
 def clean_env(tmpdir_factory, monkeypatch):
-    # avoid that we access / modify the user's normal .config / .cache directory:
-    monkeypatch.setenv("XDG_CONFIG_HOME", str(tmpdir_factory.mktemp("xdg-config-home")))
-    monkeypatch.setenv("XDG_CACHE_HOME", str(tmpdir_factory.mktemp("xdg-cache-home")))
     # also avoid to use anything from the outside environment:
     keys = [key for key in os.environ if key.startswith("BORG_") and key not in ("BORG_FUSE_IMPL",)]
     for key in keys:
         monkeypatch.delenv(key, raising=False)
+    # avoid that we access / modify the user's normal .config / .cache directory:
+    monkeypatch.setenv("BORG_BASE_DIR", str(tmpdir_factory.mktemp("borg-base-dir")))
     # Speed up tests
     monkeypatch.setenv("BORG_TESTONLY_WEAKEN_KDF", "1")
 
diff --git a/src/borg/testsuite/helpers.py b/src/borg/testsuite/helpers.py
index 5cf1900d..70cc6139 100644
--- a/src/borg/testsuite/helpers.py
+++ b/src/borg/testsuite/helpers.py
@@ -45,7 +45,7 @@
 from ..helpers import safe_unlink
 from ..helpers import text_to_json, binary_to_json
 from ..helpers.passphrase import Passphrase, PasswordRetriesExceeded
-from ..platform import is_cygwin
+from ..platform import is_cygwin, is_win32, is_darwin
 
 from . import BaseTestCase, FakeInputs, are_hardlinks_supported
 
@@ -584,60 +584,150 @@ def test_get_base_dir(monkeypatch):
     monkeypatch.delenv("BORG_BASE_DIR", raising=False)
     monkeypatch.delenv("HOME", raising=False)
     monkeypatch.delenv("USER", raising=False)
-    assert get_base_dir() == os.path.expanduser("~")
+    assert get_base_dir(legacy=True) == os.path.expanduser("~")
     monkeypatch.setenv("USER", "root")
-    assert get_base_dir() == os.path.expanduser("~root")
+    assert get_base_dir(legacy=True) == os.path.expanduser("~root")
     monkeypatch.setenv("HOME", "/var/tmp/home")
-    assert get_base_dir() == "/var/tmp/home"
+    assert get_base_dir(legacy=True) == "/var/tmp/home"
     monkeypatch.setenv("BORG_BASE_DIR", "/var/tmp/base")
-    assert get_base_dir() == "/var/tmp/base"
+    assert get_base_dir(legacy=True) == "/var/tmp/base"
+    # non-legacy is much easier:
+    monkeypatch.delenv("BORG_BASE_DIR", raising=False)
+    assert get_base_dir(legacy=False) is None
+    monkeypatch.setenv("BORG_BASE_DIR", "/var/tmp/base")
+    assert get_base_dir(legacy=False) == "/var/tmp/base"
+
+
+def test_get_base_dir_compat(monkeypatch):
+    """test that it works the same for legacy and for non-legacy implementation"""
+    monkeypatch.delenv("BORG_BASE_DIR", raising=False)
+    # old way: if BORG_BASE_DIR is not set, make something up with HOME/USER/~
+    # new way: if BORG_BASE_DIR is not set, return None and let caller deal with it.
+    assert get_base_dir(legacy=False) is None
+    # new and old way: BORG_BASE_DIR overrides all other "base path determination".
+    monkeypatch.setenv("BORG_BASE_DIR", "/var/tmp/base")
+    assert get_base_dir(legacy=False) == get_base_dir(legacy=True)
 
 
 def test_get_config_dir(monkeypatch):
     """test that get_config_dir respects environment"""
-    monkeypatch.delenv("BORG_CONFIG_DIR", raising=False)
-    monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
-    assert get_config_dir() == os.path.join(os.path.expanduser("~"), ".config", "borg")
-    monkeypatch.setenv("XDG_CONFIG_HOME", "/var/tmp/.config")
-    assert get_config_dir() == os.path.join("/var/tmp/.config", "borg")
-    monkeypatch.setenv("BORG_CONFIG_DIR", "/var/tmp")
-    assert get_config_dir() == "/var/tmp"
+    monkeypatch.delenv("BORG_BASE_DIR", raising=False)
+    home_dir = os.path.expanduser("~")
+    if is_win32:
+        monkeypatch.delenv("BORG_CONFIG_DIR", raising=False)
+        assert get_config_dir() == os.path.join(home_dir, "AppData", "Local", "borg", "borg")
+        monkeypatch.setenv("BORG_CONFIG_DIR", home_dir)
+        assert get_config_dir() == home_dir
+    elif is_darwin:
+        monkeypatch.delenv("BORG_CONFIG_DIR", raising=False)
+        assert get_config_dir() == os.path.join(home_dir, "Library", "Application Support", "borg")
+        monkeypatch.setenv("BORG_CONFIG_DIR", "/var/tmp")
+        assert get_config_dir() == "/var/tmp"
+    else:
+        monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
+        monkeypatch.delenv("BORG_CONFIG_DIR", raising=False)
+        assert get_config_dir() == os.path.join(home_dir, ".config", "borg")
+        monkeypatch.setenv("XDG_CONFIG_HOME", "/var/tmp/.config")
+        assert get_config_dir() == os.path.join("/var/tmp/.config", "borg")
+        monkeypatch.setenv("BORG_CONFIG_DIR", "/var/tmp")
+        assert get_config_dir() == "/var/tmp"
+
+
+def test_get_config_dir_compat(monkeypatch):
+    """test that it works the same for legacy and for non-legacy implementation"""
+    monkeypatch.delenv("BORG_BASE_DIR", raising=False)
+    if not is_darwin and not is_win32:
+        monkeypatch.delenv("BORG_CONFIG_DIR", raising=False)
+        monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
+        # fails on macOS: assert '/Users/tw/Library/Application Support/borg' == '/Users/tw/.config/borg'
+        # fails on win32 MSYS2 (but we do not need legacy compat there).
+        assert get_config_dir(legacy=False) == get_config_dir(legacy=True)
+    if not is_darwin and not is_win32:
+        monkeypatch.setenv("XDG_CONFIG_HOME", "/var/tmp/.config1")
+        # fails on macOS: assert '/Users/tw/Library/Application Support/borg' == '/var/tmp/.config1/borg'
+        # fails on win32 MSYS2 (but we do not need legacy compat there).
+        assert get_config_dir(legacy=False) == get_config_dir(legacy=True)
+    monkeypatch.setenv("BORG_CONFIG_DIR", "/var/tmp/.config2")
+    assert get_config_dir(legacy=False) == get_config_dir(legacy=True)
 
 
 def test_get_cache_dir(monkeypatch):
     """test that get_cache_dir respects environment"""
-    monkeypatch.delenv("BORG_CACHE_DIR", raising=False)
-    monkeypatch.delenv("XDG_CACHE_HOME", raising=False)
-    assert get_cache_dir() == os.path.join(os.path.expanduser("~"), ".cache", "borg")
-    monkeypatch.setenv("XDG_CACHE_HOME", "/var/tmp/.cache")
-    assert get_cache_dir() == os.path.join("/var/tmp/.cache", "borg")
-    monkeypatch.setenv("BORG_CACHE_DIR", "/var/tmp")
-    assert get_cache_dir() == "/var/tmp"
+    monkeypatch.delenv("BORG_BASE_DIR", raising=False)
+    home_dir = os.path.expanduser("~")
+    if is_win32:
+        monkeypatch.delenv("BORG_CACHE_DIR", raising=False)
+        assert get_cache_dir() == os.path.join(home_dir, "AppData", "Local", "borg", "borg", "Cache")
+        monkeypatch.setenv("BORG_CACHE_DIR", home_dir)
+        assert get_cache_dir() == home_dir
+    elif is_darwin:
+        monkeypatch.delenv("BORG_CACHE_DIR", raising=False)
+        assert get_cache_dir() == os.path.join(home_dir, "Library", "Caches", "borg")
+        monkeypatch.setenv("BORG_CACHE_DIR", "/var/tmp")
+        assert get_cache_dir() == "/var/tmp"
+    else:
+        monkeypatch.delenv("XDG_CACHE_HOME", raising=False)
+        monkeypatch.delenv("BORG_CACHE_DIR", raising=False)
+        assert get_cache_dir() == os.path.join(home_dir, ".cache", "borg")
+        monkeypatch.setenv("XDG_CACHE_HOME", "/var/tmp/.cache")
+        assert get_cache_dir() == os.path.join("/var/tmp/.cache", "borg")
+        monkeypatch.setenv("BORG_CACHE_DIR", "/var/tmp")
+        assert get_cache_dir() == "/var/tmp"
 
 
 def test_get_keys_dir(monkeypatch):
     """test that get_keys_dir respects environment"""
-    monkeypatch.delenv("BORG_KEYS_DIR", raising=False)
-    monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
-    assert get_keys_dir() == os.path.join(os.path.expanduser("~"), ".config", "borg", "keys")
-    monkeypatch.setenv("XDG_CONFIG_HOME", "/var/tmp/.config")
-    assert get_keys_dir() == os.path.join("/var/tmp/.config", "borg", "keys")
-    monkeypatch.setenv("BORG_KEYS_DIR", "/var/tmp")
-    assert get_keys_dir() == "/var/tmp"
+    monkeypatch.delenv("BORG_BASE_DIR", raising=False)
+    home_dir = os.path.expanduser("~")
+    if is_win32:
+        monkeypatch.delenv("BORG_KEYS_DIR", raising=False)
+        assert get_keys_dir() == os.path.join(home_dir, "AppData", "Local", "borg", "borg", "keys")
+        monkeypatch.setenv("BORG_KEYS_DIR", home_dir)
+        assert get_keys_dir() == home_dir
+    elif is_darwin:
+        monkeypatch.delenv("BORG_KEYS_DIR", raising=False)
+        assert get_keys_dir() == os.path.join(home_dir, "Library", "Application Support", "borg", "keys")
+        monkeypatch.setenv("BORG_KEYS_DIR", "/var/tmp")
+        assert get_keys_dir() == "/var/tmp"
+    else:
+        monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
+        monkeypatch.delenv("BORG_KEYS_DIR", raising=False)
+        assert get_keys_dir() == os.path.join(home_dir, ".config", "borg", "keys")
+        monkeypatch.setenv("XDG_CONFIG_HOME", "/var/tmp/.config")
+        assert get_keys_dir() == os.path.join("/var/tmp/.config", "borg", "keys")
+        monkeypatch.setenv("BORG_KEYS_DIR", "/var/tmp")
+        assert get_keys_dir() == "/var/tmp"
 
 
 def test_get_security_dir(monkeypatch):
     """test that get_security_dir respects environment"""
-    monkeypatch.delenv("BORG_SECURITY_DIR", raising=False)
-    monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
-    assert get_security_dir() == os.path.join(os.path.expanduser("~"), ".config", "borg", "security")
-    assert get_security_dir(repository_id="1234") == os.path.join(
-        os.path.expanduser("~"), ".config", "borg", "security", "1234"
-    )
-    monkeypatch.setenv("XDG_CONFIG_HOME", "/var/tmp/.config")
-    assert get_security_dir() == os.path.join("/var/tmp/.config", "borg", "security")
-    monkeypatch.setenv("BORG_SECURITY_DIR", "/var/tmp")
-    assert get_security_dir() == "/var/tmp"
+    monkeypatch.delenv("BORG_BASE_DIR", raising=False)
+    home_dir = os.path.expanduser("~")
+    if is_win32:
+        monkeypatch.delenv("BORG_SECURITY_DIR", raising=False)
+        assert get_security_dir() == os.path.join(home_dir, "AppData", "Local", "borg", "borg", "security")
+        assert get_security_dir(repository_id="1234") == os.path.join(
+            home_dir, "AppData", "Local", "borg", "borg", "security", "1234"
+        )
+        monkeypatch.setenv("BORG_SECURITY_DIR", home_dir)
+        assert get_security_dir() == home_dir
+    elif is_darwin:
+        monkeypatch.delenv("BORG_SECURITY_DIR", raising=False)
+        assert get_security_dir() == os.path.join(home_dir, "Library", "Application Support", "borg", "security")
+        assert get_security_dir(repository_id="1234") == os.path.join(
+            home_dir, "Library", "Application Support", "borg", "security", "1234"
+        )
+        monkeypatch.setenv("BORG_SECURITY_DIR", "/var/tmp")
+        assert get_security_dir() == "/var/tmp"
+    else:
+        monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
+        monkeypatch.delenv("BORG_SECURITY_DIR", raising=False)
+        assert get_security_dir() == os.path.join(home_dir, ".config", "borg", "security")
+        assert get_security_dir(repository_id="1234") == os.path.join(home_dir, ".config", "borg", "security", "1234")
+        monkeypatch.setenv("XDG_CONFIG_HOME", "/var/tmp/.config")
+        assert get_security_dir() == os.path.join("/var/tmp/.config", "borg", "security")
+        monkeypatch.setenv("BORG_SECURITY_DIR", "/var/tmp")
+        assert get_security_dir() == "/var/tmp"
 
 
 def test_file_size():