Repository navigation
known-repos: new command to list locally known repositories, fixes #10453 - #10513
KazukiKandaKK wants to merge 4 commits into
Conversation
Lists repositories this local Borg installation has state for (security
info and/or a local cache) without accessing any repository, local or
remote. Scans both the current borg2 security/cache directories and,
separately, the legacy borg 1.x security/cache directories (which can be
different default locations, e.g. on macOS).
Does not follow symlinks anywhere in the scanned trees (repo-id
directories, or the location/key-type files inside them), since borg
only ever writes plain directories and regular files there.
Key-type labels are derived from the key classes themselves
(LEGACY_KEY_TYPES + AVAILABLE_KEY_TYPES), matching how "borg repo-info"
names crypto suites, rather than a separately hand-maintained table.
dir_size() pushes each directory handle (root, and any subdirectory
opened while walking the cache tree) onto its explicit stack before
calling list_names() on it, not after, so cleanup can always close it
even if list_names() raises something other than OSError (it only
catches OSError itself) - e.g. KeyboardInterrupt. Covered by two unit
tests that monkeypatch list_names() to raise and then check the fd via
os.fstat().
has_cache and has_security_info are tri-state (True/False/None, not a
plain bool): an inspection failure (e.g. EACCES on a repo-id directory,
or on the security/cache root itself, including a root that opens but
fails during its own listing) is reported as unknown rather than being
collapsed into a false "no". A repo-id directory that could be listed
but not opened/classified is still included in the results with its
fields set to unknown, instead of silently disappearing from the
output. The JSON/text field for which state-directory layout a
repository was discovered under is named "layout" (values "borg2" /
"legacy" / null), not "version", so it cannot be confused with Borg's
actual repository format version (see "borg repo-info").
Adds docs/usage/known-repos.rst{,.inc} and the usage.rst toctree entry, and
the generated man page.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #10513 +/- ##
==========================================
+ Coverage 89.12% 89.30% +0.18%
==========================================
Files 103 105 +2
Lines 19799 20173 +374
Branches 3103 3161 +58
==========================================
+ Hits 17645 18015 +370
- Misses 1492 1500 +8
+ Partials 662 658 -4 ☔ View full report in Codecov by Harness. |
ThomasWaldmann
left a comment
There was a problem hiding this comment.
Thanks for working on this! The feature is useful and the basic behaviour works: I ran it against a real local state (~1100 known repos, borg2 + legacy security dirs, separate legacy cache root on macOS), it finished in about 0.5s and did not create anything. But there are a few defects, and the PR is much larger than the feature needs.
Blockers
- lint fails: black wants to reformat the
process_epilog("""...""")call inknown_repos_cmd.py. test_known_repos_deep_cache_tree_does_not_hit_recursion_limiterrors on Python 3.11: the 1500-level tree breaks pytest's tmpdir teardown, becauseshutil.rmtreeis still recursive on 3.11 (RecursionError). It also needs to raiseRLIMIT_NOFILEabove 1500. A 1500-deep cache directory is not a realistic case, so please just drop this test.- "Layout" is wrong on Linux (and whenever
BORG_BASE_DIRis set): there the borg2 and legacy cache roots are the same path (~/.cache/borg), so only the borg2 scan runs and every borg 1.x repo that only has a cache is reported aslayout: borg2. It only works out on platforms where the two roots differ (e.g. macOS). Either drop the field, or determine it from the cache contents (a borg 1.x cache dir haschunks/chunks.archive.d).
Design / maintainability
This is the main concern. The command basically lists 2-4 directories and reads two small files per entry, but the implementation is ~750 lines:
- fd-relative
openatwalks,O_NOFOLLOWeverywhere, FIFO / trailing-newline / symlink defences,BaseExceptionfd-cleanup paths, an explicit-stack tree walker... Anybody who could exploit what these defend against already has write access to the user's own config/cache dirs, so I don't think they are worth the complexity. - The yes/no/unknown tri-state (incomplete counters,
listing_ok,security_fully_searched,"unreadable"kinds) drives most of the code and test volume. Logging a warning and exiting with rc 1 on anyOSErrorwould be enough. - The epilog is ~90 lines and mostly documents internal failure semantics. A short paragraph is enough for help text.
- 35 tests / ~1065 lines, mostly for these edge cases, is a lot to maintain.
Please consider a rewrite using plain Path / os.scandir. I'd expect something around 150 lines plus a handful of tests.
Functional suggestions
- Output format: 6-7 lines per repo gets unwieldy fast (with ~1100 repos it is >6000 lines). A one-line-per-repo table, as proposed in #10453, would be much more usable.
Location: (unknown)for cache-only legacy repos: a borg 1.x cacheconfighasprevious_location(andtimestamp), so you could show it from there. A borg2 cache config has no location, so this only helps the legacy case.- Unknown key types print as a bare number, e.g.
Security info: yes (144). Something likeunknown key type 144would be clearer. _legacy_security_dir()duplicateslegacy.fs.get_security_dir()just because that callsget_config_dir()with create=True. Passingcreatethrough there would be the smaller change. Also, thelegacy/fs.pymodule docstring saysborg.securityis its only caller and that the module goes away together with the borg 1.x transfer support. This PR makes that docstring stale, and makes a new user-facing command depend on code that is scheduled for removal, so please keep that in mind.- The generated
docs/man/*.1anddocs/usage/*.rst.incfiles are usually regenerated at release time, so you don't need to include them.
Minor
- JSON has both
key_type_raw(a string, e.g."16") andkey_type. Is the raw value needed? - Using the cache
configmtime for "Cache last written" is fine as a best-effort proxy, since there is no manifest timestamp in the security dir anymore.
|
Thanks for the detailed review. I'll rewrite it with Path / os.scandir and simpler error handling, like you suggested, and sort out the rest too. Will push once it's ready. |
|
I haven't checked the code, but one thing came to mind: what happens if Thanks for working on this! 🎉 |
Rewrite the scanner around ordinary OS file access instead of extra symlink/size defenses, collapse duplicated scan/table/test logic, and surface real inspection failures (ENOTDIR, unparseable cache config) instead of treating them as absence. Also forward create=False through legacy get_keys_dir(), refresh the usage example, keep tests to the remaining behavioral surface, and shorten a few overly verbose test docstrings.
|
@ThomasWaldmann Design / maintainability
Blockers
Functional suggestions
Minor
358 lines is still above the ~150 estimate. Some of that is two things I |
ThomasWaldmann
left a comment
There was a problem hiding this comment.
Thanks, this is a big improvement! It's much easier to read and review now, and all points from my first review were addressed.
I checked c0c4934 locally: the new tests and legacy_fs_test.py pass. I also ran it against a real local state (~1100 known repos). The Linux layout detection via the cache content works, cache-only legacy repos now show their previous_location, and key-file-only repos show up with layout unknown, which also answers @PhrozenByte's question. Old borg2 beta key type bytes show as "unknown key type N", which is fine. It does not create anything beyond what borg's startup creates anyway.
Still blocking
- black still fails on the
process_epilog("""call inbuild_parser_known_repos(). Black wants the opening"""on its own line afterprocess_epilog(and the closing)on a separate line. Please run black (orpre-commit run --all-files) before pushing. - Docs: please restore the generated usage docs. Sorry, my earlier remark about the generated docs was wrong.
docs/usage/known-repos.rststill includesknown-repos.rst.inc, which this commit deleted, so the rendered page silently loses its usage section (the docs build only warns). Other recently added commands (e.g.copy,find) committeddocs/usage/<cmd>.rst.incanddocs/man/borg-<cmd>.1together with the command, so please regenerate both withpython scripts/make.py build_usageandpython scripts/make.py build_man, and keep only the known-repos files.
Minor
- The PR description is outdated: it still describes the tri-state, "
BORG_KEYS_DIRis not scanned" and "35 tests". location_mismatchcompares the security-dir location with the cache'sprevious_locationwithout the unicode normalization thatSecurityManager.assert_location_matches()does (normalize_local_path(), see #2913), so on macOS the same path in NFC vs NFD form would be flagged as a mismatch.dir_size()usesos.path.getsize(), which follows symlinked files.os.lstat()(counting only regular files) would match the "regular files" semantics. Borg never creates symlinks there, so this is cosmetic.- The table truncates the repo id to 15 chars. To act on an entry (e.g. remove a stale cache dir), users need the full id, so showing it in full may be worth the extra width. Your call.
legacy/fs.pydocstring: "(and now also the known-repos command)" - "now also" describes history. Just listing both callers (as the second paragraph already does) is enough.- Epilog: with
BORG_SECURITY_DIRset, the borg2 and legacy security roots coincide, so security-only repos (no cache) get layoutunknown. That's fine, but it could be mentioned briefly in the epilog.
With black and the generated docs fixed, this looks good to me.
black wants the process_epilog("""...""") call's opening triple-quote on
its own line and the closing paren on a separate line.
docs/usage/known-repos.rst still includes known-repos.rst.inc, which a
previous commit deleted along with the generated man page - this broke
the docs build (InputError: no such file). Regenerate both with
scripts/make.py build_usage/build_man, following the convention other
recently added commands (copy, find) already use.
- location_mismatch now compares the security location and cache previous_location through normalize_local_path(), the same way assert_location_matches() does, so an NFC vs. NFD unicode-form difference alone isn't flagged as a mismatch on macOS (borgbackup#2913). Added a regression test for it. - The table no longer truncates the repository id to 15 chars - users acting on an entry (e.g. removing a stale cache dir) need the full id. Strengthened the existing test assertion to match. Updated the epilog and the usage example accordingly (full id was previously JSON-only), and regenerated docs/usage/known-repos.rst.inc and docs/man/borg-known-repos.1 to match. - legacy/fs.py: dropped the "(and now also the known-repos command)" historical phrasing from the module docstring; the following paragraph already lists both callers. - Epilog: mentioned that a security-only repo gets layout "unknown" when BORG_SECURITY_DIR makes the borg2 and legacy security roots coincide. Thomas noted the behavior itself is fine either way, just worth a brief mention.
|
@ThomasWaldmann
Minors:
Also updated the PR description, so it no longer describes the tri-state, "BORG_KEYS_DIR is not scanned", or "35 tests". |
Summary
Closes #10453. Adds
borg known-repos, which lists local security info,cache, and/or key files for repositories this installation knows about.
It does not open any repository.
--jsonis supported, with the full repository id either way.Notes
layout(borg2/legacy) is determined from the security or cacheroot it was found in, or the cache's own content. If it can't be
determined, it shows as
unknownin the table,nullin--json.It's not the on-disk format version - see
borg repo-infofor that.Inspection failures are logged and make the command exit nonzero.
A legacy repo's security location and cache
previous_locationarecompared through the same unicode normalization
assert_location_matches()uses, so NFC vs. NFD alone isn't a mismatch on macOS (#2913).
Cache size sums file sizes found while walking the cache directory - not
disk usage. "Last written" is the cache config's mtime.
Key files in the current and legacy keys directories are counted from
their plaintext header.
18 tests cover discovery, legacy paths, inspection-failure handling,
layout detection, and the NFC/NFD location comparison.