diff --git a/HISTORY b/HISTORY index cbaacadf..189db396 100644 --- a/HISTORY +++ b/HISTORY @@ -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 diff --git a/cmdline/elem.h b/cmdline/elem.h index 18d8e213..22602930 100644 --- a/cmdline/elem.h +++ b/cmdline/elem.h @@ -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 diff --git a/cmdline/scan.c b/cmdline/scan.c index 723d97d2..e4978c53 100644 --- a/cmdline/scan.c +++ b/cmdline/scan.c @@ -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) { diff --git a/doc/snapraid.1 b/doc/snapraid.1 index b669fc47..14d5f8bb 100644 --- a/doc/snapraid.1 +++ b/doc/snapraid.1 @@ -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 diff --git a/doc/snapraid.d b/doc/snapraid.d index 1febe51a..09099e00 100644 --- a/doc/snapraid.d +++ b/doc/snapraid.d @@ -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 diff --git a/doc/snapraid.txt b/doc/snapraid.txt index e38ceced..b89f18b8 100644 --- a/doc/snapraid.txt +++ b/doc/snapraid.txt @@ -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 diff --git a/doc/snapraid_log.1 b/doc/snapraid_log.1 index ba322bb7..45f50b99 100644 --- a/doc/snapraid_log.1 +++ b/doc/snapraid_log.1 @@ -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::: -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:::: -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:::: +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:: -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: +Total number of new files created by relocating an existing +protected file to a new path (uint). +.TP .B summary:restored: Total number of new files matching a parity entry by name/time/size but with a new inode (uint). diff --git a/doc/snapraid_log.d b/doc/snapraid_log.d index 38196ecc..34933866 100644 --- a/doc/snapraid_log.d +++ b/doc/snapraid_log.d @@ -287,23 +287,33 @@ Scan Tags This item will have its content data blocks re-hashed in a `sync`. =scan:move::: - 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. - The old path to the file (escaped). - The new path to the file (escaped). =scan:copy:::: - 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. - The original file's disk. - The original file's location (escaped). + =scan:relocate:::: + 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:: - 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: + Total number of new files created by relocating an existing + protected file to a new path (uint). + =summary:restored: Total number of new files matching a parity entry by name/time/size but with a new inode (uint). diff --git a/doc/snapraid_log.txt b/doc/snapraid_log.txt index 333e65ed..dd6b2152 100644 --- a/doc/snapraid_log.txt +++ b/doc/snapraid_log.txt @@ -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::: - 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. - The old path to the file (escaped). - The new path to the file (escaped). scan:copy:::: - 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. - The original file's disk. - The original file's location (escaped). + scan:relocate:::: + 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:: - 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: + Total number of new files created by relocating an existing + protected file to a new path (uint). + summary:restored: Total number of new files matching a parity entry by name/time/size but with a new inode (uint).