Introduce "relocated" state to unify and replace "copied/removed" logic

This commit is contained in:
Andrea Mazzoleni 2026-03-14 17:48:30 +01:00
parent 189029749e
commit c449864fc7
9 changed files with 167 additions and 64 deletions

View file

@ -5,6 +5,9 @@ SnapRAID HISTORY
==================
* New 'extra' config option to define additional disks to monitor with the
'probe' and 'smart' commands. This replaces the previous autodetection.
* Introduced the 'relocated' file state to unify the previous 'copied/removed'
logic. This new status specifically identifies files moved to a different
path or disk with a new inode where the original has disappeared.
* Removed the UAC request for Windows executable.
14.0rc2 08/03/2026

View file

@ -265,11 +265,21 @@ struct snapraid_block {
*/
#define FILE_IS_MISSING 0x800
#define FILE_IS_HARDLINK 0x1000 /**< If it's an hardlink. */
#define FILE_IS_SYMLINK 0x2000 /**< If it's a file symlink. */
#define FILE_IS_SYMDIR 0x4000 /**< If it's a dir symlink for Windows. Not yet supported. */
#define FILE_IS_JUNCTION 0x8000 /**< If it's a junction for Windows. Not yet supported. */
#define FILE_IS_LINK_MASK 0xF000 /**< Mask for link type. */
/**
* During scan if the file is found missing and relocated to another place
*
* This is used to avoid to report it "removed".
*
* This specific bit is written using protection from the stamp_mutex during
* the multithread scan.
*/
#define FILE_IS_RELOCATED 0x1000
#define FILE_IS_HARDLINK 0x10000 /**< If it's an hardlink. */
#define FILE_IS_SYMLINK 0x20000 /**< If it's a file symlink. */
#define FILE_IS_SYMDIR 0x40000 /**< If it's a dir symlink for Windows. Not yet supported. */
#define FILE_IS_JUNCTION 0x80000 /**< If it's a junction for Windows. Not yet supported. */
#define FILE_IS_LINK_MASK 0xF0000 /**< Mask for link type. */
/**
* File.
@ -405,6 +415,10 @@ struct snapraid_disk {
* Mutex for protecting the scan process.
*
* It's used during the scan process to protect the stampset to identify copy of files
* and the FILE_IS_RELOCATED flag of files.
*
* Note that only the FILE_IS_RELOCATED single bit is protected.
* Not the other bits.
*/
thread_mutex_t stamp_mutex;
#endif

View file

@ -30,13 +30,6 @@ struct snapraid_scan {
int is_diff; /**< If it's a diff command or a scanning */
int need_write; /**< If a state write is required */
#if HAVE_THREAD
/**
* Mutex for protecting the disk stampset table
*/
thread_mutex_t mutex;
#endif
/**
* Counters of changes.
*/
@ -45,6 +38,7 @@ struct snapraid_scan {
unsigned count_restore; /**< Files with equal name, size and timestamp, but different inode. */
unsigned count_change; /**< Files with same name, but different size and/or timestamp. */
unsigned count_copy; /**< Files new, with same name size and timestamp of a file in a different disk. */
unsigned count_relocate; /**< Like copy, but with the original disappeared. */
unsigned count_insert; /**< Files new. */
unsigned count_remove; /**< Files removed. */
@ -67,6 +61,7 @@ static struct snapraid_scan* scan_alloc(struct snapraid_state* state, struct sna
scan->count_equal = 0;
scan->count_move = 0;
scan->count_copy = 0;
scan->count_relocate = 0;
scan->count_restore = 0;
scan->count_change = 0;
scan->count_remove = 0;
@ -1058,15 +1053,38 @@ static void scan_file(struct snapraid_scan* scan, int is_diff, const char* sub,
/* if found, and it's a fully hashed file */
if (other_file && file_is_full_hashed_and_stable(scan->state, other_disk, other_file)) {
char path_other[PATH_MAX];
struct stat other_st;
/*
* Protect the write as multiple threads may write the same FILE_IS_RELOCATED bit.
*
* The bit is always written as 1 and never read, so protection is likely unnecessary
* but still valuable to avoid data race reports from checker tools
*/
stamp_lock(other_disk);
file_flag_set(other_file, FILE_IS_RELOCATED);
stamp_unlock(other_disk);
/* assume that the file is a copy, and reuse the hash */
file_copy(other_file, file);
/* revert old counter and use the copy one */
++scan->count_copy;
/* check if other file still exists */
pathprint(path_other, sizeof(path_other), "%s%s", other_disk->dir, other_file->sub);
if (lstat(path_other, &other_st) == 0) {
++scan->count_copy;
log_tag("scan:copy:%s:%s:%s:%s\n", other_disk->name, esc_tag(other_file->sub, esc_buffer), disk->name, esc_tag(file->sub, esc_buffer_alt));
if (is_diff) {
msg_info("copy %s -> %s\n", fmt_term(other_disk, other_file->sub, esc_buffer), fmt_term(disk, file->sub, esc_buffer_alt));
log_tag("scan:copy:%s:%s:%s:%s\n", other_disk->name, esc_tag(other_file->sub, esc_buffer), disk->name, esc_tag(file->sub, esc_buffer_alt));
if (is_diff) {
msg_info("copy %s -> %s\n", fmt_term(other_disk, other_file->sub, esc_buffer), fmt_term(disk, file->sub, esc_buffer_alt));
}
} else {
++scan->count_relocate;
log_tag("scan:relocate:%s:%s:%s:%s\n", other_disk->name, esc_tag(other_file->sub, esc_buffer), disk->name, esc_tag(file->sub, esc_buffer_alt));
if (is_diff) {
msg_info("relocate %s -> %s\n", fmt_term(other_disk, other_file->sub, esc_buffer), fmt_term(disk, file->sub, esc_buffer_alt));
}
}
/* mark it as reported */
@ -1700,11 +1718,13 @@ static int state_diffscan(struct snapraid_state* state, int is_diff)
/* remove if not present */
if (!file_flag_has(file, FILE_IS_PRESENT)) {
++scan->count_remove;
if (!file_flag_has(file, FILE_IS_RELOCATED)) {
++scan->count_remove;
log_tag("scan:remove:%s:%s\n", disk->name, esc_tag(file->sub, esc_buffer));
if (is_diff) {
msg_info("remove %s\n", fmt_term(disk, file->sub, esc_buffer));
log_tag("scan:remove:%s:%s\n", disk->name, esc_tag(file->sub, esc_buffer));
if (is_diff) {
msg_info("remove %s\n", fmt_term(disk, file->sub, esc_buffer));
}
}
scan_file_remove(scan, file);
@ -1969,6 +1989,7 @@ static int state_diffscan(struct snapraid_state* state, int is_diff)
total.count_equal = 0;
total.count_move = 0;
total.count_copy = 0;
total.count_relocate = 0;
total.count_restore = 0;
total.count_change = 0;
total.count_remove = 0;
@ -1979,6 +2000,7 @@ static int state_diffscan(struct snapraid_state* state, int is_diff)
total.count_equal += scan->count_equal;
total.count_move += scan->count_move;
total.count_copy += scan->count_copy;
total.count_relocate += scan->count_relocate;
total.count_restore += scan->count_restore;
total.count_change += scan->count_change;
total.count_remove += scan->count_remove;
@ -1998,6 +2020,7 @@ static int state_diffscan(struct snapraid_state* state, int is_diff)
msg("%8u updated\n", total.count_change);
msg("%8u moved\n", total.count_move);
msg("%8u copied\n", total.count_copy);
msg("%8u relocated\n", total.count_relocate);
msg("%8u restored\n", total.count_restore);
log_tag("summary:equal:%u\n", total.count_equal);
@ -2006,6 +2029,7 @@ static int state_diffscan(struct snapraid_state* state, int is_diff)
log_tag("summary:updated:%u\n", total.count_change);
log_tag("summary:moved:%u\n", total.count_move);
log_tag("summary:copied:%u\n", total.count_copy);
log_tag("summary:relocated:%u\n", total.count_relocate);
log_tag("summary:restored:%u\n", total.count_restore);
log_tag("list:scan_end\n");
@ -2013,7 +2037,7 @@ static int state_diffscan(struct snapraid_state* state, int is_diff)
state->removed_files = total.count_remove;
state->updated_files = total.count_change;
no_difference = !total.count_move && !total.count_copy && !total.count_restore
no_difference = !total.count_move && !total.count_copy && !total.count_relocate && !total.count_restore
&& !total.count_change && !total.count_remove && !total.count_insert;
if (is_diff) {

View file

@ -793,15 +793,23 @@ They are identified by having the same name, size, timestamp,
and inode, but a different directory.
.HP 4
.I copied
Files copied on the same or a different disk. Note that if
they are truly moved to a different disk, they will also be
counted in \`removed\`.
They are identified by having the same name, size, and
timestamp. If the sub\-second timestamp is zero,
the full path must match, not just the name.
Files copied on the same or a different disk where the
original file still exists. They are identified by having
the same name, size, and timestamp. If the sub\-second
timestamp is zero, the full path must match to be
identified, not just the name.
.HP 4
.I relocated
Files moved on the same or a different disk where
the original has disappeared. They are identified by
having the same name, size, and timestamp. If the
sub\-second timestamp is zero, the full path must match
to be identified. Unlike \'moved\' files on the same disk,
relocated files have a different inode.
.HP 4
.I restored
Files with a different inode but matching name, size, and timestamp.
Files with a different inode but matching directory,
name, size, and timestamp.
These are usually files restored after being deleted.
.PD
.RE

View file

@ -505,13 +505,19 @@ Commands
moved - Files moved to a different directory on the same disk.
They are identified by having the same name, size, timestamp,
and inode, but a different directory.
copied - Files copied on the same or a different disk. Note that if
they are truly moved to a different disk, they will also be
counted in `removed`.
They are identified by having the same name, size, and
timestamp. If the sub-second timestamp is zero,
the full path must match, not just the name.
restored - Files with a different inode but matching name, size, and timestamp.
copied - Files copied on the same or a different disk where the
original file still exists. They are identified by having
the same name, size, and timestamp. If the sub-second
timestamp is zero, the full path must match to be
identified, not just the name.
relocated - Files moved on the same or a different disk where
the original has disappeared. They are identified by
having the same name, size, and timestamp. If the
sub-second timestamp is zero, the full path must match
to be identified. Unlike 'moved' files on the same disk,
relocated files have a different inode.
restored - Files with a different inode but matching directory,
name, size, and timestamp.
These are usually files restored after being deleted.
If a `sync` is required, the process return code is 2, instead of the

View file

@ -541,13 +541,19 @@ presented, grouped by:
moved - Files moved to a different directory on the same disk.
They are identified by having the same name, size, timestamp,
and inode, but a different directory.
copied - Files copied on the same or a different disk. Note that if
they are truly moved to a different disk, they will also be
counted in `removed`.
They are identified by having the same name, size, and
timestamp. If the sub-second timestamp is zero,
the full path must match, not just the name.
restored - Files with a different inode but matching name, size, and timestamp.
copied - Files copied on the same or a different disk where the
original file still exists. They are identified by having
the same name, size, and timestamp. If the sub-second
timestamp is zero, the full path must match to be
identified, not just the name.
relocated - Files moved on the same or a different disk where
the original has disappeared. They are identified by
having the same name, size, and timestamp. If the
sub-second timestamp is zero, the full path must match
to be identified. Unlike 'moved' files on the same disk,
relocated files have a different inode.
restored - Files with a different inode but matching directory,
name, size, and timestamp.
These are usually files restored after being deleted.
If a `sync` is required, the process return code is 2, instead of the

View file

@ -431,8 +431,9 @@ A file or link has been updated (size, timestamp, or link target changed).
This item will have its content data blocks re\-hashed in a \`sync\`.
.TP
.B scan:move:<disk_name>:<old_path>:<new_path>
A file was moved on the same disk. This is an efficient operation
that only updates the path metadata without re\-hashing blocks.
A file was moved to a different directory on the same disk.
This is an efficient operation that only updates the path
metadata without rehashing blocks.
.RS 4
.PD 0
.HP 4
@ -445,9 +446,11 @@ The new path to the file (escaped).
.RE
.TP
.B scan:copy:<disk_name>:<path>:<source_disk>:<source_path>
A new file was found to be a copy of a file from another disk.
This is detected by matching file size and timestamp, and is an
efficient operation that avoids re\-hashing all blocks.
A new file was found to be a copy of a file on the same or
any other disk.
This is detected by matching the file name,
size, and timestamp, and is an efficient operation that avoids
rehashing all blocks.
.RS 4
.PD 0
.HP 4
@ -459,9 +462,16 @@ The original file\'s location (escaped).
.PD
.RE
.TP
.B scan:relocate:<disk_name>:<path>:<source_disk>:<source_path>
Like \`copy\`, but the original file has disappeared.
The difference between \`move\` and \`relocate\` on the same
disk is that \`move\` is only used if the inode remains
the same.
.TP
.B scan:restore:<disk_name>:<path>
A file\'s inode has changed but not its date\-time and size,
which suggests the file may be restored from backup.
A file\'s inode has changed but not its directory, name,
timestamp and size, which suggests the file may be restored
from backup.
The file won\'t be re\-hashed in \`sync\` as its precomputed hash
is assumed to be correct.
.PP
@ -491,6 +501,10 @@ retaining the same inode (uint).
Total number of new files created by copying an existing
protected file to a new path (uint).
.TP
.B summary:relocated:<count>
Total number of new files created by relocating an existing
protected file to a new path (uint).
.TP
.B summary:restored:<count>
Total number of new files matching a parity entry by
name/time/size but with a new inode (uint).

View file

@ -287,23 +287,33 @@ Scan Tags
This item will have its content data blocks re-hashed in a `sync`.
=scan:move:<disk_name>:<old_path>:<new_path>
A file was moved on the same disk. This is an efficient operation
that only updates the path metadata without re-hashing blocks.
A file was moved to a different directory on the same disk.
This is an efficient operation that only updates the path
metadata without rehashing blocks.
<old_path> - The old path to the file (escaped).
<new_path> - The new path to the file (escaped).
=scan:copy:<disk_name>:<path>:<source_disk>:<source_path>
A new file was found to be a copy of a file from another disk.
This is detected by matching file size and timestamp, and is an
efficient operation that avoids re-hashing all blocks.
A new file was found to be a copy of a file on the same or
any other disk.
This is detected by matching the file name,
size, and timestamp, and is an efficient operation that avoids
rehashing all blocks.
<source_disk> - The original file's disk.
<source_path> - The original file's location (escaped).
=scan:relocate:<disk_name>:<path>:<source_disk>:<source_path>
Like `copy`, but the original file has disappeared.
The difference between `move` and `relocate` on the same
disk is that `move` is only used if the inode remains
the same.
=scan:restore:<disk_name>:<path>
A file's inode has changed but not its date-time and size,
which suggests the file may be restored from backup.
A file's inode has changed but not its directory, name,
timestamp and size, which suggests the file may be restored
from backup.
The file won't be re-hashed in `sync` as its precomputed hash
is assumed to be correct.
@ -333,6 +343,10 @@ Scan Tags
Total number of new files created by copying an existing
protected file to a new path (uint).
=summary:relocated:<count>
Total number of new files created by relocating an existing
protected file to a new path (uint).
=summary:restored:<count>
Total number of new files matching a parity entry by
name/time/size but with a new inode (uint).

View file

@ -303,23 +303,33 @@ content file. They are output for every discovered change.
This item will have its content data blocks re-hashed in a `sync`.
scan:move:<disk_name>:<old_path>:<new_path>
A file was moved on the same disk. This is an efficient operation
that only updates the path metadata without re-hashing blocks.
A file was moved to a different directory on the same disk.
This is an efficient operation that only updates the path
metadata without rehashing blocks.
<old_path> - The old path to the file (escaped).
<new_path> - The new path to the file (escaped).
scan:copy:<disk_name>:<path>:<source_disk>:<source_path>
A new file was found to be a copy of a file from another disk.
This is detected by matching file size and timestamp, and is an
efficient operation that avoids re-hashing all blocks.
A new file was found to be a copy of a file on the same or
any other disk.
This is detected by matching the file name,
size, and timestamp, and is an efficient operation that avoids
rehashing all blocks.
<source_disk> - The original file's disk.
<source_path> - The original file's location (escaped).
scan:relocate:<disk_name>:<path>:<source_disk>:<source_path>
Like `copy`, but the original file has disappeared.
The difference between `move` and `relocate` on the same
disk is that `move` is only used if the inode remains
the same.
scan:restore:<disk_name>:<path>
A file's inode has changed but not its date-time and size,
which suggests the file may be restored from backup.
A file's inode has changed but not its directory, name,
timestamp and size, which suggests the file may be restored
from backup.
The file won't be re-hashed in `sync` as its precomputed hash
is assumed to be correct.
@ -349,6 +359,10 @@ These tags provide a final count of all detected changes.
Total number of new files created by copying an existing
protected file to a new path (uint).
summary:relocated:<count>
Total number of new files created by relocating an existing
protected file to a new path (uint).
summary:restored:<count>
Total number of new files matching a parity entry by
name/time/size but with a new inode (uint).