def run(self, args=None):
        check_extension_modules()
        keys_dir = get_keys_dir()
        if not os.path.exists(keys_dir):
            os.makedirs(keys_dir)
            os.chmod(keys_dir, stat.S_IRWXU)
        cache_dir = get_cache_dir()
        if not os.path.exists(cache_dir):
            os.makedirs(cache_dir)
            os.chmod(cache_dir, stat.S_IRWXU)
            with open(os.path.join(cache_dir, 'CACHEDIR.TAG'), 'w') as fd:
                fd.write(textwrap.dedent("""
                    Signature: 8a477f597d28d172789f06886806bc55
                    # This file is a cache directory tag created by Attic.
                    # For information about cache directory tags, see:
                    #       http://www.brynosaurus.com/cachedir/
                    """).lstrip())
        common_parser = argparse.ArgumentParser(add_help=False)
        common_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
                            default=False,
                            help='verbose output')

        # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
        if args:
            args = self.preprocess_args(args)

        parser = argparse.ArgumentParser(description='Attic %s - Deduplicated Backups' % __version__)
        subparsers = parser.add_subparsers(title='Available commands')

        subparser = subparsers.add_parser('serve', parents=[common_parser],
                                          description=self.do_serve.__doc__)
        subparser.set_defaults(func=self.do_serve)
        subparser.add_argument('--restrict-to-path', dest='restrict_to_paths', action='append',
                               metavar='PATH', help='restrict repository access to PATH')
        init_epilog = textwrap.dedent("""
        This command initializes an empty repository. A repository is a filesystem
        directory containing the deduplicated data from zero or more archives.
        Encryption can be enabled at repository init time.
        """)
        subparser = subparsers.add_parser('init', parents=[common_parser],
                                          description=self.do_init.__doc__, epilog=init_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_init)
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to create')
        subparser.add_argument('-e', '--encryption', dest='encryption',
                               choices=('none', 'passphrase', 'keyfile'), default='none',
                               help='select encryption method')

        check_epilog = textwrap.dedent("""
        The check command verifies the consistency of a repository and the corresponding
        archives. The underlying repository data files are first checked to detect bit rot
        and other types of damage. After that the consistency and correctness of the archive
        metadata is verified.

        The archive metadata checks can be time consuming and requires access to the key
        file and/or passphrase if encryption is enabled. These checks can be skipped using
        the --repository-only option.
        """)
        subparser = subparsers.add_parser('check', parents=[common_parser],
                                          description=self.do_check.__doc__,
                                          epilog=check_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_check)
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to check consistency of')
        subparser.add_argument('--repository-only', dest='repo_only', action='store_true',
                               default=False,
                               help='only perform repository checks')
        subparser.add_argument('--archives-only', dest='archives_only', action='store_true',
                               default=False,
                               help='only perform archives checks')
        subparser.add_argument('--repair', dest='repair', action='store_true',
                               default=False,
                               help='attempt to repair any inconsistencies found')

        change_passphrase_epilog = textwrap.dedent("""
        The key files used for repository encryption are optionally passphrase
        protected. This command can be used to change this passphrase.
        """)
        subparser = subparsers.add_parser('change-passphrase', parents=[common_parser],
                                          description=self.do_change_passphrase.__doc__,
                                          epilog=change_passphrase_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_change_passphrase)
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False))

        create_epilog = textwrap.dedent("""
        This command creates a backup archive containing all files found while recursively
        traversing all paths specified. The archive will consume almost no disk space for
        files or parts of files that have already been stored in other archives.

        See "attic help patterns" for more help on exclude patterns.
        """)

        subparser = subparsers.add_parser('create', parents=[common_parser],
                                          description=self.do_create.__doc__,
                                          epilog=create_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_create)
        subparser.add_argument('-s', '--stats', dest='stats',
                               action='store_true', default=False,
                               help='print statistics for the created archive')
        subparser.add_argument('-e', '--exclude', dest='excludes',
                               type=ExcludePattern, action='append',
                               metavar="PATTERN", help='exclude paths matching PATTERN')
        subparser.add_argument('--exclude-from', dest='exclude_files',
                               type=argparse.FileType('r'), action='append',
                               metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
        subparser.add_argument('--exclude-caches', dest='exclude_caches',
                               action='store_true', default=False,
                               help='exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html)')
        subparser.add_argument('-c', '--checkpoint-interval', dest='checkpoint_interval',
                               type=int, default=300, metavar='SECONDS',
                               help='write checkpoint every SECONDS seconds (Default: 300)')
        subparser.add_argument('--do-not-cross-mountpoints', dest='dontcross',
                               action='store_true', default=False,
                               help='do not cross mount points')
        subparser.add_argument('--numeric-owner', dest='numeric_owner',
                               action='store_true', default=False,
                               help='only store numeric user and group identifiers')
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to create')
        subparser.add_argument('paths', metavar='PATH', nargs='+', type=str,
                               help='paths to archive')

        extract_epilog = textwrap.dedent("""
        This command extracts the contents of an archive. By default the entire
        archive is extracted but a subset of files and directories can be selected
        by passing a list of ``PATHs`` as arguments. The file selection can further
        be restricted by using the ``--exclude`` option.

        See "attic help patterns" for more help on exclude patterns.
        """)
        subparser = subparsers.add_parser('extract', parents=[common_parser],
                                          description=self.do_extract.__doc__,
                                          epilog=extract_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_extract)
        subparser.add_argument('-n', '--dry-run', dest='dry_run',
                               default=False, action='store_true',
                               help='do not actually change any files')
        subparser.add_argument('-e', '--exclude', dest='excludes',
                               type=ExcludePattern, action='append',
                               metavar="PATTERN", help='exclude paths matching PATTERN')
        subparser.add_argument('--exclude-from', dest='exclude_files',
                               type=argparse.FileType('r'), action='append',
                               metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
        subparser.add_argument('--numeric-owner', dest='numeric_owner',
                               action='store_true', default=False,
                               help='only obey numeric user and group identifiers')
        subparser.add_argument('--strip-components', dest='strip_components',
                               type=int, default=0, metavar='NUMBER',
                               help='Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.')
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to extract')
        subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
                               help='paths to extract')

        delete_epilog = textwrap.dedent("""
        This command deletes an archive from the repository. Any disk space not
        shared with any other existing archive is also reclaimed.
        """)
        subparser = subparsers.add_parser('delete', parents=[common_parser],
                                          description=self.do_delete.__doc__,
                                          epilog=delete_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_delete)
        subparser.add_argument('-s', '--stats', dest='stats',
                               action='store_true', default=False,
                               help='print statistics for the deleted archive')
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to delete')

        list_epilog = textwrap.dedent("""
        This command lists the contents of a repository or an archive.
        """)
        subparser = subparsers.add_parser('list', parents=[common_parser],
                                          description=self.do_list.__doc__,
                                          epilog=list_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_list)
        subparser.add_argument('src', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(),
                               help='repository/archive to list contents of')
        mount_epilog = textwrap.dedent("""
        This command mounts an archive as a FUSE filesystem. This can be useful for
        browsing an archive or restoring individual files. Unless the ``--foreground``
        option is given the command will run in the background until the filesystem
        is ``umounted``.
        """)
        subparser = subparsers.add_parser('mount', parents=[common_parser],
                                          description=self.do_mount.__doc__,
                                          epilog=mount_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_mount)
        subparser.add_argument('src', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(),
                               help='repository/archive to mount')
        subparser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str,
                               help='where to mount filesystem')
        subparser.add_argument('-f', '--foreground', dest='foreground',
                               action='store_true', default=False,
                               help='stay in foreground, do not daemonize')
        subparser.add_argument('-o', dest='options', type=str,
                               help='Extra mount options')

        info_epilog = textwrap.dedent("""
        This command displays some detailed information about the specified archive.
        """)
        subparser = subparsers.add_parser('info', parents=[common_parser],
                                          description=self.do_info.__doc__,
                                          epilog=info_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_info)
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to display information about')

        prune_epilog = textwrap.dedent("""
        The prune command prunes a repository by deleting archives not matching
        any of the specified retention options. This command is normally used by
        automated backup scripts wanting to keep a certain number of historic backups.

        As an example, "-d 7" means to keep the latest backup on each day for 7 days.
        Days without backups do not count towards the total.
        The rules are applied from hourly to yearly, and backups selected by previous
        rules do not count towards those of later rules. The time that each backup
        completes is used for pruning purposes. Dates and times are interpreted in
        the local timezone, and weeks go from Monday to Sunday. Specifying a
        negative number of archives to keep means that there is no limit.

        The "--keep-within" option takes an argument of the form "<int><char>",
        where char is "H", "d", "w", "m", "y". For example, "--keep-within 2d" means
        to keep all archives that were created within the past 48 hours.
        "1m" is taken to mean "31d". The archives kept with this option do not
        count towards the totals specified by any other options.

        If a prefix is set with -p, then only archives that start with the prefix are
        considered for deletion and only those archives count towards the totals
        specified by the rules.
        """)
        subparser = subparsers.add_parser('prune', parents=[common_parser],
                                          description=self.do_prune.__doc__,
                                          epilog=prune_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_prune)
        subparser.add_argument('-n', '--dry-run', dest='dry_run',
                               default=False, action='store_true',
                               help='do not change repository')
        subparser.add_argument('-s', '--stats', dest='stats',
                               action='store_true', default=False,
                               help='print statistics for the deleted archive')
        subparser.add_argument('--keep-within', dest='within', type=str, metavar='WITHIN',
                               help='keep all archives within this time interval')
        subparser.add_argument('-H', '--keep-hourly', dest='hourly', type=int, default=0,
                               help='number of hourly archives to keep')
        subparser.add_argument('-d', '--keep-daily', dest='daily', type=int, default=0,
                               help='number of daily archives to keep')
        subparser.add_argument('-w', '--keep-weekly', dest='weekly', type=int, default=0,
                               help='number of weekly archives to keep')
        subparser.add_argument('-m', '--keep-monthly', dest='monthly', type=int, default=0,
                               help='number of monthly archives to keep')
        subparser.add_argument('-y', '--keep-yearly', dest='yearly', type=int, default=0,
                               help='number of yearly archives to keep')
        subparser.add_argument('-p', '--prefix', dest='prefix', type=str,
                               help='only consider archive names starting with this prefix')
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to prune')

        subparser = subparsers.add_parser('help', parents=[common_parser],
                                          description='Extra help')
        subparser.add_argument('--epilog-only', dest='epilog_only',
                               action='store_true', default=False)
        subparser.add_argument('--usage-only', dest='usage_only',
                               action='store_true', default=False)
        subparser.set_defaults(func=functools.partial(self.do_help, parser, subparsers.choices))
        subparser.add_argument('topic', metavar='TOPIC', type=str, nargs='?',
                               help='additional help on TOPIC')

        args = parser.parse_args(args or ['-h'])
        self.verbose = args.verbose
        update_excludes(args)
        return args.func(args)
