Ver código fonte

Merge pull request #1712 from ThomasWaldmann/mount-some

borg mount --first / --last / --sort / --prefix, fixes #1542
TW 8 anos atrás
pai
commit
64d128cf22
5 arquivos alterados com 59 adições e 40 exclusões
  1. 1 1
      src/borg/archive.py
  2. 8 27
      src/borg/archiver.py
  3. 10 6
      src/borg/fuse.py
  4. 18 6
      src/borg/helpers.py
  5. 22 0
      src/borg/testsuite/archiver.py

+ 1 - 1
src/borg/archive.py

@@ -1295,7 +1295,7 @@ class ArchiveChecker:
 
 
         if archive is None:
         if archive is None:
             # we need last N or all archives
             # we need last N or all archives
-            archive_infos = self.manifest.archives.list(sort_by='ts', reverse=True)
+            archive_infos = self.manifest.archives.list(sort_by=['ts'], reverse=True)
             if prefix is not None:
             if prefix is not None:
                 archive_infos = [info for info in archive_infos if info.name.startswith(prefix)]
                 archive_infos = [info for info in archive_infos if info.name.startswith(prefix)]
             num_archives = len(archive_infos)
             num_archives = len(archive_infos)

+ 8 - 27
src/borg/archiver.py

@@ -783,7 +783,7 @@ class Archiver:
         if args.location.archive:
         if args.location.archive:
             archive_names = (args.location.archive,)
             archive_names = (args.location.archive,)
         else:
         else:
-            archive_names = tuple(x.name for x in self._get_filtered_archives(args, manifest))
+            archive_names = tuple(x.name for x in manifest.archives.list_considering(args))
             if not archive_names:
             if not archive_names:
                 return self.exit_code
                 return self.exit_code
 
 
@@ -825,7 +825,7 @@ class Archiver:
             else:
             else:
                 msg.append("You requested to completely DELETE the repository *including* all archives it "
                 msg.append("You requested to completely DELETE the repository *including* all archives it "
                            "contains:")
                            "contains:")
-                for archive_info in manifest.archives.list(sort_by='ts'):
+                for archive_info in manifest.archives.list(sort_by=['ts']):
                     msg.append(format_archive(archive_info))
                     msg.append(format_archive(archive_info))
             msg.append("Type 'YES' if you understand this and want to continue: ")
             msg.append("Type 'YES' if you understand this and want to continue: ")
             msg = '\n'.join(msg)
             msg = '\n'.join(msg)
@@ -853,12 +853,7 @@ class Archiver:
             return self.exit_code
             return self.exit_code
 
 
         with cache_if_remote(repository) as cached_repo:
         with cache_if_remote(repository) as cached_repo:
-            if args.location.archive:
-                archive = Archive(repository, key, manifest, args.location.archive,
-                                  consider_part_files=args.consider_part_files)
-            else:
-                archive = None
-            operations = FuseOperations(key, repository, manifest, archive, cached_repo)
+            operations = FuseOperations(key, repository, manifest, args, cached_repo)
             logger.info("Mounting filesystem")
             logger.info("Mounting filesystem")
             try:
             try:
                 operations.mount(args.mountpoint, args.options, args.foreground)
                 operations.mount(args.mountpoint, args.options, args.foreground)
@@ -909,7 +904,7 @@ class Archiver:
             format = "{archive:<36} {time} [{id}]{NL}"
             format = "{archive:<36} {time} [{id}]{NL}"
         formatter = ArchiveFormatter(format)
         formatter = ArchiveFormatter(format)
 
 
-        for archive_info in self._get_filtered_archives(args, manifest):
+        for archive_info in manifest.archives.list_considering(args):
             write(safe_encode(formatter.format_item(archive_info)))
             write(safe_encode(formatter.format_item(archive_info)))
 
 
         return self.exit_code
         return self.exit_code
@@ -929,7 +924,7 @@ class Archiver:
         if args.location.archive:
         if args.location.archive:
             archive_names = (args.location.archive,)
             archive_names = (args.location.archive,)
         else:
         else:
-            archive_names = tuple(x.name for x in self._get_filtered_archives(args, manifest))
+            archive_names = tuple(x.name for x in manifest.archives.list_considering(args))
             if not archive_names:
             if not archive_names:
                 return self.exit_code
                 return self.exit_code
 
 
