11579 Commits
Author SHA1 Message Date
Thomas WaldmannandClaude Opus 5.5 01d42bec43 tag: add --clear to remove all normal tags
Special tags like @PROT are kept. --clear can be combined with --add
to replace the normal tags, it is mutually exclusive with --set.

Previously, a bare "--set" cleared all tags, but that is not possible
anymore since --set now takes exactly one tag.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 23:19:35 +02:00
Thomas WaldmannandClaude Opus 5.5 2712d2f3bb tag: --set/--add/--remove take one tag per option, fixes #10430
They used nargs="*" and thus swallowed the archive NAME if it was given
after them, e.g. "borg tag --set keep aid:1ddaae55" failed.

Now each option takes exactly one tag and can be given multiple times,
e.g. "borg tag --add a --add b NAME".

Also add examples to the "borg tag" help.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 23:19:31 +02:00
TW 5aac325e48 Merge pull request #10436 from ThomasWaldmann/gather-archive-meta
load all archives' metadata with one Store.gather, reuse it for Archive
2026-09-26 23:12:23 +02:00
Thomas WaldmannandClaude Opus 5.5 925c55f3c8 reuse the listed archive metadata when opening an Archive
ArchiveInfo now carries the parsed ArchiveItem (as an attribute, not a
tuple field, so equality and hashing are unchanged). Archive accepts an
ArchiveInfo instead of a name or id; if it carries the metadata, the
archive is neither looked up again (1 list + 1 load) nor its metadata
loaded again (1 load). Without it, Archive looks the archive up by id as
before, e.g. for the archives of borg 1.x repositories.

ArchiveFormatter (repo-list, prune --list, ...), the per-archive loops
of info, list, tag, find, compact, recreate and analyze, and transfer
(for the source archives) pass the ArchiveInfo. repo-list with the
default format for 1000 archives with 50 ms emulated store latency takes
2.1 s instead of 238 s.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 22:24:47 +02:00
Thomas WaldmannandClaude Opus 5.5 1a9f864725 load all archives' metadata with Store.gather
Listing the archives (repo-list and every archive match) read each archive
metadata object with its own repository.get, i.e. one store roundtrip per
archive. Archive metadata objects are small and usually each in a tiny pack
of its own, which is the case Store.gather is made for.

Repository.gather_many reads objects in batches (at most 1000 objects, or
until 16 MiB) with one store.gather per batch, keeping the order of the ids.
Objects in cached packs are sliced from the cache; unknown or still buffered
ids and batches hitting a missing or truncated pack fall back to get().

Archives._infos uses it: repo-list --short for 1000 archives with 50 ms
emulated store latency takes 2.0 s instead of 56 s.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 20:20:34 +02:00
TW 8b5733db28 Merge pull request #10433 from ThomasWaldmann/borgstore-070
require borgstore 0.7.0
2026-09-26 12:46:35 +02:00
Thomas WaldmannandClaude Opus 5.5 dca232741b require borgstore 0.7.0
borgstore 0.7.0 adds Store.gather (many byte ranges with one call, one
roundtrip for REST) and fixes partial loads with a negative offset from
cached namespaces.

Store.defrag now requires all sources to be items of the given namespace,
which borg's defrag calls (hex pack names in "packs") already satisfy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-26 12:04:56 +02:00
TW 5f1d3cd15d Merge pull request #10431 from ThomasWaldmann/key-paper-hint-10428
key export --paper: fix restore hint for borg 2 syntax, fixes #10428
2026-09-25 22:04:11 +02:00
Thomas WaldmannandClaude Opus 5.5 10394b0efd key export --paper: fix restore hint for borg 2 syntax, fixes #10428
In borg 2, the positional argument of borg key import is the key backup
path, not the repository. Like most of the docs, assume BORG_REPO is set.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 21:34:36 +02:00
TW 5b2090b452 Merge pull request #10427 from ThomasWaldmann/update-changes-master
update CHANGES
2026-09-25 21:13:12 +02:00
Thomas Waldmann ce507ec480 update CHANGES 2026-09-25 21:05:38 +02:00
TW 73e2284a67 Merge pull request #10423 from ThomasWaldmann/encrypt-locks-8386
encrypt repository locks, fixes #8386
2026-09-25 20:31:53 +02:00
Thomas WaldmannandClaude Opus 5.5 dd902f8db5 docs: lock objects are protected by the repository key, #8386
Update the data structures and security docs: the lock objects are in the
store object envelope, locking needs the key (it is loaded before waiting for
the lock), unreadable lock objects are treated as exclusive locks, and what
the store still sees about the locks. Add a change log entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 19:16:40 +02:00
Thomas WaldmannandClaude Opus 5.5 cf05f9708c repository: protect the lock objects with the repository key, fixes #8386
The lock objects below locks/ were stored as plaintext JSON, named by the
store hash of that JSON. Anybody with read access to the store could see
which hosts and processes use the repository, and as the content is easy to
guess, even the names alone could reveal it.

