浏览代码

Merge pull request #1050 from enkore/feature/linux-bsdflags

Translate Linux fsflags to BSD flags and vice versa
enkore 9 年之前
父节点
当前提交
ac2617f665
共有 10 个文件被更改,包括 132 次插入28 次删除
  1. 6 7
      borg/archive.py
  2. 2 3
      borg/archiver.py
  3. 2 1
      borg/helpers.py
  4. 2 2
      borg/platform.py
  5. 14 0
      borg/platform_base.py
  6. 65 2
      borg/platform_linux.pyx
  7. 19 3
      borg/testsuite/__init__.py
  8. 4 6
      borg/testsuite/archiver.py
  9. 0 4
      borg/testsuite/conftest.py
  10. 18 0
      conftest.py

+ 6 - 7
borg/archive.py

@@ -25,14 +25,13 @@ from .helpers import Chunk, Error, uid2user, user2uid, gid2group, group2gid, \
     CompressionDecider1, CompressionDecider2, CompressionSpec, \
     CompressionDecider1, CompressionDecider2, CompressionSpec, \
     IntegrityError
     IntegrityError
 from .repository import Repository
 from .repository import Repository
-from .platform import acl_get, acl_set
+from .platform import acl_get, acl_set, set_flags, get_flags
 from .chunker import Chunker
 from .chunker import Chunker
 from .hashindex import ChunkIndex, ChunkIndexEntry
 from .hashindex import ChunkIndex, ChunkIndexEntry
 from .cache import ChunkListEntry
 from .cache import ChunkListEntry
 import msgpack
 import msgpack
 
 
 has_lchmod = hasattr(os, 'lchmod')
 has_lchmod = hasattr(os, 'lchmod')
-has_lchflags = hasattr(os, 'lchflags')
 
 
 flags_normal = os.O_RDONLY | getattr(os, 'O_BINARY', 0)
 flags_normal = os.O_RDONLY | getattr(os, 'O_BINARY', 0)
 flags_noatime = flags_normal | getattr(os, 'O_NOATIME', 0)
 flags_noatime = flags_normal | getattr(os, 'O_NOATIME', 0)
