Skip to content

Commit 88d0c42

Browse files
Merge pull request #10438 from ThomasWaldmann/repo-default-compression
repo-create: store default compression and chunker params in the repository, fixes #346
2 parents 69f7bb2 + f694ba3 commit 88d0c42

18 files changed

Lines changed: 390 additions & 33 deletions

‎docs/internals/data-structures.rst‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ of their content. The store hash is the unkeyed 256 bit BLAKE3 hash, see
3939
config/
4040
config
4141
the repository config (see :ref:`repo_config`), a text object
42+
defaults
43+
the repository defaults (see :ref:`repo_defaults`), in the key's store object
44+
envelope (see below). Only present if ``borg repo-create`` was given a default.
4245
space-reserve.N
4346
purely random binary data to reserve space, e.g. for disk-full emergencies.
4447
These objects are created and removed by ``borg repo-space``.
@@ -110,8 +113,8 @@ locks/
110113

111114
.. _store_object_envelope:
112115

113-
The index fragments, the lock objects, ``checked-packs`` and the
114-
``referenced-by-archive.*`` objects are stored in the **store object envelope**: the repository key's ``encrypt()``,
116+
The index fragments, the lock objects, ``checked-packs``, the
117+
``referenced-by-archive.*`` objects and ``config/defaults`` are stored in the **store object envelope**: the repository key's ``encrypt()``,
115118
exactly as for the metadata and data slots of the objects in a pack (see
116119
:ref:`security_encryption`), with an empty id and an AAD of
117120
``b"borg-store-object\0"`` followed by the repository id, the tag ``b"n"`` and the
@@ -129,7 +132,8 @@ packs; any other command that needs the chunks index aborts, except ``borg compa
129132
and ``borg repo-compress``, which rebuild it from the packs, as they rewrite the whole
130133
chunks index anyway (under an exclusive lock). A corrupted cache is ignored and
131134
rebuilt. A lock object that fails the authentication is treated as a foreign exclusive
132-
lock, see :ref:`storelocking`. The ``chunkindex-invalid`` marker has no content and is
135+
lock, see :ref:`storelocking`. Commands that use ``config/defaults`` abort if it fails
136+
the authentication. The ``chunkindex-invalid`` marker has no content and is
133137
stored as is.
134138

135139

@@ -308,6 +312,36 @@ a ``keys/`` object, see :ref:`key_files`), and the encryption mode and id hash
308312
are what ``borg repo-create`` was given (the key type byte of any repository
309313
object encodes them as well, see ``KeyType`` in ``constants.py``).
310314

315+
.. _repo_defaults:
316+
317+
Repository defaults
318+
~~~~~~~~~~~~~~~~~~~
319+
320+
The ``config/defaults`` store object holds default values for command options,
321+
as a msgpacked dict mapping the option name to its value, both as strings::
322+
323+
{"compression": "zstd,3", "chunker_params": "fastcdc,19,23,21,2"}
324+
325+
*compression* is the default compression spec (see ``borg help compression``),
326+
set by ``borg repo-create --compression``. The commands with a ``--compression``
327+
option use it if no compression was given via the command line, the environment or
328+
the default config file; without it, they use lz4.
329+
330+
*chunker_params* are the default :ref:`chunker-params <chunker-params>`, set by
331+
``borg repo-create --chunker-params``. ``borg create`` and ``borg import-tar`` use
332+
them if no chunker params were given (in the same ways as above), ``borg recreate``
333+
and ``borg transfer`` for ``--chunker-params default``; without them, the built-in
334+
default chunker params are used.
335+
336+
Each entry is optional, a missing entry means that the repository has no default
337+
for it.
338+
339+
Unlike the repository config, which borg must read before it knows the key, the
340+
defaults are stored in the :ref:`store object envelope <store_object_envelope>`,
341+
so they are authenticated: an attacker with write access to the storage can not
342+
change them (e.g. remove an ``obfuscate`` compression) without being noticed. A
343+
repository without the object has no defaults.
344+
311345
.. _archive:
312346

313347
Archives

‎docs/internals/frontends.rst‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,14 @@ path
345345
Path to the local repository cache
346346

347347
:ref:`borg_repo-info` additionally emits a *security_dir* key with the path of the local security
348-
directory of the repository.
348+
directory of the repository and a *defaults* key with an object containing:
349+
350+
compression
351+
The compression spec the commands use if no compression is given: the repository default (see
352+
:ref:`borg_repo-create` ``--compression``), else ``lz4``
353+
chunker_params
354+
The chunker params the commands use if no chunker params are given: the repository default (see
355+
:ref:`borg_repo-create` ``--chunker-params``), else the built-in default
349356

