Browse Source

extract: fs flags: use get/set to influence only specific flags, #9090, FreeBSD only.

Thomas Waldmann 4 days ago
parent
commit
8a895f55ae
2 changed files with 69 additions and 0 deletions
  1. 1 0
      src/borg/platform/__init__.py
  2. 68 0
      src/borg/platform/freebsd.pyx

+ 1 - 0
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 API_VERSION as OS_API_VERSION
     from .freebsd import listxattr, getxattr, setxattr
     from .freebsd import listxattr, getxattr, setxattr
     from .freebsd import acl_get, acl_set
     from .freebsd import acl_get, acl_set
+    from .freebsd import set_flags
 elif is_darwin:  # pragma: darwin only
 elif is_darwin:  # pragma: darwin only
     from .darwin import API_VERSION as OS_API_VERSION
     from .darwin import API_VERSION as OS_API_VERSION
     from .darwin import listxattr, getxattr, setxattr
     from .darwin import listxattr, getxattr, setxattr

+ 68 - 0
src/borg/platform/freebsd.pyx

@@ -222,3 +222,71 @@ def acl_set(path, item, numeric_ids=False, fd=None):
     if ret == 1:
     if ret == 1:
         _set_acl(path, ACL_TYPE_ACCESS, item, 'acl_access', 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)
         _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 |= <unsigned long> 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 = <unsigned long> 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))