From b27df15b36912fb7043927fedc5ef554031465c4 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 18 Jul 2025 20:40:39 +0200 Subject: [PATCH] create --files-changed=MODE option control how borg detects whether a file has changed while it was backed up, valid modes are ctime, mtime or disabled. ctime is the safest mode and the default. mtime can be useful if ctime does not work correctly for some reason (e.g. OneDrive files change their ctime without the user changing the file). disabled (= disabling change detection) is not recommended as it could lead to inconsistent backups. Only use if you know what you are doing. --- src/borg/archive.py | 40 ++++++++++++++++++++++++--------- src/borg/archiver/create_cmd.py | 17 ++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 5bbb6b7fd..fcc427e5d 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -1215,6 +1215,7 @@ class FilesystemObjectProcessors: log_json, iec, file_status_printer=None, + files_changed="ctime", ): self.metadata_collector = metadata_collector self.cache = cache @@ -1223,6 +1224,7 @@ class FilesystemObjectProcessors: self.process_file_chunks = process_file_chunks self.show_progress = show_progress self.print_file_status = file_status_printer or (lambda *args: None) + self.files_changed = files_changed self.hlm = HardLinkManager(id_type=tuple, info_type=(list, type(None))) # (dev, ino) -> chunks or None self.stats = Statistics(output_json=log_json, iec=iec) # threading: done by cache (including progress) @@ -1445,21 +1447,37 @@ class FilesystemObjectProcessors: if not is_win32: # TODO for win32 with backup_io("fstat2"): st2 = os.fstat(fd) - if is_special_file: + 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 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 == "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 8a7aa4f82..9345264df 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -262,6 +262,7 @@ class CreateMixIn: log_json=args.log_json, iec=args.iec, file_status_printer=self.print_file_status, + files_changed=args.files_changed, ) create_inner(archive, cache, fso) else: @@ -611,6 +612,13 @@ class CreateMixIn: it had before a content change happened. This can be used maliciously as well as 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. + - 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. + The mount points of filesystems or filesystem snapshots should be the same for every creation of a new archive to ensure fast operation. This is because the file cache that is used to determine changed files quickly uses absolute filenames. @@ -888,6 +896,15 @@ class CreateMixIn: default=FILES_CACHE_MODE_UI_DEFAULT, help="operate files cache in MODE. default: %s" % FILES_CACHE_MODE_UI_DEFAULT, ) + fs_group.add_argument( + "--files-changed", + metavar="MODE", + 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", + ) fs_group.add_argument( "--read-special", dest="read_special",