extract --continue: skip complete dirs, count skipped files in --progress, refs #6857

Directories: same_item() already considered an existing directory with the archived
mode and mtime as complete, but do_extract never passed continue_extraction when
restoring directory metadata, so all directories got their metadata restored again.

--progress: the total includes the files that --continue skips, but their size was
never added to the progress, so the percentage stayed far below the real one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Thomas Waldmann
2026-10-06 23:39:33 +02:00
co-authored by Claude Opus 5.5
parent c4c067e0f5
commit 10bbecb90f
3 changed files with 25 additions and 4 deletions
+2
View File
@@ -1031,6 +1031,8 @@ Duration: {0.duration}
st = os.stat(path, follow_symlinks=False)
if continue_extraction and same_item(item, st):
# we already have fully extracted this file in a previous run.
if pi:
pi.show(increase=item.get_size(), info=[remove_surrogates(item.path)])
if "hlid" not in item or not has_link:
return # done!
# it is part of a group of hard links, keep the group together:
+6 -4
View File
@@ -88,7 +88,7 @@ class ExtractMixIn:
while dirs and not item.path.startswith(dirs[-1].path):
dir_item = dirs.pop(-1)
try:
archive.extract_item(dir_item, stdout=stdout)
archive.extract_item(dir_item, stdout=stdout, continue_extraction=continue_extraction)
except BackupError as e:
self.print_warning_instance(BackupWarning(remove_surrogates(dir_item.path), e))
@@ -122,7 +122,7 @@ class ExtractMixIn:
pi.show()
dir_item = dirs.pop(-1)
try:
archive.extract_item(dir_item, stdout=stdout)
archive.extract_item(dir_item, stdout=stdout, continue_extraction=continue_extraction)
except BackupError as e:
self.print_warning_instance(BackupWarning(remove_surrogates(dir_item.path), e))
for pattern in matcher.get_unmatched_include_patterns():
@@ -170,8 +170,10 @@ class ExtractMixIn:
``--continue`` extracts into a non-empty directory. It is made for continuing a previously
interrupted extraction of the same archive into the same directory: an existing regular
file that has the same type, permissions (mode), size and modification time as the
archived file is considered to be fully extracted already and is skipped. Everything else
is extracted, replacing existing files. Files that are in the directory, but not in the
archived file is considered to be fully extracted already and is skipped. Likewise, the
metadata of an existing directory that has the same permissions (mode) and modification
time as the archived directory is not restored again. Everything else is extracted,
replacing existing files. Files that are in the directory, but not in the
archive, are left as they are. ``--continue`` is thus also needed to restore files into an
existing directory tree. Note that a file that was damaged without a change of its size
and modification time (e.g. by bit rot) is skipped, not replaced: remove it before
@@ -1035,6 +1035,7 @@ def test_extract_continue(archivers, request):
assert file3_st.st_mtime_ns == new_file3_st.st_mtime_ns # file3 was extracted again
# windows has a strange ctime behaviour when deleting and recreating a file
if not is_win32:
assert dir1_st.st_ctime_ns == now_dir1_st.st_ctime_ns # dir1 metadata not restored again
assert file1_st.st_ctime_ns == now_file1_st.st_ctime_ns # file not extracted again
assert file2_st.st_ctime_ns != new_file2_st.st_ctime_ns # file extracted again
assert file3_st.st_ctime_ns != new_file3_st.st_ctime_ns # file extracted again
@@ -1047,6 +1048,22 @@ def test_extract_continue(archivers, request):
assert f.read() == CONTENTS3
def test_extract_continue_progress(archivers, request, monkeypatch):
# --progress must count the files that --continue skips as extracted.
archiver = request.getfixturevalue(archivers)
monkeypatch.setenv("BORG_PROGRESS_FPS", "1000000") # do not rate limit the progress output
cmd(archiver, "repo-create", RK_ENCRYPTION)
create_regular_file(archiver.input_path, "file1", size=900 * 1024)
create_regular_file(archiver.input_path, "file2", size=100 * 1024)
cmd(archiver, "create", "arch", "input/file1", "input/file2") # extract file1 before file2
with changedir("output"):
cmd(archiver, "extract", "arch")
os.truncate("input/file2", 123) # file1 is fully extracted, file2 is not
output = cmd(archiver, "extract", "arch", "--continue", "--progress")
# file2 has a single chunk, its extraction starts after the skipped 90% of the data in file1:
assert " 90.0% Extracting: input/file2" in output
@requires_hardlinks
@pytest.mark.parametrize("missing", ["first", "last"])
def test_extract_continue_hardlinks(archivers, request, missing):