Repository: borgbackup/borg

Issue:
should print a warning if file changed during backup
GNU tar(1) does this by stat(2)ing before and after reading from the file. If the ctime changed‡, a warning is printed.

From the mailing list thread about annoying stuff in Borg.

‡ this is platform dependent and needs to be handled different on Windows.



Discussion:
Hmm, isn't that a bit a questionable / superfluous feature?

Usually one knows whether one backups a changing or a not-changing set of files.

If you know the stuff is changing and you directly back it up without doing a snapshot (fs, lvm, ...), such warnings would be expected and you know it - so you usually either live with it (if harmless) or you just don't do that.

To implement this feature, we would have to do 2 stat() calls instead of 1. For many (new or changed) small files this might impact performance. The files cache helps a bit, as unchanged files will be quickly processed without reading the contents.

If the files cache is used (file_known_and_unchanged and all chunks presents) then this check doesn't make sense, since we never read from the file.

If we read from the file (chunker loop), then we can do it. I suspect the extra cost will be very low due to inode cache locality (stat - read stuff - stat); if it's a big file it's not a big difference either way.

Since we already have an open FD at that time we can stat through the FD - no directory traversal done.

Right, it is likely cached, so just 1 OS call overhead + a bit of python.

If we consider it superfluous, that doesn't matter though. ;)

As there was no work on this and we are close to RC phase, I am removing this from the milestone.
This would also in many cases add a lot of output if funning with output/progress enabled. In many cases, snapshots are not really necessary but still, if a filesystem continues to be used, file will change, if only cache files. (which you could exclude, but that is also a lot of work. I noticed this already now when backing up /home -  at least each app that has a cache or similar generated errors about files having been deleted.
@ethchest in case of dirs with files that do not need to be backed up, it is way better adding them to the exclude list than having a lot of warnings in each backup log and having unnecessary big backups due to backing them up. nowadays a lot of that is in `/home/*/.cache/`.
Do this after PR #4043 is merged.
It would be very nice if this can go on 👍 In some environments (cloud server for example) we have to backup an active filesystem and sometimes the backup fails because of `stat: [Errno 2] No such file or directory` when the file were deleted during backup
@christophlehmann that is a different case.

In your case, the file vanished after borg listed the directory and before borg began processing the file.

So, borg noticed that and gave you a warning (not an error) and continued with the next file(s).

borg later will terminate with rc 1 for warning status (not with rc 2 as when an error occured).

This ticket is rather about a file having been modified after borg started processing it and before borg finished processing it. That means the file is backed up in an inconsistent state (which should raise a warning at least and could be avoided by doing a snapshot with lvm/zfs/...).
@ThomasWaldmann Thanks for clarification!

Resolving patch:
diff --git a/src/borg/archive.py b/src/borg/archive.py
--- a/src/borg/archive.py
+++ b/src/borg/archive.py
@@ -1226,9 +1226,22 @@ def process_file(self, *, path, parent_fd, name, st, cache, flags=flags_normal):
                     else:
                         with backup_io('read'):
                             self.process_file_chunks(item, cache, self.stats, self.show_progress, backup_io_iter(self.chunker.chunkify(None, fd)))
-                        if not is_special_file:
+                        if is_win32:
+                            changed_while_backup = False  # TODO
+                        else:
+                            with backup_io('fstat2'):
+                                st2 = os.fstat(fd)
+                            # special files:
+                            # - fifos change naturally, because they are fed from the other side. no problem.
+                            # - blk/chr devices don't change ctime anyway.
+                            changed_while_backup = not is_special_file and st.st_ctime_ns != st2.st_ctime_ns
+                        if changed_while_backup:
+                            status = 'C'  # regular file changed while we backed it up, might be inconsistent/corrupt!
+                        if not is_special_file and not changed_while_backup:
                             # we must not memorize special files, because the contents of e.g. a
                             # block or char device will change without its mtime/size/inode changing.
+                            # also, we must not memorize a potentially inconsistent/corrupt file that
+                            # changed while we backed it up.
                             cache.memorize_file(path_hash, st, [c.id for c in item.chunks])
                     self.stats.nfiles += 1
                 md = self.metadata_collector.stat_attrs(st, path, fd=fd)
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -695,6 +695,8 @@ def _process(self, *, path, parent_fd=None, name=None,
         except (BackupOSError, BackupError) as e:
             self.print_warning('%s: %s', path, e)
             status = 'E'
+        if status == 'C':
+            self.print_warning('%s: file changed while we backed it up', path)
         # Status output
         if status is None:
             if not dry_run:
@@ -3106,6 +3108,7 @@ def define_archive_filters_group(subparser, *, sort_by=True, first_last=True):
         - 'A' = regular file, added (see also :ref:`a_status_oddity` in the FAQ)
         - 'M' = regular file, modified
         - 'U' = regular file, unchanged
+        - 'C' = regular file, it changed while we backed it up
         - 'E' = regular file, an error happened while accessing/reading *this* file
 
         A lowercase character means a file type other than a regular file,


Test patch: