11579 Commits
Author SHA1 Message Date
TW caccfceca5 Merge pull request #10413 from ThomasWaldmann/update-changes-master
update CHANGES
2026-09-23 22:54:10 +02:00
Thomas Waldmann dc3c3628a6 update CHANGES
also: fix some reST syntax issues with "*"
2026-09-23 22:52:51 +02:00
TW 0c02052864 Merge pull request #10406 from ThomasWaldmann/encrypt-cache-index
authenticate (and encrypt) index and cache store objects, fixes #9819, fixes #10235
2026-09-23 22:07:29 +02:00
TW 96ead8a309 Merge pull request #10379 from mr-raj12/check-finish-narrow-walk-8466
check --repair: re-read only the packs the repair wrote, refs #8466
2026-09-23 14:17:06 +02:00
TW c27f341f64 Merge pull request #10409 from ThomasWaldmann/aead-length-py-ssize-t
crypto: feed long messages to the AEAD ciphers in chunks
2026-09-23 14:05:25 +02:00
Thomas WaldmannandClaude Fable 5.1 2c7f710d42 crypto: add _set_cipher_update_chunk_size to the low_level stub
mypy checks the tests against low_level.pyi, so the test hook the
chunked AEAD update introduced has to be declared there, too.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-23 03:31:21 +02:00
Thomas WaldmannandClaude Fable 5.1 90434cb531 crypto: feed long messages to the AEAD ciphers in chunks
EVP_EncryptUpdate() / EVP_DecryptUpdate() take the input length as a
C int, and the AEAD ciphers passed the whole message in one call, with
all lengths held in C ints: a message of 2 GiB or more failed with
SystemError (negative size for the result allocation), and above 4 GiB
the payload was silently truncated to its length modulo 2^32.

Feed the input (data as well as AAD) to OpenSSL in chunks of at most
1 GiB and keep every byte count in a Py_ssize_t. Both AEAD modes (OCB,
chacha20-poly1305) stream, so chunked calls give the same result as a
single one; the chunk size is a module variable so a test can shrink it
and cover chunk boundaries that fall inside a cipher block. A second
test round-trips a 2 GiB message, skipped unless BORG_TESTS_BIG_MEMORY
is set.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-23 03:21:27 +02:00
Mrityunjay Raj 4d970f4db5 check --repair: verify the written packs in two passes, refs #8466
Remove stale index entries of all written packs before indexing any object, so the result
does not depend on the pack order. Look up each pack once, update the docs.
2026-09-23 06:45:52 +05:30
Mrityunjay Raj a7d2062770 check --repair: re-read only the packs the repair wrote, refs #8466
finish() validates the written packs against the shared index instead of rebuilding it from all packs.
2026-09-23 06:45:52 +05:30
Thomas WaldmannandClaude Opus 5.5 cc10f6137e index/cache store objects: address review findings
- the envelope AAD also holds the repository id and a tag that tells a full
  object name and the namespace of a hashed-name object (index/) apart, so
  objects of another repository using the same key material, or envelopes
  made for the other kind of name, fail the authentication.
- index fragments are not hashed on load to verify their names: the
  authentication already proves their content (borg check still verifies
  the names). The error message says "authentication failed".
- a corrupt chunk index (a fragment that fails the authentication or does
  not deserialize) aborts every command that needs it, with the new
  CorruptChunkIndexFragment error (rc 94): run "borg check --repair".
  Only the commands that rewrite the whole index anyway, under an exclusive
  lock (borg check --repair, borg compact, borg repo-compress), rebuild it
  from the packs; borg compact --dry-run aborts.
- borg check: such a fragment is an index error, --repair rebuilds the
  index from the packs. A check without --repair never rebuilds the index,
  and the index cross-check is skipped if the store hash check already found
  the index corrupt. Help text updated.
- remember the plaintext hash of the fragments read in a session and do not
  store a fragment with known content again (unless forced), so e.g. a
  crashed repack converges again.