Now the lock content is stored in the repository key's store object
envelope, like the index/ and cache/ objects (encrypted and authenticated in
the encrypting modes, authenticated only in the authenticated-* modes), bound
to the repository id and the locks namespace. The name is the store hash of
the envelope. Lock objects are encrypted with encrypt_oneshot(), as a
LockRefresher thread may refresh the lock while the main thread encrypts.

storelocking stays independent of the crypto code: Lock gets seal/unseal
callables, Repository provides them. A Lock keeps using the key it was made
with, so our own lock objects stay readable for us.

A lock object that can not be unsealed (corrupt, tampered with, written by
an older borg 2 beta, ...) is treated as a foreign exclusive lock (fail
closed): it only expires if the store's clock confirms that it was not
written for longer than the stale timeout; without store-side mtimes, only
break-lock removes it. borg warns once about it.

break-lock now loads the key, too (it shows the lock owners with --info).
repo-delete (also with --force) needs the key, as locking needs it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 19:16:35 +02:00
Thomas WaldmannandClaude Opus 5.5 f33a6fa9a9 load the repository key before locking the repository
Repository.open() now loads the key (via key_factory, or a key_loader given
to Repository) before it acquires the lock, see Repository.acquire_lock().
This is a preparation for protecting the lock objects with the key (#8386),
and it also means that borg asks for the passphrase before waiting for the
lock (and no lock is held while the user types the passphrase).

- key_factory() returns the repository's key if it was loaded already, so
  commands loading the key later (Manifest.load, check, ...) do not ask for
  the passphrase again.
- the "other" repository (--other-repo) gets a key_loader using the
  BORG_OTHER_* settings.
- repo-create locks after it created the key and before it writes the config
  (until then, the store is not a repository for other clients).
- tests opening a Repository directly: set a key (or key_loader) before the
  lock is taken, or open it with lock=False.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 19:16:33 +02:00
Thomas WaldmannandClaude Opus 5.5 e5b785a83d key: add encrypt_oneshot(), thread-safe encryption in a one-off session
AEADKeyBase.encrypt() uses the key's current session: the shared cipher and
its IV counter. Two threads calling it concurrently could encrypt two messages
with the same IV, which must never happen with AES-OCB or ChaCha20-Poly1305.

encrypt_oneshot() encrypts in a fresh session (new random session id, IV 0)
and neither uses nor changes the key's current session, so it is safe to call
from any thread. decrypt() reads the result like any other envelope.

This is for rare, small objects that may get written from a background
thread, like the repository lock objects (a LockRefresher thread refreshes
the lock). For the MAC-only (authenticated-*) keys, encrypt() has no state and
encrypt_oneshot() is the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 19:10:21 +02:00
TW ee1a01eaba Merge pull request #10416 from mr-raj12/repoobj-parse-header
repoobj: share one object header parser, refs #10318
2026-09-25 19:08:07 +02:00
TW 5b5adc2ff8 Merge pull request #10412 from mr-raj12/compact-skip-corrupt-packs
compact: do not rewrite or merge packs recorded corrupt, refs #10410
2026-09-25 18:46:10 +02:00
TW dd089c03be Merge pull request #10426 from mr-raj12/lock-acquire-slow-store
storelocking: try at least once before a lock acquire times out
2026-09-25 16:51:49 +02:00
Mrityunjay Raj 7b892ee9d0 storelocking: document the at-least-once lock acquire 2026-09-25 12:35:53 +05:30
Mrityunjay Raj e265935c25 storelocking: try at least once before a lock acquire times out, also with --lock-wait 0 2026-09-25 12:32:15 +05:30
Mrityunjay Raj 461c3127f7 storelocking: check for other locks at least once before an exclusive acquire times out 2026-09-25 12:23:26 +05:30
Mrityunjay Raj 7d2b0f2b38 repoobj: address review, catch get() errors in --find-lost-archives, refs #10318 2026-09-25 09:51:56 +05:30
Mrityunjay Raj 4145634e2c repoobj: share one object header parser, refs #10318
RepoObj.parse_header(buf) checks the header length, OBJ_MAGIC and the
version and returns (ObjHeader, None) or (None, problem). parse_meta(),
parse(), object_validator, PackReader._parse_header and
Repository.get(read_data=False) use it instead of unpacking the header
themselves; each keeps only its own size checks.

The error messages now agree: a wrong magic is "no object header" and a
wrong version is "unsupported object version N" everywhere.
get(read_data=False) now rejects a bad magic or version before it uses
meta_size to size its read.
2026-09-25 09:51:56 +05:30
Mrityunjay Raj 2fbebabd9f repoobj: remove unused RepoObj.extract_crypted_data, refs #10318
Only tests called it. Crypto type detection calls RepoObj1.extract_crypted_data
in borg.legacy.repoobj, which stays.
2026-09-25 09:51:56 +05:30
Mrityunjay Raj 1f7fc576fd compact, repo-compress: clarify corrupt pack help and warning, add stats and internals docs, refs #10410 2026-09-25 09:47:02 +05:30
Mrityunjay Raj 14f55788fc repo-compress: do not rewrite packs recorded corrupt, refs #10410
A rewritten copy of a corrupt pack gets a new id that passes borg check. repo-compress keeps these packs unchanged and warns to run "borg check --repair --verify-data".
2026-09-25 09:47:02 +05:30
Mrityunjay Raj 5c37731f01 compact: do not rewrite or merge packs recorded corrupt, refs #10410
A rewritten or merged copy of a corrupt pack gets a new id that passes borg check. compact keeps these packs unchanged and warns to run "borg check --repair --verify-data".
2026-09-25 09:47:02 +05:30
TW 6df17a5c11 Merge pull request #10424 from ThomasWaldmann/check-corrupt-item-ptrs
check: report missing/corrupted item_ptrs chunks, fixes #10421
2026-09-24 23:03:56 +02:00
TW 3e640ecd4e Merge pull request #10422 from ThomasWaldmann/legacy-authenticated-key
legacy: decrypt the key of borg 1.x authenticated repositories
2026-09-24 22:28:18 +02:00
TW f324781130 Merge pull request #10425 from ThomasWaldmann/remove-changes-symlink
remove CHANGES.rst symlink
2026-09-24 22:09:50 +02:00
Thomas WaldmannandClaude Opus 5.5 b2de6a99b4 remove CHANGES.rst symlink
It just pointed to docs/changes.rst, refer to that file directly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 22:00:31 +02:00
Thomas WaldmannandClaude Opus 5.5 8cee5bf938 check: report missing/corrupted item_ptrs chunks, fixes #10421
borg check crashed with an uncaught IntegrityError (or ObjectNotFound)
when one of an archive's item_ptrs chunks (the list of item metadata
chunk ids) was corrupted (or missing).

