Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/borg/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,8 @@ def same_item(item, st):
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:
Expand Down
10 changes: 6 additions & 4 deletions src/borg/archiver/extract_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def do_extract(self, args, repository, manifest, archive):
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))

Expand Down Expand Up @@ -122,7 +122,7 @@ def do_extract(self, args, repository, manifest, archive):
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():
Expand Down Expand Up @@ -170,8 +170,10 @@ def build_parser_extract(self, subparsers, common_parser, mid_common_parser):
``--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
Expand Down
17 changes: 17 additions & 0 deletions src/borg/testsuite/archiver/extract_cmd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
Loading