- debug get-obj/put-obj/delete-obj: load the key via with_repository(),
  including the security checks.
- tests: open_repository() always loads the real key, more tests; docs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 02:31:05 +02:00
Thomas WaldmannandClaude Opus 5.5 9bf24e103f docs: the index/ and cache/ store objects are protected by the repository key
Describe the store object envelope and update what the store sees of the
index/ and cache/ objects, see #9819, #10235.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 02:30:41 +02:00
Thomas WaldmannandClaude Opus 5.5 4e69b1ec45 cache: store checked-packs and the archive reference caches in the key's envelope, fixes #10235
cache/checked-packs and cache/referenced-by-archive.* are now protected like
the objects in the packs (encrypted and authenticated in the encrypting modes,
authenticated only in the authenticated-* modes), instead of having a store
hash appended.

The envelope binds each object to its name: a references cache copied to
another archive's name is ignored and rebuilt by scanning that archive.

PackTracker takes the repository instead of its store.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 02:30:33 +02:00
Thomas WaldmannandClaude Opus 5.5 c3cc447512 index: store the chunk index fragments in the key's envelope, fixes #9819
The index/ fragments are now protected like the objects in the packs:
encrypted and authenticated in the encrypting modes, authenticated only in
the authenticated-* modes. A fragment's name is the store hash of the stored
envelope, so borg check keeps verifying index/ by name, also server-side.

As the envelope is randomized in the encrypting modes, there is no "fragment
with this content already exists" upload skip any more.

A fragment that does not match its name or fails the authentication (e.g.
a plaintext fragment written by an older borg 2 beta) is corrupt: the chunk
index is rebuilt from the packs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 02:30:30 +02:00
Thomas WaldmannandClaude Opus 5.5 83c99fa1ae set the repository key wherever a key is loaded or created
key_factory, key_creator and Manifest set the key as the repository key,
so the index/ and cache/ store objects can be protected by it.

borg repo-create writes the empty chunk index after the key was created,
Repository.create() does not write one any more.

borg debug get-obj, put-obj and delete-obj load the key, as they access
the chunk index; delete-obj without a key aborts now instead of keeping the
superseded gap bytes.

Tests: repositories opened by the tests get a key (a test key, or the real
key via open_repository).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 02:30:13 +02:00
Thomas WaldmannandClaude Opus 5.5 255438d517 repository: add set_key, store_encrypt_store and store_load_decrypt
The index/ and cache/ store objects are to be protected by the repository
key, like the objects in the packs. These methods wrap a store object in the
key's envelope (encrypted and authenticated in the encrypting modes,
authenticated only in the authenticated-* modes), binding the object name
(or, for content-addressed objects, the namespace) into the envelope as AAD.

Without a key, they fail with the new Repository.KeyRequired error (rc 28).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-23 02:29:50 +02:00
TW 0dd454d7f8 Merge pull request #10405 from ThomasWaldmann/docs-yubikey
docs: derive the borg passphrase from a YubiKey (challenge-response)
2026-09-23 02:25:48 +02:00
TW 285aefb721 Merge pull request #10404 from ThomasWaldmann/docs-age
docs: protect the borg passphrase with age, fixes #4549
2026-09-23 02:25:31 +02:00
TW 3a1a40e142 Merge pull request #10407 from ThomasWaldmann/new-passcommand
Support BORG_NEW_PASSCOMMAND and BORG_NEW_PASSPHRASE_FD
2026-09-23 02:17:08 +02:00
d576bb41b8 Support BORG_NEW_PASSCOMMAND and BORG_NEW_PASSPHRASE_FD
A new passphrase (e.g. for borg key change-passphrase or borg key add)
can now also be given via a passcommand or a file descriptor, not only
via BORG_NEW_PASSPHRASE: the BORG_NEW_* variables form a group like
BORG_* and BORG_OTHER_* (mutually exclusive within the group,
independent of the other groups), checked before the documented
BORG_PASSPHRASE / BORG_PASSCOMMAND / BORG_PASSPHRASE_FD fallback,
which stays as it is.

