119 Commits
Author SHA1 Message Date
Thomas Waldmann 97f03619c7 build_usage build_man 2026-09-27 23:02:35 +02:00
Thomas Waldmann e337644500 build_usage build_man 2026-09-02 21:39:09 +02:00
Thomas Waldmann e97cba32cc build_man ; build_usage 2026-08-29 17:42:35 +02:00
Thomas WaldmannandClaude Fable 5 8013b2f512 docs: fix borgfs man page generation, drop the stale borgfs HTML page
The generated borgfs.1 was corrupted: jsonargparse injects --config and
--print_config into the borgfs parser (a top-level parser), and
--print_config's "[=flags]" metavar is invalid rST option-list syntax,
so the man page embedded a docutils WARNING and an unformatted blob of
options. Hide jsonargparse-injected actions (detected structurally by
their module, so future injected options stay filtered) and borg's
top-level-only --cockpit from the borgfs docs via help=SUPPRESS, which
all make.py writers already honor. -V/--version stays documented, as
in borg 1.x. Regenerate borgfs.1 accordingly (now also showing the
REPOSITORY positional).

Remove docs/usage/borgfs.rst{,.inc}: build_usage intentionally stopped
generating a borgfs HTML page long ago (#3404), so the leftover
.rst.inc still documented the borg 1.x CLI (REPOSITORY_OR_ARCHIVE,
--prefix, ...) and was published as an orphan page. Nothing references
these files; borgfs is documented on the "borg mount" page.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-28 12:32:33 +02:00
Thomas WaldmannandClaude Fable 5 cf58f9a05a copy: new command to copy an archive to a new archive name, #2300
Copying is cheap and fast: no file content is read or written, only a new
archive metadata object is created. Like any deduplicated archives, the two
archives share their data, so a copy needs almost no additional repository
space.

The copy is an independent archive: deleting either of the two archives
keeps the other one intact, because "borg compact" only frees chunks that no
remaining archive references.

Archive.copy() is Archive.rename() without removing the original archive
entry. Copying an archive to its own name is refused: the new metadata would
be identical, thus have the same archive ID, and no second archive would be
created. NEWNAME may well be the name of some other existing archive, the
copy then just joins that archive series.

OLDNAME must match precisely one archive, so it accepts an archive name (if
it is unique) or an archive ID like "aid:d34db33f".

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-28 06:30:19 +02:00
Thomas Waldmann 5d0832d515 docs: fix outdated --encryption mode names in examples
"--encryption=none" and "--encryption=authenticated" have not existed since
the id hash became part of the mode name for the modes that do not encrypt:
they are now "none-sha256"/"none-blake3" and "authenticated-sha256"/
"authenticated-blake3". The examples in docs/usage/repo-create.rst still used
the old names, so copying them gave an argparse error.

The "authenticated --id-hash=blake3" example was doubly wrong: those modes
name their id hash themselves and reject a separate --id-hash. Replaced it
with plain "authenticated-blake3"/"none-blake3" examples and dropped the
now-wrong claim that the non-encrypting modes only support sha256.

Also:

- "--encryption=aes-ocb" (docs/usage/key.rst, docs/quickstart_example.rst.inc)
  is not a valid mode either, it is "aes256-ocb".
- scripts/rest_perf_test.sh used "--encryption none".
- regenerated the affected man pages; borg-repo-create(1) additionally picked
  up the already-fixed --key-location help text.

All examples were verified by actually running them.
2026-08-26 14:43:18 +02:00
Thomas Waldmann 950e966f96 build_usage build_man 2026-08-23 14:52:14 +02:00
Thomas Waldmann d83f509fa6 create: move the --map usage notes to the end of the section, see #4363
The "--map requires exactly one input path" and "the reference archive must
contain exactly one file item" paragraphs interrupted the description of the
map file format. Move both to the end of the Input maps section and rebuild
the generated usage docs / man page.
2026-08-21 12:38:03 +02:00
Thomas WaldmannandClaude Fable 5 14d05b60b7 scripts: add lvm-thin-map.py to generate input maps from thin metadata, see #4363
Converts thin_dump XML (full mode: allocation map of one thin LV) and
thin_delta XML (delta mode: differences between two thin snapshots)
into the borg create --map format. The docstring documents the full
workflow including the reserve/release_metadata_snap steps and the
snapshot discipline needed for --reuse-from.

Also add a docs section pointing from the --read-special LVM example
to the input map based approach.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-21 12:37:58 +02:00
Thomas WaldmannandClaude Fable 5 6c395aca67 create: add --reuse-from to reuse a reference archive's chunks, see #4363
For input map ranges marked "same", reuse the chunks of the reference
archive's item instead of reading the input. Reference chunks that only
partially overlap "same" ranges are re-read completely, so the result
is correct with any chunker; with the fixed chunker, read windows align
with the reference chunk grid. Reference chunks missing from the repo
are read again (like the files cache does on a lost chunk).

Together with --map, this implements efficient incremental backups of
block device snapshots: only changed ranges need to be read, e.g. as
reported by thin_delta for LVM thin volume snapshots.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-21 12:37:55 +02:00
Thomas WaldmannandClaude Fable 5 74622607be create: add --map to give an input map, see #4363
An input map describes the content ranges of the single input file:
data ranges are read and stored, zero ranges are stored as holes
without reading them. Primary use case: backing up snapshots of large
(esp. thin-provisioned) block devices, where the storage layer knows
which ranges are in use.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-21 12:37:50 +02:00
Thomas WaldmannandClaude Fable 5 69acada45c create --sparse: fix stale help text, sparse input works with all chunkers
The help text still claimed sparse hole detection is "supported only by
fixed chunker", which is stale: --sparse is passed to every chunker via
get_chunker(), and ChunkerBase/FileFMAPReader implement sparse processing
generically (build the hole map via SEEK_HOLE/SEEK_DATA, seek over holes
instead of reading them, process their content as all-zero).

Verified for all chunkers (buzhash, buzhash64, fastcdc, rabin-aes,
goldilocks-aes, toeplitz-aes, fixed) by chunking a real sparse file with
sparse=True and a hole-poisoned file with an explicit fmap: content is
reproduced exactly and hole ranges are seeked over, not read.

Also move the sparse processing note in docs/internals/data-structures.rst
from the "fixed" chunker section to the chunker overview, and regenerate
the affected generated docs (docs/usage/create.rst.inc, borg-create.1).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-17 03:26:38 +02:00
Thomas Waldmann 7433e1c4c4 create: follow symlinks given as recursion roots, fixes #4737
If a recursion root (a path given on the command line or via a patterns
file) is a symlink, borg now follows it and archives what it points to,
using the path as given.

Symlinks encountered while recursing are not affected, they are archived
as symlinks as before. Symlinks given via --paths-from-* are never
followed either.

Archiving the symlink target under the symlink's path also keeps the files
cache working if the symlink target changes its name (e.g. a 'current'
symlink pointing to the latest of a series of directories).

A recursion root that is a symlink with a non-existing target is skipped
with a warning (new BackupBrokenSymlinkError, rc 112).

As a followed symlink must be opened without O_NOFOLLOW, add flags_dir_follow
and flags_normal_follow and let the callers choose the flags.
2026-08-15 16:45:57 +02:00
TW bce7398ade Merge pull request #10099 from ThomasWaldmann/find-9974
find: search files across archives
2026-08-14 16:06:52 +02:00
Thomas Waldmann cfd368624e find: new command to search files across archives, #9974
borg find iterates over the archives selected by the usual archive
filter options (all archives by default) from newest to oldest, over
all items of each archive, and outputs one line per item matching the
given PATHs (paths or patterns, matching like in borg list /
borg extract).

Output lines are formatted like borg list default output, but are
prefixed with a short archive ID and the archive name. As the
archives of a series all share the same name, only the archive ID
uniquely identifies the archive.

Output is customizable via --format / BORG_FIND_FORMAT (ItemFormatter
keys) and --json-lines.

There is no extra index for file paths (keeping one up to date would
cost considerable runtime and space), so this reads the metadata of
all selected archives.
2026-08-14 14:10:11 +02:00
Thomas Waldmann b8546e9a8f docs: add generated man page borg-environment.1
PR #10063 added the "borg help environment" topic and build_man
generates borg-environment.1 since then, but the generated man page
was never committed. Add it (built via scripts/make.py build_man).
2026-08-14 13:29:04 +02:00
Thomas WaldmannandClaude Fable 5 9f07fd8057 with-lock: fix epilog about leftover lock auto-removal, see #9870
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 12:23:11 +02:00
Thomas WaldmannandClaude Fable 5 5b9da3bc0b create: add --read-special-timeout option, fixes #5422
Reading from a fifo (or char device) with --read-special could block
borg forever: opening a fifo read-only blocks until a writer connects,
and reads block until data arrives - a broken producer thus hangs an
unattended backup indefinitely.

Now, when no data arrives for --read-special-timeout SECONDS (default:
1800, i.e. 30 minutes; this includes waiting for a fifo's writer to
connect), borg gives up on that file: fifos and char devices are opened
with O_NONBLOCK and all content reads go through the new
SpecialFileReader, which waits for data via select() and raises
OSError(ETIMEDOUT) when the timeout expires. That maps to the new
BackupTimeoutError (rc 111), so the file is skipped with an error (and
not retried) while the backup continues with the remaining files.
--read-special-timeout=0 means: wait forever (previous behavior).

Note: with a timeout active (the default), a writer connecting but
closing without sending anything is reported as a timeout instead of
being stored as a file with empty content.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-13 11:56:36 +02:00
Thomas WaldmannandClaude Opus 5 a90b1e29a4 list: add {blake3} format key, fixes #9984
borg2 uses blake3 (as keyed hash) for chunk ids, but the --format
hash keys only offered the python stdlib hashes, so {blake3} raised
a KeyError.

The blake3 package is a hard dependency of borg2 anyway, so just add
blake3 to the set of supported hash algorithms. Output is the default
256bit hexdigest, like for the other hash keys.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-31 22:04:34 +02:00
TW 5fd48e06ee Merge pull request #9944 from ThomasWaldmann/webdav
webdav: serve archives via WebDAV / HTTP, fixes #9942
2026-07-26 16:22:41 +02:00
Thomas Waldmann 6d4a51f181 remove --debug-profile option, use BORG_DEBUG_PROFILE
Set BORG_DEBUG_PROFILE to a filename to get an execution profile written
there; a ".pyprof" suffix still selects the Python-compatible format.

Note that this now applies to EVERY borg invocation while the variable is
set, not just to a single command like the option did - the docs say so and
the test switches it off around the "borg debug convert-profile" call, which
would otherwise profile itself over the profile it is reading.
2026-07-25 12:15:02 +02:00
Thomas Waldmann e5fb5c0d58 remove --iec option, use BORG_IEC
Unlike the other options removed recently, --iec did work for borg2 repos -
but the same setting is better placed in the environment: it is a display
preference one usually wants for all borg invocations, not per command.

BORG_IEC=yes now selects IEC units (1KiB = 1024B); "true" and "1" are
accepted, too, because jsonargparse's automatic BORG_<OPTION> environment
variables accepted these for --iec. Note that removing the option also
removes that automatic environment variable, so the new use_iec_units()
helper reads BORG_IEC explicitly.

The iec=... plumbing through Archive, Statistics, ArchiveFormatter and Cache
is unchanged, only the places reading args.iec now call use_iec_units().
2026-07-25 09:29:44 +02:00
Thomas Waldmann d1a2aec3da remove --upload-ratelimit and --upload-buffer options
Both were only ever read by the legacy (borg 1.x, ssh://) remote repository
code, so they had no effect on borg2 repositories - borgstore does not do
rate limiting or upload buffering at all.

For the legacy code path they are not worth keeping either: we only support
transferring FROM borg1 repos, so there is not much traffic in the upload
direction of a borg1 repo anyway.

Removes the SleepingBandwidthLimiter class with them; the only thing left
from it is write_to_fd(), which turns a BrokenPipeError into the nicer
ConnectionBrokenWithHint. Without an upload buffer size limit, the "queue
more calls" condition simplifies to "the send buffer is empty".
2026-07-25 09:00:47 +02:00
Thomas Waldmann f144bac320 remove --rsh option, use BORG_RSH
Like --remote-path, --rsh was only honoured on the legacy (borg 1.x, ssh://)
code path. For borg2 repositories the remote shell command comes from
borgstore, which reads its own BORGSTORE_RSH environment variable, so --rsh
silently had no effect there.

Remove the option; BORG_RSH is now the only way to set the remote shell
command. For borgstore-based repositories, borg gives the BORG_RSH value to
borgstore as BORGSTORE_RSH, except if that was set explicitly (then the user
wants a different command for borgstore).

borg benchmark crud does not need to inherit --rsh / --remote-path into its
sub-invocations any more (environment variables are inherited anyway), so the
parse_args wrapper is gone.
2026-07-24 23:10:50 +02:00
Thomas Waldmann 6d5d5478c3 docs: BORG_REMOTE_PATH is the replacement for --remote-path 2026-07-24 22:43:36 +02:00
Thomas Waldmann 6c30ed8b6c remove --remote-path option, use BORG_REMOTE_PATH
The --remote-path option was only honoured on the legacy (borg 1.x, ssh://)
code path in legacy/remote.py. For borg2 rest:// repositories, the remote
borg is determined by rest_serve_command(), which only ever looked at the
BORG_REMOTE_PATH environment variable - so giving --remote-path silently had
no effect and the far end failed with "command not found: borg".

Rather than plumbing args into rest_serve_command(), drop the option
completely and use BORG_REMOTE_PATH consistently for both repo types. Also
apply replace_placeholders() to it for rest:// repos, as the legacy code
path already did.
2026-07-24 22:40:45 +02:00
Thomas WaldmannandClaude Opus 4.8 7aa93814bc webdav: download any directory as a (PAX) tar archive
Add a per-directory tar download: append ?tar (or ?tar=1) to a directory URL,
or click the download icon shown next to the heading in the browser listings.
Unlike a plain file download, the tar preserves POSIX metadata (owner, group,
mode, sub-second timestamps, symlinks, special files, xattrs, ACLs), so it is
the metadata-lossless way to restore a whole directory tree over the server.
The tar is rooted at the requested directory (paths above it are stripped);
?tar on the archive root exports the whole archive.

The tar size is not known in advance (PAX header sizes vary), so it is streamed
with chunked transfer encoding. Repository access (item iteration and chunk
fetching) is serialized under repo_lock, but each chunk is written to the client
outside the lock: a slow client cannot block other requests, and the
LockRefresher can keep the repository lock alive during a long download (holding
the lock for the whole stream would starve it and let the lock go stale). A
missing chunk leaves the chunked stream unterminated and closes the connection,
so the client detects the truncation instead of receiving a corrupt archive.

To avoid duplicating the borg-item -> tar mapping, item_to_tarinfo() and
item_to_paxheaders() are factored out of TarMixIn._export_tar to module level in
tar_cmds.py (behaviour-preserving; item_to_tarinfo now returns needs_content and
the caller builds the content stream). webdav writes the tar blocks itself
(TarInfo.tobuf for the header, chunk data, 512-byte padding, end-of-archive
marker) so it can control the per-chunk locking.

Verified end-to-end with the system tar tool: modes, symlink targets and a
multi-chunk file are preserved byte-identically.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-23 22:10:15 +02:00
Thomas WaldmannandClaude Opus 4.8 4959c06f9b webdav: daemonize by default, like borg mount
Add a -f/--foreground flag; without it, the command daemonizes and runs
in the background, matching borg mount. Bind the socket in the foreground
first (so bind errors like "port in use" are reported before forking, and
the listening fd survives the fork), then migrate the repository lock to
the forked process. The LockRefresher and server threads are started after
the fork, since threads do not survive fork().

The daemon stops on SIGTERM (the usual way to stop a daemon) or SIGINT,
shutting down cleanly and releasing the repository lock.

Daemonizing needs os.fork(), which does not exist on Windows; there the
command stays in the foreground (borg webdav is meant to be usable on
Windows, unlike borg mount).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-23 21:20:24 +02:00
Thomas WaldmannandClaude Fable 5 b6110bae66 webdav: cache decrypted chunks (BORG_MOUNT_DATA_CACHE_ENTRIES)
A mounted WebDAV file system reads a big file as many small, sequential
range requests, which tend to hit the same content chunk over and over.
Without caching, each request re-fetched and re-decrypted the chunk.

Add a shared LRU cache of decrypted chunks (keyed by chunk id, so it is
valid across archives and requests), the same approach borg mount uses.
Its size is set by BORG_MOUNT_DATA_CACHE_ENTRIES (default: number of
CPUs), just like for borg mount. The cache is guarded by repo_lock (the
server is multi-threaded and LRUCache is not thread-safe), while writing
to the client still happens outside the lock.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-23 21:02:31 +02:00
Thomas WaldmannandClaude Fable 5 e7047c42be webdav: implement WebDAV (class 1), so archives can be mounted, #9942
The served archives can now be mounted as a read-only network file system
using the WebDAV client built into most operating systems and file managers:

- Windows Explorer: "Map network drive" -> http://localhost:8000/
- macOS Finder: "Go > Connect to Server" -> http://localhost:8000
- GNOME Files / KDE Dolphin: dav://localhost:8000/
- Linux kernel mount: mount -t davfs (davfs2 package)

Implemented (still python stdlib only):

- OPTIONS advertising class 1 DAV support.
- PROPFIND with Depth 0/1 (infinity is refused, as RFC 4918 permits),
  supporting allprop / propname / named-prop request bodies, answering
  with 207 multistatus XML. Request body size is limited.
- live properties: resourcetype, displayname, getcontentlength,
  getcontenttype, getlastmodified, creationdate, getetag, and empty
  supportedlock/lockdiscovery (no locking - read-only server).
- Range requests (single range): only the chunks overlapping the
  requested range are fetched and decrypted (chunk sizes are known in
  advance), so the many small ranged reads DAV file systems do for big
  files stay efficient. 206/416 handling, If-Range.
- conditional requests: ETag (archive contents are immutable, so
  mtime+size is a solid validator), If-None-Match -> 304.
- all writing/locking methods (PUT, DELETE, PROPPATCH, MKCOL, COPY,
  MOVE, LOCK, UNLOCK, ...) are rejected with 405 + Allow, and clients
  see a read-only file system.
- symbolic links and special files are not exposed via WebDAV (the
  protocol has no concept of them); they remain visible in the web
  browser listings.

Verified against real clients: rclone (recursive PROPFIND listing and
reads) and the macOS WebDAV file system (mount_webdav): mount, ls, byte-
identical reads of multi-chunk files, write attempts correctly rejected
with EROFS.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-23 18:24:44 +02:00
Thomas WaldmannandClaude Fable 5 d4b3d7a721 webdav: serve archive contents read-only via HTTP, #9942
New "borg webdav" command: it starts a read-only HTTP server on localhost,
so archive contents can be browsed and files downloaded with a web browser.

This first step implements plain HTTP only (GET/HEAD, HTML directory
listings, file downloads). The WebDAV methods (OPTIONS/PROPFIND), which
enable native mounting by Windows Explorer, macOS Finder and the Linux
desktop file managers, are planned to be added on top of this later.

Implementation notes:

- only uses the python standard library, no new dependencies.
- the directory tree of an archive is built in memory when it is first
  accessed, so a big archive only costs time/memory if it is browsed.
- all repository access is serialized with a lock, because borgstore
  connections are not thread-safe.
- a LockRefresher keeps the repository lock of an idle server alive,
  so it is not killed as stale, see #9872.
- missing chunks abort the download connection, so damaged files are
  never served as silently corrupted content.
- symlinks and special files are listed, but not downloadable.

Note: downloads via HTTP do not preserve POSIX metadata (owner, group,
mode, timestamps, xattrs, ACLs) - use borg extract or borg export-tar
for full-fidelity restores.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-23 18:03:23 +02:00
Thomas Waldmann 3069e8b940 build_man / build_usage 2026-07-21 15:28:38 +02:00
Thomas Waldmann c4347a9dce build usage / man 2026-06-16 14:17:43 +02:00
TW 5e2da157be Merge pull request #9769 from ThomasWaldmann/docs-key-subcommands
docs: add docs for "key add", "key list" and "key remove"
2026-06-13 23:47:58 +02:00
Thomas WaldmannandClaude Opus 4.8 d5cd72b179 docs: add docs for "key add", "key list" and "key remove"
The multi-key feature (#9743) added the "borg key add", "borg key list"
and "borg key remove" subcommands but never wired up their docs:

- scripts/make.py: map key_add/key_list/key_remove to the "key" usage
  group, so build_man can locate their examples (it previously aborted
  with FileNotFoundError: docs/usage/key_add.rst).
- docs/usage/key.rst: include the three new generated snippets so they
  show up on the HTML key page.
- regenerate the affected docs: new key_{add,list,remove}.rst.inc usage
  snippets, new borg-key-{add,list,remove}.1 man pages, and borg-key.1
  (SYNOPSIS + SEE ALSO now reference the new subcommands).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-13 21:37:21 +02:00
Thomas WaldmannandClaude Opus 4.8 8f4231d21b key: allow --key-location for authenticated* modes
The authenticated and authenticated-blake3 modes do not encrypt data, but
they still have a real key (id/auth key material) stored as a key blob.
That blob can live as a keyfile or as a repokey just like the encrypted
modes, so make it configurable instead of always forcing repokey storage.

- AuthenticatedKeyBase: set LOCATION_CONFIGURABLE = True so --key-location
  (at repo-create) and "borg key change-location" apply.
- key change-location: only copy sessionid/cipher when present (those are
  AEAD-only; authenticated keys do not have them).
- repo-info: report the key storage location for authenticated keys too,
  and handle the authenticated-blake3 variant (was only "authenticated").
- repo-create help: stop claiming authenticated* has no keyfile/repokey
  storage; only "none" truly has no key.
- add change-location round-trip tests for authenticated mode.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-13 20:57:26 +02:00
Thomas Waldmann f12f67a76d build_usage build_man 2026-03-15 14:37:50 +01:00
Thomas Waldmann fcbf7f3572 build_usage build_man 2025-12-23 18:00:09 +01:00
Thomas Waldmann 15f59233b5 completion: borg can now generate completion scripts for supported shells, fixes #9172
Added `shtab` dependency for shell completion functionality:
- bash completion (works).
- zsh completion (known-broken due to iterative/shtab#183).
2025-11-17 18:47:00 +01:00
Thomas Waldmann f327af11a6 build_usage build_man 2025-08-02 11:12:20 +02:00
Thomas Waldmann 9b26b1011a build_usage build_man 2025-06-18 17:35:37 +02:00
Thomas Waldmann fa01a2d46a build_usage build_man 2025-05-22 13:21:21 +02:00
Thomas Waldmann 6e548ed1f0 build_man build_usage 2025-05-19 23:20:38 +02:00
Thomas Waldmann 7644b04df2 docs: remove outdated docs/man files about borg change-passphrase
this is borg key change-passphrase now.
2025-05-19 23:18:19 +02:00
Thomas Waldmann 524d927871 build_usage build_man 2025-05-06 01:37:17 +02:00
Thomas Waldmann 477e8f53fb build_man build_usage 2025-04-28 21:42:30 +02:00
Thomas Waldmann 4550a49b0f build_usage / build_man 2025-04-21 21:08:42 +02:00
Björn Ketelaars cf6a324afa borg2 - Support other OpenSSL versions on OpenBSD
`setup.py` hardcoded crypto library paths for OpenBSD, causing build
issue when OpenBSD drops specific OpenSSL version. Solution is to make
paths configurable.

Addresses #8553.
2024-11-23 23:01:44 +01:00
Thomas Waldmann 37077a55e7 build_usage build_man 2024-11-16 20:14:16 +01:00
Thomas Waldmann 66e17ff6c8 build_usage, build_man 2024-10-31 20:50:06 +01:00