The archives check now reports such a chunk as an error and continues
with the item metadata chunk ids from the remaining item_ptrs chunks.
With --repair, the archive is rebuilt from the items it could read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 21:53:38 +02:00
Thomas WaldmannandClaude Opus 5.5 3b6a377894 legacy: decrypt the key of borg 1.x authenticated repositories
borg 1.x protected the key of the authenticated and authenticated-blake2
modes with the passphrase (pbkdf2, "sha256" algorithm), like the key of
the encrypted modes. The legacy authenticated key classes lacked
Pbkdf2FileMixin, so borg 2 could not decrypt that key and --from-borg1
(e.g. borg transfer) failed with "passphrase is incorrect".

The test uses keys exported from borg 1.4.5 authenticated and
authenticated-blake2 repositories.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 21:35:34 +02:00
TW 8867ca828e Merge pull request #10418 from ThomasWaldmann/legacy-repo-error
repository: clear error for borg 1.x repositories, fixes crash w/o --from-borg1
2026-09-24 21:14:20 +02:00
TW 3ef3441d0c Merge pull request #10420 from ThomasWaldmann/from-borg1-borg14-serve
legacy client: work with a borg 1.x "borg serve"
2026-09-24 21:04:19 +02:00
Thomas WaldmannandClaude Opus 5.5 4164c094fd repository: clear error for borg 1.x repositories, fixes crash w/o --from-borg1
Opening a borg 1.x repository without --from-borg1 crashed with a traceback while
loading the config/config store object, because a borg 1.x repository has a config
*file* where borg 2 has the config/ namespace (NotADirectoryError for a local
repository, HTTP 500 via ssh://; InvalidRepository on Windows).

borgstore 0.6.4 reports a name below an object as ObjectNotFound on all platforms,
so require it. If config/config is not found, look at "config" via the store
backend and raise Repository.LegacyRepository (rc 29) if it is a file, telling the
user to use --from-borg1 (e.g. with borg transfer).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 20:31:01 +02:00
Thomas WaldmannandClaude Opus 5.5 d1c77716c2 legacy client: work with a borg 1.x "borg serve"
--from-borg1 via ssh:// (e.g. borg transfer from a borg 1.x repository)
failed with a borg 1.x "borg serve" on the remote host:

- the client called info() after open, which borg 1.x servers do not
  have ("Borg server is too old for info"),
- then close() sent the close RPC, which borg 1.x servers do not have
  either ("RPC method close is not valid"), masking the first error,
- get_manifest is also missing on borg 1.x servers,
- borg 1.x servers pack bytes into the msgpack "raw" type, which the
  client unpacks to str, so e.g. the repository id and object data
  arrived as str.

For borg 1.x servers, the client now uses repository version 1 (the only
version borg 1.x serves), does not send the close RPC (borg 1.x closes
the repository when stdin gets closed), gets the manifest object via get
and converts the bytes results back to bytes.

Tested with a real borg 1.4.5 "borg serve": repo-list, version,
repo-create --other-repo and transfer with --from-borg1, repokey and
keyfile mode. The testsuite emulates a borg 1.x server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 17:12:53 +02:00
TW e0fc008eea Merge pull request #10417 from ThomasWaldmann/ssh-scheme-9765
ssh:// URLs for current repositories
2026-09-24 16:55:23 +02:00
Thomas WaldmannandClaude Opus 5.5 db0c4ac2c1 docs: ssh:// repository URLs, #9765
Describe ssh:// repository URLs for current repositories (served by a
remote "borg serve --rest") and for legacy borg 1.x repositories (with
--from-borg1, served by a remote "borg serve") and add a change log
entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 15:55:01 +02:00
Thomas WaldmannandClaude Opus 5.5 93481e0fb8 ssh://: explain a remote borg serve that fails to start
If the first request to the remote "borg serve" fails (e.g. a borg 1.x
borg serve does not know --rest, or borg is not installed there), raise
an Error showing the server's stderr and a hint (BORG_REMOTE_PATH,
--from-borg1) instead of a "Local Exception" traceback.