Adapted from borgbackup/borg#8995 (the group selection was rewritten:
the original selected the NEW passcommand on the wrong flag, which
broke BORG_OTHER_PASSCOMMAND and never used BORG_NEW_PASSCOMMAND).

Co-authored-by: Łukasz Stelmach <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-09-23 01:42:43 +02:00
TW b050cefdf9 Merge pull request #10382 from mr-raj12/check-interruptible-index-rebuild-10042
check: make the chunk index rebuild interruptible, fixes #10042
2026-09-23 01:30:47 +02:00
Mrityunjay Raj 0ac9addcd5 check: stop the index rebuild per object, stop read-only checks within an archive
An interrupted --repair index rebuild no longer reports the index as rebuilt, finish() warns
that its rebuild can not be interrupted, and ChunkIndexRebuildInterrupted is listed in frontends.rst.
2026-09-23 03:30:55 +05:30
Thomas WaldmannandClaude Fable 5 eff90e0c39 docs: deployment guide for YubiKey challenge-response passphrases
New deployment chapter deriving the borg passphrase from a YubiKey OTP
slot's HMAC-SHA1 challenge-response via BORG_PASSCOMMAND: only a random
challenge is stored, the passphrase itself is computed by the token on
every unlock (the long-proven KeePassXC-style mechanism, as also
documented for restic in the Arch wiki).

Covers programming a slot with and without touch (touchless =
unattended backups, --touch = presence-gated interactive use),
redundant twin YubiKeys via a shared slot secret, adding the derived
passphrase as an additional borg key to an existing repository, the
paper copy of the derived passphrase as the recovery path, and honest
caveats: Yubico-proprietary (not on other vendors' keys nor the
FIDO2-only Security Key series - the age guide covers those), two
scarce slots, device-bound vs presence-gated threat model, macOS Input
Monitoring permission, and ykman instead of the deprecated ykpers
tooling.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-09-22 19:16:02 +02:00
Thomas WaldmannandClaude Fable 5 08e891c0f4 docs: deployment guide for age-based passphrase protection (hardware keys)
New deployment chapter showing how to combine BORG_PASSCOMMAND with
age: a strong random borg passphrase stored age-encrypted, decrypted on
demand - and, via the age plugin ecosystem, bound to hardware: any
FIDO2 security key (age-plugin-fido2-hmac, touch-gated by CTAP spec,
interactive use), YubiKey PIV (age-plugin-yubikey, policies allow
silent unattended use), TPM 2.0 (age-plugin-tpm) and Apple Secure
Enclave (age-plugin-se) for machine-bound unattended backups.

The guide starts with a plain no-hardware example to introduce the
pattern, gives an at-a-glance table of what works unattended vs. what
requires user presence, covers multi-recipient redundancy (several
YubiKeys / TPM unlocking the same passphrase), the offline paper copy
as the recovery path, revocation (change-passphrase, not just
re-encrypting) and an honest threat-model note (at-rest protection;
presence-gated options prevent silent unlocking, device-bound ones do
not).

