소스 검색

implement --nobsdflags and --exclude-nodump, fixes #3160

do no read/archive bsdflags: borg create --nobsdflags ...
do not extract/set bsdflags: borg extract --nobsdflags ...

use cases:

- fs shows wrong / random bsdflags (bug in filesystem)
- fs does not support bsdflags anyway
- already archived bsdflags are wrong / unwanted
- borg shows any sort of unwanted effect due to get_flags, esp. on Linux

the nodump flag ("do not backup this file") is not honoured any more by
default because this functionality (esp. if it happened by error or
unexpected) was rather confusing and unexplainable at first to users.

if you want that "do not backup NODUMP-flagged files" behaviour, use:
borg create --exclude-nodump ...
Thomas Waldmann 7 년 전
부모
커밋
10adadf685
3개의 변경된 파일36개의 추가작업 그리고 19개의 파일을 삭제
  1. 10 5
      src/borg/archive.py
  2. 20 8
      src/borg/archiver.py
  3. 6 6
      src/borg/testsuite/archiver.py

+ 10 - 5
src/borg/archive.py

@@ -282,8 +282,8 @@ class Archive:
         """Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable."""
         """Failed to encode filename "{}" into file system encoding "{}". Consider configuring the LANG environment variable."""
 
 
     def __init__(self, repository, key, manifest, name, cache=None, create=False,
     def __init__(self, repository, key, manifest, name, cache=None, create=False,
-                 checkpoint_interval=300, numeric_owner=False, noatime=False, noctime=False, progress=False,
-                 chunker_params=CHUNKER_PARAMS, start=None, start_monotonic=None, end=None,
+                 checkpoint_interval=300, numeric_owner=False, noatime=False, noctime=False, nobsdflags=False,
+                 progress=False, chunker_params=CHUNKER_PARAMS, start=None, start_monotonic=None, end=None,
                  consider_part_files=False, log_json=False):
                  consider_part_files=False, log_json=False):
         self.cwd = os.getcwd()
         self.cwd = os.getcwd()
         self.key = key
         self.key = key
@@ -300,6 +300,7 @@ class Archive:
         self.numeric_owner = numeric_owner
         self.numeric_owner = numeric_owner
         self.noatime = noatime
         self.noatime = noatime
         self.noctime = noctime
         self.noctime = noctime
+        self.nobsdflags = nobsdflags
         assert (start is None) == (start_monotonic is None), 'Logic error: if start is given, start_monotonic must be given as well and vice versa.'
         assert (start is None) == (start_monotonic is None), 'Logic error: if start is given, start_monotonic must be given as well and vice versa.'
         if start is None:
         if start is None:
             start = datetime.utcnow()
             start = datetime.utcnow()
@@ -691,7 +692,8 @@ Utilization of max. archive size: {csize_max:.0%}
             # some systems don't support calling utime on a symlink
             # some systems don't support calling utime on a symlink
             pass
             pass
         acl_set(path, item, self.numeric_owner)
         acl_set(path, item, self.numeric_owner)
-        if 'bsdflags' in item:
+
+        if not self.nobsdflags and 'bsdflags' in item:
             try:
             try:
                 set_flags(path, item.bsdflags, fd=fd)
                 set_flags(path, item.bsdflags, fd=fd)
             except OSError:
             except OSError:
@@ -903,10 +905,11 @@ Utilization of max. archive size: {csize_max:.0%}
 
 
 
 
 class MetadataCollector:
 class MetadataCollector:
-    def __init__(self, *, noatime, noctime, numeric_owner):
+    def __init__(self, *, noatime, noctime, numeric_owner, nobsdflags):
         self.noatime = noatime
         self.noatime = noatime
         self.noctime = noctime
         self.noctime = noctime
         self.numeric_owner = numeric_owner
         self.numeric_owner = numeric_owner
