diff --git a/NEWS b/NEWS index 0bb1e29b7..d16e5d7b8 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,7 @@ * #536: Fix generate-borgmatic-config to support more complex schema changes like the new Healthchecks configuration options when the "--source" flag is used. * #538: Add support for "borgmatic borg debug" command. + * #539: Add "generate-borgmatic-config --overwrite" flag to replace an existing destination file. * Add Bash completion script so you can tab-complete the borgmatic command-line. See the documentation for more information: https://torsion.org/borgmatic/docs/how-to/set-up-backups/#shell-completion diff --git a/borgmatic/commands/generate_config.py b/borgmatic/commands/generate_config.py index 8ea3d5c13..7f7750c24 100644 --- a/borgmatic/commands/generate_config.py +++ b/borgmatic/commands/generate_config.py @@ -23,10 +23,16 @@ def parse_arguments(*arguments): '--destination', dest='destination_filename', default=DEFAULT_DESTINATION_CONFIG_FILENAME, - help='Destination YAML configuration file. Default: {}'.format( + help='Destination YAML configuration file, default: {}'.format( DEFAULT_DESTINATION_CONFIG_FILENAME ), ) + parser.add_argument( + '--overwrite', + default=False, + action='store_true', + help='Whether to overwrite any existing destination file, defaults to false', + ) return parser.parse_args(arguments) @@ -36,7 +42,10 @@ def main(): # pragma: no cover args = parse_arguments(*sys.argv[1:]) generate.generate_sample_configuration( - args.source_filename, args.destination_filename, validate.schema_filename() + args.source_filename, + args.destination_filename, + validate.schema_filename(), + overwrite=args.overwrite, ) print('Generated a sample configuration file at {}.'.format(args.destination_filename)) diff --git a/borgmatic/config/generate.py b/borgmatic/config/generate.py index 62f16fbab..a454f662c 100644 --- a/borgmatic/config/generate.py +++ b/borgmatic/config/generate.py @@ -109,13 +109,18 @@ def render_configuration(config): return rendered.getvalue() -def write_configuration(config_filename, rendered_config, mode=0o600): +def write_configuration(config_filename, rendered_config, mode=0o600, overwrite=False): ''' Given a target config filename and rendered config YAML, write it out to file. Create any - containing directories as needed. + containing directories as needed. But if the file already exists and overwrite is False, + abort before writing anything. ''' - if os.path.exists(config_filename): - raise FileExistsError('{} already exists. Aborting.'.format(config_filename)) + if not overwrite and os.path.exists(config_filename): + raise FileExistsError( + '{} already exists. Aborting. Use --overwrite to replace the file.'.format( + config_filename + ) + ) try: os.makedirs(os.path.dirname(config_filename), mode=0o700) @@ -263,12 +268,15 @@ def merge_source_configuration_into_destination(destination_config, source_confi return destination_config -def generate_sample_configuration(source_filename, destination_filename, schema_filename): +def generate_sample_configuration( + source_filename, destination_filename, schema_filename, overwrite=False +): ''' Given an optional source configuration filename, and a required destination configuration - filename, and the path to a schema filename in a YAML rendition of the JSON Schema format, - write out a sample configuration file based on that schema. If a source filename is provided, - merge the parsed contents of that configuration into the generated configuration. + filename, the path to a schema filename in a YAML rendition of the JSON Schema format, and + whether to overwrite a destination file, write out a sample configuration file based on that + schema. If a source filename is provided, merge the parsed contents of that configuration into + the generated configuration. ''' schema = yaml.round_trip_load(open(schema_filename)) source_config = None @@ -284,4 +292,5 @@ def generate_sample_configuration(source_filename, destination_filename, schema_ write_configuration( destination_filename, _comment_out_optional_configuration(render_configuration(destination_config)), + overwrite=overwrite, ) diff --git a/tests/integration/commands/test_generate_config.py b/tests/integration/commands/test_generate_config.py index ad1404c50..4cd54429f 100644 --- a/tests/integration/commands/test_generate_config.py +++ b/tests/integration/commands/test_generate_config.py @@ -1,13 +1,25 @@ from borgmatic.commands import generate_config as module -def test_parse_arguments_with_no_arguments_uses_defaults(): +def test_parse_arguments_with_no_arguments_uses_default_destination(): parser = module.parse_arguments() assert parser.destination_filename == module.DEFAULT_DESTINATION_CONFIG_FILENAME -def test_parse_arguments_with_filename_argument_overrides_defaults(): +def test_parse_arguments_with_destination_argument_overrides_default(): parser = module.parse_arguments('--destination', 'config.yaml') assert parser.destination_filename == 'config.yaml' + + +def test_parse_arguments_parses_source(): + parser = module.parse_arguments('--source', 'source.yaml', '--destination', 'config.yaml') + + assert parser.source_filename == 'source.yaml' + + +def test_parse_arguments_parses_overwrite(): + parser = module.parse_arguments('--destination', 'config.yaml', '--overwrite') + + assert parser.overwrite diff --git a/tests/integration/config/test_generate.py b/tests/integration/config/test_generate.py index 54435e857..1b030a5b9 100644 --- a/tests/integration/config/test_generate.py +++ b/tests/integration/config/test_generate.py @@ -87,7 +87,7 @@ location: assert module._comment_out_optional_configuration(config.strip()) == expected_config.strip() -def testrender_configuration_converts_configuration_to_yaml_string(): +def test_render_configuration_converts_configuration_to_yaml_string(): yaml_string = module.render_configuration({'foo': 'bar'}) assert yaml_string == 'foo: bar\n' @@ -110,6 +110,12 @@ def test_write_configuration_with_already_existing_file_raises(): module.write_configuration('config.yaml', 'config: yaml') +def test_write_configuration_with_already_existing_file_and_overwrite_does_not_raise(): + flexmock(os.path).should_receive('exists').and_return(True) + + module.write_configuration('config.yaml', 'config: yaml', overwrite=True) + + def test_write_configuration_with_already_existing_directory_does_not_raise(): flexmock(os.path).should_receive('exists').and_return(False) flexmock(os).should_receive('makedirs').and_raise(FileExistsError)