mirror of https://github.com/borgbackup/borg.git
132 lines
5.5 KiB
Python
132 lines
5.5 KiB
Python
"""A basic extended attributes (xattr) implementation for Linux, FreeBSD and macOS."""
|
|
|
|
import errno
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
from packaging.version import parse as parse_version
|
|
|
|
from .helpers import prepare_subprocess_env
|
|
|
|
from .logger import create_logger
|
|
|
|
logger = create_logger()
|
|
|
|
from .platform import listxattr, getxattr, setxattr, ENOATTR
|
|
|
|
# If we are running with fakeroot on Linux, then use the xattr functions of fakeroot. This is needed by
|
|
# the 'test_extract_capabilities' test, but also allows xattrs to work with fakeroot on Linux in normal use.
|
|
# TODO: Check whether fakeroot supports xattrs on all platforms supported below.
|
|
# TODO: If that's the case then we can make Borg fakeroot-xattr-compatible on these as well.
|
|
XATTR_FAKEROOT = False
|
|
if sys.platform.startswith("linux"):
|
|
LD_PRELOAD = os.environ.get("LD_PRELOAD", "")
|
|
preloads = re.split("[ :]", LD_PRELOAD)
|
|
for preload in preloads:
|
|
if preload.startswith("libfakeroot"):
|
|
env = prepare_subprocess_env(system=True)
|
|
fakeroot_output = subprocess.check_output(["fakeroot", "-v"], env=env)
|
|
fakeroot_version = parse_version(fakeroot_output.decode("ascii").split()[-1])
|
|
if fakeroot_version >= parse_version("1.20.2"):
|
|
# 1.20.2 has been confirmed to have xattr support
|
|
# 1.18.2 has been confirmed not to have xattr support
|
|
# Versions in-between are unknown
|
|
XATTR_FAKEROOT = True
|
|
break
|
|
|
|
|
|
def is_enabled(path=None):
|
|
"""Determine if xattr is enabled on the filesystem"""
|
|
with tempfile.NamedTemporaryFile(dir=path, prefix="borg-tmp") as f:
|
|
fd = f.fileno()
|
|
name, value = b"user.name", b"value"
|
|
try:
|
|
setxattr(fd, name, value)
|
|
except OSError:
|
|
return False
|
|
try:
|
|
names = listxattr(fd)
|
|
except OSError:
|
|
return False
|
|
if name not in names:
|
|
return False
|
|
return getxattr(fd, name) == value
|
|
|
|
|
|
def get_all(path, follow_symlinks=False):
|
|
"""
|
|
Return all extended attributes on *path* as a mapping.
|
|
|
|
*path* can either be a path (str or bytes) or an open file descriptor (int).
|
|
*follow_symlinks* indicates whether symlinks should be followed
|
|
and only applies when *path* is not an open file descriptor.
|
|
|
|
The returned mapping maps xattr names (bytes) to values (bytes or None).
|
|
None indicates, as a xattr value, an empty value, i.e. a value of length zero.
|
|
"""
|
|
if isinstance(path, str):
|
|
path = os.fsencode(path)
|
|
result = {}
|
|
try:
|
|
names = listxattr(path, follow_symlinks=follow_symlinks)
|
|
for name in names:
|
|
try:
|
|
# xattr name is a bytes object, we directly use it.
|
|
result[name] = getxattr(path, name, follow_symlinks=follow_symlinks)
|
|
except OSError as e:
|
|
# note: platform.xattr._check has already made a nice exception e with errno, msg, path/fd
|
|
if e.errno in (ENOATTR,): # errors we just ignore silently
|
|
# ENOATTR: a race has happened: xattr names were deleted after list.
|
|
pass
|
|
else: # all others: warn, skip this single xattr name, continue processing other xattrs
|
|
# EPERM: we were not permitted to read this attribute
|
|
# EINVAL: maybe xattr name is invalid or other issue, #6988
|
|
logger.warning("when getting extended attribute %s: %s", name.decode(errors="replace"), str(e))
|
|
except OSError as e:
|
|
if e.errno in (errno.ENOTSUP, errno.EPERM):
|
|
# if xattrs are not supported on the filesystem, we give up.
|
|
# EPERM might be raised by listxattr.
|
|
pass
|
|
else:
|
|
raise
|
|
return result
|
|
|
|
|
|
def set_all(path, xattrs, follow_symlinks=False):
|
|
"""
|
|
Set all extended attributes on *path* from a mapping.
|
|
|
|
*path* can either be a path (str or bytes) or an open file descriptor (int).
|
|
*follow_symlinks* indicates whether symlinks should be followed
|
|
and only applies when *path* is not an open file descriptor.
|
|
*xattrs* is mapping maps xattr names (bytes) to values (bytes or None).
|
|
None indicates, as a xattr value, an empty value, i.e. a value of length zero.
|
|
|
|
Return warning status (True means a non-fatal exception has happened and was dealt with).
|
|
"""
|
|
if isinstance(path, str):
|
|
path = os.fsencode(path)
|
|
warning = False
|
|
for k, v in xattrs.items():
|
|
try:
|
|
setxattr(path, k, v, follow_symlinks=follow_symlinks)
|
|
except OSError as e:
|
|
# note: platform.xattr._check has already made a nice exception e with errno, msg, path/fd
|
|
warning = True
|
|
if e.errno == errno.E2BIG:
|
|
err_str = "too big for this filesystem (%s)" % str(e)
|
|
elif e.errno == errno.ENOSPC:
|
|
# ext4 reports ENOSPC when trying to set an xattr with >4kiB while ext4 can only support 4kiB xattrs
|
|
# (in this case, this is NOT a "disk full" error, just a ext4 limitation).
|
|
err_str = "fs full or xattr too big? [xattr len = %d] (%s)" % (len(v), str(e))
|
|
else:
|
|
# generic handler
|
|
# EACCES: permission denied to set this specific xattr (this may happen related to security.* keys)
|
|
# EPERM: operation not permitted
|
|
err_str = str(e)
|
|
logger.warning("when setting extended attribute %s: %s", k.decode(errors="replace"), err_str)
|
|
return warning
|