350357
.. highlight: json
351358
@@ -355,6 +362,10 @@ Example ``borg repo-info --json`` output::
355362
"cache": {
356363
"path": "/home/user/.cache/borg/65d7898e2142485f44506fb11c0fcd6d7dfd0341716385246068584a62632a94"
357364
},
365+
"defaults": {
366+
"chunker_params": "fastcdc,19,23,21,2",
367+
"compression": "lz4"
368+
},
358369
"encryption": {
359370
"encryption": "aes256-ocb",
360371
"id_hash": "sha256"

‎docs/quickstart.rst‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,14 @@ specified algorithm::
352352
You'll need to experiment a bit to find the best compression for your use case.
353353
Keep an eye on CPU load and throughput.
354354

355+
Instead of giving ``--compression`` to every command, you can set a default compression
356+
for the repository when creating it::
357+
358+
$ borg repo-create --encryption aes256-ocb --compression zstd,3
359+
360+
Commands that compress data use this default when you do not give ``--compression``.
361+
``borg repo-create --chunker-params`` sets the default chunker parameters in the same way.
362+
355363
.. _encrypted_repos:
356364

357365
Repository encryption

‎src/borg/archiver/_common.py‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
from ..helpers import CommandError, Error
1111
from ..helpers import SortBySpec, location_validator, Location, relative_time_marker_validator
1212
from ..helpers import FilesystemPathSpec
13+
from ..helpers import ChunkerParams, CompressionSpec
1314
from ..helpers import Highlander, octal_int
14-
from ..helpers.argparsing import SUPPRESS, PositiveInt
15+
from ..helpers.argparsing import SUPPRESS, ArgumentTypeError, PositiveInt
1516
from ..helpers.nanorst import rst_to_terminal
1617
from ..manifest import Manifest, AI_HUMAN_SORT_KEYS
1718
from ..patterns import PatternMatcher
@@ -80,6 +81,37 @@ def get_repository(
8081
return repository
8182

8283

84+
def _repository_default(repository, name, parse, builtin):
85+
"""Return the repository default for option name (see Repository.save_defaults), parsed, else builtin."""
86+
value = repository.load_defaults().get(name) if isinstance(repository, Repository) else None
87+
if value is None:
88+
return builtin
89+
try:
90+
return parse(value)
91+
except (ArgumentTypeError, ValueError) as err:
92+
raise Repository.InvalidRepositoryConfig(
93+
repository._location.canonical_path(), f"invalid default {name} {value!r}: {err}"
94+
) from None
95+
96+
97+
def default_compression(repository):
98+
"""Return the CompressionSpec a command uses if --compression was not given.
99+
100+
That is the repository default (set by "borg repo-create --compression"), else lz4.
101+
An explicitly configured compression (command line, environment, default.yaml) always wins.
102+
"""
103+
return _repository_default(repository, "compression", CompressionSpec, CompressionSpec(BUILTIN_COMPRESSION))
104+
105+
106+
def default_chunker_params(repository):
107+
"""Return the chunker params for "--chunker-params default" (the default of create and import-tar).
108+
109+
That is the repository default (set by "borg repo-create --chunker-params"), else CHUNKER_PARAMS.
110+
Explicitly configured chunker params (command line, environment, default.yaml) always win.
111+
"""
112+
return _repository_default(repository, "chunker_params", ChunkerParams, CHUNKER_PARAMS)
113+
114+
83115
def with_repository(
84116
create=False,
85117
lock=True,
@@ -152,7 +184,11 @@ def wrapper(self, args, **kwargs):
152184
manifest_ = Manifest.load(repository, other=False, ro_cls=ro_cls)
153185
kwargs["manifest"] = manifest_
154186
if "compression" in args:
187+
if args.compression is None: # not given, see default_compression()
188+
args.compression = default_compression(repository)
155189
manifest_.repo_objs.compressor = args.compression.compressor
190+
if "chunker_params" in args and args.chunker_params == DEFAULT_CHUNKER_PARAMS:
191+
args.chunker_params = default_chunker_params(repository)
156192
if secure:
157193
assert_secure(repository, manifest_)
158194
if cache:

‎src/borg/archiver/create_cmd.py‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,20 +1333,23 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser):
13331333
metavar="PARAMS",
13341334
dest="chunker_params",
13351335
type=ChunkerParams,
1336-
default=CHUNKER_PARAMS,
1336+
default=DEFAULT_CHUNKER_PARAMS, # see default_chunker_params()
13371337
action=Highlander,
13381338
help="specify the chunker parameters (ALGO, CHUNK_MIN_EXP, CHUNK_MAX_EXP, "
1339-
"HASH_MASK_BITS, NC_LEVEL). default: %s,%d,%d,%d,%d" % CHUNKER_PARAMS,
1339+
"HASH_MASK_BITS, NC_LEVEL). default: the repository default (see borg repo-create), "
1340+
"else %s,%d,%d,%d,%d" % CHUNKER_PARAMS,
13401341
)
13411342
archive_group.add_argument(
13421343
"-C",
13431344
"--compression",
13441345
metavar="COMPRESSION",
13451346
dest="compression",
13461347
type=CompressionSpec,
1347-
default=CompressionSpec("lz4"),
1348+
default=None, # None: not given, see default_compression()
13481349
action=Highlander,
1349-
help="select compression algorithm, see the output of the " '"borg help compression" command for details.',
1350+
help="select compression algorithm, see the output of the "
1351+
'"borg help compression" command for details. '
1352+
"Default: the repository default (see borg repo-create), else lz4.",
13501353
)
13511354
archive_group.add_argument(
13521355
"--tag",

‎src/borg/archiver/debug_cmd.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,9 +462,11 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser):
462462
metavar="COMPRESSION",
463463
dest="compression",
464464
type=CompressionSpec,
465-
default=CompressionSpec("lz4"),
465+
default=None, # None: not given, see default_compression()
466466
action=Highlander,
467-
help="select compression algorithm, see the output of the " '"borg help compression" command for details.',
467+
help="select compression algorithm, see the output of the "
468+
'"borg help compression" command for details. '
469+
"Default: the repository default (see borg repo-create), else lz4.",
468470
)
469471
subparser.add_argument(
470472
"object_path",

‎src/borg/archiver/help_cmd.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,15 +487,17 @@ class HelpMixIn:
487487
So if you use different compression specs for the backups, whichever stores a
488488
chunk first determines its compression. See also ``borg recreate``.
489489
490-
Compression is lz4 by default. If you want something else, you have to specify what you want.
490+
If you do not specify a compression via ``--compression`` (or the environment or the
491+
default config file), the repository's default compression is used, which can be set
492+
with ``borg repo-create --compression``. Without a repository default, compression is lz4.
491493
492494
Valid compression specifiers are:
493495
494496
none
495497
Do not compress.
496498
497499
lz4
498-
Use lz4 compression. Very high speed, very low compression. (default)
500+
Use lz4 compression. Very high speed, very low compression. (built-in default)
499501
500502
zstd[,L]
501503
Use zstd ("zstandard") compression, a modern wide-range algorithm.

‎src/borg/archiver/recreate_cmd.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,14 @@ def build_parser_recreate(self, subparsers, common_parser, mid_common_parser):
159159
metavar="COMPRESSION",
160160
dest="compression",
161161
type=CompressionSpec,
162-
default=CompressionSpec("lz4"),
162+
default=None, # None: not given, see default_compression()
163163
action=Highlander,
164164
help="select compression algorithm, see the output of the "
165165
'"borg help compression" command for details. '
166166
"Only applies to newly written data, e.g. when re-chunking with --chunker-params "
167167
"(and to the new archive metadata); data chunks reused from the existing archive "
168-
"are not recompressed, use borg repo-compress for that.",
168+
"are not recompressed, use borg repo-compress for that. "
169+
"Default: the repository default (see borg repo-create), else lz4.",
169170
)
170171
archive_group.add_argument(
171172
"--chunker-params",
@@ -178,7 +179,8 @@ def build_parser_recreate(self, subparsers, common_parser, mid_common_parser):
178179
"buzhash,CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,WINDOW_SIZE or "
179180
"buzhash64,CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,WINDOW_SIZE,NC_LEVEL or "
180181
"fastcdc,CHUNK_MIN_EXP,CHUNK_MAX_EXP,HASH_MASK_BITS,NC_LEVEL or "
181-
"`default` to use the chunker defaults. default: do not rechunk",
182+
"`default` to use the repository default (see borg repo-create), else the built-in "
183+
"chunker defaults. default: do not rechunk",
182184
)
183185

