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
5 changes: 5 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ New features:
- archive_dir_format
- numeric_ids

- mount: without a MOUNTPOINT, list the mountpoints of the current borg mounts
(``--json`` for a JSON list), #2579. Linux, macOS, FreeBSD, NetBSD, illumos
and Windows (drive letter mounts only) are supported.
On Linux, borg mounts now have the file system type ``fuse.borgfs``.

Fixes:

- legacy remote: use deques for the RPC call queues (performance fix for
Expand Down
5 changes: 5 additions & 0 deletions docs/usage/mount.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ Examples
$ borg mount -r /path/to/repo /tmp/mymountpoint only/that/path
$ borg mount --exclude '...' /tmp/mymountpoint

# Without a mountpoint, "borg mount" lists the current borg mounts.
$ borg mount
/tmp/mymountpoint
$ borg umount /tmp/mymountpoint


borgfs
++++++
Expand Down
51 changes: 46 additions & 5 deletions src/borg/archiver/mount_cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
from ..helpers import RTError
from ..helpers import PathSpec, FilesystemDirSpec
from ..helpers import location_validator
from ..helpers import umount
from ..helpers import umount, json_print
from ..helpers.argparsing import ArgumentParser
from ..platformflags import is_win32
from .. import platform

from ..logger import create_logger

Expand Down Expand Up @@ -38,9 +39,29 @@ def use_passcommand_mount_option(args):
os.environ["BORG_PASSCOMMAND"] = passcommand


def list_borgfs_mounts():
"""Return the mount table entries of the file systems mounted by borg mount, see platform.list_mounts()."""
from ..vfs import is_borgfs_mount

return [entry for entry in platform.list_mounts() if is_borgfs_mount(entry.source, entry.fstype)]


class MountMixIn:
def do_mount(self, args):
"""Mounts an archive or an entire repository as a FUSE filesystem."""
"""Mounts an archive or an entire repository as a FUSE filesystem, or lists the borg mounts."""
if args.mountpoint is None:
# Just list the mounted borg file systems. This needs neither a repository nor FUSE.
try:
mounts = list_borgfs_mounts()
except OSError as e:
raise RTError(f"listing the borg mounts failed: {e}")
if args.json:
json_print([entry._asdict() for entry in mounts])
else:
for entry in mounts:
print(entry.mountpoint)
return

# Perform these checks before opening the repository and asking for a passphrase.

from ..fuse_impl import llfuse, has_mfusepy, BORG_FUSE_IMPL, fuse_import_errors
Expand Down Expand Up @@ -103,6 +124,13 @@ def build_parser_mount_umount(self, subparsers, common_parser, mid_common_parser
This command mounts a repository or an archive as a FUSE filesystem.
This can be useful for browsing or restoring individual files.

Without a MOUNTPOINT, it lists the mountpoints of all file systems currently
mounted by ``borg mount`` (on Windows: only the mounts on a drive letter, there is
no way to find the directory mountpoints), one per line. ``--json`` gives a JSON
list instead, with the ``mountpoint``, the ``source`` (what the OS reports as
mounted, usually ``borgfs``) and the ``fstype`` (the file system type the OS
reports) of each. This does not need a repository or FUSE.

When restoring, take into account that the current FUSE implementation does
not support special fs flags. On Linux, POSIX ACLs of the archived fs objects
are exposed (read-only) via the ``system.posix_acl_access`` and
Expand Down Expand Up @@ -288,9 +316,22 @@ def _define_borg_mount(self, parser, borgfs=False):
default=None,
help="repository to mount (default: as given by -r/--repo or BORG_REPO)",
)
parser.add_argument(
"mountpoint", metavar="MOUNTPOINT", type=FilesystemDirSpec, help="where to mount the filesystem"
)
if borgfs:
parser.add_argument(
"mountpoint", metavar="MOUNTPOINT", type=FilesystemDirSpec, help="where to mount the filesystem"
)
else:
parser.add_argument(
"mountpoint",
metavar="MOUNTPOINT",
nargs="?",
type=FilesystemDirSpec,
default=None,
help="where to mount the filesystem (default: list the mountpoints of the current borg mounts)",
)
parser.add_argument(
"--json", action="store_true", help="format the list of the current borg mounts as JSON"
)
parser.add_argument(
"-f", "--foreground", dest="foreground", action="store_true", help="stay in foreground, do not daemonize"
)
Expand Down
7 changes: 7 additions & 0 deletions src/borg/platform/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .base import get_process_id, get_hostname, get_fqdn, get_hostid, swidth
from .base import acl_text_to_xattr # overridden below for platforms supporting it
from .base import set_times # overridden below for win32
from .base import list_mounts, MountEntry # list_mounts is overridden below for the platforms supporting it


# work around pyinstaller "forgetting" to include the xattr module
Expand All @@ -31,6 +32,7 @@
from .posix import get_errno
from .posix import getosusername
from . import posix_ug as platform_ug
from .linux import list_mounts # type: ignore[no-redef]
elif is_freebsd: # pragma: freebsd only
from .freebsd import listxattr, getxattr, setxattr
from .freebsd import acl_get, acl_set
Expand All @@ -42,6 +44,7 @@
from .posix import get_errno
from .posix import getosusername
from . import posix_ug as platform_ug
from .freebsd import list_mounts # type: ignore[no-redef]
elif is_netbsd: # pragma: netbsd only
from .netbsd import listxattr, getxattr, setxattr
from .base import acl_get, acl_set
Expand All @@ -52,6 +55,7 @@
from .posix import get_errno
from .posix import getosusername
from . import posix_ug as platform_ug
from .netbsd import list_mounts # type: ignore[no-redef]
elif is_darwin: # pragma: darwin only
from .darwin import listxattr, getxattr, setxattr
from .darwin import acl_get, acl_set
Expand All @@ -65,6 +69,7 @@
from .posix import get_errno
from .posix import getosusername
from . import posix_ug as platform_ug
from .darwin import list_mounts # type: ignore[no-redef]
elif is_sunos: # pragma: sunos only
from .solaris import listxattr, getxattr, setxattr
from .base import acl_get, acl_set
Expand All @@ -75,6 +80,7 @@
from .posix import get_errno
from .posix import getosusername
from . import posix_ug as platform_ug
from .solaris import list_mounts # type: ignore[no-redef]
elif not is_win32: # pragma: posix only
# Generic code for all other POSIX OSes
from .base import listxattr, getxattr, setxattr
Expand All @@ -96,6 +102,7 @@
from .windows import set_times # type: ignore[no-redef]
from .windows import process_alive, local_pid_alive
from .windows import getosusername
from .windows import list_mounts # type: ignore[no-redef]
from . import windows_ug as platform_ug


Expand Down
35 changes: 35 additions & 0 deletions src/borg/platform/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
import unicodedata
import uuid
from pathlib import Path
from typing import NamedTuple

from ..helpers import safe_unlink
from ..logger import create_logger
from ..platformflags import is_win32

logger = create_logger()

"""
platform base module
====================
Expand Down Expand Up @@ -162,6 +166,37 @@ def on_different_mounts(fd1, fd2):
return os.fstat(fd1).st_dev != os.fstat(fd2).st_dev


class MountEntry(NamedTuple):
"""An entry of the mount table, see list_mounts()."""

source: str # what is mounted ("device"), e.g. /dev/sda1, borgfs, /dev/puffs; on Windows: the volume label
fstype: str # file system type, e.g. ext4, fuse.borgfs, macfuse, fusefs, puffs|borgfs; on Windows: FUSE, NTFS
mountpoint: str # where it is mounted; on Windows: the drive, e.g. X:


def list_mounts():
"""
Return the mount table: a list of MountEntry (source, fstype, mountpoint) tuples, one per
mounted file system, as the OS reports them. The platform modules implement this via
/proc/self/mountinfo (Linux), /etc/mnttab (illumos), getmntinfo() (macOS, FreeBSD),
getvfsstat() (NetBSD) or the drive letters (Windows: only mounts on a drive letter are found,
a file system mounted on a directory is not).

How a FUSE file system mounted with ``-o fsname=borgfs`` looks like per platform (measured):

- Linux: source ``borgfs``, fstype ``fuse`` (``fuse.borgfs`` with the ``subtype=borgfs`` option)
- macOS (macFUSE): source ``borgfs``, fstype ``macfuse``
- FreeBSD: source ``borgfs``, fstype ``fusefs``
- NetBSD (librefuse / puffs): source ``/dev/puffs``, fstype ``puffs|borgfs``
- illumos (libfuse): source ``borgfs``, fstype ``fuse``
- Windows (WinFsp): the fs name is ``FUSE``, the volume label comes from the ``volname`` option

Not supported on this platform: returns an empty list after warning about it.
"""
logger.warning("Listing the mounted file systems is not supported on this platform.")
return []


def set_times(path, *, atime_ns, mtime_ns, birthtime_ns=None, fd=None, follow_symlinks=True):
"""
Set the timestamps of *path* (or of the open file descriptor *fd*, if given).
Expand Down
27 changes: 27 additions & 0 deletions src/borg/platform/darwin.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ from libc cimport errno
from posix.time cimport timespec

from . import posix_ug
from .base import MountEntry
from ..helpers import safe_decode, safe_encode
from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_string0

Expand All @@ -22,6 +23,16 @@ cdef extern from *:

is_darwin_feature_64_bit_inode = DARWIN_FEATURE_64_BIT_INODE_DEFINED != 0

cdef extern from "sys/mount.h":
# only the members we use (the header defines the struct; with 64-bit inodes - the default on
# current SDKs - getmntinfo maps to getmntinfo$INODE64 on x86_64, the header takes care of that)
cdef struct statfs:
char f_fstypename[16] # MFSTYPENAMELEN
char f_mntonname[1024] # MAXPATHLEN
char f_mntfromname[1024] # MAXPATHLEN
int getmntinfo(statfs **mntbufp, int mode)
int MNT_NOWAIT

cdef extern from "sys/xattr.h":
ssize_t c_listxattr "listxattr" (const char *path, char *list, size_t size, int flags)
ssize_t c_flistxattr "flistxattr" (int filedes, char *list, size_t size, int flags)
Expand Down Expand Up @@ -285,3 +296,19 @@ def sync_dir(path):
raise
finally:
os.close(fd)


def list_mounts():
"""See platform.base.list_mounts, macOS implementation via getmntinfo(3)."""
cdef statfs *mntbuf
# getmntinfo points mntbuf to an array (allocated by libc, not to be freed) of count struct statfs.
# MNT_NOWAIT: do not ask the file systems for fresh statistics, we only need the names.
cdef int count = getmntinfo(&mntbuf, MNT_NOWAIT)
if count <= 0:
raise OSError(errno.errno, "getmntinfo failed")
return [
MountEntry(
os.fsdecode(mntbuf[i].f_mntfromname), os.fsdecode(mntbuf[i].f_fstypename), os.fsdecode(mntbuf[i].f_mntonname)
)
for i in range(count)
]
26 changes: 26 additions & 0 deletions src/borg/platform/freebsd.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,21 @@ import stat
from libc cimport errno

from .posix import posix_acl_use_stored_uid_gid
from .base import MountEntry
from ..helpers import safe_encode, safe_decode
from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_lstring



cdef extern from "sys/mount.h":
# only the members we use, the header defines the struct
cdef struct statfs:
char f_fstypename[16] # MFSNAMELEN
char f_mntfromname[1024] # MNAMELEN
char f_mntonname[1024] # MNAMELEN
int getmntinfo(statfs **mntbufp, int mode)
int MNT_NOWAIT

cdef extern from "sys/extattr.h":
ssize_t c_extattr_list_file "extattr_list_file" (const char *path, int attrnamespace, void *data, size_t nbytes)
ssize_t c_extattr_list_link "extattr_list_link" (const char *path, int attrnamespace, void *data, size_t nbytes)
Expand Down Expand Up @@ -283,3 +293,19 @@ def set_flags(path, bsd_flags, fd=None):
mask = OWNER_SETTABLE_FLAGS_MASK
continue
raise OSError(err, os.strerror(err), path)


def list_mounts():
"""See platform.base.list_mounts, FreeBSD implementation via getmntinfo(3)."""
cdef statfs *mntbuf
# getmntinfo points mntbuf to an array (allocated by libc, not to be freed) of count struct statfs.
# MNT_NOWAIT: do not ask the file systems for fresh statistics, we only need the names.
cdef int count = getmntinfo(&mntbuf, MNT_NOWAIT)
if count <= 0:
raise OSError(errno.errno, "getmntinfo failed")
return [
MountEntry(
os.fsdecode(mntbuf[i].f_mntfromname), os.fsdecode(mntbuf[i].f_fstypename), os.fsdecode(mntbuf[i].f_mntonname)
)
for i in range(count)
]
48 changes: 47 additions & 1 deletion src/borg/platform/linux.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ from . import posix_ug
from ..helpers import workarounds
from ..helpers import safe_decode, safe_encode
from .base import SyncFile as BaseSyncFile
from .base import safe_fadvise
from .base import safe_fadvise, MountEntry
from .xattr import _listxattr_inner, _getxattr_inner, _setxattr_inner, split_string0
try:
from .syncfilerange import sync_file_range, SYNC_FILE_RANGE_WRITE, SYNC_FILE_RANGE_WAIT_BEFORE, SYNC_FILE_RANGE_WAIT_AFTER
Expand Down Expand Up @@ -502,3 +502,49 @@ else:
# tell the OS that it does not need to cache what we just wrote,
# avoids spoiling the cache for the OS and other processes.
safe_fadvise(self.fd, 0, 0, 'DONTNEED')


_MOUNTINFO_ESCAPE_RE = re.compile(rb"\\([0-7]{3})")


def _unescape_mountinfo(field):
# /proc/self/mountinfo escapes space, tab, newline and backslash as \040, \011, \012 and \134.
# It escapes bytes, so unescape bytes and decode the result, not the other way round.
return os.fsdecode(_MOUNTINFO_ESCAPE_RE.sub(lambda m: bytes([int(m.group(1), 8)]), field))


def parse_mountinfo(lines):
"""Parse the lines of /proc/self/mountinfo (bytes), see proc_pid_mountinfo(5).

Each line is: mount ID, parent ID, major:minor, root, mountpoint, mount options, zero or
more optional fields, a lone "-", fstype, source, super options. As the number of optional
fields varies, the fields after the separator are found from the separator, not from the start.
"""
entries = []
for line in lines:
fields = line.split()
try:
sep = fields.index(b"-")
except ValueError:
continue # not a mountinfo line
if sep < 5 or len(fields) < sep + 3:
continue
mountpoint, fstype, source = fields[4], fields[sep + 1], fields[sep + 2]
entries.append(MountEntry(_unescape_mountinfo(source), os.fsdecode(fstype), _unescape_mountinfo(mountpoint)))
return entries


MOUNTINFO = "/proc/self/mountinfo"


def list_mounts():
"""See platform.base.list_mounts, Linux implementation via /proc/self/mountinfo.

Raises OSError if that file cannot be read (e.g. because /proc is not mounted).
"""
try:
with open(MOUNTINFO, "rb") as f:
lines = f.read().splitlines()
except OSError as e:
raise OSError(e.errno, f"cannot read {MOUNTINFO} (is /proc mounted?): {e.strerror}") from e
return parse_mountinfo(lines)
Loading
Loading