From 37792bb4b7b175e85c4921fb886155614d3dfd87 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 31 Oct 2025 00:22:40 +0100 Subject: [PATCH] extract: fs flags: use get/set to influence only specific flags, #9090, FreeBSD only. (cherry picked from commit 8a895f55aef2161addae867c2b14cc477b0cc74a) --- src/borg/platform/__init__.py | 1 + src/borg/platform/freebsd.pyx | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/borg/platform/__init__.py b/src/borg/platform/__init__.py index 98f926230..eccef9762 100644 --- a/src/borg/platform/__init__.py +++ b/src/borg/platform/__init__.py @@ -36,6 +36,7 @@ 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 .freebsd import set_flags elif is_darwin: # pragma: darwin only from .darwin import API_VERSION as OS_API_VERSION from .darwin import listxattr, getxattr, setxattr diff --git a/src/borg/platform/freebsd.pyx b/src/borg/platform/freebsd.pyx index 1acd18185..d2a8d5810 100644 --- a/src/borg/platform/freebsd.pyx +++ b/src/borg/platform/freebsd.pyx @@ -9,6 +9,7 @@ API_VERSION = '1.2_05' cdef extern from "errno.h": int errno int EINVAL + int EOPNOTSUPP 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) @@ -199,3 +200,71 @@ def acl_set(path, item, numeric_ids=False, fd=None): _set_acl(path, ACL_TYPE_NFS4, item, 'acl_nfs4', numeric_ids, fd=fd) _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 != 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 != EOPNOTSUPP: + raise OSError(err, os.strerror(err), os.fsdecode(path_bytes))