This is the supported way to use hardware-backed key protection with
borg, instead of borg-native hardware integrations. See the discussion
in borgbackup/borg#8995 / borgbackup/borg#10399.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-09-22 18:50:57 +02:00
TW 505b5bc49a Merge pull request #10385 from ThomasWaldmann/check-requires-key
check: always require the key
2026-09-22 15:36:18 +02:00
TW 5d88c2e1dc Merge pull request #10403 from ThomasWaldmann/skip-repo-space-haiku-10400
tests: skip repo-space tests on Haiku OS
2026-09-22 12:48:16 +02:00
Thomas WaldmannandClaude Fable 5.1 de01d9ac1a check tests: assert the error text and that neither check started
Assert the passphrase error text in test_check_aborts_on_wrong_passphrase,
as test_check_aborts_without_key_info does for its error, and in both tests
assert that neither the repository check nor the archive consistency check
started, instead of the looser "Starting" not in output.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-22 12:43:54 +02:00
TW 1d281cbe14 Merge pull request #10401 from borgbackup/dependabot/pip/requirements.d/pip-dependencies-21c275768c
build(deps-dev): Bump the pip-dependencies group in /requirements.d with 2 updates
2026-09-22 12:36:24 +02:00
Thomas WaldmannandClaude Fable 5 f7fbc3569b tests: skip repo-space tests on Haiku OS, see #10400
All tests in repo_space_cmd_test.py reserve space, which makes borg
generate 64 MiB objects via os.urandom. os.urandom is currently
extremely slow on Haiku, so these tests run into the 300s test timeout
there.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-09-22 12:27:06 +02:00
TW 98d4cd0b35 Merge pull request #10402 from borgbackup/dependabot/github_actions/actions-b0dacf6015
build(deps): Bump the actions group with 4 updates
2026-09-22 12:19:20 +02:00
Mrityunjay Raj 1a1d0268cf check: make the chunk index rebuild interruptible, fixes #10042
build_chunkindex_from_repo() reads every pack of the repository when it has
to do a slow rebuild. On a large repository that takes a long time, and it
did not look at sig_int, so a Ctrl-C during it had no visible effect until
it was done.

It now takes an interruptible kwarg, default False. With it set, the pack
loop checks sig_int before reading the next pack, discards the index it
built so far and raises ChunkIndexRebuildInterrupted. A partial index is
never returned nor stored: it lacks chunks that are still in the packs, so
a check would report them as lost and --repair would drop the archives'
references to them.

Interruptibility is opt-in per call site because one caller must not have
it. ArchiveChecker.finish() rebuilds the index with sig_int already set
(check() calls it after the interrupt, #9850) and then calls
delete_chunkindex_invalid(), which declares the stored index to match the
packs. That only holds once every pack was indexed, so that rebuild has to
run to completion.

The two call sites that do get interruptible=True are the rebuild in
ArchiveChecker.check(), which runs before any archive is looked at, and the
--repair index rebuild in Repository.check(). The latter catches the
exception, warns, and leaves index_repaired False, so the existing summary
reports "Interrupted full repository check, index still corrupt so far."
and check() returns False. The corrupt index/ fragments are still there and
the next use rebuilds from the packs.

The index object verification loop in Repository.check() stays
uninterruptible on purpose: index_errors decides whether the packs are
verified and whether the index is rebuilt, so breaking out of it early
makes an interrupted check on a fully corrupt index report no problems. It
is bounded by the index size, not by the repository size.

Also update the check epilog: it still described a SIGINT-deaf key recovery
scan in ArchiveChecker.make_key(), which does not exist anymore since
2026-09-22 12:19:37 +05:30
dependabot[bot] da1e3060eb build(deps): Bump the actions group with 4 updates
Bumps the actions group with 4 updates: [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action), [codecov/codecov-action](https://github.com/codecov/codecov-action), [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action).


Updates `docker/setup-qemu-action` from 4.3.0 to 4.4.0
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/1f40c72289eff860ee54a304f1438e3cff362e0a...99012661954931238ded8c8b007157a8430204e1)

Updates `codecov/codecov-action` from 7.0.0 to 7.1.1
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/fb8b3582c8e4def4969c97caa2f19720cb33a72f...303a32d7a59b442fa8d48b6a1cc6825c09c847a5)

