diff --git a/src/borg/platform/__init__.py b/src/borg/platform/__init__.py index 0929e8a2d..1f008cd41 100644 --- a/src/borg/platform/__init__.py +++ b/src/borg/platform/__init__.py @@ -24,7 +24,8 @@ elif is_freebsd: # pragma: freebsd only from .freebsd import API_VERSION as OS_API_VERSION from .freebsd import listxattr, getxattr, setxattr from .freebsd import acl_get, acl_set - from .base import set_flags, get_flags + from .freebsd import set_flags + from .base import get_flags from .base import SyncFile from .posix import process_alive, local_pid_alive from .posix import swidth diff --git a/src/borg/platform/freebsd.pyx b/src/borg/platform/freebsd.pyx index 7e9460ce9..66a71bd4c 100644 --- a/src/borg/platform/freebsd.pyx +++ b/src/borg/platform/freebsd.pyx @@ -225,3 +225,71 @@ def acl_set(path, item, numeric_ids=False, fd=None): if ret == 1: _set_acl(path, ACL_TYPE_ACCESS, item, 'acl_access', numeric_ids, fd=fd) _set_acl(path, ACL_TYPE_DEFAULT, item, 'acl_default', numeric_ids, fd=fd) + + +cdef extern from "sys/stat.h": + int chflags(const char *path, unsigned long flags) + int lchflags(const char *path, unsigned long flags) + int fchflags(int fd, unsigned long flags) + +# ---------------------------- +# BSD file flags (FreeBSD) +# ---------------------------- +# Only influence flags that are known to be settable and leave system-managed/read-only flags untouched. +# We express the mask in terms of names to avoid hard failures if a constant does +# not exist on a given FreeBSD version; missing names simply contribute 0. +import stat as stat_mod + +SETTABLE_FLAG_NAMES = ( + # Owner-settable (UF_*) + 'UF_NODUMP', + 'UF_IMMUTABLE', + 'UF_APPEND', + 'UF_OPAQUE', + 'UF_NOUNLINK', + 'UF_HIDDEN', + # Super-user-only (SF_*) + 'SF_ARCHIVED', + 'SF_IMMUTABLE', + 'SF_APPEND', + 'SF_NOUNLINK', +) + +cdef unsigned long SETTABLE_FLAGS_MASK = 0 +for _name in SETTABLE_FLAG_NAMES: + # getattr(..., 0) keeps this importable when flags are missing on some FreeBSD versions + SETTABLE_FLAGS_MASK |= getattr(stat_mod, _name, 0) + + +def set_flags(path, bsd_flags, fd=None): + """Set a subset of BSD file flags on FreeBSD without disturbing other bits. + + Only flags that are known to be settable are influenced. All other flag bits are preserved as-is. + """ + # Determine current flags. + try: + if fd is not None: + st = os.fstat(fd) + else: + st = os.lstat(path) + current = st.st_flags + except (OSError, AttributeError): + # We can't determine the current flags, so better give up than corrupting anything. + return + + new_flags = (current & ~SETTABLE_FLAGS_MASK) | (bsd_flags & SETTABLE_FLAGS_MASK) + + cdef unsigned long c_flags = new_flags + if fd is not None: + if fchflags(fd, c_flags) == -1: + err = errno.errno + # Some filesystems may not support flags; ignore EOPNOTSUPP quietly. + if err != errno.EOPNOTSUPP: + # Keep error signature consistent with other platforms; st may not exist here. + raise OSError(err, os.strerror(err), path) + else: + path_bytes = os.fsencode(path) + if lchflags(path_bytes, c_flags) == -1: + err = errno.errno + if err != errno.EOPNOTSUPP: + raise OSError(err, os.strerror(err), os.fsdecode(path_bytes)) diff --git a/src/borg/testsuite/archiver/extract_cmd_test.py b/src/borg/testsuite/archiver/extract_cmd_test.py index 918a40a30..a259bde0e 100644 --- a/src/borg/testsuite/archiver/extract_cmd_test.py +++ b/src/borg/testsuite/archiver/extract_cmd_test.py @@ -16,7 +16,7 @@ from ...helpers import flags_noatime, flags_normal from .. import changedir, same_ts_ns from .. import are_symlinks_supported, are_hardlinks_supported, is_utime_fully_supported, is_birthtime_fully_supported from ...platform import get_birthtime_ns -from ...platformflags import is_darwin, is_win32 +from ...platformflags import is_darwin, is_freebsd, is_win32 from . import ( RK_ENCRYPTION, requires_hardlinks, @@ -608,7 +608,7 @@ def test_extract_xattrs_resourcefork(archivers, request): # assert atime_extracted == atime_expected # still broken, but not really important. -@pytest.mark.skipif(not is_darwin, reason="only for macOS") +@pytest.mark.skipif(not (is_darwin or is_freebsd), reason="only for macOS or FreeBSD") def test_extract_restores_append_flag(archivers, request): archiver = request.getfixturevalue(archivers) # create a file and set the append flag on it