diff --git a/src/borg/archiver/rinfo_cmd.py b/src/borg/archiver/rinfo_cmd.py index 23cbf8799..612f97408 100644 --- a/src/borg/archiver/rinfo_cmd.py +++ b/src/borg/archiver/rinfo_cmd.py @@ -3,7 +3,7 @@ from ._common import with_repository from ..constants import * # NOQA -from ..helpers import bin_to_hex, json_print, basic_json_data +from ..helpers import bin_to_hex, json_print, basic_json_data, format_file_size from ..manifest import Manifest from ..logger import create_logger @@ -30,7 +30,7 @@ def do_rinfo(self, args, repository, manifest, cache): encryption += "\nKey file: %s" % key.find_key() info["encryption"] = encryption - print( + output = ( textwrap.dedent( """ Repository ID: {id} @@ -38,8 +38,6 @@ def do_rinfo(self, args, repository, manifest, cache): Repository version: {version} Append only: {append_only} {encryption} - Cache: {cache.path} - Security dir: {security_dir} """ ) .strip() @@ -48,9 +46,31 @@ def do_rinfo(self, args, repository, manifest, cache): location=repository._location.canonical_path(), version=repository.version, append_only=repository.append_only, - **info, + encryption=info["encryption"], ) ) + + response = repository.info() + storage_quota = response["storage_quota"] + used = format_file_size(response["storage_quota_use"]) + + output += f"Storage quota: {used} used" + if storage_quota: + output += f" out of {format_file_size(storage_quota)}" + output += "\n" + + output += ( + textwrap.dedent( + """ + Cache: {cache.path} + Security dir: {security_dir} + """ + ) + .strip() + .format(**info) + ) + + print(output) print(str(cache)) return self.exit_code diff --git a/src/borg/repository.py b/src/borg/repository.py index 8c980834f..19e348082 100644 --- a/src/borg/repository.py +++ b/src/borg/repository.py @@ -493,9 +493,22 @@ def open(self, path, exclusive, lock_wait=None, lock=True): self.id = unhexlify(self.config.get("repository", "id").strip()) self.io = LoggedIO(self.path, self.max_segment_size, self.segments_per_dir) + def _load_hints(self): + if (transaction_id := self.get_transaction_id()) is None: + # self is a fresh repo, so transaction_id is None and there is no hints file + return + hints = self._unpack_hints(transaction_id) + self.version = hints["version"] + self.storage_quota_use = hints["storage_quota_use"] + self.shadow_index = hints["shadow_index"] + def info(self): """return some infos about the repo (must be opened first)""" - return dict(id=self.id, version=self.version, append_only=self.append_only) + info = dict(id=self.id, version=self.version, append_only=self.append_only) + self._load_hints() + info["storage_quota"] = self.storage_quota + info["storage_quota_use"] = self.storage_quota_use + return info def close(self): if self.lock: @@ -512,7 +525,6 @@ def commit(self, compact=True, threshold=0.1): self.rollback() raise exception self.check_free_space() - self.log_storage_quota() segment = self.io.write_commit() self.segments.setdefault(segment, 0) self.compact[segment] += LoggedIO.header_fmt.size @@ -556,6 +568,12 @@ def open_index(self, transaction_id, auto_recover=True): self.commit(compact=False) return self.open_index(self.get_transaction_id()) + def _unpack_hints(self, transaction_id): + hints_path = os.path.join(self.path, "hints.%d" % transaction_id) + integrity_data = self._read_integrity(transaction_id, "hints") + with IntegrityCheckedFile(hints_path, write=False, integrity_data=integrity_data) as fd: + return msgpack.unpack(fd) + def prepare_txn(self, transaction_id, do_cleanup=True): self._active_txn = True if self.do_lock and not self.lock.got_exclusive_lock(): @@ -592,10 +610,8 @@ def prepare_txn(self, transaction_id, do_cleanup=True): self.io.cleanup(transaction_id) hints_path = os.path.join(self.path, "hints.%d" % transaction_id) index_path = os.path.join(self.path, "index.%d" % transaction_id) - integrity_data = self._read_integrity(transaction_id, "hints") try: - with IntegrityCheckedFile(hints_path, write=False, integrity_data=integrity_data) as fd: - hints = msgpack.unpack(fd) + hints = self._unpack_hints(transaction_id) except (msgpack.UnpackException, FileNotFoundError, FileIntegrityError) as e: logger.warning("Repository hints file missing or corrupted, trying to recover: %s", e) if not isinstance(e, FileNotFoundError): @@ -622,7 +638,6 @@ def prepare_txn(self, transaction_id, do_cleanup=True): self.compact = FreeSpace(hints["compact"]) self.storage_quota_use = hints.get("storage_quota_use", 0) self.shadow_index = hints.get("shadow_index", {}) - self.log_storage_quota() # Drop uncommitted segments in the shadow index for key, shadowed_segments in self.shadow_index.items(): for segment in list(shadowed_segments): @@ -762,14 +777,6 @@ def check_free_space(self): formatted_free = format_file_size(free_space) raise self.InsufficientFreeSpaceError(formatted_required, formatted_free) - def log_storage_quota(self): - if self.storage_quota: - logger.info( - "Storage quota: %s out of %s used.", - format_file_size(self.storage_quota_use), - format_file_size(self.storage_quota), - ) - def compact_segments(self, threshold): """Compact sparse segments by copying data into new segments""" if not self.compact: diff --git a/src/borg/testsuite/archiver/rinfo_cmd.py b/src/borg/testsuite/archiver/rinfo_cmd.py index a661afcaf..6b0fafc79 100644 --- a/src/borg/testsuite/archiver/rinfo_cmd.py +++ b/src/borg/testsuite/archiver/rinfo_cmd.py @@ -1,4 +1,5 @@ import json +from random import randbytes import unittest from ...constants import * # NOQA @@ -36,6 +37,20 @@ def test_info_json(self): assert all(isinstance(o, int) for o in stats.values()) assert all(key in stats for key in ("total_chunks", "total_size", "total_unique_chunks", "unique_size")) + def test_info_on_repository_with_storage_quota(self): + self.create_regular_file("file1", contents=randbytes(1000 * 1000)) + self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION, "--storage-quota=1G") + self.cmd(f"--repo={self.repository_location}", "create", "test", "input") + info_repo = self.cmd(f"--repo={self.repository_location}", "rinfo") + assert "Storage quota: 1.00 MB used out of 1.00 GB" in info_repo + + def test_info_on_repository_without_storage_quota(self): + self.create_regular_file("file1", contents=randbytes(1000 * 1000)) + self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION) + self.cmd(f"--repo={self.repository_location}", "create", "test", "input") + info_repo = self.cmd(f"--repo={self.repository_location}", "rinfo") + assert "Storage quota: 1.00 MB used" in info_repo + class RemoteArchiverTestCase(RemoteArchiverTestCaseBase, ArchiverTestCase): """run the same tests, but with a remote repository"""