diff --git a/src/borg/archive.py b/src/borg/archive.py index 0c83fd2f1f..a4cdea93aa 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1242,7 +1242,7 @@ def __init__( log_json, iec, file_status_printer=None, - files_changed="ctime", + files_changed="mtime" if is_win32 else "ctime", ): self.metadata_collector = metadata_collector self.cache = cache @@ -1471,40 +1471,35 @@ def process_file(self, *, path, parent_fd, name, st, cache, flags=flags_normal, ) self.stats.chunking_time = self.chunker.chunking_time end_reading = time.time_ns() - if not is_win32: # TODO for win32 - with backup_io("fstat2"): - st2 = os.fstat(fd) - if self.files_changed == "disabled" or is_special_file: - # special files: - # - fifos change naturally, because they are fed from the other side. no problem. - # - blk/chr devices don't change ctime anyway. - pass - elif self.files_changed == "ctime": - if st.st_ctime_ns != st2.st_ctime_ns: - # ctime was changed, this is either a metadata or a data change. - changed_while_backup = True - elif ( - start_reading - TIME_DIFFERS1_NS < st2.st_ctime_ns < end_reading + TIME_DIFFERS1_NS - ): - # this is to treat a very special race condition, see #3536. - # - file was changed right before st.ctime was determined. - # - then, shortly afterwards, but already while we read the file, the - # file was changed again, but st2.ctime is the same due to ctime granularity. - # when comparing file ctime to local clock, widen interval by TIME_DIFFERS1_NS. - changed_while_backup = True - elif self.files_changed == "mtime": - if st.st_mtime_ns != st2.st_mtime_ns: - # mtime was changed, this is either a data change. - changed_while_backup = True - elif ( - start_reading - TIME_DIFFERS1_NS < st2.st_mtime_ns < end_reading + TIME_DIFFERS1_NS - ): - # this is to treat a very special race condition, see #3536. - # - file was changed right before st.mtime was determined. - # - then, shortly afterwards, but already while we read the file, the - # file was changed again, but st2.mtime is the same due to mtime granularity. - # when comparing file mtime to local clock, widen interval by TIME_DIFFERS1_NS. - changed_while_backup = True + with backup_io("fstat2"): + st2 = os.fstat(fd) + if self.files_changed == "disabled" or is_special_file: + # special files: + # - fifos change naturally, because they are fed from the other side. no problem. + # - blk/chr devices don't change ctime anyway. + pass + elif self.files_changed == "ctime": + if st.st_ctime_ns != st2.st_ctime_ns: + # ctime was changed, this is either a metadata or a data change. + changed_while_backup = True + elif start_reading - TIME_DIFFERS1_NS < st2.st_ctime_ns < end_reading + TIME_DIFFERS1_NS: + # this is to treat a very special race condition, see #3536. + # - file was changed right before st.ctime was determined. + # - then, shortly afterwards, but already while we read the file, the + # file was changed again, but st2.ctime is the same due to ctime granularity. + # when comparing file ctime to local clock, widen interval by TIME_DIFFERS1_NS. + changed_while_backup = True + elif self.files_changed == "mtime": + if st.st_mtime_ns != st2.st_mtime_ns: + # mtime was changed, this is either a data change. + changed_while_backup = True + elif start_reading - TIME_DIFFERS1_NS < st2.st_mtime_ns < end_reading + TIME_DIFFERS1_NS: + # this is to treat a very special race condition, see #3536. + # - file was changed right before st.mtime was determined. + # - then, shortly afterwards, but already while we read the file, the + # file was changed again, but st2.mtime is the same due to mtime granularity. + # when comparing file mtime to local clock, widen interval by TIME_DIFFERS1_NS. + changed_while_backup = True if changed_while_backup: # regular file changed while we backed it up, might be inconsistent/corrupt! if last_try: diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index cbbdefc3cc..cc89960733 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -252,6 +252,13 @@ def create_inner(archive, cache, fso): nobirthtime=args.nobirthtime, ) cp = ChunksProcessor(cache=cache, key=key, add_item=archive.add_item, rechunkify=False) + if is_win32 and args.files_changed == "ctime": + self.print_warning( + "--files-changed=ctime is not supported on Windows " + "(ctime is file creation time, not change time). Using mtime instead.", + wc=None, + ) + args.files_changed = "mtime" fso = FilesystemObjectProcessors( metadata_collector=metadata_collector, cache=cache, @@ -621,8 +628,9 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): well-meant, but in both cases mtime-based cache modes can be problematic. The ``--files-changed`` option controls how Borg detects if a file has changed during backup: - - ctime (default): Use ctime to detect changes. This is the safest option. - - mtime: Use mtime to detect changes. + - ctime (default on POSIX): Use ctime to detect changes. This is the safest option. + Not supported on Windows (ctime is file creation time there). + - mtime (default on Windows): Use mtime to detect changes. - disabled: Disable the "file has changed while we backed it up" detection completely. This is not recommended unless you know what you're doing, as it could lead to inconsistent backups if files change during the backup process. @@ -910,8 +918,9 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): dest="files_changed", action=Highlander, choices=["ctime", "mtime", "disabled"], - default="ctime", - help="specify how to detect if a file has changed during backup (ctime, mtime, disabled). default: ctime", + default="mtime" if is_win32 else "ctime", + help="specify how to detect if a file has changed during backup (ctime, mtime, disabled). " + "default: ctime (on Windows: mtime, because ctime is file creation time there).", ) fs_group.add_argument( "--read-special", diff --git a/src/borg/testsuite/archiver/create_cmd_test.py b/src/borg/testsuite/archiver/create_cmd_test.py index 04bc6b1abc..00edc0935d 100644 --- a/src/borg/testsuite/archiver/create_cmd_test.py +++ b/src/borg/testsuite/archiver/create_cmd_test.py @@ -687,6 +687,20 @@ def test_file_status_cs_cache_mode(archivers, request): assert "M input/file1" in output +def test_files_changed_modes(archivers, request): + """test that all --files-changed modes are accepted and work""" + archiver = request.getfixturevalue(archivers) + create_regular_file(archiver.input_path, "file1", size=10) + cmd(archiver, "repo-create", RK_ENCRYPTION) + # test mtime mode (works on all platforms including Windows) + cmd(archiver, "create", "test_mtime", "input", "--files-changed=mtime") + # test disabled mode + cmd(archiver, "create", "test_disabled", "input", "--files-changed=disabled") + if not is_win32: + # test ctime mode (only meaningful on POSIX, where ctime = inode change time) + cmd(archiver, "create", "test_ctime", "input", "--files-changed=ctime") + + def test_file_status_ms_cache_mode(archivers, request): """test that a chmod'ed file with no content changes does not get chunked again in mtime,size cache_mode""" archiver = request.getfixturevalue(archivers)