@@ -435,10 +434,9 @@ Number of files: {0.stats.nfiles}'''.format(
         else:
         else:
             os.utime(path, None, ns=(atime, mtime), follow_symlinks=False)
             os.utime(path, None, ns=(atime, mtime), follow_symlinks=False)
         acl_set(path, item, self.numeric_owner)
         acl_set(path, item, self.numeric_owner)
-        # Only available on OS X and FreeBSD
-        if has_lchflags and b'bsdflags' in item:
+        if b'bsdflags' in item:
             try:
             try:
-                os.lchflags(path, item[b'bsdflags'])
+                set_flags(path, item[b'bsdflags'], fd=fd)
             except OSError:
             except OSError:
                 pass
                 pass
         # chown removes Linux capabilities, so set the extended attributes at the end, after chown, since they include
         # chown removes Linux capabilities, so set the extended attributes at the end, after chown, since they include
@@ -506,8 +504,9 @@ Number of files: {0.stats.nfiles}'''.format(
         xattrs = xattr.get_all(path, follow_symlinks=False)
         xattrs = xattr.get_all(path, follow_symlinks=False)
         if xattrs:
         if xattrs:
             item[b'xattrs'] = StableDict(xattrs)
             item[b'xattrs'] = StableDict(xattrs)
-        if has_lchflags and st.st_flags:
-            item[b'bsdflags'] = st.st_flags
+        bsdflags = get_flags(path, st)
+        if bsdflags:
+            item[b'bsdflags'] = bsdflags
         acl_get(path, item, st, self.numeric_owner)
         acl_get(path, item, st, self.numeric_owner)
         return item
         return item
 
 

+ 2 - 3
borg/archiver.py

@@ -38,8 +38,7 @@ from .archive import Archive, ArchiveChecker, ArchiveRecreater
 from .remote import RepositoryServer, RemoteRepository, cache_if_remote
 from .remote import RepositoryServer, RemoteRepository, cache_if_remote
 from .selftest import selftest
 from .selftest import selftest
 from .hashindex import ChunkIndexEntry
 from .hashindex import ChunkIndexEntry
-
-has_lchflags = hasattr(os, 'lchflags')
+from .platform import get_flags
 
 
 
 
 def argument(args, str_or_bool):
 def argument(args, str_or_bool):
@@ -316,7 +315,7 @@ class Archiver:
             return
             return
         status = None
         status = None
         # Ignore if nodump flag is set
         # Ignore if nodump flag is set
-        if has_lchflags and (st.st_flags & stat.UF_NODUMP):
+        if get_flags(path, st) & stat.UF_NODUMP:
             return
             return
         if stat.S_ISREG(st.st_mode) or read_special and not stat.S_ISDIR(st.st_mode):
         if stat.S_ISREG(st.st_mode) or read_special and not stat.S_ISDIR(st.st_mode):
             if not dry_run:
             if not dry_run:

+ 2 - 1
borg/helpers.py

@@ -1166,7 +1166,7 @@ class ItemFormatter:
         'NUL': 'NUL character for creating print0 / xargs -0 like ouput, see bpath',
         'NUL': 'NUL character for creating print0 / xargs -0 like ouput, see bpath',
     }
     }
     KEY_GROUPS = (
     KEY_GROUPS = (
-        ('type', 'mode', 'uid', 'gid', 'user', 'group', 'path', 'bpath', 'source', 'linktarget'),
+        ('type', 'mode', 'uid', 'gid', 'user', 'group', 'path', 'bpath', 'source', 'linktarget', 'flags'),
         ('size', 'csize', 'num_chunks', 'unique_chunks'),
         ('size', 'csize', 'num_chunks', 'unique_chunks'),
         ('mtime', 'ctime', 'atime', 'isomtime', 'isoctime', 'isoatime'),
         ('mtime', 'ctime', 'atime', 'isomtime', 'isoctime', 'isoatime'),
         tuple(sorted(hashlib.algorithms_guaranteed)),
         tuple(sorted(hashlib.algorithms_guaranteed)),
@@ -1259,6 +1259,7 @@ class ItemFormatter:
         item_data['source'] = source
         item_data['source'] = source
         item_data['linktarget'] = source
         item_data['linktarget'] = source
         item_data['extra'] = extra
         item_data['extra'] = extra
+        item_data['flags'] = item.get(b'bsdflags')
         for key in self.used_call_keys:
         for key in self.used_call_keys:
             item_data[key] = self.call_keys[key](item)
             item_data[key] = self.call_keys[key](item)
         return item_data
         return item_data

+ 2 - 2
borg/platform.py

@@ -1,9 +1,9 @@
 import sys
 import sys
 
 
-from .platform_base import acl_get, acl_set, SyncFile, sync_dir, API_VERSION
+from .platform_base import acl_get, acl_set, SyncFile, sync_dir, set_flags, get_flags, API_VERSION
 
 
 if sys.platform.startswith('linux'):  # pragma: linux only
 if sys.platform.startswith('linux'):  # pragma: linux only
-    from .platform_linux import acl_get, acl_set, SyncFile, API_VERSION
+    from .platform_linux import acl_get, acl_set, SyncFile, set_flags, get_flags, API_VERSION
 elif sys.platform.startswith('freebsd'):  # pragma: freebsd only
 elif sys.platform.startswith('freebsd'):  # pragma: freebsd only
     from .platform_freebsd import acl_get, acl_set, API_VERSION
     from .platform_freebsd import acl_get, acl_set, API_VERSION
 elif sys.platform == 'darwin':  # pragma: darwin only
 elif sys.platform == 'darwin':  # pragma: darwin only

+ 14 - 0
borg/platform_base.py

@@ -21,6 +21,20 @@ def acl_set(path, item, numeric_owner=False):
     of the user/group names
     of the user/group names
     """
     """
 
 
+try:
+    from os import lchflags
+
+    def set_flags(path, bsd_flags, fd=None):
+        lchflags(path, bsd_flags)
+except ImportError:
+    def set_flags(path, bsd_flags, fd=None):
+        pass
+
+
+def get_flags(path, st):
+    """Return BSD-style file flags for path or stat without following symlinks."""
+    return getattr(st, 'st_flags', 0)
+
 
 
 def sync_dir(path):
 def sync_dir(path):
     fd = os.open(path, os.O_RDONLY)
     fd = os.open(path, os.O_RDONLY)

+ 65 - 2
borg/platform_linux.pyx

@@ -1,7 +1,8 @@
 import os
 import os
 import re
 import re
 import resource
 import resource
-from stat import S_ISLNK
+import stat
+
 from .helpers import posix_acl_use_stored_uid_gid, user2uid, group2gid, safe_decode, safe_encode
 from .helpers import posix_acl_use_stored_uid_gid, user2uid, group2gid, safe_decode, safe_encode
 from .platform_base import SyncFile as BaseSyncFile
 from .platform_base import SyncFile as BaseSyncFile
 from libc cimport errno
 from libc cimport errno
@@ -33,10 +34,72 @@ cdef extern from "fcntl.h":
     unsigned int SYNC_FILE_RANGE_WAIT_BEFORE
     unsigned int SYNC_FILE_RANGE_WAIT_BEFORE
     unsigned int SYNC_FILE_RANGE_WAIT_AFTER
     unsigned int SYNC_FILE_RANGE_WAIT_AFTER
 
 
+cdef extern from "linux/fs.h":
+    # ioctls
+    int FS_IOC_SETFLAGS
+    int FS_IOC_GETFLAGS
+
+    # inode flags
+    int FS_NODUMP_FL
+    int FS_IMMUTABLE_FL
+    int FS_APPEND_FL
+    int FS_COMPR_FL
+
+cdef extern from "stropts.h":
+    int ioctl(int fildes, int request, ...)
+
+cdef extern from "errno.h":
+    int errno
+
+cdef extern from "string.h":
+    char *strerror(int errnum)
 
 
 _comment_re = re.compile(' *#.*', re.M)
 _comment_re = re.compile(' *#.*', re.M)
 
 
 
 
+BSD_TO_LINUX_FLAGS = {
+    stat.UF_NODUMP: FS_NODUMP_FL,
+    stat.UF_IMMUTABLE: FS_IMMUTABLE_FL,
+    stat.UF_APPEND: FS_APPEND_FL,
+    stat.UF_COMPRESSED: FS_COMPR_FL,
+}
+
+
+def set_flags(path, bsd_flags, fd=None):
+    if fd is None and stat.S_ISLNK(os.lstat(path).st_mode):
+        return
+    cdef int flags = 0
+    for bsd_flag, linux_flag in BSD_TO_LINUX_FLAGS.items():
+        if bsd_flags & bsd_flag:
+            flags |= linux_flag
+    open_fd = fd is None
+    if open_fd:
+        fd = os.open(path, os.O_RDONLY|os.O_NONBLOCK|os.O_NOFOLLOW)
+    try:
+        if ioctl(fd, FS_IOC_SETFLAGS, &flags) == -1:
+            raise OSError(errno, strerror(errno).decode(), path)
+    finally:
+        if open_fd:
+            os.close(fd)
+
+
+def get_flags(path, st):
+    if stat.S_ISLNK(st.st_mode):
+        return 0
+    cdef int linux_flags
+    fd = os.open(path, os.O_RDONLY|os.O_NONBLOCK|os.O_NOFOLLOW)
+    try:
+        if ioctl(fd, FS_IOC_GETFLAGS, &linux_flags) == -1:
+            return 0
+    finally:
+        os.close(fd)
+    bsd_flags = 0
+    for bsd_flag, linux_flag in BSD_TO_LINUX_FLAGS.items():
+        if linux_flags & linux_flag:
+            bsd_flags |= bsd_flag
+    return bsd_flags
+
+
 def acl_use_local_uid_gid(acl):
 def acl_use_local_uid_gid(acl):
     """Replace the user/group field with the local uid/gid if possible
     """Replace the user/group field with the local uid/gid if possible
     """
     """
@@ -93,7 +156,7 @@ def acl_get(path, item, st, numeric_owner=False):
     cdef char *access_text = NULL
     cdef char *access_text = NULL
 
 
     p = <bytes>os.fsencode(path)
     p = <bytes>os.fsencode(path)
-    if S_ISLNK(st.st_mode) or acl_extended_file(p) <= 0:
+    if stat.S_ISLNK(st.st_mode) or acl_extended_file(p) <= 0:
         return
         return
     if numeric_owner:
     if numeric_owner:
         converter = acl_numeric_ids
         converter = acl_numeric_ids

+ 19 - 3
borg/testsuite/__init__.py

@@ -5,9 +5,13 @@ import posix
 import stat
 import stat
 import sys
 import sys
 import sysconfig
 import sysconfig
+import tempfile
 import time
 import time
 import unittest
 import unittest
+
 from ..xattr import get_all
 from ..xattr import get_all
+from ..platform import get_flags
+from .. import platform
 
 
 # Note: this is used by borg.selftest, do not use or import py.test functionality here.
 # Note: this is used by borg.selftest, do not use or import py.test functionality here.
 
 
@@ -23,8 +27,20 @@ try:
 except ImportError:
 except ImportError:
     raises = None
     raises = None
 
 
-has_lchflags = hasattr(os, 'lchflags')
+has_lchflags = hasattr(os, 'lchflags') or sys.platform.startswith('linux')
+no_lchlfags_because = '' if has_lchflags else '(not supported on this platform)'
+try:
+    with tempfile.NamedTemporaryFile() as file:
+        platform.set_flags(file.name, stat.UF_NODUMP)
+except OSError:
+    has_lchflags = False
+    no_lchlfags_because = '(the file system at %s does not support flags)' % tempfile.gettempdir()
 
 
+try:
+    import llfuse
+    has_llfuse = True or llfuse  # avoids "unused import"
+except ImportError:
+    has_llfuse = False
 
 
 # The mtime get/set precision varies on different OS and Python versions
 # The mtime get/set precision varies on different OS and Python versions
 if 'HAVE_FUTIMENS' in getattr(posix, '_have_functions', []):
 if 'HAVE_FUTIMENS' in getattr(posix, '_have_functions', []):
@@ -75,13 +91,13 @@ class BaseTestCase(unittest.TestCase):
             # Assume path2 is on FUSE if st_dev is different
             # Assume path2 is on FUSE if st_dev is different
             fuse = s1.st_dev != s2.st_dev
             fuse = s1.st_dev != s2.st_dev
             attrs = ['st_mode', 'st_uid', 'st_gid', 'st_rdev']
             attrs = ['st_mode', 'st_uid', 'st_gid', 'st_rdev']
-            if has_lchflags:
-                attrs.append('st_flags')
             if not fuse or not os.path.isdir(path1):
             if not fuse or not os.path.isdir(path1):
                 # dir nlink is always 1 on our fuse filesystem
                 # dir nlink is always 1 on our fuse filesystem
                 attrs.append('st_nlink')
                 attrs.append('st_nlink')
             d1 = [filename] + [getattr(s1, a) for a in attrs]
             d1 = [filename] + [getattr(s1, a) for a in attrs]
             d2 = [filename] + [getattr(s2, a) for a in attrs]
             d2 = [filename] + [getattr(s2, a) for a in attrs]
+            d1.append(get_flags(path1, s1))
+            d2.append(get_flags(path2, s2))
             # ignore st_rdev if file is not a block/char device, fixes #203
             # ignore st_rdev if file is not a block/char device, fixes #203
             if not stat.S_ISCHR(d1[1]) and not stat.S_ISBLK(d1[1]):
             if not stat.S_ISCHR(d1[1]) and not stat.S_ISBLK(d1[1]):
                 d1[4] = None
                 d1[4] = None

+ 4 - 6
borg/testsuite/archiver.py

@@ -16,7 +16,7 @@ from hashlib import sha256
 
 
 import pytest
 import pytest
 
 
-from .. import xattr, helpers
+from .. import xattr, helpers, platform
 from ..archive import Archive, ChunkBuffer, ArchiveRecreater
 from ..archive import Archive, ChunkBuffer, ArchiveRecreater
 from ..archiver import Archiver
 from ..archiver import Archiver
 from ..cache import Cache
 from ..cache import Cache
@@ -26,15 +26,13 @@ from ..helpers import Chunk, Manifest, EXIT_SUCCESS, EXIT_WARNING, EXIT_ERROR, b
 from ..key import KeyfileKeyBase
 from ..key import KeyfileKeyBase
 from ..remote import RemoteRepository, PathNotAllowed
 from ..remote import RemoteRepository, PathNotAllowed
 from ..repository import Repository
 from ..repository import Repository
+from . import has_lchflags, has_llfuse
 from . import BaseTestCase, changedir, environment_variable
 from . import BaseTestCase, changedir, environment_variable
 
 
 try:
 try:
     import llfuse
     import llfuse
-    has_llfuse = True or llfuse  # avoids "unused import"
 except ImportError:
 except ImportError:
-    has_llfuse = False
-
-has_lchflags = hasattr(os, 'lchflags')
+    pass
 
 
 src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
 
 
@@ -280,7 +278,7 @@ class ArchiverTestCaseBase(BaseTestCase):
         # FIFO node
         # FIFO node
         os.mkfifo(os.path.join(self.input_path, 'fifo1'))
         os.mkfifo(os.path.join(self.input_path, 'fifo1'))
         if has_lchflags:
         if has_lchflags:
-            os.lchflags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP)
+            platform.set_flags(os.path.join(self.input_path, 'flagfile'), stat.UF_NODUMP)
         try:
         try:
             # Block device
             # Block device
             os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
             os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))

+ 0 - 4
borg/testsuite/conftest.py

@@ -1,4 +0,0 @@
-from ..logger import setup_logging
-
-# Ensure that the loggers exist for all tests
-setup_logging()

+ 18 - 0
conftest.py

@@ -0,0 +1,18 @@
+from borg.logger import setup_logging
+
+# Ensure that the loggers exist for all tests
+setup_logging()
+
+from borg.testsuite import has_lchflags, no_lchlfags_because, has_llfuse
+from borg.testsuite.platform import fakeroot_detected
+from borg import xattr
+
+
+def pytest_report_header(config, startdir):
+    yesno = ['no', 'yes']
+    flags = 'Testing BSD-style flags: %s %s' % (yesno[has_lchflags], no_lchlfags_because)
+    fakeroot = 'fakeroot: %s (>=1.20.2: %s)' % (
+        yesno[fakeroot_detected()],
+        yesno[xattr.XATTR_FAKEROOT])
+    llfuse = 'Testing fuse: %s' % yesno[has_llfuse]
+    return '\n'.join((flags, llfuse, fakeroot))