diff --git a/src/borg/archive.py b/src/borg/archive.py index 621ad50b4..bfc583612 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1295,7 +1295,7 @@ class ArchiveChecker: if archive is None: # 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: archive_infos = [info for info in archive_infos if info.name.startswith(prefix)] num_archives = len(archive_infos) diff --git a/src/borg/archiver.py b/src/borg/archiver.py index 11ace7056..40024261e 100644 --- a/src/borg/archiver.py +++ b/src/borg/archiver.py @@ -783,7 +783,7 @@ class Archiver: if args.location.archive: archive_names = (args.location.archive,) 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: return self.exit_code @@ -825,7 +825,7 @@ class Archiver: else: msg.append("You requested to completely DELETE the repository *including* all archives it " "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("Type 'YES' if you understand this and want to continue: ") msg = '\n'.join(msg) @@ -853,12 +853,7 @@ class Archiver: return self.exit_code 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") try: operations.mount(args.mountpoint, args.options, args.foreground) @@ -909,7 +904,7 @@ class Archiver: format = "{archive:<36} {time} [{id}]{NL}" 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))) return self.exit_code @@ -929,7 +924,7 @@ class Archiver: if args.location.archive: archive_names = (args.location.archive,) 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: return self.exit_code @@ -981,7 +976,7 @@ class Archiver: '"keep-secondly", "keep-minutely", "keep-hourly", "keep-daily", ' '"keep-weekly", "keep-monthly" or "keep-yearly" settings must be specified.') 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: archives_checkpoints = [arch for arch in archives_checkpoints if arch.name.startswith(args.prefix)] is_checkpoint = re.compile(r'\.checkpoint(\.\d+)?$').search @@ -1099,7 +1094,7 @@ class Archiver: if args.target is not None: self.print_error('--target: Need to specify single archive') 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 if recreater.is_temporary_archive(name): continue @@ -2115,6 +2110,7 @@ class Archiver: help='stay in foreground, do not daemonize') subparser.add_argument('-o', dest='options', type=str, help='Extra mount options') + self.add_archives_filters_args(subparser) info_epilog = textwrap.dedent(""" 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.") 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 """search the stack for infos about the currently processed file and print them""" diff --git a/src/borg/fuse.py b/src/borg/fuse.py index b822332dd..8de141fef 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -58,11 +58,11 @@ class FuseOperations(llfuse.Operations): allow_damaged_files = False versions = False - def __init__(self, key, repository, manifest, archive, cached_repo): + def __init__(self, key, repository, manifest, args, cached_repo): super().__init__() self.repository_uncached = repository self.repository = cached_repo - self.archive = archive + self.args = args self.manifest = manifest self.key = key self._inode_count = 0 @@ -79,11 +79,15 @@ class FuseOperations(llfuse.Operations): def _create_filesystem(self): 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: - 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: # process archives immediately self.process_archive(archive) diff --git a/src/borg/helpers.py b/src/borg/helpers.py index 94545fcb5..42d96b166 100644 --- a/src/borg/helpers.py +++ b/src/borg/helpers.py @@ -142,17 +142,29 @@ class Archives(abc.MutableMapping): name = safe_encode(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 - 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)] - 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() - 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): """set the dict we get from the msgpack unpacker""" diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index aa1a99eec..a2c44280d 100644 --- a/src/borg/testsuite/archiver.py +++ b/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'): 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): seen = set() # Chunks already seen used = set() # counter values already used