+        self.nobsdflags = nobsdflags
 
 
     def stat_simple_attrs(self, st):
     def stat_simple_attrs(self, st):
         attrs = dict(
         attrs = dict(
@@ -931,9 +934,11 @@ class MetadataCollector:
 
 
     def stat_ext_attrs(self, st, path):
     def stat_ext_attrs(self, st, path):
         attrs = {}
         attrs = {}
+        bsdflags = 0
         with backup_io('extended stat'):
         with backup_io('extended stat'):
             xattrs = xattr.get_all(path, follow_symlinks=False)
             xattrs = xattr.get_all(path, follow_symlinks=False)
-            bsdflags = get_flags(path, st)
+            if not self.nobsdflags:
+                bsdflags = get_flags(path, st)
             acl_get(path, attrs, st, self.numeric_owner)
             acl_get(path, attrs, st, self.numeric_owner)
         if xattrs:
         if xattrs:
             attrs['xattrs'] = StableDict(xattrs)
             attrs['xattrs'] = StableDict(xattrs)

+ 20 - 8
src/borg/archiver.py

@@ -154,7 +154,9 @@ def with_archive(method):
     @functools.wraps(method)
     @functools.wraps(method)
     def wrapper(self, args, repository, key, manifest, **kwargs):
     def wrapper(self, args, repository, key, manifest, **kwargs):
         archive = Archive(repository, key, manifest, args.location.archive,
         archive = Archive(repository, key, manifest, args.location.archive,
-                          numeric_owner=getattr(args, 'numeric_owner', False), cache=kwargs.get('cache'),
+                          numeric_owner=getattr(args, 'numeric_owner', False),
+                          nobsdflags=getattr(args, 'nobsdflags', False),
+                          cache=kwargs.get('cache'),
                           consider_part_files=args.consider_part_files, log_json=args.log_json)
                           consider_part_files=args.consider_part_files, log_json=args.log_json)
         return method(self, args, repository=repository, manifest=manifest, key=key, archive=archive, **kwargs)
         return method(self, args, repository=repository, manifest=manifest, key=key, archive=archive, **kwargs)
     return wrapper
     return wrapper
@@ -485,6 +487,8 @@ class Archiver:
         self.output_filter = args.output_filter
         self.output_filter = args.output_filter
         self.output_list = args.output_list
         self.output_list = args.output_list
         self.ignore_inode = args.ignore_inode
         self.ignore_inode = args.ignore_inode
+        self.nobsdflags = args.nobsdflags
+        self.exclude_nodump = args.exclude_nodump
         self.files_cache_mode = args.files_cache_mode
         self.files_cache_mode = args.files_cache_mode
         dry_run = args.dry_run
         dry_run = args.dry_run
         t0 = datetime.utcnow()
         t0 = datetime.utcnow()
@@ -499,7 +503,7 @@ class Archiver:
                                   chunker_params=args.chunker_params, start=t0, start_monotonic=t0_monotonic,
                                   chunker_params=args.chunker_params, start=t0, start_monotonic=t0_monotonic,
                                   log_json=args.log_json)
                                   log_json=args.log_json)
                 metadata_collector = MetadataCollector(noatime=args.noatime, noctime=args.noctime,
                 metadata_collector = MetadataCollector(noatime=args.noatime, noctime=args.noctime,
-                    numeric_owner=args.numeric_owner)
+                    nobsdflags=args.nobsdflags, numeric_owner=args.numeric_owner)
                 cp = ChunksProcessor(cache=cache, key=key,
                 cp = ChunksProcessor(cache=cache, key=key,
                     add_item=archive.add_item, write_checkpoint=archive.write_checkpoint,
                     add_item=archive.add_item, write_checkpoint=archive.write_checkpoint,
                     checkpoint_interval=args.checkpoint_interval)
                     checkpoint_interval=args.checkpoint_interval)
@@ -542,11 +546,12 @@ class Archiver:
             # directory of the mounted filesystem that shadows the mountpoint dir).
             # directory of the mounted filesystem that shadows the mountpoint dir).
             recurse = restrict_dev is None or st.st_dev == restrict_dev
             recurse = restrict_dev is None or st.st_dev == restrict_dev
             status = None
             status = None
