diff --git a/NEWS b/NEWS index 873596ee..2d28fecb 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,7 @@ * For the MariaDB and MySQL database hooks, escape quotes in passwords when the "password_transport" option is "pipe". * Add a development script for upgrading pinned dependencies. + * Fix the PostgreSQL database hook to properly parse "*options" values containing quoted spaces. 2.1.5 * #1229: Document the permissions needed for the PostgreSQL database hook: diff --git a/borgmatic/hooks/data_source/postgresql.py b/borgmatic/hooks/data_source/postgresql.py index 4b97b2e0..885bd5f4 100644 --- a/borgmatic/hooks/data_source/postgresql.py +++ b/borgmatic/hooks/data_source/postgresql.py @@ -100,7 +100,11 @@ def database_names_to_dump(database, config, environment, dry_run): if 'username' in database else () ) - + (tuple(database['list_options'].split(' ')) if 'list_options' in database else ()) + + ( + tuple(shlex.quote(part) for part in shlex.split(database['list_options'])) + if 'list_options' in database + else () + ) ) logger.debug('Querying for "all" PostgreSQL databases to dump') list_lines = execute_command_and_capture_output( @@ -226,7 +230,7 @@ def dump_data_sources( + (('--compress', shlex.quote(str(compression))) if compression is not None else ()) + (('--file', shlex.quote(dump_filename)) if dump_format == 'directory' else ()) + ( - tuple(shlex.quote(option) for option in database['options'].split(' ')) + tuple(shlex.quote(part) for part in shlex.split(database['options'])) if 'options' in database else () ) @@ -393,7 +397,7 @@ def restore_data_source_dump( + (('--username', username) if username else ()) + (('--dbname', data_source['name']) if not all_databases else ()) + ( - tuple(data_source['analyze_options'].split(' ')) + tuple(shlex.quote(part) for part in shlex.split(data_source['analyze_options'])) if 'analyze_options' in data_source else () ) @@ -414,7 +418,7 @@ def restore_data_source_dump( + (('--username', username) if username else ()) + (('--no-owner',) if data_source.get('no_owner', False) else ()) + ( - tuple(data_source['restore_options'].split(' ')) + tuple(shlex.quote(part) for part in shlex.split(data_source['restore_options'])) if 'restore_options' in data_source else () ) diff --git a/tests/unit/hooks/data_source/test_postgresql.py b/tests/unit/hooks/data_source/test_postgresql.py index a07f1624..44bc2099 100644 --- a/tests/unit/hooks/data_source/test_postgresql.py +++ b/tests/unit/hooks/data_source/test_postgresql.py @@ -172,13 +172,22 @@ def test_database_names_to_dump_with_all_and_format_lists_databases_with_usernam def test_database_names_to_dump_with_all_and_format_lists_databases_with_options(): - database = {'name': 'all', 'format': 'custom', 'list_options': '--harder'} + database = {'name': 'all', 'format': 'custom', 'list_options': '--harder "foo bar"'} flexmock(module.borgmatic.hooks.credential.parse).should_receive( 'resolve_credential', ).replace_with(lambda value, config: value) flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None) flexmock(module).should_receive('execute_command_and_capture_output').with_args( - ('psql', '--list', '--no-password', '--no-psqlrc', '--csv', '--tuples-only', '--harder'), + ( + 'psql', + '--list', + '--no-password', + '--no-psqlrc', + '--csv', + '--tuples-only', + '--harder', + "'foo bar'", + ), environment=object, working_directory=None, ).and_yield('foo,test,', 'bar,test,"stuff and such"') @@ -803,7 +812,7 @@ def test_dump_data_sources_runs_pg_dump_with_integer_compression(): def test_dump_data_sources_runs_pg_dump_with_options(): - databases = [{'name': 'foo', 'options': '--stuff=such'}] + databases = [{'name': 'foo', 'options': '--stuff "foo bar"'}] process = flexmock() flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'}) flexmock(module).should_receive('make_dump_path').and_return('') @@ -826,7 +835,8 @@ def test_dump_data_sources_runs_pg_dump_with_options(): '--if-exists', '--format', 'custom', - '--stuff=such', + '--stuff', + "'foo bar'", 'foo', '>', 'databases/localhost/foo', @@ -1340,8 +1350,8 @@ def test_restore_data_source_dump_runs_pg_restore_with_options(): hook_config = [ { 'name': 'foo', - 'restore_options': '--harder', - 'analyze_options': '--smarter', + 'restore_options': '--harder "foo bar"', + 'analyze_options': '--smarter "baz quux"', 'schemas': None, }, ] @@ -1364,6 +1374,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_options(): '--dbname', 'foo', '--harder', + "'foo bar'", ), processes=[extract_process], output_log_level=logging.DEBUG, @@ -1381,6 +1392,7 @@ def test_restore_data_source_dump_runs_pg_restore_with_options(): '--dbname', 'foo', '--smarter', + "'baz quux'", '--command', 'ANALYZE', ),