Beispiel #2
0
    def run(self, args=None):
        check_extension_modules()
        keys_dir = get_keys_dir()
        if not os.path.exists(keys_dir):
            os.makedirs(keys_dir)
            os.chmod(keys_dir, stat.S_IRWXU)
        cache_dir = get_cache_dir()
        if not os.path.exists(cache_dir):
            os.makedirs(cache_dir)
            os.chmod(cache_dir, stat.S_IRWXU)
            with open(os.path.join(cache_dir, 'CACHEDIR.TAG'), 'w') as fd:
                fd.write(
                    textwrap.dedent("""
                    Signature: 8a477f597d28d172789f06886806bc55
                    # This file is a cache directory tag created by Attic.
                    # For information about cache directory tags, see:
                    #       http://www.brynosaurus.com/cachedir/
                    """).lstrip())
        common_parser = argparse.ArgumentParser(add_help=False)
        common_parser.add_argument('-v',
                                   '--verbose',
                                   dest='verbose',
                                   action='store_true',
                                   default=False,
                                   help='verbose output')

        # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
        if args:
            args = self.preprocess_args(args)

        parser = argparse.ArgumentParser(
            description='Attic %s - Deduplicated Backups' % __version__)
        subparsers = parser.add_subparsers(title='Available commands')

        subparser = subparsers.add_parser('serve',
                                          parents=[common_parser],
                                          description=self.do_serve.__doc__)
        subparser.set_defaults(func=self.do_serve)
        subparser.add_argument('--restrict-to-path',
                               dest='restrict_to_paths',
                               action='append',
                               metavar='PATH',
                               help='restrict repository access to PATH')
        init_epilog = textwrap.dedent("""
        This command initializes an empty repository. A repository is a filesystem
        directory containing the deduplicated data from zero or more archives.
        Encryption can be enabled at repository init time.
        """)
        subparser = subparsers.add_parser(
            'init',
            parents=[common_parser],
            description=self.do_init.__doc__,
            epilog=init_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_init)
        subparser.add_argument('repository',
                               metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to create')
        subparser.add_argument('-e',
                               '--encryption',
                               dest='encryption',
                               choices=('none', 'passphrase', 'keyfile'),
                               default='none',
                               help='select encryption method')

        check_epilog = textwrap.dedent("""
        The check command verifies the consistency of a repository and the corresponding
        archives. The underlying repository data files are first checked to detect bit rot
        and other types of damage. After that the consistency and correctness of the archive
        metadata is verified.

        The archive metadata checks can be time consuming and requires access to the key
        file and/or passphrase if encryption is enabled. These checks can be skipped using
        the --repository-only option.
        """)
        subparser = subparsers.add_parser(
            'check',
            parents=[common_parser],
            description=self.do_check.__doc__,
            epilog=check_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_check)
        subparser.add_argument('repository',
                               metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to check consistency of')
        subparser.add_argument('--repository-only',
                               dest='repo_only',
                               action='store_true',
                               default=False,
                               help='only perform repository checks')
        subparser.add_argument('--archives-only',
                               dest='archives_only',
                               action='store_true',
                               default=False,
                               help='only perform archives checks')
        subparser.add_argument(
            '--repair',
            dest='repair',
            action='store_true',
            default=False,
            help='attempt to repair any inconsistencies found')

        change_passphrase_epilog = textwrap.dedent("""
        The key files used for repository encryption are optionally passphrase
        protected. This command can be used to change this passphrase.
        """)
        subparser = subparsers.add_parser(
            'change-passphrase',
            parents=[common_parser],
            description=self.do_change_passphrase.__doc__,
            epilog=change_passphrase_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_change_passphrase)
        subparser.add_argument('repository',
                               metavar='REPOSITORY',
                               type=location_validator(archive=False))

        create_epilog = textwrap.dedent("""
        This command creates a backup archive containing all files found while recursively
        traversing all paths specified. The archive will consume almost no disk space for
        files or parts of files that have already been stored in other archives.

        See "attic help patterns" for more help on exclude patterns.
        """)

        subparser = subparsers.add_parser(
            'create',
            parents=[common_parser],
            description=self.do_create.__doc__,
            epilog=create_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_create)
        subparser.add_argument('-s',
                               '--stats',
                               dest='stats',
                               action='store_true',
                               default=False,
                               help='print statistics for the created archive')
        subparser.add_argument('-e',
                               '--exclude',
                               dest='excludes',
                               type=ExcludePattern,
                               action='append',
                               metavar="PATTERN",
                               help='exclude paths matching PATTERN')
        subparser.add_argument(
            '--exclude-from',
            dest='exclude_files',
            type=argparse.FileType('r'),
            action='append',
            metavar='EXCLUDEFILE',
            help='read exclude patterns from EXCLUDEFILE, one per line')
        subparser.add_argument(
            '--exclude-caches',
            dest='exclude_caches',
            action='store_true',
            default=False,
            help=
            'exclude directories that contain a CACHEDIR.TAG file (http://www.brynosaurus.com/cachedir/spec.html)'
        )
        subparser.add_argument(
            '-c',
            '--checkpoint-interval',
            dest='checkpoint_interval',
            type=int,
            default=300,
            metavar='SECONDS',
            help='write checkpoint every SECONDS seconds (Default: 300)')
        subparser.add_argument('--do-not-cross-mountpoints',
                               dest='dontcross',
                               action='store_true',
                               default=False,
                               help='do not cross mount points')
        subparser.add_argument(
            '--numeric-owner',
            dest='numeric_owner',
            action='store_true',
            default=False,
            help='only store numeric user and group identifiers')
        subparser.add_argument('archive',
                               metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to create')
        subparser.add_argument('paths',
                               metavar='PATH',
                               nargs='+',
                               type=str,
                               help='paths to archive')

        extract_epilog = textwrap.dedent("""
        This command extracts the contents of an archive. By default the entire
        archive is extracted but a subset of files and directories can be selected
        by passing a list of ``PATHs`` as arguments. The file selection can further
        be restricted by using the ``--exclude`` option.

        See "attic help patterns" for more help on exclude patterns.
        """)
        subparser = subparsers.add_parser(
            'extract',
            parents=[common_parser],
            description=self.do_extract.__doc__,
            epilog=extract_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_extract)
        subparser.add_argument('-n',
                               '--dry-run',
                               dest='dry_run',
                               default=False,
                               action='store_true',
                               help='do not actually change any files')
        subparser.add_argument('-e',
                               '--exclude',
                               dest='excludes',
                               type=ExcludePattern,
                               action='append',
                               metavar="PATTERN",
                               help='exclude paths matching PATTERN')
        subparser.add_argument(
            '--exclude-from',
            dest='exclude_files',
            type=argparse.FileType('r'),
            action='append',
            metavar='EXCLUDEFILE',
            help='read exclude patterns from EXCLUDEFILE, one per line')
        subparser.add_argument(
            '--numeric-owner',
            dest='numeric_owner',
            action='store_true',
            default=False,
            help='only obey numeric user and group identifiers')
        subparser.add_argument(
            '--strip-components',
            dest='strip_components',
            type=int,
            default=0,
            metavar='NUMBER',
            help=
            'Remove the specified number of leading path elements. Pathnames with fewer elements will be silently skipped.'
        )
        subparser.add_argument('archive',
                               metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to extract')
        subparser.add_argument('paths',
                               metavar='PATH',
                               nargs='*',
                               type=str,
                               help='paths to extract')

        delete_epilog = textwrap.dedent("""
        This command deletes an archive from the repository. Any disk space not
        shared with any other existing archive is also reclaimed.
        """)
        subparser = subparsers.add_parser(
            'delete',
            parents=[common_parser],
            description=self.do_delete.__doc__,
            epilog=delete_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_delete)
        subparser.add_argument('-s',
                               '--stats',
                               dest='stats',
                               action='store_true',
                               default=False,
                               help='print statistics for the deleted archive')
        subparser.add_argument('archive',
                               metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to delete')

        list_epilog = textwrap.dedent("""
        This command lists the contents of a repository or an archive.
        """)
        subparser = subparsers.add_parser(
            'list',
            parents=[common_parser],
            description=self.do_list.__doc__,
            epilog=list_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_list)
        subparser.add_argument('src',
                               metavar='REPOSITORY_OR_ARCHIVE',
                               type=location_validator(),
                               help='repository/archive to list contents of')
        mount_epilog = textwrap.dedent("""
        This command mounts an archive as a FUSE filesystem. This can be useful for
        browsing an archive or restoring individual files. Unless the ``--foreground``
        option is given the command will run in the background until the filesystem
        is ``umounted``.
        """)
        subparser = subparsers.add_parser(
            'mount',
            parents=[common_parser],
            description=self.do_mount.__doc__,
            epilog=mount_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_mount)
        subparser.add_argument('src',
                               metavar='REPOSITORY_OR_ARCHIVE',
                               type=location_validator(),
                               help='repository/archive to mount')
        subparser.add_argument('mountpoint',
                               metavar='MOUNTPOINT',
                               type=str,
                               help='where to mount filesystem')
        subparser.add_argument('-f',
                               '--foreground',
                               dest='foreground',
                               action='store_true',
                               default=False,
                               help='stay in foreground, do not daemonize')
        subparser.add_argument('-o',
                               dest='options',
                               type=str,
                               help='Extra mount options')

        info_epilog = textwrap.dedent("""
        This command displays some detailed information about the specified archive.
        """)
        subparser = subparsers.add_parser(
            'info',
            parents=[common_parser],
            description=self.do_info.__doc__,
            epilog=info_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_info)
        subparser.add_argument('archive',
                               metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to display information about')

        prune_epilog = textwrap.dedent("""
        The prune command prunes a repository by deleting archives not matching
        any of the specified retention options. This command is normally used by
        automated backup scripts wanting to keep a certain number of historic backups.

        As an example, "-d 7" means to keep the latest backup on each day for 7 days.
        Days without backups do not count towards the total.
        The rules are applied from hourly to yearly, and backups selected by previous
        rules do not count towards those of later rules. The time that each backup
        completes is used for pruning purposes. Dates and times are interpreted in
        the local timezone, and weeks go from Monday to Sunday. Specifying a
        negative number of archives to keep means that there is no limit.

        The "--keep-within" option takes an argument of the form "<int><char>",
        where char is "H", "d", "w", "m", "y". For example, "--keep-within 2d" means
        to keep all archives that were created within the past 48 hours.
        "1m" is taken to mean "31d". The archives kept with this option do not
        count towards the totals specified by any other options.

        If a prefix is set with -p, then only archives that start with the prefix are
        considered for deletion and only those archives count towards the totals
        specified by the rules.
        """)
        subparser = subparsers.add_parser(
            'prune',
            parents=[common_parser],
            description=self.do_prune.__doc__,
            epilog=prune_epilog,
            formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_prune)
        subparser.add_argument('-n',
                               '--dry-run',
                               dest='dry_run',
                               default=False,
                               action='store_true',
                               help='do not change repository')
        subparser.add_argument('-s',
                               '--stats',
                               dest='stats',
                               action='store_true',
                               default=False,
                               help='print statistics for the deleted archive')
        subparser.add_argument(
            '--keep-within',
            dest='within',
            type=str,
            metavar='WITHIN',
            help='keep all archives within this time interval')
        subparser.add_argument('-H',
                               '--keep-hourly',
                               dest='hourly',
                               type=int,
                               default=0,
                               help='number of hourly archives to keep')
        subparser.add_argument('-d',
                               '--keep-daily',
                               dest='daily',
                               type=int,
                               default=0,
                               help='number of daily archives to keep')
        subparser.add_argument('-w',
                               '--keep-weekly',
                               dest='weekly',
                               type=int,
                               default=0,
                               help='number of weekly archives to keep')
        subparser.add_argument('-m',
                               '--keep-monthly',
                               dest='monthly',
                               type=int,
                               default=0,
                               help='number of monthly archives to keep')
        subparser.add_argument('-y',
                               '--keep-yearly',
                               dest='yearly',
                               type=int,
                               default=0,
                               help='number of yearly archives to keep')
        subparser.add_argument(
            '-p',
            '--prefix',
            dest='prefix',
            type=str,
            help='only consider archive names starting with this prefix')
        subparser.add_argument('repository',
                               metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to prune')

        subparser = subparsers.add_parser('help',
                                          parents=[common_parser],
                                          description='Extra help')
        subparser.add_argument('--epilog-only',
                               dest='epilog_only',
                               action='store_true',
                               default=False)
        subparser.add_argument('--usage-only',
                               dest='usage_only',
                               action='store_true',
                               default=False)
        subparser.set_defaults(
            func=functools.partial(self.do_help, parser, subparsers.choices))
        subparser.add_argument('topic',
                               metavar='TOPIC',
                               type=str,
                               nargs='?',
                               help='additional help on TOPIC')

        args = parser.parse_args(args or ['-h'])
        self.verbose = args.verbose
        update_excludes(args)
        return args.func(args)
Beispiel #3
0
    def run(self, args=None):
        keys_dir = get_keys_dir()
        if not os.path.exists(keys_dir):
            os.makedirs(keys_dir)
            os.chmod(keys_dir, stat.S_IRWXU)
        cache_dir = get_cache_dir()
        if not os.path.exists(cache_dir):
            os.makedirs(cache_dir)
            os.chmod(cache_dir, stat.S_IRWXU)
        common_parser = argparse.ArgumentParser(add_help=False)
        common_parser.add_argument('-v',
                                   '--verbose',
                                   dest='verbose',
                                   action='store_true',
                                   default=False,
                                   help='verbose output')

        # We can't use argpase for "serve" since we don't want it to show up in "Available commands"
        if args and args[0] == 'serve':
            return self.do_serve()

        parser = argparse.ArgumentParser(
            description='Attic %s - Deduplicated Backups' % __version__)
        subparsers = parser.add_subparsers(title='Available commands')

        subparser = subparsers.add_parser('init',
                                          parents=[common_parser],
                                          description=self.do_init.__doc__)
        subparser.set_defaults(func=self.do_init)
        subparser.add_argument('repository',
                               metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to create')
        subparser.add_argument('-e',
                               '--encryption',
                               dest='encryption',
                               choices=('none', 'passphrase', 'keyfile'),
                               default='none',
                               help='select encryption method')

        subparser = subparsers.add_parser(
            'change-passphrase',
            parents=[common_parser],
            description=self.do_change_passphrase.__doc__)
        subparser.set_defaults(func=self.do_change_passphrase)
        subparser.add_argument('repository',
                               metavar='REPOSITORY',
                               type=location_validator(archive=False))

        subparser = subparsers.add_parser('create',
                                          parents=[common_parser],
                                          description=self.do_create.__doc__)
        subparser.set_defaults(func=self.do_create)
        subparser.add_argument('-s',
                               '--stats',
                               dest='stats',
                               action='store_true',
                               default=False,
                               help='print statistics for the created archive')
        subparser.add_argument('-e',
                               '--exclude',
                               dest='excludes',
                               type=ExcludePattern,
                               action='append',
                               metavar="PATTERN",
                               help='exclude paths matching PATTERN')
        subparser.add_argument(
            '-c',
            '--checkpoint-interval',
            dest='checkpoint_interval',
            type=int,
            default=300,
            metavar='SECONDS',
            help='write checkpoint every SECONDS seconds (Default: 300)')
        subparser.add_argument('--do-not-cross-mountpoints',
                               dest='dontcross',
                               action='store_true',
                               default=False,
                               help='do not cross mount points')
        subparser.add_argument(
            '--numeric-owner',
            dest='numeric_owner',
            action='store_true',
            default=False,
            help='only store numeric user and group identifiers')
        subparser.add_argument('archive',
                               metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to create')
        subparser.add_argument('paths',
                               metavar='PATH',
                               nargs='+',
                               type=str,
                               help='paths to archive')

        subparser = subparsers.add_parser('extract',
                                          parents=[common_parser],
                                          description=self.do_extract.__doc__)
        subparser.set_defaults(func=self.do_extract)
        subparser.add_argument('-e',
                               '--exclude',
                               dest='excludes',
                               type=ExcludePattern,
                               action='append',
                               metavar="PATTERN",
                               help='exclude paths matching PATTERN')
        subparser.add_argument(
            '--numeric-owner',
            dest='numeric_owner',
            action='store_true',
            default=False,
            help='only obey numeric user and group identifiers')
        subparser.add_argument('archive',
                               metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to extract')
        subparser.add_argument('paths',
                               metavar='PATH',
                               nargs='*',
                               type=str,
                               help='paths to extract')

        subparser = subparsers.add_parser('delete',
                                          parents=[common_parser],
                                          description=self.do_delete.__doc__)
        subparser.set_defaults(func=self.do_delete)
        subparser.add_argument('archive',
                               metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to delete')

        subparser = subparsers.add_parser('list',
                                          parents=[common_parser],
                                          description=self.do_list.__doc__)
        subparser.set_defaults(func=self.do_list)
        subparser.add_argument('src',
                               metavar='REPOSITORY_OR_ARCHIVE',
                               type=location_validator(),
                               help='repository/archive to list contents of')

        subparser = subparsers.add_parser('mount',
                                          parents=[common_parser],
                                          description=self.do_mount.__doc__)
        subparser.set_defaults(func=self.do_mount)
        subparser.add_argument('archive',
                               metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to mount')
        subparser.add_argument('mountpoint',
                               metavar='MOUNTPOINT',
                               type=str,
                               help='where to mount filesystem')
        subparser.add_argument('-f',
                               '--foreground',
                               dest='foreground',
                               action='store_true',
                               default=False,
                               help='stay in foreground, do not daemonize')
        subparser.add_argument('-o',
                               dest='options',
                               type=str,
                               help='Extra mount options')

        subparser = subparsers.add_parser('verify',
                                          parents=[common_parser],
                                          description=self.do_verify.__doc__)
        subparser.set_defaults(func=self.do_verify)
        subparser.add_argument('-e',
                               '--exclude',
                               dest='excludes',
                               type=ExcludePattern,
                               action='append',
                               metavar="PATTERN",
                               help='exclude paths matching PATTERN')
        subparser.add_argument('archive',
                               metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to verity integrity of')
        subparser.add_argument('paths',
                               metavar='PATH',
                               nargs='*',
                               type=str,
                               help='paths to verify')

        subparser = subparsers.add_parser('info',
                                          parents=[common_parser],
                                          description=self.do_info.__doc__)
        subparser.set_defaults(func=self.do_info)
        subparser.add_argument('archive',
                               metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to display information about')

        subparser = subparsers.add_parser('prune',
                                          parents=[common_parser],
                                          description=self.do_prune.__doc__)
        subparser.set_defaults(func=self.do_prune)
        subparser.add_argument('-H',
                               '--hourly',
                               dest='hourly',
                               type=int,
                               default=0,
                               help='number of hourly archives to keep')
        subparser.add_argument('-d',
                               '--daily',
                               dest='daily',
                               type=int,
                               default=0,
                               help='number of daily archives to keep')
        subparser.add_argument('-w',
                               '--weekly',
                               dest='weekly',
                               type=int,
                               default=0,
                               help='number of daily archives to keep')
        subparser.add_argument('-m',
                               '--monthly',
                               dest='monthly',
                               type=int,
                               default=0,
                               help='number of monthly archives to keep')
        subparser.add_argument('-y',
                               '--yearly',
                               dest='yearly',
                               type=int,
                               default=0,
                               help='number of yearly archives to keep')
        subparser.add_argument(
            '-p',
            '--prefix',
            dest='prefix',
            type=str,
            help='only consider archive names starting with this prefix')
        subparser.add_argument('repository',
                               metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to prune')
        args = parser.parse_args(args or ['-h'])
        self.verbose = args.verbose
        return args.func(args)
Beispiel #4
0
    def run(self, args=None):
        keys_dir = get_keys_dir()
        if not os.path.exists(keys_dir):
            os.makedirs(keys_dir)
            os.chmod(keys_dir, stat.S_IRWXU)
        cache_dir = get_cache_dir()
        if not os.path.exists(cache_dir):
            os.makedirs(cache_dir)
            os.chmod(cache_dir, stat.S_IRWXU)
        common_parser = argparse.ArgumentParser(add_help=False)
        common_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
                            default=False,
                            help='verbose output')

        # We can't use argpase for "serve" since we don't want it to show up in "Available commands"
        if args and args[0] == 'serve':
            return self.do_serve()

        parser = argparse.ArgumentParser(description='Attic %s - Deduplicated Backups' % __version__)
        subparsers = parser.add_subparsers(title='Available commands')

        subparser = subparsers.add_parser('init', parents=[common_parser],
                                          description=self.do_init.__doc__)
        subparser.set_defaults(func=self.do_init)
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to create')
        subparser.add_argument('-e', '--encryption', dest='encryption',
                               choices=('none', 'passphrase', 'keyfile'), default='none',
                               help='select encryption method')

        subparser = subparsers.add_parser('change-passphrase', parents=[common_parser],
                                          description=self.do_change_passphrase.__doc__)
        subparser.set_defaults(func=self.do_change_passphrase)
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False))

        subparser = subparsers.add_parser('create', parents=[common_parser],
                                          description=self.do_create.__doc__)
        subparser.set_defaults(func=self.do_create)
        subparser.add_argument('-s', '--stats', dest='stats',
                               action='store_true', default=False,
                               help='print statistics for the created archive')
        subparser.add_argument('-e', '--exclude', dest='excludes',
                               type=ExcludePattern, action='append',
                               metavar="PATTERN", help='exclude paths matching PATTERN')
        subparser.add_argument('-c', '--checkpoint-interval', dest='checkpoint_interval',
                               type=int, default=300, metavar='SECONDS',
                               help='write checkpoint every SECONDS seconds (Default: 300)')
        subparser.add_argument('--do-not-cross-mountpoints', dest='dontcross',
                               action='store_true', default=False,
                               help='do not cross mount points')
        subparser.add_argument('--numeric-owner', dest='numeric_owner',
                               action='store_true', default=False,
                               help='only store numeric user and group identifiers')
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to create')
        subparser.add_argument('paths', metavar='PATH', nargs='+', type=str,
                               help='paths to archive')

        subparser = subparsers.add_parser('extract', parents=[common_parser],
                                          description=self.do_extract.__doc__)
        subparser.set_defaults(func=self.do_extract)
        subparser.add_argument('-e', '--exclude', dest='excludes',
                               type=ExcludePattern, action='append',
                               metavar="PATTERN", help='exclude paths matching PATTERN')
        subparser.add_argument('--numeric-owner', dest='numeric_owner',
                               action='store_true', default=False,
                               help='only obey numeric user and group identifiers')
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to extract')
        subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
                               help='paths to extract')

        subparser = subparsers.add_parser('delete', parents=[common_parser],
                                          description=self.do_delete.__doc__)
        subparser.set_defaults(func=self.do_delete)
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to delete')

        subparser = subparsers.add_parser('list', parents=[common_parser],
                                          description=self.do_list.__doc__)
        subparser.set_defaults(func=self.do_list)
        subparser.add_argument('src', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(),
                               help='repository/archive to list contents of')

        subparser = subparsers.add_parser('mount', parents=[common_parser],
                                          description=self.do_mount.__doc__)
        subparser.set_defaults(func=self.do_mount)
        subparser.add_argument('archive', metavar='ARCHIVE', type=location_validator(archive=True),
                               help='archive to mount')
        subparser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str,
                               help='where to mount filesystem')
        subparser.add_argument('-f', '--foreground', dest='foreground',
                               action='store_true', default=False,
                               help='stay in foreground, do not daemonize')
        subparser.add_argument('-o', dest='options', type=str,
                               help='Extra mount options')

        subparser = subparsers.add_parser('verify', parents=[common_parser],
                                          description=self.do_verify.__doc__)
        subparser.set_defaults(func=self.do_verify)
        subparser.add_argument('-e', '--exclude', dest='excludes',
                               type=ExcludePattern, action='append',
                               metavar="PATTERN", help='exclude paths matching PATTERN')
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to verity integrity of')
        subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
                               help='paths to verify')

        subparser = subparsers.add_parser('info', parents=[common_parser],
                                          description=self.do_info.__doc__)
        subparser.set_defaults(func=self.do_info)
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to display information about')

        subparser = subparsers.add_parser('prune', parents=[common_parser],
                                          description=self.do_prune.__doc__)
        subparser.set_defaults(func=self.do_prune)
        subparser.add_argument('-H', '--hourly', dest='hourly', type=int, default=0,
                               help='number of hourly archives to keep')
        subparser.add_argument('-d', '--daily', dest='daily', type=int, default=0,
                               help='number of daily archives to keep')
        subparser.add_argument('-w', '--weekly', dest='weekly', type=int, default=0,
                               help='number of daily archives to keep')
        subparser.add_argument('-m', '--monthly', dest='monthly', type=int, default=0,
                               help='number of monthly archives to keep')
        subparser.add_argument('-y', '--yearly', dest='yearly', type=int, default=0,
                               help='number of yearly archives to keep')
        subparser.add_argument('-p', '--prefix', dest='prefix', type=str,
                               help='only consider archive names starting with this prefix')
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to prune')
        args = parser.parse_args(args or ['-h'])
        self.verbose = args.verbose
        return args.func(args)
Beispiel #5
0
    def run(self, args=None):
        keys_dir = get_keys_dir()
        if not os.path.exists(keys_dir):
            os.makedirs(keys_dir)
            os.chmod(keys_dir, stat.S_IRWXU)
        cache_dir = get_cache_dir()
        if not os.path.exists(cache_dir):
            os.makedirs(cache_dir)
            os.chmod(cache_dir, stat.S_IRWXU)
        common_parser = argparse.ArgumentParser(add_help=False)
        common_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
                            default=False,
                            help='verbose output')

        # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
        if args and args[0] == 'serve':
            return self.do_serve()
        if args:
            args = self.preprocess_args(args)

        parser = argparse.ArgumentParser(description='Attic %s - Deduplicated Backups' % __version__)
        subparsers = parser.add_subparsers(title='Available commands')

        subparser = subparsers.add_parser('init', parents=[common_parser],
                                          description=self.do_init.__doc__)
        subparser.set_defaults(func=self.do_init)
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to create')
        subparser.add_argument('-e', '--encryption', dest='encryption',
                               choices=('none', 'passphrase', 'keyfile'), default='none',
                               help='select encryption method')

        check_epilog = textwrap.dedent("""
        The check command verifies the consistency of a repository and the corresponding
        archives. The underlying repository data files are first checked to detect bit rot
        and other types of damage. After that the consistency and correctness of the archive
        metadata is verified.

        The archive metadata checks can be time consuming and requires access to the key
        file and/or passphrase if encryption is enabled. These checks can be skipped using
        the --repository-only option.
        """)
        subparser = subparsers.add_parser('check', parents=[common_parser],
                                          description=self.do_check.__doc__,
                                          epilog=check_epilog,
                                          formatter_class=argparse.RawDescriptionHelpFormatter)
        subparser.set_defaults(func=self.do_check)
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to check consistency of')
        subparser.add_argument('--repository-only', dest='repo_only', action='store_true',
                               default=False,
                               help='only perform repository checks')
        subparser.add_argument('--archives-only', dest='archives_only', action='store_true',
                               default=False,
                               help='only perform archives checks')
        subparser.add_argument('--repair', dest='repair', action='store_true',
                               default=False,
                               help='attempt to repair any inconsistencies found')

        subparser = subparsers.add_parser('change-passphrase', parents=[common_parser],
                                          description=self.do_change_passphrase.__doc__)
        subparser.set_defaults(func=self.do_change_passphrase)
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False))

        create_epilog = '''See "attic help patterns" for more help on exclude patterns.'''

        subparser = subparsers.add_parser('create', parents=[common_parser],
                                          description=self.do_create.__doc__,
                                          epilog=create_epilog)
        subparser.set_defaults(func=self.do_create)
        subparser.add_argument('-s', '--stats', dest='stats',
                               action='store_true', default=False,
                               help='print statistics for the created archive')
        subparser.add_argument('-e', '--exclude', dest='excludes',
                               type=ExcludePattern, action='append',
                               metavar="PATTERN", help='exclude paths matching PATTERN')
        subparser.add_argument('--exclude-from', dest='exclude_files',
                               type=argparse.FileType('r'), action='append',
                               metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
        subparser.add_argument('-c', '--checkpoint-interval', dest='checkpoint_interval',
                               type=int, default=300, metavar='SECONDS',
                               help='write checkpoint every SECONDS seconds (Default: 300)')
        subparser.add_argument('--do-not-cross-mountpoints', dest='dontcross',
                               action='store_true', default=False,
                               help='do not cross mount points')
        subparser.add_argument('--numeric-owner', dest='numeric_owner',
                               action='store_true', default=False,
                               help='only store numeric user and group identifiers')
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to create')
        subparser.add_argument('paths', metavar='PATH', nargs='+', type=str,
                               help='paths to archive')

        extract_epilog = '''See "attic help patterns" for more help on exclude patterns.'''

        subparser = subparsers.add_parser('extract', parents=[common_parser],
                                          description=self.do_extract.__doc__,
                                          epilog=extract_epilog)
        subparser.set_defaults(func=self.do_extract)
        subparser.add_argument('-n', '--dry-run', dest='dry_run',
                               default=False, action='store_true',
                               help='do not actually change any files')
        subparser.add_argument('-e', '--exclude', dest='excludes',
                               type=ExcludePattern, action='append',
                               metavar="PATTERN", help='exclude paths matching PATTERN')
        subparser.add_argument('--exclude-from', dest='exclude_files',
                               type=argparse.FileType('r'), action='append',
                               metavar='EXCLUDEFILE', help='read exclude patterns from EXCLUDEFILE, one per line')
        subparser.add_argument('--numeric-owner', dest='numeric_owner',
                               action='store_true', default=False,
                               help='only obey numeric user and group identifiers')
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to extract')
        subparser.add_argument('paths', metavar='PATH', nargs='*', type=str,
                               help='paths to extract')

        subparser = subparsers.add_parser('delete', parents=[common_parser],
                                          description=self.do_delete.__doc__)
        subparser.set_defaults(func=self.do_delete)
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to delete')

        subparser = subparsers.add_parser('list', parents=[common_parser],
                                          description=self.do_list.__doc__)
        subparser.set_defaults(func=self.do_list)
        subparser.add_argument('src', metavar='REPOSITORY_OR_ARCHIVE', type=location_validator(),
                               help='repository/archive to list contents of')

        subparser = subparsers.add_parser('mount', parents=[common_parser],
                                          description=self.do_mount.__doc__)
        subparser.set_defaults(func=self.do_mount)
        subparser.add_argument('archive', metavar='ARCHIVE', type=location_validator(archive=True),
                               help='archive to mount')
        subparser.add_argument('mountpoint', metavar='MOUNTPOINT', type=str,
                               help='where to mount filesystem')
        subparser.add_argument('-f', '--foreground', dest='foreground',
                               action='store_true', default=False,
                               help='stay in foreground, do not daemonize')
        subparser.add_argument('-o', dest='options', type=str,
                               help='Extra mount options')

        subparser = subparsers.add_parser('info', parents=[common_parser],
                                          description=self.do_info.__doc__)
        subparser.set_defaults(func=self.do_info)
        subparser.add_argument('archive', metavar='ARCHIVE',
                               type=location_validator(archive=True),
                               help='archive to display information about')

        prune_epilog = '''The prune command prunes a repository by deleting archives
        not matching any of the specified retention options. This command is normally
        used by automated backup scripts wanting to keep a certain number of historic
        backups. As an example, "-d 7" means to keep the latest backup on each day
        for 7 days. Days without backups do not count towards the total. The rules
        are applied from hourly to yearly, and backups selected by previous rules do
        not count towards those of later rules. The time that each backup completes
        is used for pruning purposes. Dates and times are interpreted in
        the local timezone, and weeks go from Monday to Sunday. Specifying a
        negative number of archives to keep means that there is no limit.
        The "--keep-within" option takes an argument of the form "<int><char>",
        where char is "H", "d", "w", "m", "y". For example, "--keep-within 2d" means
        to keep all archives that were created within the past 48 hours.
        "1m" is taken to mean "31d". The archives kept with this option do not
        count towards the totals specified by any other options. If a
        prefix is set with -p, then only archives that start with the prefix are
        considered for deletion and only those archives count towards the totals
        specified by the rules.'''

        subparser = subparsers.add_parser('prune', parents=[common_parser],
                                          description=self.do_prune.__doc__,
                                          epilog=prune_epilog)
        subparser.set_defaults(func=self.do_prune)
        subparser.add_argument('-n', '--dry-run', dest='dry_run',
                               default=False, action='store_true',
                               help='do not change repository')
        subparser.add_argument('--keep-within', dest='within', type=str, metavar='WITHIN',
                               help='keep all archives within this time interval')
        subparser.add_argument('-H', '--keep-hourly', dest='hourly', type=int, default=0,
                               help='number of hourly archives to keep')
        subparser.add_argument('-d', '--keep-daily', dest='daily', type=int, default=0,
                               help='number of daily archives to keep')
        subparser.add_argument('-w', '--keep-weekly', dest='weekly', type=int, default=0,
                               help='number of weekly archives to keep')
        subparser.add_argument('-m', '--keep-monthly', dest='monthly', type=int, default=0,
                               help='number of monthly archives to keep')
        subparser.add_argument('-y', '--keep-yearly', dest='yearly', type=int, default=0,
                               help='number of yearly archives to keep')
        subparser.add_argument('-p', '--prefix', dest='prefix', type=str,
                               help='only consider archive names starting with this prefix')
        subparser.add_argument('repository', metavar='REPOSITORY',
                               type=location_validator(archive=False),
                               help='repository to prune')

        subparser = subparsers.add_parser('help', parents=[common_parser],
                                          description='Extra help')
        subparser.set_defaults(func=self.do_help)
        subparser.add_argument('topic', metavar='TOPIC', type=str,
                               help='additional help on TOPIC')

        args = parser.parse_args(args or ['-h'])
        self.verbose = args.verbose
        update_excludes(args)
        return args.func(args)