@@ -981,7 +976,7 @@ class Archiver:
                              '"keep-secondly", "keep-minutely", "keep-hourly", "keep-daily", '
                              '"keep-secondly", "keep-minutely", "keep-hourly", "keep-daily", '
                              '"keep-weekly", "keep-monthly" or "keep-yearly" settings must be specified.')
                              '"keep-weekly", "keep-monthly" or "keep-yearly" settings must be specified.')
             return self.exit_code
             return self.exit_code
-        archives_checkpoints = manifest.archives.list(sort_by='ts', reverse=True)  # just a ArchiveInfo list
+        archives_checkpoints = manifest.archives.list(sort_by=['ts'], reverse=True)  # just a ArchiveInfo list
         if args.prefix:
         if args.prefix:
             archives_checkpoints = [arch for arch in archives_checkpoints if arch.name.startswith(args.prefix)]
             archives_checkpoints = [arch for arch in archives_checkpoints if arch.name.startswith(args.prefix)]
         is_checkpoint = re.compile(r'\.checkpoint(\.\d+)?$').search
         is_checkpoint = re.compile(r'\.checkpoint(\.\d+)?$').search
@@ -1099,7 +1094,7 @@ class Archiver:
                 if args.target is not None:
                 if args.target is not None:
                     self.print_error('--target: Need to specify single archive')
                     self.print_error('--target: Need to specify single archive')
                     return self.exit_code
                     return self.exit_code
-                for archive in manifest.archives.list(sort_by='ts'):
+                for archive in manifest.archives.list(sort_by=['ts']):
                     name = archive.name
                     name = archive.name
                     if recreater.is_temporary_archive(name):
                     if recreater.is_temporary_archive(name):
                         continue
                         continue
@@ -2115,6 +2110,7 @@ class Archiver:
                                help='stay in foreground, do not daemonize')
                                help='stay in foreground, do not daemonize')
         subparser.add_argument('-o', dest='options', type=str,
         subparser.add_argument('-o', dest='options', type=str,
                                help='Extra mount options')
                                help='Extra mount options')