Updates `github/codeql-action/init` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/analyze` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: codecov/codecov-action
  dependency-version: 7.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-22 05:47:24 +00:00
dependabot[bot] 52665deccc build(deps-dev): Bump the pip-dependencies group
Bumps the pip-dependencies group in /requirements.d with 2 updates: [virtualenv](https://github.com/pypa/virtualenv) and [tox](https://github.com/tox-dev/tox).


Updates `virtualenv` from 21.7.9 to 21.7.16
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/virtualenv/compare/21.7.9...21.7.16)

Updates `tox` from 4.60.0 to 4.60.1
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.60.0...4.60.1)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 21.7.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: pip-dependencies
- dependency-name: tox
  dependency-version: 4.60.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: pip-dependencies
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-09-22 05:43:41 +00:00
TW e9817c33a3 Merge pull request #10398 from borgbackup/auto/update-fame
FAME.md: update contributor statistics
2026-09-21 14:15:20 +02:00
ThomasWaldmann 1f48943729 FAME.md: update contributor statistics 2026-09-21 04:32:51 +00:00
TW 177d09c915 Merge pull request #10220 from ThomasWaldmann/import-tar-strip-components-6461
import-tar: add --strip-components
2026-09-20 19:38:30 +02:00
Thomas WaldmannandClaude Fable 5.1 219d14573a import-tar: show the stored paths in the file status output
Regular files were listed with the path stored in the archive, all other
member types with the raw tar member name, e.g. "./dir" instead of "dir".
Show the stored path for all of them.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-19 23:01:27 +02:00
Thomas WaldmannandClaude Fable 5.1 113bf633cd import-tar --strip-components: keep the hard links in the BORG format test
The BORG tar format transfers the hlid in the BORG.item.meta pax header, so
hard links do not become separate files there. Keep them in the test input,
so the test also covers hard links plus stripping for the pax header path.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-19 23:01:22 +02:00
Thomas WaldmannandClaude Fable 5.1 0b27210b3d import-tar --strip-components: test a hard link pointing to a skipped member
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-19 23:01:19 +02:00
Thomas WaldmannandClaude Fable 5.1 2ffdd5e3fa import-tar --strip-components: show the stripped paths in the file status output
Regular files were listed with their stripped path, all other member types
with the unstripped tar member name. Show the stripped path for all of them.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-19 23:00:57 +02:00
Thomas WaldmannandClaude Fable 5.1 a7219b235c import-tar --strip-components: document the counting difference to tar
The path elements are counted on the normalized member name, so a leading
"./" is not a path element. GNU tar and bsdtar count it, so the same
tarball needs a count that is higher by one there.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-19 23:00:53 +02:00
Thomas WaldmannandClaude Fable 5 ca67ca70df import-tar: add --strip-components, fixes #6461
Remove the given number of leading path elements from the tar member
names when creating the archive items; members whose path has fewer or
equally many elements are silently skipped (same semantics as extract /
export-tar). Hard link targets are stripped accordingly so they keep
referencing their target's chunks; symbolic link targets are content and
are left unchanged. Also works for BORG format tars, where the items are
restored from pax headers.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-09-19 23:00:48 +02:00
TW ec4fe8a727 Merge pull request #10395 from ThomasWaldmann/fix-benchmark-extract-10057
benchmark tests: extract into an empty directory, fixes #10057 regression
2026-09-19 22:03:50 +02:00
Thomas WaldmannandClaude Opus 5 64892484ad benchmark tests: extract into an empty directory, fixes #10057 regression
test_extract ran "borg extract" with the cwd set to the tmpdir that the
repo_url fixture also uses for the repository, the keys dir and the cache dir.
That directory is therefore never empty, so since extract refuses to extract
into a non-empty directory (see #10057), all 8 parametrisations failed with
rc 33 (ExtractionDirNotEmpty) already on the first (and, with the pedantic
defaults rounds=1/iterations=1/warmup_rounds=0, only) call.

Extract into a dedicated, per-round empty subdirectory created by pedantic's
setup hook, which is not timed. Using --continue instead would keep the test
green, but it makes extract stat and compare the files that are already there,
which is not the extract that this benchmark is supposed to measure. The
per-round directory also keeps the benchmark correct if rounds or warmup_rounds
are ever raised above the defaults.

The other benchmark tests are not affected: they do not write into the cwd.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 21:22:30 +02:00
TW b83318bf42 Merge pull request #10390 from ThomasWaldmann/extract-nonempty-10057
extract: refuse to extract into a non-empty directory, fixes #10057
2026-09-19 15:39:58 +02:00
TW 31b68b7d63 Merge pull request #10391 from ThomasWaldmann/mount-winfsp-2316
mount: support Windows using WinFsp (via mfusepy), fixes #2316
2026-09-19 15:37:08 +02:00
TW 8022eba8c2 Merge pull request #10392 from ThomasWaldmann/fix-test-timestamps-win32-atime
tests: test_timestamps_win32: compare the extracted atime with the archived one
2026-09-19 14:47:54 +02:00
Thomas WaldmannandClaude Fable 5.1 1b7ff2b768 extract: refuse to extract into a non-empty directory, fixes #10057
Extracting into a non-empty directory replaces existing files that are in
the way and leaves all other existing files as they are, so the result is a
mix of existing and extracted files. Accidentally running "borg extract" in
a directory that is in use (like a home directory) thus replaced current
files by their archived state and blended the archive into the directory.

Now borg aborts with Archive.ExtractionDirNotEmpty (rc 33) before writing
anything if the current directory is not empty, unless --continue is given.
This also applies when only a part of the archive is extracted. --dry-run
and --stdout do not write into the directory, so they are not checked.

No new option: --continue already is the way to extract into a directory
that has contents. Its help and the extract docs now describe what it does
(skip files that are fully extracted already, replace other existing files)
and that a damaged file with unchanged size and mtime must be removed
before it can be extracted again.

Tests that extracted repeatedly into the same directory now use --continue
or a fresh directory.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-19 14:30:09 +02:00
Thomas WaldmannandClaude Fable 5.1 e6de95d6a7 CI: windows: install WinFsp and mfusepy, refs #2316
So the borg mount tests also run on Windows and the Windows binary
can mount (if WinFsp is installed).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-19 14:17:30 +02:00
Thomas WaldmannandClaude Fable 5.1 0e316b1b52 mount: support Windows using WinFsp (via mfusepy), fixes #2316
mfusepy already knows how to talk to the FUSE 2 API of WinFsp, so this is
about the borg side:

- mountpoint: WinFsp mounts on an unused drive (X:) or creates (and later
  removes) the mountpoint directory, so it must not exist yet.
- there is no fork: borg mount always stays in the foreground. Ctrl-C or
  Ctrl-Break unmount. borg umount is not supported there.
- WinFsp ignores the "ro" mount option, a read-only volume has to be
  requested from the init callback (FSP_FUSE_CAP_READ_ONLY).
- WinFsp makes a security descriptor from uid / gid / mode and checks all
  access against it. The archived uids / gids do not mean anything on
  Windows, so the uid and gid mount options are left to WinFsp and default
  to -1 (the user who mounts).
- volname gives the volume label (default: borgfs).
- st_birthtime is given to mfusepy, WinFsp uses it as the creation time.
- readlink of something that is not a symlink fails with EINVAL (WinFsp
  tries that for "/" to find out whether symlinks are supported).

Tests: on Windows, fuse_mount runs borg mount as a child process and stops
it like Ctrl-Break does. WinFsp does not support hard link counts. With
mfusepy 3.1.1, symlinks show up as regular files, because WinFsp has getdir
and readlink in a different order in struct fuse_operations than libfuse has.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-19 14:17:09 +02:00
TW cc8b70eaed Merge pull request #10387 from ThomasWaldmann/extract-continue-hardlinks
extract --continue: keep groups of hard links together
2026-09-19 12:05:22 +02:00