mirror of
https://github.com/borgbackup/borg.git
synced 2026-10-12 02:11:36 +00:00
Merge pull request #10505 from ThomasWaldmann/mount-opt-strip-numeric
mount: add strip_components, numeric_ids and archive_dir_format mount options, fixes #4880
This commit is contained in:
@@ -23,6 +23,9 @@ Examples
|
||||
root-2016-02-14 root-2016-02-15
|
||||
$ borg umount /tmp/mymountpoint
|
||||
|
||||
# The archive_dir_format mount option does the same, e.g. for fstab / autofs entries:
|
||||
$ borg mount -o 'archive_dir_format={name}-{time:%Y-%m-%d}' /tmp/mymountpoint
|
||||
|
||||
# The "versions view" merges all archives in the repository
|
||||
# and provides a versioned view on files.
|
||||
$ borg mount -o versions /tmp/mymountpoint
|
||||
|
||||
@@ -991,7 +991,8 @@ class HelpMixIn:
|
||||
Giving the format of the archive directory names when ``borg mount`` or
|
||||
``borg webdav`` show a whole repository, default: ``{name}``. The placeholders
|
||||
are the ones of ``borg repo-list --format``; names that are not unique get
|
||||
``-{id:.8}`` appended. See ``borg mount --help``.
|
||||
``-{id:.8}`` appended. ``borg mount`` also has an ``archive_dir_format`` mount
|
||||
option for this, which overrides this variable. See ``borg mount --help``.
|
||||
BORG_JSON_INDENT
|
||||
Indentation of the ``--json`` output (default: ``4``).
|
||||
A number gives that many spaces per nesting level (``0`` still puts every item on
|
||||
|
||||
@@ -93,10 +93,10 @@ class MountMixIn:
|
||||
By default, these top directories are named like the archives; as the archives
|
||||
of a series all have the same name, ``-{id:.8}`` (the first 8 hex digits of the
|
||||
archive id) is appended whenever a name is not unique. To name them differently,
|
||||
set the ``BORG_MOUNT_ARCHIVE_DIR_FORMAT`` environment variable to a format string
|
||||
using the placeholders of ``borg repo-list --format``, e.g.
|
||||
``{name}-{time:%Y-%m-%dT%H:%M:%S}`` or ``{hostname}-{name}``; names that are
|
||||
still not unique get ``-{id:.8}`` appended.
|
||||
set the ``BORG_MOUNT_ARCHIVE_DIR_FORMAT`` environment variable (or the
|
||||
``archive_dir_format`` mount option) to a format string using the placeholders
|
||||
of ``borg repo-list --format``, e.g. ``{name}-{time:%Y-%m-%dT%H:%M:%S}`` or
|
||||
``{hostname}-{name}``; names that are still not unique get ``-{id:.8}`` appended.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -142,8 +142,8 @@ class MountMixIn:
|
||||
|
||||
Borg's default behavior is to use the archived user and group names of each
|
||||
file and map them to the system's respective user and group IDs.
|
||||
Alternatively, using ``numeric-ids`` will instead use the archived user and
|
||||
group IDs without any mapping.
|
||||
Alternatively, using ``--numeric-ids`` (or the ``numeric_ids`` mount option)
|
||||
will instead use the archived user and group IDs without any mapping.
|
||||
|
||||
The ``uid`` and ``gid`` mount options (implemented by Borg) can be used to
|
||||
override the user and group IDs of all files (i.e., ``borg mount -o
|
||||
@@ -167,6 +167,14 @@ class MountMixIn:
|
||||
- ``ignore_permissions``: for security reasons the ``default_permissions`` mount
|
||||
option is internally enforced by Borg. ``ignore_permissions`` can be given to
|
||||
not enforce ``default_permissions``.
|
||||
- ``strip_components=NUMBER``: same as ``--strip-components NUMBER``; useful for
|
||||
fstab / autofs entries, which can only give mount options. If both are given,
|
||||
the mount option is used.
|
||||
- ``numeric_ids``: same as ``--numeric-ids``; useful for fstab / autofs entries.
|
||||
``numeric_ids=no`` overrides ``--numeric-ids``.
|
||||
- ``archive_dir_format=FORMAT``: same as ``BORG_MOUNT_ARCHIVE_DIR_FORMAT=FORMAT``;
|
||||
useful for fstab / autofs entries. If both are given, the mount option is used.
|
||||
As mount options are separated by commas, FORMAT can not contain a comma.
|
||||
|
||||
On Windows, ``borg mount`` needs `WinFsp <https://winfsp.dev/>`_ and mfusepy.
|
||||
MOUNTPOINT must either be an unused drive (like ``X:``) or a not yet existing
|
||||
|
||||
@@ -339,6 +339,9 @@ def test_fuse_archive_dir_format(archivers, request, monkeypatch):
|
||||
monkeypatch.setenv("BORG_MOUNT_ARCHIVE_DIR_FORMAT", "{hostname}")
|
||||
with fuse_mount(archiver, mountpoint):
|
||||
assert set(os.listdir(mountpoint)) == {f"{hostname}-{id[:8]}" for name, hostname, id in archives}
|
||||
# the archive_dir_format mount option does the same, it wins over BORG_MOUNT_ARCHIVE_DIR_FORMAT:
|
||||
with fuse_mount(archiver, mountpoint, "-o", "archive_dir_format={name}-{id}"):
|
||||
assert set(os.listdir(mountpoint)) == {f"{name}-{id}" for name, hostname, id in archives}
|
||||
|
||||
|
||||
@pytest.mark.skipif(not has_any_fuse, reason="FUSE not available")
|
||||
|
||||
@@ -130,6 +130,51 @@ def test_parse_mount_options_posix(monkeypatch):
|
||||
assert (vfs_options.uid_forced, vfs_options.gid_forced) == (0, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cli_strip_components, mount_options, expected",
|
||||
[(0, None, 0), (2, None, 2), (0, "strip_components=3", 3), (2, "strip_components=3", 3)],
|
||||
)
|
||||
def test_parse_mount_options_strip_components(cli_strip_components, mount_options, expected):
|
||||
args = MountArgs()
|
||||
args.strip_components = cli_strip_components
|
||||
options, vfs_options = parse_mount_options(args, "/mnt/point", mount_options)
|
||||
# strip_components is implemented by borg, so it is not passed on to libfuse.
|
||||
assert not [option for option in options if option.startswith("strip_components")]
|
||||
assert vfs_options.strip_components == expected
|
||||
# the item filter also uses it: it skips paths with no more than strip_components elements.
|
||||
assert vfs_options.item_filter(Item(path="a/b/c")) == (expected < 3)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cli_numeric_ids, mount_options, expected",
|
||||
[
|
||||
(False, None, False),
|
||||
(True, None, True),
|
||||
(False, "numeric_ids", True),
|
||||
(False, "numeric_ids=yes", True),
|
||||
(True, "numeric_ids=no", False),
|
||||
],
|
||||
)
|
||||
def test_parse_mount_options_numeric_ids(cli_numeric_ids, mount_options, expected):
|
||||
args = MountArgs()
|
||||
args.numeric_ids = cli_numeric_ids
|
||||
options, vfs_options = parse_mount_options(args, "/mnt/point", mount_options)
|
||||
# numeric_ids is implemented by borg, so it is not passed on to libfuse.
|
||||
assert not [option for option in options if option.startswith("numeric_ids")]
|
||||
assert vfs_options.numeric_ids is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mount_options, expected",
|
||||
[(None, None), ("allow_other", None), ("archive_dir_format={name}-{time:%Y-%m-%d}", "{name}-{time:%Y-%m-%d}")],
|
||||
)
|
||||
def test_parse_mount_options_archive_dir_format(mount_options, expected):
|
||||
options, vfs_options = parse_mount_options(MountArgs(), "/mnt/point", mount_options)
|
||||
# archive_dir_format is implemented by borg, so it is not passed on to libfuse.
|
||||
assert not [option for option in options if option.startswith("archive_dir_format")]
|
||||
assert vfs_options.archive_dir_format == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mount_options, expected",
|
||||
[
|
||||
|
||||
+25
-8
@@ -66,7 +66,9 @@ class VFSOptions:
|
||||
*numeric_ids*, *uid_forced*, *gid_forced* and *umask* control the ownership and
|
||||
permissions mapping, *strip_components* and *item_filter* which items are shown,
|
||||
*allow_damaged_files* whether reads of files with missing or corrupted chunks return
|
||||
zeros instead of failing, and *dir_item* is the item used for synthesized directories.
|
||||
zeros instead of failing, *archive_dir_format* how the archive directories are named
|
||||
(None: as given by BORG_MOUNT_ARCHIVE_DIR_FORMAT) and *dir_item* is the item used for
|
||||
synthesized directories.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -80,6 +82,7 @@ class VFSOptions:
|
||||
allow_damaged_files=False,
|
||||
strip_components=0,
|
||||
item_filter=None,
|
||||
archive_dir_format=None,
|
||||
dir_item=None,
|
||||
):
|
||||
self.versions = versions
|
||||
@@ -90,6 +93,7 @@ class VFSOptions:
|
||||
self.allow_damaged_files = allow_damaged_files
|
||||
self.strip_components = strip_components
|
||||
self.item_filter = item_filter
|
||||
self.archive_dir_format = archive_dir_format
|
||||
self.dir_item = dir_item
|
||||
|
||||
|
||||
@@ -263,12 +267,15 @@ class ArchiveVFS:
|
||||
|
||||
def _archive_dir_names(self, archives):
|
||||
"""The root directory names of *archives*, see BORG_MOUNT_ARCHIVE_DIR_FORMAT (borg mount --help)."""
|
||||
format = os.environ.get("BORG_MOUNT_ARCHIVE_DIR_FORMAT", "{name}")
|
||||
if self.options.archive_dir_format is not None:
|
||||
format, source = self.options.archive_dir_format, "archive_dir_format mount option"
|
||||
else:
|
||||
format, source = os.environ.get("BORG_MOUNT_ARCHIVE_DIR_FORMAT", "{name}"), "BORG_MOUNT_ARCHIVE_DIR_FORMAT"
|
||||
try:
|
||||
formatter = ArchiveFormatter(format, self.repository, self.manifest, self.manifest.key)
|
||||
names = [formatter.format_item(archive) for archive in archives]
|
||||
except (CommandError, ValueError) as err: # unknown placeholder, malformed format string / format spec
|
||||
raise Error(f"BORG_MOUNT_ARCHIVE_DIR_FORMAT: {err}") from None
|
||||
raise Error(f"{source}: {err}") from None
|
||||
# "/" and NUL can not be part of a directory name
|
||||
return [name.replace("/", "_").replace("\0", "_") for name in names]
|
||||
|
||||
@@ -645,7 +652,7 @@ def data_cache_capacity():
|
||||
return max(1, capacity)
|
||||
|
||||
|
||||
def build_item_filter(args):
|
||||
def build_item_filter(args, strip_components):
|
||||
"""Build the item filter selecting the paths/patterns given on the command line."""
|
||||
# lazy import: pulling in the archiver package at module import time would be heavy
|
||||
# (it defines all subcommands) and risks an import cycle.
|
||||
@@ -653,7 +660,7 @@ def build_item_filter(args):
|
||||
|
||||
# omitting args.pattern_roots here, restricting to paths only by cli args.paths:
|
||||
matcher = build_matcher(getattr(args, "patterns", None) or [], getattr(args, "paths", None) or [])
|
||||
return build_filter(matcher, getattr(args, "strip_components", 0))
|
||||
return build_filter(matcher, strip_components)
|
||||
|
||||
|
||||
def pop_option(options, key, present, not_present, wanted_type, int_base=0):
|
||||
@@ -731,15 +738,25 @@ def parse_mount_options(args, mountpoint, mount_options):
|
||||
uid_forced = pop_option(options, "uid", None, None, int)
|
||||
gid_forced = pop_option(options, "gid", None, None, int)
|
||||
default_dir_uid, default_dir_gid = os.getuid(), os.getgid()
|
||||
# the strip_components, numeric_ids and archive_dir_format mount options are for fstab / autofs
|
||||
# entries, which can only give mount options.
|
||||
strip_components = pop_option(options, "strip_components", None, None, int)
|
||||
if strip_components is None:
|
||||
strip_components = getattr(args, "strip_components", 0)
|
||||
numeric_ids = pop_option(options, "numeric_ids", True, None, bool)
|
||||
if numeric_ids is None:
|
||||
numeric_ids = getattr(args, "numeric_ids", False)
|
||||
archive_dir_format = pop_option(options, "archive_dir_format", None, None, str)
|
||||
vfs_options = VFSOptions(
|
||||
allow_damaged_files=pop_option(options, "allow_damaged_files", True, False, bool),
|
||||
versions=pop_option(options, "versions", True, False, bool),
|
||||
uid_forced=uid_forced,
|
||||
gid_forced=gid_forced,
|
||||
umask=pop_option(options, "umask", 0, 0, int, int_base=8), # umask is octal, e.g. 222 or 0222
|
||||
numeric_ids=getattr(args, "numeric_ids", False),
|
||||
strip_components=getattr(args, "strip_components", 0),
|
||||
item_filter=build_item_filter(args),
|
||||
numeric_ids=numeric_ids,
|
||||
strip_components=strip_components,
|
||||
item_filter=build_item_filter(args, strip_components),
|
||||
archive_dir_format=archive_dir_format,
|
||||
)
|
||||
dir_uid = vfs_options.uid_forced if vfs_options.uid_forced is not None else default_dir_uid
|
||||
dir_gid = vfs_options.gid_forced if vfs_options.gid_forced is not None else default_dir_gid
|
||||
|
||||
Reference in New Issue
Block a user