From e3fa18b892eea36b498c7c75eb2df839cc887d2a Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 14 Feb 2015 09:23:40 -0800 Subject: [PATCH] After pruning, run attic's consistency checks on all archives. --- NEWS | 3 ++- README.md | 13 ++++++----- atticmatic/attic.py | 18 ++++++++++++++- atticmatic/command.py | 6 +++-- atticmatic/tests/unit/test_attic.py | 35 +++++++++++++++++++++++++++-- 5 files changed, 64 insertions(+), 11 deletions(-) diff --git a/NEWS b/NEWS index 17bf8d3f1..93d33a5d6 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,6 @@ -default +0.0.3 + * After pruning, run attic's consistency checks on all archives. * Integration tests for argument parsing. * Documentation updates about repository encryption. diff --git a/README.md b/README.md index 034bbfecc..4a2d2110c 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,11 @@ save_as: atticmatic/index.html ## Overview atticmatic is a simple Python wrapper script for the [Attic backup -software](https://attic-backup.org/) that initiates a backup and prunes any -old backups according to a retention policy. The script supports specifying -your settings in a declarative configuration file rather than having to put -them all on the command-line, and handles common errors. +software](https://attic-backup.org/) that initiates a backup, prunes any old +backups according to a retention policy, and validates backups for +consistency. The script supports specifying your settings in a declarative +configuration file rather than having to put them all on the command-line, and +handles common errors. Here's an example config file: @@ -68,7 +69,9 @@ arguments: atticmatic -This will also prune any old backups as per the configured retention policy. +This will also prune any old backups as per the configured retention policy, +and check backups for consistency problems due to things like file damage. + By default, the backup will proceed silently except in the case of errors. But if you'd like to to get additional information about the progress of the backup as it proceeds, use the verbose option instead: diff --git a/atticmatic/attic.py b/atticmatic/attic.py index c8842103c..d0c678d68 100644 --- a/atticmatic/attic.py +++ b/atticmatic/attic.py @@ -1,5 +1,5 @@ from datetime import datetime - +import os import platform import subprocess @@ -63,3 +63,19 @@ def prune_archives(verbose, repository, retention_config): ) + (('--verbose',) if verbose else ()) subprocess.check_call(command) + + +def check_archives(verbose, repository): + ''' + Given a verbosity flag and a local or remote repository path, check the contained attic archives + for consistency. + ''' + command = ( + 'attic', 'check', + repository, + ) + (('--verbose',) if verbose else ()) + + # Attic's check command spews to stdout even without the verbose flag. Suppress it. + stdout = None if verbose else open(os.devnull, 'w') + + subprocess.check_call(command, stdout=stdout) diff --git a/atticmatic/command.py b/atticmatic/command.py index ff31ca786..92976f0ea 100644 --- a/atticmatic/command.py +++ b/atticmatic/command.py @@ -3,7 +3,7 @@ from argparse import ArgumentParser from subprocess import CalledProcessError import sys -from atticmatic.attic import create_archive, prune_archives +from atticmatic.attic import check_archives, create_archive, prune_archives from atticmatic.config import parse_configuration @@ -41,9 +41,11 @@ def main(): try: args = parse_arguments(*sys.argv[1:]) location_config, retention_config = parse_configuration(args.config_filename) + repository = location_config['repository'] create_archive(args.excludes_filename, args.verbose, **location_config) - prune_archives(args.verbose, location_config['repository'], retention_config) + prune_archives(args.verbose, repository, retention_config) + check_archives(args.verbose, repository) except (ValueError, IOError, CalledProcessError) as error: print(error, file=sys.stderr) sys.exit(1) diff --git a/atticmatic/tests/unit/test_attic.py b/atticmatic/tests/unit/test_attic.py index 44b38bbd0..2c93e8c78 100644 --- a/atticmatic/tests/unit/test_attic.py +++ b/atticmatic/tests/unit/test_attic.py @@ -5,9 +5,9 @@ from flexmock import flexmock from atticmatic import attic as module -def insert_subprocess_mock(check_call_command): +def insert_subprocess_mock(check_call_command, **kwargs): subprocess = flexmock() - subprocess.should_receive('check_call').with_args(check_call_command).once() + subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once() flexmock(module).subprocess = subprocess @@ -111,3 +111,34 @@ def test_prune_archives_with_verbose_should_call_attic_with_verbose_parameters() verbose=True, retention_config=retention_config, ) + + +def test_check_archives_should_call_attic_with_parameters(): + stdout = flexmock() + insert_subprocess_mock( + ('attic', 'check', 'repo'), + stdout=stdout, + ) + insert_platform_mock() + insert_datetime_mock() + flexmock(module).open = lambda filename, mode: stdout + flexmock(module).os = flexmock().should_receive('devnull').mock + + module.check_archives( + verbose=False, + repository='repo', + ) + + +def test_check_archives_with_verbose_should_call_attic_with_verbose_parameters(): + insert_subprocess_mock( + ('attic', 'check', 'repo', '--verbose'), + stdout=None, + ) + insert_platform_mock() + insert_datetime_mock() + + module.check_archives( + verbose=True, + repository='repo', + )