diff --git a/NEWS b/NEWS index 5e48a5e2..ac0e36b7 100644 --- a/NEWS +++ b/NEWS @@ -1,4 +1,5 @@ 1.9.13.dev0 + * #975: Add a "compression" option to the PostgreSQL database hook. * #1001: Fix a ZFS error during snapshot cleanup. * #1003: In the Zabbix monitoring hook, support Zabbix 7.2's authentication changes. * #1009: Send database passwords to MariaDB and MySQL via anonymous pipe, which is more secure than diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 33f39a70..109b1a8f 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -1040,6 +1040,17 @@ properties: individual databases. See the pg_dump documentation for more about formats. example: directory + compression: + type: ["string", "integer"] + description: | + Database dump compression level (integer) or method + ("gzip", "lz4", "zstd", or "none") and optional + colon-separated detail. Defaults to moderate "gzip" for + "custom" and "directory" formats and no compression for + the "plain" format. Compression is not supported for the + "tar" format. Be aware that Borg does its own + compression as well, so you may not need it in both + places. ssl_mode: type: string enum: ['disable', 'allow', 'prefer', diff --git a/borgmatic/hooks/data_source/postgresql.py b/borgmatic/hooks/data_source/postgresql.py index 559cc4b8..4f470c52 100644 --- a/borgmatic/hooks/data_source/postgresql.py +++ b/borgmatic/hooks/data_source/postgresql.py @@ -159,6 +159,7 @@ def dump_data_sources( for database_name in dump_database_names: dump_format = database.get('format', None if database_name == 'all' else 'custom') + compression = database.get('compression') default_dump_command = 'pg_dumpall' if database_name == 'all' else 'pg_dump' dump_command = tuple( shlex.quote(part) @@ -199,6 +200,7 @@ def dump_data_sources( ) + (('--no-owner',) if database.get('no_owner', False) else ()) + (('--format', shlex.quote(dump_format)) if dump_format else ()) + + (('--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(' ')) diff --git a/tests/unit/hooks/data_source/test_postgresql.py b/tests/unit/hooks/data_source/test_postgresql.py index 1037cf66..f1589a6e 100644 --- a/tests/unit/hooks/data_source/test_postgresql.py +++ b/tests/unit/hooks/data_source/test_postgresql.py @@ -555,6 +555,100 @@ def test_dump_data_sources_runs_pg_dump_with_directory_format(): ) +def test_dump_data_sources_runs_pg_dump_with_string_compression(): + databases = [{'name': 'foo', 'compression': 'winrar'}] + processes = [flexmock()] + flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'}) + flexmock(module).should_receive('make_dump_path').and_return('') + flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)) + flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return( + 'databases/localhost/foo' + ) + flexmock(module.os.path).should_receive('exists').and_return(False) + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential' + ).replace_with(lambda value, config: value) + flexmock(module.dump).should_receive('create_named_pipe_for_dump') + + flexmock(module).should_receive('execute_command').with_args( + ( + 'pg_dump', + '--no-password', + '--clean', + '--if-exists', + '--format', + 'custom', + '--compress', + 'winrar', + 'foo', + '>', + 'databases/localhost/foo', + ), + shell=True, + environment={'PGSSLMODE': 'disable'}, + run_to_completion=False, + ).and_return(processes[0]).once() + + assert ( + module.dump_data_sources( + databases, + {}, + config_paths=('test.yaml',), + borgmatic_runtime_directory='/run/borgmatic', + patterns=[], + dry_run=False, + ) + == processes + ) + + +def test_dump_data_sources_runs_pg_dump_with_integer_compression(): + databases = [{'name': 'foo', 'compression': 0}] + processes = [flexmock()] + flexmock(module).should_receive('make_environment').and_return({'PGSSLMODE': 'disable'}) + flexmock(module).should_receive('make_dump_path').and_return('') + flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)) + flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return( + 'databases/localhost/foo' + ) + flexmock(module.os.path).should_receive('exists').and_return(False) + flexmock(module.borgmatic.hooks.credential.parse).should_receive( + 'resolve_credential' + ).replace_with(lambda value, config: value) + flexmock(module.dump).should_receive('create_named_pipe_for_dump') + + flexmock(module).should_receive('execute_command').with_args( + ( + 'pg_dump', + '--no-password', + '--clean', + '--if-exists', + '--format', + 'custom', + '--compress', + '0', + 'foo', + '>', + 'databases/localhost/foo', + ), + shell=True, + environment={'PGSSLMODE': 'disable'}, + run_to_completion=False, + ).and_return(processes[0]).once() + + assert ( + module.dump_data_sources( + databases, + {}, + config_paths=('test.yaml',), + borgmatic_runtime_directory='/run/borgmatic', + patterns=[], + dry_run=False, + ) + == processes + ) + + def test_dump_data_sources_runs_pg_dump_with_options(): databases = [{'name': 'foo', 'options': '--stuff=such'}] process = flexmock()