Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions src/borg/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from .helpers import BackupOSError, BackupPermissionError, BackupFileNotFoundError, BackupIOError
from .helpers import HardLinkManager
from .helpers import ChunkIteratorFileWrapper, open_item
from .helpers import Error, IntegrityError, set_ec
from .helpers import Error, IntegrityError, set_ec, sig_int
from .platform import uid2user, user2uid, gid2group, group2gid, get_birthtime_ns
from .helpers import parse_timestamp, archive_ts_now, CompressionSpec
from .helpers import OutputTimestamp, format_timedelta, format_file_size, file_status, FileSize
Expand Down Expand Up @@ -1879,12 +1879,25 @@ def check(
rebuild_manifest = True
if rebuild_manifest:
self.manifest = self.rebuild_manifest()
if find_lost_archives:
# Skip the remaining scans on Ctrl-C, but still run finish() below.
if find_lost_archives and not sig_int:
self.rebuild_archives_directory()
self.rebuild_archives(
match=match, first=first, last=last, sort_by=sort_by, older=older, oldest=oldest, newer=newer, newest=newest
)
if not sig_int:
self.rebuild_archives(
match=match,
first=first,
last=last,
sort_by=sort_by,
older=older,
oldest=oldest,
newer=newer,
newest=newest,
)
# finish() drops the chunk index and writes the manifest; run it even on Ctrl-C so --repair
# leaves a valid index (#9850).
self.finish()
if sig_int:
raise Error("Got Ctrl-C / SIGINT.")
if self.error_found:
logger.error("Archive consistency check complete, problems found.")
else:
Expand Down Expand Up @@ -1937,6 +1950,8 @@ def verify_data(self):
total=chunks_count, msg="Verifying data %6.2f%%", step=0.01, msgid="check.verify_data"
)
for chunk_id, _ in self.chunks.iteritems():
if sig_int: # stop at a chunk boundary
break
pi.show()
try:
encrypted_data = self.repository.get(chunk_id)
Expand Down Expand Up @@ -2023,6 +2038,8 @@ def valid_archive(obj):
msgid="check.rebuild_archives_directory",
)
for chunk_id, _ in self.chunks.iteritems():
if sig_int: # stop at a chunk boundary
break
pi.show()
cdata = self.repository.get(chunk_id, read_data=False) # only get metadata
try:
Expand Down Expand Up @@ -2216,6 +2233,9 @@ def valid_item(obj):
total=num_archives, msg="Checking archives %3.1f%%", step=0.1, msgid="check.rebuild_archives"
)
for i, info in enumerate(archive_infos):
if sig_int:
# Break only between archives: --repair rewrites each archive as a whole below.
break
pi.show(i)
archive_id, archive_id_hex = info.id, bin_to_hex(info.id)
try:
Expand Down
6 changes: 4 additions & 2 deletions src/borg/archiver/check_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from ._common import with_repository, Highlander
from ..archive import ArchiveChecker
from ..constants import * # NOQA
from ..helpers import set_ec, EXIT_WARNING, CancelledByUser, CommandError, IntegrityError
from ..helpers import yes, ArchiveFormatter, use_iec_units
from ..helpers import set_ec, EXIT_WARNING, CancelledByUser, CommandError, Error, IntegrityError
from ..helpers import yes, ArchiveFormatter, use_iec_units, sig_int
from ..helpers.argparsing import ArgumentParser

from ..logger import create_logger
Expand Down Expand Up @@ -66,6 +66,8 @@ def do_check(self, args, repository):
if not args.archives_only:
if not repository.check(repair=args.repair, max_duration=args.max_duration):
set_ec(EXIT_WARNING)
if sig_int: # repository check interrupted; skip the archive check
raise Error("Got Ctrl-C / SIGINT.")
if not args.repo_only and not archive_checker.check(
repository,
verify_data=args.verify_data,
Expand Down
4 changes: 4 additions & 0 deletions src/borg/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,10 @@ def store_list(namespace):
pack_infos = store_list("packs")
pack_pi = ProgressIndicatorPercent(total=len(pack_infos), msg="Checking packs %3.0f%%", msgid="check.packs")
for info in pack_infos:
if sig_int: # save progress so a later check resumes, then stop
logger.info(f"Interrupted repository check, {len(tracker)} packs checked so far.")
tracker.save()
break
self._lock_refresh()
pack_pi.show(increase=1) # advance for skipped packs too, so the bar tracks packs/, not work done
pack_id = hex_to_bin(info.name)
Expand Down
54 changes: 54 additions & 0 deletions src/borg/testsuite/archiver/check_cmd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,60 @@ def test_check_usage(archivers, request):
assert "archive2" in output


def test_check_soft_interrupt(archivers, request):
"""A Ctrl-C during a read-only check stops at a safe boundary and raises 'Got Ctrl-C' (#7893).
It changes nothing, so a normal check still passes afterwards."""
from ...archive import ArchiveChecker
from ...helpers import sig_int, Error

archiver = request.getfixturevalue(archivers)
check_cmd_setup(archiver)

try:
with Repository(archiver.repository_path, exclusive=True) as repository:
# repository check: stops at a pack boundary, still reports no errors.
sig_int._sig_int_triggered = True
assert repository.check() is True
with Repository(archiver.repository_path, exclusive=True) as repository:
# archive check: runs finish(), then raises.
sig_int._sig_int_triggered = True
with pytest.raises(Error, match="Got Ctrl-C"):
ArchiveChecker().check(repository, verify_data=True, sort_by="ts", format="{archive} {time} {id}")
finally:
sig_int._sig_int_triggered = False # reset the global flag for the following tests

cmd(archiver, "check", exit_code=0)


def test_check_repair_soft_interrupt(archivers, request, monkeypatch):
"""A Ctrl-C after the first archive of a --repair archive check stops at the archive boundary, runs
finish() (dropping the chunk index, writing the manifest), then raises. A later check confirms the
repository is still consistent."""
from ...archive import ArchiveChecker
from ...manifest import Archives
from ...helpers import sig_int, Error

archiver = request.getfixturevalue(archivers)
check_cmd_setup(archiver) # two archives

orig_create = Archives.create

def create_then_interrupt(self, *args, **kwargs):
orig_create(self, *args, **kwargs)
sig_int._sig_int_triggered = True # one Ctrl-C after the first archive was rebuilt

monkeypatch.setattr(Archives, "create", create_then_interrupt)
try:
with Repository(archiver.repository_path, exclusive=True) as repository:
with pytest.raises(Error, match="Got Ctrl-C"):
ArchiveChecker().check(repository, repair=True, sort_by="ts", format="{archive} {time} {id}")
finally:
sig_int._sig_int_triggered = False # reset the global flag for the following tests

# a normal check does not rebuild archives, so the patched Archives.create never fires here
cmd(archiver, "check", exit_code=0)


def test_date_matching(archivers, request):
archiver = request.getfixturevalue(archivers)
check_cmd_setup(archiver)
Expand Down
Loading