diff --git a/src/borg/archive.py b/src/borg/archive.py index 61f1f0bfa..c0ae8978a 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -865,6 +865,7 @@ Duration: {0.duration} with backup_io("open"): fd = open(path, "wb") with fd: + trailing_hole = False for data in self.pipeline.fetch_many(item.chunks, is_preloaded=True, ro_type=ROBJ_FILE_STREAM): if pi: pi.show(increase=len(data), info=[remove_surrogates(item.path)]) @@ -872,10 +873,18 @@ Duration: {0.duration} if sparse and zeros.startswith(data): # all-zero chunk: create a hole in a sparse file fd.seek(len(data), 1) + trailing_hole = True else: fd.write(data) + trailing_hole = False with backup_io("truncate_and_attrs"): pos = item_chunks_size = fd.tell() + if is_win32 and trailing_hole and pos > 0: + # Windows: truncate() does not zero-fill properly (no VDL update). + # Writing a single zero at the end forces NTFS to zero-fill the hole + # and update valid data length. + fd.seek(pos - 1) + fd.write(b"\0") fd.truncate(pos) fd.flush() self.restore_attrs(path, item, fd=fd.fileno()) diff --git a/src/borg/testsuite/archiver/extract_cmd_test.py b/src/borg/testsuite/archiver/extract_cmd_test.py index 2b89db1cb..c65f385f1 100644 --- a/src/borg/testsuite/archiver/extract_cmd_test.py +++ b/src/borg/testsuite/archiver/extract_cmd_test.py @@ -182,7 +182,6 @@ def test_birthtime(archivers, request): assert same_ts_ns(sto.st_mtime_ns, mtime * 10**9) -@pytest.mark.skipif(is_win32, reason="frequent test failures on github CI on win32") def test_sparse_file(archivers, request): archiver = request.getfixturevalue(archivers)