Repository.close() does not raise from closing the store while unwinding
an error anymore (like the lock release before it): a dead store fails
to close, too, which masked the original error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 15:54:57 +02:00
Thomas WaldmannandClaude Opus 5.5 669b43980d use ssh:// URLs for current remote repositories, fixes #9765
ssh:// now addresses a current repository via REST over stdio over ssh
(served by a remote "borg serve --rest"); with --from-borg1, it still
addresses a legacy borg 1.x repository via the legacy RPC protocol.

The testsuite uses ssh://__testsuite__/path (like the legacy client) to
run "borg serve --rest" locally instead of via ssh.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-24 15:54:54 +02:00
TW 507ff0dd06 Merge pull request #10411 from mr-raj12/check-repair-drop-missing-packs
check --repair: remove the index entries of missing packs, refs #8572, #9898
2026-09-24 01:14:41 +02:00
TW 34bd3c7116 Merge pull request #10408 from mr-raj12/check-verify-data-iter-packs
verify_data: read the repo pack by pack
2026-09-24 01:14:26 +02:00
TW e48f24110b Merge pull request #10415 from ThomasWaldmann/omnios-ci-swap
CI: add a swap zvol on omniOS to fix sporadic fork ENOMEM
2026-09-24 00:45:05 +02:00
Mrityunjay Raj ccb0da50fa check --repair: remove the index entries of missing packs, refs #8572, #9898
A check without repair removes them from self.chunks only, so the archives check reports their chunks as missing.
2026-09-24 03:43:33 +05:30
Thomas WaldmannandClaude Fable 5 8b31085584 CI: add a swap zvol on omniOS to fix sporadic fork ENOMEM
illumos does not overcommit memory: fork() must reserve swap for the
child's entire address space. The big pytest-xdist workers therefore
sporadically failed to spawn subprocesses (borg serve, for the
remote_archiver tests) with OSError: [Errno 12] "Not enough space"
(illumos' strerror for ENOMEM) when the image's small swap was
exhausted - the swap-backed tmpfs /tmp and ZFS-ARC-squeezed availrmem
eat into the same reservation pool. Seen e.g. in the omniOS job of
runs 35858263044 (PR #10411) and 35913850908 (master).

Add a 4 GiB sparse swap zvol before running the tests so fork()
reservations have room; being sparse, it takes no pool space unless
actually paged to. Also log swap -lh / swap -sh for diagnosability.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-09-24 00:08:56 +02:00
Mrityunjay Raj 34aa744451 verify_data: read the repo pack by pack, see #9998
Add ChunkIndex.iter_packs(), yielding each pack's chunk ids in offset order,
so check --verify-data fetches every pack once.
2026-09-24 03:27:15 +05:30
TW 02de9de7e2 Merge pull request #10414 from mr-raj12/fix-repair-finish-keyed-repository
tests: use KeyedRepository in the repair finish tests, refs #10406, #10379
2026-09-23 23:51:48 +02:00
Mrityunjay Raj 111ed6ca29 tests: use KeyedRepository in the repair finish tests, refs #10406, #10379 2026-09-24 02:33:07 +05:30