184186
subparser.add_argument(

‎src/borg/archiver/repo_compress_cmd.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,9 @@ def build_parser_repo_compress(self, subparsers, common_parser, mid_common_parse
223223
Repository (re-)compression (and/or re-obfuscation).
224224
225225
Reads all repository objects and recompresses the ones that are not already using
226-
the compression type/level and obfuscation level given via ``--compression``.
226+
the compression type/level and obfuscation level given via ``--compression``. Without
227+
``--compression``, that is the repository's default compression (see ``borg repo-create``),
228+
else lz4.
227229
228230
The repository is processed one pack file at a time: a pack is read as a whole and,
229231
if it holds objects that need recompression, rewritten as a whole - objects already
@@ -267,9 +269,10 @@ def build_parser_repo_compress(self, subparsers, common_parser, mid_common_parse
267269
metavar="COMPRESSION",
268270
dest="compression",
269271
type=CompressionSpec,
270-
default=CompressionSpec("lz4"),
272+
default=None, # None: not given, see default_compression()
271273
action=Highlander,
272-
help='select compression algorithm, see the output of the "borg help compression" command for details.',
274+
help='select compression algorithm, see the output of the "borg help compression" command for details. '
275+
"Default: the repository default (see borg repo-create), else lz4.",
273276
)
274277

275278
subparser.add_argument("-s", "--stats", dest="stats", action="store_true", help="print statistics")

‎src/borg/archiver/repo_create_cmd.py‎

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from ..constants import * # NOQA
44
from ..crypto.key import key_creator, encryption_argument_names, id_hash_argument_names
55
from ..helpers import CancelledByUser
6-
from ..helpers import location_validator, Location
6+
from ..helpers import location_validator, Location, ChunkerParams, CompressionSpec
77
from ..hashindex import ChunkIndex
88
from ..helpers.argparsing import ArgumentParser
99
from ..manifest import Manifest
@@ -41,6 +41,13 @@ def do_repo_create(self, args, repository, *, other_repository=None, other_manif
4141
repository.acquire_lock()
4242
# writing the config is what makes the store a repository, see Repository.create().
4343
repository.save_config(key)
44+
defaults = {}
45+
if args.compression is not None:
46+
defaults["compression"] = str(args.compression)
47+
if args.chunker_params not in (None, DEFAULT_CHUNKER_PARAMS):
48+
defaults["chunker_params"] = ",".join(str(p) for p in args.chunker_params)
49+
if defaults:
50+
repository.save_defaults(defaults)
4451
# we know repo/packs/ still does not have any chunks stored in it, but for some stores, there
4552
# might be a lot of empty directories and listing them all might be rather slow, so we better
4653
# store an empty ChunkIndex now, so that the first repo operation does not have to build the
@@ -191,6 +198,29 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser)
191198
To normally work with ``authenticated-*`` repositories, you will need the passphrase, but
192199
there is an emergency workaround; see ``BORG_WORKAROUNDS=authenticated_no_key`` docs.
193200
201+
Repository defaults
202+
+++++++++++++++++++
203+
204+
``--compression`` sets the repository's default compression: the commands that compress
205+
data (``borg create``, ``borg recreate``, ``borg import-tar``, ``borg transfer``,
206+
``borg repo-compress``) use it if no compression was given via ``--compression``, the
207+
environment or the default config file (``default.yaml``). Without a repository default,
208+
they use lz4. See ``borg help compression`` for the compression specs.
209+
210+
``--chunker-params`` sets the repository's default chunker parameters: ``borg create`` and
211+
``borg import-tar`` use them if no chunker parameters were given (in the same ways as above).
212+
``borg recreate`` and ``borg transfer`` only rechunk if ``--chunker-params`` is given, with
213+
``--chunker-params default``, they rechunk to the repository's default chunker parameters.
214+
Without a repository default, the built-in default chunker parameters are used.
215+
216+
This is useful if several clients back up into the same repository or if some commands are
217+
run manually: they all compress and chunk the same way without having to give these options.
218+
Using the same chunker parameters is important for deduplication.
219+
``borg repo-info`` shows the defaults.
220+
221+
The defaults are stored in the repository and protected by the repository key, so nobody without
222+
the key can change them (e.g. remove an ``obfuscate`` compression) without being noticed.
223+
194224
Creating a related repository
195225
+++++++++++++++++++++++++++++
196226
@@ -263,6 +293,27 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser)
263293
help="where to store the key: 'repokey' (in the repository, default) or 'keyfile' "
264294
"(in the local keys directory).",
265295
)
296+
subparser.add_argument(
297+
"-C",
298+
"--compression",
299+
metavar="COMPRESSION",
300+
dest="compression",
301+
type=CompressionSpec,
302+
default=None,
303+
action=Highlander,
304+
help="set the default compression of the repository, see the output of the "
305+
'"borg help compression" command for details. Default: no repository default (lz4 is used).',
306+
)
307+
subparser.add_argument(
308+
"--chunker-params",
309+
metavar="PARAMS",
310+
dest="chunker_params",
311+
type=ChunkerParams,
312+
default=None,
313+
action=Highlander,
314+
help="set the default chunker parameters of the repository (same format as for borg create). "
315+
"Default: no repository default (%s,%d,%d,%d,%d is used)." % CHUNKER_PARAMS,
316+
)
266317
subparser.add_argument(
267318
"--copy-crypt-key",
268319
dest="copy_crypt_key",

0 commit comments

Comments
 (0)