diff --git a/src/borg/archive.py b/src/borg/archive.py index e5884efc62..b3b0510368 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -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: diff --git a/src/borg/archiver/extract_cmd.py b/src/borg/archiver/extract_cmd.py index 19b0e51f05..37c12273c8 100644 --- a/src/borg/archiver/extract_cmd.py +++ b/src/borg/archiver/extract_cmd.py @@ -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)) @@ -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(): @@ -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 diff --git a/src/borg/testsuite/archiver/extract_cmd_test.py b/src/borg/testsuite/archiver/extract_cmd_test.py index ebb843d261..c07ed47e86 100644 --- a/src/borg/testsuite/archiver/extract_cmd_test.py +++ b/src/borg/testsuite/archiver/extract_cmd_test.py @@ -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):