+        self.add_archives_filters_args(subparser)
 
 
         info_epilog = textwrap.dedent("""
         info_epilog = textwrap.dedent("""
         This command displays detailed information about the specified archive or repository.
         This command displays detailed information about the specified archive or repository.
@@ -2630,21 +2626,6 @@ class Archiver:
             logger.warning("Using a pure-python msgpack! This will result in lower performance.")
             logger.warning("Using a pure-python msgpack! This will result in lower performance.")
         return args.func(args)
         return args.func(args)
 
 
-    def _get_filtered_archives(self, args, manifest):
-        if args.location.archive:
-            raise Error('The options --first, --last and --prefix can only be used on repository targets.')
-
-        archives = manifest.archives.list(prefix=args.prefix)
-
-        for sortkey in reversed(args.sort_by.split(',')):
-            archives.sort(key=attrgetter(sortkey))
-        if args.last:
-            archives.reverse()
-
-        n = args.first or args.last or len(archives)
-
-        return archives[:n]
-
 
 
 def sig_info_handler(sig_no, stack):  # pragma: no cover
 def sig_info_handler(sig_no, stack):  # pragma: no cover
     """search the stack for infos about the currently processed file and print them"""
     """search the stack for infos about the currently processed file and print them"""

+ 10 - 6
src/borg/fuse.py

@@ -58,11 +58,11 @@ class FuseOperations(llfuse.Operations):
     allow_damaged_files = False
     allow_damaged_files = False
     versions = False
     versions = False
 
 
-    def __init__(self, key, repository, manifest, archive, cached_repo):
+    def __init__(self, key, repository, manifest, args, cached_repo):
         super().__init__()
         super().__init__()
         self.repository_uncached = repository
         self.repository_uncached = repository
         self.repository = cached_repo
         self.repository = cached_repo
-        self.archive = archive
+        self.args = args
         self.manifest = manifest
         self.manifest = manifest
         self.key = key
         self.key = key
         self._inode_count = 0
         self._inode_count = 0
@@ -79,11 +79,15 @@ class FuseOperations(llfuse.Operations):
 
 
     def _create_filesystem(self):
     def _create_filesystem(self):
         self._create_dir(parent=1)  # first call, create root dir (inode == 1)
         self._create_dir(parent=1)  # first call, create root dir (inode == 1)
-        if self.archive:
-            self.process_archive(self.archive)
+        if self.args.location.archive:
+            archive = Archive(self.repository_uncached, self.key, self.manifest, self.args.location.archive,
+                              consider_part_files=self.args.consider_part_files)
+            self.process_archive(archive)
         else:
         else:
-            for name in self.manifest.archives:
-                archive = Archive(self.repository_uncached, self.key, self.manifest, name)
+            archive_names = (x.name for x in self.manifest.archives.list_considering(self.args))
+            for name in archive_names:
+                archive = Archive(self.repository_uncached, self.key, self.manifest, name,
+                                  consider_part_files=self.args.consider_part_files)
                 if self.versions:
                 if self.versions:
                     # process archives immediately
                     # process archives immediately
                     self.process_archive(archive)
                     self.process_archive(archive)

+ 18 - 6
src/borg/helpers.py

@@ -142,17 +142,29 @@ class Archives(abc.MutableMapping):
         name = safe_encode(name)
         name = safe_encode(name)
         del self._archives[name]
         del self._archives[name]
 
 
-    def list(self, sort_by=None, reverse=False, prefix=''):
+    def list(self, sort_by=(), reverse=False, prefix='', first=None, last=None):
         """
         """
         Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts
         Inexpensive Archive.list_archives replacement if we just need .name, .id, .ts
-        Returns list of borg.helpers.ArchiveInfo instances
+        Returns list of borg.helpers.ArchiveInfo instances.
+        sort_by can be a list of sort keys, they are applied in reverse order.
         """
         """
+        if isinstance(sort_by, (str, bytes)):
+            raise TypeError('sort_by must be a sequence of str')
         archives = [x for x in self.values() if x.name.startswith(prefix)]
         archives = [x for x in self.values() if x.name.startswith(prefix)]
-        if sort_by is not None:
-            archives = sorted(archives, key=attrgetter(sort_by))
-        if reverse:
+        for sortkey in reversed(sort_by):
+            archives.sort(key=attrgetter(sortkey))
+        if reverse or last:
             archives.reverse()
             archives.reverse()
-        return archives
+        n = first or last or len(archives)
+        return archives[:n]
+
+    def list_considering(self, args):
+        """
+        get a list of archives, considering --first/last/prefix/sort cmdline args
+        """
+        if args.location.archive:
+            raise Error('The options --first, --last and --prefix can only be used on repository targets.')
+        return self.list(sort_by=args.sort_by.split(','), prefix=args.prefix, first=args.first, last=args.last)
 
 
     def set_raw_dict(self, d):
     def set_raw_dict(self, d):
         """set the dict we get from the msgpack unpacker"""
         """set the dict we get from the msgpack unpacker"""

+ 22 - 0
src/borg/testsuite/archiver.py

@@ -1508,6 +1508,28 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         with self.fuse_mount(self.repository_location + '::archive', mountpoint, '-o', 'allow_damaged_files'):
         with self.fuse_mount(self.repository_location + '::archive', mountpoint, '-o', 'allow_damaged_files'):
             open(os.path.join(mountpoint, path)).close()
             open(os.path.join(mountpoint, path)).close()
 
 
+    @unittest.skipUnless(has_llfuse, 'llfuse not installed')
+    def test_fuse_mount_options(self):
+        self.cmd('init', self.repository_location)
+        self.create_src_archive('arch11')
+        self.create_src_archive('arch12')
+        self.create_src_archive('arch21')
+        self.create_src_archive('arch22')
+
+        mountpoint = os.path.join(self.tmpdir, 'mountpoint')
+        with self.fuse_mount(self.repository_location, mountpoint, '--first=2', '--sort=name'):
+            assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch11', 'arch12']
+        with self.fuse_mount(self.repository_location, mountpoint, '--last=2', '--sort=name'):
+            assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch21', 'arch22']
+        with self.fuse_mount(self.repository_location, mountpoint, '--prefix=arch1'):
+            assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch11', 'arch12']
+        with self.fuse_mount(self.repository_location, mountpoint, '--prefix=arch2'):
+            assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch21', 'arch22']
+        with self.fuse_mount(self.repository_location, mountpoint, '--prefix=arch'):
+            assert sorted(os.listdir(os.path.join(mountpoint))) == ['arch11', 'arch12', 'arch21', 'arch22']
+        with self.fuse_mount(self.repository_location, mountpoint, '--prefix=nope'):
+            assert sorted(os.listdir(os.path.join(mountpoint))) == []
+
     def verify_aes_counter_uniqueness(self, method):
     def verify_aes_counter_uniqueness(self, method):
         seen = set()  # Chunks already seen
         seen = set()  # Chunks already seen
         used = set()  # counter values already used
         used = set()  # counter values already used