-            # Ignore if nodump flag is set
-            with backup_io('flags'):
-                if get_flags(path, st) & stat.UF_NODUMP:
-                    self.print_file_status('x', path)
-                    return
+            if self.exclude_nodump:
+                # Ignore if nodump flag is set
+                with backup_io('flags'):
+                    if get_flags(path, st) & stat.UF_NODUMP:
+                        self.print_file_status('x', path)
+                        return
             if stat.S_ISREG(st.st_mode):
             if stat.S_ISREG(st.st_mode):
                 if not dry_run:
                 if not dry_run:
                     status = fso.process_file(path, st, cache, self.ignore_inode, self.files_cache_mode)
                     status = fso.process_file(path, st, cache, self.ignore_inode, self.files_cache_mode)
@@ -2198,6 +2203,7 @@ class Archiver:
         def define_exclusion_group(subparser, **kwargs):
         def define_exclusion_group(subparser, **kwargs):
             exclude_group = subparser.add_argument_group('Exclusion options')
             exclude_group = subparser.add_argument_group('Exclusion options')
             define_exclude_and_patterns(exclude_group.add_argument, **kwargs)
             define_exclude_and_patterns(exclude_group.add_argument, **kwargs)
+            return exclude_group
 
 
         def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
         def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
             filters_group = subparser.add_argument_group('Archive filters',
             filters_group = subparser.add_argument_group('Archive filters',
@@ -2727,7 +2733,9 @@ class Archiver:
         subparser.add_argument('--no-files-cache', dest='cache_files', action='store_false',
         subparser.add_argument('--no-files-cache', dest='cache_files', action='store_false',
                                help='do not load/update the file metadata cache used to detect unchanged files')
                                help='do not load/update the file metadata cache used to detect unchanged files')
 
 
-        define_exclusion_group(subparser, tag_files=True)
+        exclude_group = define_exclusion_group(subparser, tag_files=True)
+        exclude_group.add_argument('--exclude-nodump', dest='exclude_nodump', action='store_true',
+                                   help='exclude files flagged NODUMP')
 
 
         fs_group = subparser.add_argument_group('Filesystem options')
         fs_group = subparser.add_argument_group('Filesystem options')
         fs_group.add_argument('-x', '--one-file-system', dest='one_file_system', action='store_true',
         fs_group.add_argument('-x', '--one-file-system', dest='one_file_system', action='store_true',
@@ -2738,6 +2746,8 @@ class Archiver:
                               help='do not store atime into archive')
                               help='do not store atime into archive')
         fs_group.add_argument('--noctime', dest='noctime', action='store_true',
         fs_group.add_argument('--noctime', dest='noctime', action='store_true',
                               help='do not store ctime into archive')
                               help='do not store ctime into archive')
+        fs_group.add_argument('--nobsdflags', dest='nobsdflags', action='store_true',
+                              help='do not read and store bsdflags (e.g. NODUMP, IMMUTABLE) into archive')
         fs_group.add_argument('--ignore-inode', dest='ignore_inode', action='store_true',
         fs_group.add_argument('--ignore-inode', dest='ignore_inode', action='store_true',
                               help='ignore inode data in the file metadata cache used to detect unchanged files.')
                               help='ignore inode data in the file metadata cache used to detect unchanged files.')
         fs_group.add_argument('--files-cache', metavar='MODE', dest='files_cache_mode',
         fs_group.add_argument('--files-cache', metavar='MODE', dest='files_cache_mode',
@@ -2804,6 +2814,8 @@ class Archiver:
                                help='do not actually change any files')
                                help='do not actually change any files')
         subparser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true',
         subparser.add_argument('--numeric-owner', dest='numeric_owner', action='store_true',
                                help='only obey numeric user and group identifiers')
                                help='only obey numeric user and group identifiers')
+        subparser.add_argument('--nobsdflags', dest='nobsdflags', action='store_true',
+                               help='do not extract/set bsdflags (e.g. NODUMP, IMMUTABLE)')
         subparser.add_argument('--stdout', dest='stdout', action='store_true',
         subparser.add_argument('--stdout', dest='stdout', action='store_true',
                                help='write all extracted data to stdout')
                                help='write all extracted data to stdout')
         subparser.add_argument('--sparse', dest='sparse', action='store_true',
         subparser.add_argument('--sparse', dest='sparse', action='store_true',

+ 6 - 6
src/borg/testsuite/archiver.py

@@ -380,8 +380,8 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         output = self.cmd('init', '--encryption=repokey', '--show-version', '--show-rc', self.repository_location, fork=True)
         output = self.cmd('init', '--encryption=repokey', '--show-version', '--show-rc', self.repository_location, fork=True)
         self.assert_in('borgbackup version', output)
         self.assert_in('borgbackup version', output)
         self.assert_in('terminating with success status, rc 0', output)
         self.assert_in('terminating with success status, rc 0', output)
-        self.cmd('create', self.repository_location + '::test', 'input')
-        output = self.cmd('create', '--stats', self.repository_location + '::test.2', 'input')
+        self.cmd('create', '--exclude-nodump', self.repository_location + '::test', 'input')
+        output = self.cmd('create', '--exclude-nodump', '--stats', self.repository_location + '::test.2', 'input')
         self.assert_in('Archive name: test.2', output)
         self.assert_in('Archive name: test.2', output)
         self.assert_in('This archive: ', output)
         self.assert_in('This archive: ', output)
         with changedir('output'):
         with changedir('output'):
@@ -1655,13 +1655,13 @@ class ArchiverTestCase(ArchiverTestCaseBase):
             self.create_regular_file('file3', size=1024 * 80)
             self.create_regular_file('file3', size=1024 * 80)
             platform.set_flags(os.path.join(self.input_path, 'file3'), stat.UF_NODUMP)
             platform.set_flags(os.path.join(self.input_path, 'file3'), stat.UF_NODUMP)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         self.cmd('init', '--encryption=repokey', self.repository_location)
-        output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
+        output = self.cmd('create', '--list', '--exclude-nodump', self.repository_location + '::test', 'input')
         self.assert_in("A input/file1", output)
         self.assert_in("A input/file1", output)
         self.assert_in("A input/file2", output)
         self.assert_in("A input/file2", output)
         if has_lchflags:
         if has_lchflags:
             self.assert_in("x input/file3", output)
             self.assert_in("x input/file3", output)
         # should find second file as excluded
         # should find second file as excluded
-        output = self.cmd('create', '--list', self.repository_location + '::test1', 'input', '--exclude', '*/file2')
+        output = self.cmd('create', '--list', '--exclude-nodump', self.repository_location + '::test1', 'input', '--exclude', '*/file2')
         self.assert_in("U input/file1", output)
         self.assert_in("U input/file1", output)
         self.assert_in("x input/file2", output)
         self.assert_in("x input/file2", output)
         if has_lchflags:
         if has_lchflags:
@@ -2059,8 +2059,8 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         self.cmd('init', '--encryption=repokey', self.repository_location)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         self.create_test_files()
         self.create_test_files()
         have_noatime = has_noatime('input/file1')
         have_noatime = has_noatime('input/file1')
-        self.cmd('create', self.repository_location + '::archive', 'input')
-        self.cmd('create', self.repository_location + '::archive2', 'input')
+        self.cmd('create', '--exclude-nodump', self.repository_location + '::archive', 'input')
+        self.cmd('create', '--exclude-nodump', self.repository_location + '::archive2', 'input')
         if has_lchflags:
         if has_lchflags:
             # remove the file we did not backup, so input and output become equal
             # remove the file we did not backup, so input and output become equal
             os.remove(os.path.join('input', 'flagfile'))
             os.remove(os.path.join('input', 'flagfile'))