From 2fc38017a88213696a955f3c624cfc72b57bcb79 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Tue, 21 Apr 2026 15:19:25 +0200 Subject: [PATCH] Add Btrfs snapshot support for atomic sync and recovery consistency --- HISTORY | 18 ++++ cmdline/elem.c | 10 +- cmdline/elem.h | 4 +- cmdline/mingw.c | 46 ++++++++ cmdline/portable.h | 49 +++++++++ cmdline/scan.c | 8 +- cmdline/snapraid.c | 13 +++ cmdline/state.c | 241 ++++++++++++++++++++++++++++++++++++++++-- cmdline/state.h | 21 ++++ cmdline/support.c | 19 ++++ cmdline/support.h | 7 ++ cmdline/thermal.c | 2 +- cmdline/unix.c | 218 ++++++++++++++++++++++++++++++++++++++ configure.ac | 2 +- doc/snapraid.1 | 139 ++++++++++++++++++++++++ doc/snapraid.d | 130 +++++++++++++++++++++++ doc/snapraid.txt | 185 ++++++++++++++++++++++++++++---- snapraid.conf.example | 21 ++++ 18 files changed, 1094 insertions(+), 39 deletions(-) diff --git a/HISTORY b/HISTORY index a6c33cba..cd480d76 100644 --- a/HISTORY +++ b/HISTORY @@ -1,6 +1,24 @@ SnapRAID HISTORY ================ +15.0 WIP +======== + * Added support for automated filesystem snapshots to ensure data + consistency and improve recovery reliability. When the 'snapshot' + option is enabled, SnapRAID manages a 'pending' snapshot at the start + of a sync and promotes it to a read-only 'stable' state upon + successful completion (currently supported on Btrfs). + This ensures that 'scrub', 'check', and 'fix' operations can use a + frozen point-in-time reference for data disks, eliminating mismatches + caused by concurrent live filesystem changes. + The implementation includes a "time-capsule" feature that reflinks + the .content file into the snapshot area, preserving the + metadata-to-parity correlation even if files are later deleted from + the live disk. During targeted 'check' or 'fix' operations, SnapRAID + intelligently mixes live access for the disks being repaired with + snapshot access for reference disks to maintain mathematical + integrity across the array. + 14.10 2026/08 ============= * Enable lock file support on Windows to prevent concurrent runs. diff --git a/cmdline/elem.c b/cmdline/elem.c index 517df38b..cb600076 100644 --- a/cmdline/elem.c +++ b/cmdline/elem.c @@ -753,11 +753,15 @@ struct snapraid_disk* disk_alloc(const char* name, const char* dir, uint64_t dev disk = malloc_nofail(sizeof(struct snapraid_disk)); pathcpy(disk->name, sizeof(disk->name), name); - pathimport(disk->dir, sizeof(disk->dir), dir); + pathimport(disk->mount_point, sizeof(disk->mount_point), dir); pathcpy(disk->uuid, sizeof(disk->uuid), uuid); /* ensure that the dir terminate with "/" if it isn't empty */ - pathslash(disk->dir, sizeof(disk->dir)); + pathslash(disk->mount_point, sizeof(disk->mount_point)); + + /* no snapshot by default */ + disk->snapshot_root[0] = 0; + pathcpy(disk->dir, sizeof(disk->dir), disk->mount_point); #if HAVE_THREAD disk->single_thread = 0; @@ -1578,7 +1582,7 @@ const char* fmt_term(const struct snapraid_disk* disk, const char* str) out[2] = str; return esc_shell_multi(out, 3, esc_buf()); case FMT_PATH : - out[0] = disk->dir; + out[0] = disk->mount_point; out[1] = str; return esc_shell_multi(out, 2, esc_buf()); } diff --git a/cmdline/elem.h b/cmdline/elem.h index 6c8e616f..97404f7a 100644 --- a/cmdline/elem.h +++ b/cmdline/elem.h @@ -358,7 +358,9 @@ struct snapraid_extra { */ struct snapraid_disk { char name[PATH_MAX]; /**< Name of the disk. */ - char dir[PATH_MAX]; /**< Mount point of the disk. It always terminates with /. */ + char mount_point[PATH_MAX]; /**< Configured mount point of the disk. It always terminates with /. */ + char dir[PATH_MAX]; /**< Effective mount point of the disk. It could be either the real mount point or a snapshot. It always terminates with /. */ + char snapshot_root[PATH_MAX]; /**< Subvolume root directory. It always terminates with /. Empty if not supported. */ char smartctl[PATH_MAX]; /**< Custom command for smartctl. Empty means auto. */ int smartignore[SMART_IGNORE_MAX]; /**< Smart attributes to ignore for this device. */ char uuid[UUID_MAX]; /**< UUID of the disk. They are probed during the config reading. */ diff --git a/cmdline/mingw.c b/cmdline/mingw.c index ff00cea4..df1aad43 100644 --- a/cmdline/mingw.c +++ b/cmdline/mingw.c @@ -2038,6 +2038,52 @@ int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_har return 0; } +int fssnapshot(const char* path, char* root, size_t root_size) +{ + (void)path; + (void)root; + (void)root_size; + return -1; +} + +int fssnapshot_create(const char* source, const char* parent_dir, const char* name) +{ + (void)source; + (void)parent_dir; + (void)name; + return -1; +} + +int fssnapshot_readonly(const char* parent_dir, const char* name) +{ + (void)parent_dir; + (void)name; + return -1; +} + +int fssnapshot_delete(const char* parent_dir, const char* name) +{ + (void)parent_dir; + (void)name; + return -1; +} + +int fssnapshot_rename(const char* parent_dir, const char* old_name, const char* new_name) +{ + (void)parent_dir; + (void)old_name; + (void)new_name; + return -1; +} + +int fssnapshot_clone(const char* source, const char* dest) +{ + (void)source; + (void)dest; + return -1; +} + + /* ensure to call the real C strerror() */ #undef strerror diff --git a/cmdline/portable.h b/cmdline/portable.h index 6967a1fb..8fef0a61 100644 --- a/cmdline/portable.h +++ b/cmdline/portable.h @@ -111,6 +111,10 @@ #include #endif +#if HAVE_LINUX_BTRFS_H +#include +#endif + #if HAVE_LINUX_FIEMAP_H #include #endif @@ -358,6 +362,51 @@ int filephy(const char* path, uint64_t size, uint64_t* physical); */ int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_hardlinks, uint64_t* total_space, uint64_t* free_space, char* fstype, size_t fstype_size, char* fslabel, size_t fslabel_size); +/** + * Get the subvolume directory if the underline filesystem support it + * @param path Full path of the mount point to check. It must end with / + * @param root The full path of the subvolume root that contains the specified path. It will end with /. + */ +int fssnapshot(const char* path, char* root, size_t root_size); + +/** + * Creates a snapshot of a subvolume. + * @param source Path to the source subvolume. + * @param parent_dir Directory where the snapshot should be created. + * @param name Name of snapshot. + * @return 0 on success, -1 on failure. + */ +int fssnapshot_create(const char* source, const char* parent_dir, const char* name); + + +/** + * Make a snapshot read-only + * @param parent_dir Directory where the snapshot is. + * @param name Name of snapshot. + * @return 0 on success, -1 on failure. + */ +int fssnapshot_readonly(const char* parent_dir, const char* name); + +/** + * Deletes a subvolume/snapshot. + * @param parent_dir Directory where the snapshot is. + * @param name Name of snapshot. + * @return 0 on success, -1 on failure. + */ +int fssnapshot_delete(const char* parent_dir, const char* name); + +/** + * Renames a subvolume. + * @return 0 on success, -1 on failure. + */ +int fssnapshot_rename(const char* parent_dir, const char* old_name, const char* new_name); + +/** + * Clone a file between snapshots + * @return 0 on success, -1 on failure. + */ +int fssnapshot_clone(const char* source, const char* dest); + /** * Log file. * diff --git a/cmdline/scan.c b/cmdline/scan.c index 9512baed..b39ca293 100644 --- a/cmdline/scan.c +++ b/cmdline/scan.c @@ -1013,7 +1013,7 @@ static void scan_file(struct snapraid_scan* scan, int is_diff, const char* sub, if (is_original_file_size_different_than_zero && st->st_size == 0) { if (!state->opt.force_zero) { /* LCOV_EXCL_START */ - log_error(ESOFT, "The file '%s%s' has unexpected zero size!\n", disk->dir, sub); + log_error(ESOFT, "The file '%s%s' has unexpected zero size!\n", disk->mount_point, sub); log_error(ESOFT, "It's possible that after a kernel crash this file was lost,\n"); log_error(ESOFT, "and you can use 'snapraid fix -f /%s' to recover it.\n", fmt_poll(disk, sub)); if (!is_diff) { @@ -1822,7 +1822,7 @@ static int state_diffscan(struct snapraid_state* state, int is_diff) /* if verbose, print the list of duplicates real offsets */ /* other cases are for offsets not supported, so we don't need to report them file by file */ if (phy_last >= FILEPHY_REAL_OFFSET) { - log_info(ESOFT, "WARNING! Files '%s%s' and '%s%s' share the same physical offset %" PRId64 ".\n", disk->dir, phy_file_last->sub, disk->dir, file->sub, phy_last); + log_info(ESOFT, "WARNING! Files '%s%s' and '%s%s' share the same physical offset %" PRId64 ".\n", disk->mount_point, phy_file_last->sub, disk->mount_point, file->sub, phy_last); } ++phy_dup; } @@ -1892,9 +1892,9 @@ static int state_diffscan(struct snapraid_state* state, int is_diff) ) { if (!done) { done = 1; - log_error(ESOFT, "WARNING! All the files previously present in disk '%s' at dir '%s'", disk->name, disk->dir); + log_error(ESOFT, "WARNING! All the files previously present in disk '%s' at dir '%s'", disk->name, disk->mount_point); } else { - log_error(ESOFT, ", disk '%s' at dir '%s'", disk->name, disk->dir); + log_error(ESOFT, ", disk '%s' at dir '%s'", disk->name, disk->mount_point); } /* detect the special condition of all files missing */ diff --git a/cmdline/snapraid.c b/cmdline/snapraid.c index 09798d81..eace6fba 100644 --- a/cmdline/snapraid.c +++ b/cmdline/snapraid.c @@ -1840,6 +1840,11 @@ int snapraid_main(int argc, char* argv[]) state_write(&state); } + ret = state_snapshot_new(&state); + if (ret != 0) { + exit(EXIT_FAILURE); + } + state_scan(&state); if (opt.gui_threshold_removes != 0 && state.removed_files >= opt.gui_threshold_removes) { @@ -1889,6 +1894,10 @@ int snapraid_main(int argc, char* argv[]) sleep(2); ret = state_sync(&state, blockstart, blockcount); + + /* commit the snapshot as stable */ + if (ret == 0) + state_snapshot_commit(&state); } else if (operation == OPERATION_DRY) { state_read(&state); @@ -1934,6 +1943,8 @@ int snapraid_main(int argc, char* argv[]) /* intercept signals while operating */ signal_init(); + state_snapshot_read(&state); + ret = state_scrub(&state, plan100, olderthan); } else if (operation == OPERATION_REWRITE) { state_read(&state); @@ -2050,6 +2061,8 @@ int snapraid_main(int argc, char* argv[]) /* intercept signals while operating */ signal_init(); + state_snapshot_write(&state, &filterlist_disk); + if (operation == OPERATION_CHECK) { ret = state_check(&state, 0, blockstart, blockcount); } else { /* it's fix */ diff --git a/cmdline/state.c b/cmdline/state.c index 5e7a3aee..947dff85 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -125,6 +125,7 @@ void state_init(struct snapraid_state* state) memset(&state->opt, 0, sizeof(state->opt)); state->mapped_device = 0; + state->snapshot = 0; state->filter_hidden = 0; state->autosave = 0; state->need_write = 0; @@ -294,7 +295,7 @@ static void state_config_check(struct snapraid_state* state, const char* path, t log_fatal(EUSER, "DANGER! Ignoring that disks '%s' and '%s' are on the same device\n", disk->name, other->name); } else { /* LCOV_EXCL_START */ - log_fatal(ESOFT, "Disks '%s' and '%s' are on the same device.\n", disk->dir, other->dir); + log_fatal(ESOFT, "Disks '%s' and '%s' are on the same device.\n", disk->mount_point, other->mount_point); #ifdef _WIN32 log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->device); log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n"); @@ -321,10 +322,10 @@ static void state_config_check(struct snapraid_state* state, const char* path, t /* and we DON'T mark the disk to be skipped */ /* because we want to use these disks */ if (!state->opt.no_warnings) - log_fatal(EUSER, "DANGER! Ignoring that disks '%s' and %s '%s' are on the same device\n", disk->dir, lev_name(l), state->parity[l].split_map[s].path); + log_fatal(EUSER, "DANGER! Ignoring that disks '%s' and %s '%s' are on the same device\n", disk->mount_point, lev_name(l), state->parity[l].split_map[s].path); } else { /* LCOV_EXCL_START */ - log_fatal(ESOFT, "Disk '%s' and %s '%s' are on the same device.\n", disk->dir, lev_name(l), state->parity[l].split_map[s].path); + log_fatal(ESOFT, "Disk '%s' and %s '%s' are on the same device.\n", disk->mount_point, lev_name(l), state->parity[l].split_map[s].path); #ifdef _WIN32 log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->device); log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n"); @@ -340,7 +341,7 @@ static void state_config_check(struct snapraid_state* state, const char* path, t if (state->pool[0] != 0 && disk->device == state->pool_device) { /* LCOV_EXCL_START */ - log_fatal(ESOFT, "Disk '%s' and pool '%s' are on the same device.\n", disk->dir, state->pool); + log_fatal(ESOFT, "Disk '%s' and pool '%s' are on the same device.\n", disk->mount_point, state->pool); #ifdef _WIN32 log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->device); log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n"); @@ -1323,6 +1324,8 @@ void state_config(struct snapraid_state* state, const char* path, const char* co state->thermal_cooldown_time = time * 60; } else if (strcmp(tag, "nohidden") == 0) { state->filter_hidden = 1; + } else if (strcmp(tag, "snapshot") == 0) { + state->snapshot = 1; } else if (strcmp(tag, "exclude") == 0) { struct snapraid_filter* filter; @@ -1477,7 +1480,7 @@ void state_config(struct snapraid_state* state, const char* path, const char* co log_tag("blocksize:%u\n", state->block_size); for (i = state->disklist; i != 0; i = i->next) { struct snapraid_disk* disk = i->data; - log_tag("data:%s:%s:%s\n", esc_tag(disk->name), esc_tag(disk->dir), disk->uuid); + log_tag("data:%s:%s:%s\n", esc_tag(disk->name), esc_tag(disk->mount_point), disk->uuid); } for (i = state->extralist; i != 0; i = i->next) { struct snapraid_extra* extra = i->data; @@ -1803,10 +1806,10 @@ void state_refresh(struct snapraid_state* state) if (disk->skip_access) continue; - ret = fsinfo(disk->dir, 0, 0, &total_space, &free_space, disk->fstype, sizeof(disk->fstype), disk->fslabel, sizeof(disk->fslabel)); + ret = fsinfo(disk->mount_point, 0, 0, &total_space, &free_space, disk->fstype, sizeof(disk->fstype), disk->fslabel, sizeof(disk->fslabel)); if (ret != 0) { /* LCOV_EXCL_START */ - log_fatal(errno, "Error accessing disk '%s' to retrieve file-system info. %s.\n", disk->dir, strerror(errno)); + log_fatal(errno, "Error accessing disk '%s' to retrieve file-system info. %s.\n", disk->mount_point, strerror(errno)); exit(EXIT_FAILURE); /* LCOV_EXCL_STOP */ } @@ -5615,3 +5618,227 @@ void state_load_ignore_file(tommy_list* filter_list, const char* path, const cha sclose(f); } +#define SNAPSHOT_CONTAINER ".snapraid" +#define SNAPSHOT_PENDING "pending" +#define SNAPSHOT_STABLE "stable" + +int state_snapshot_new(struct snapraid_state* state) +{ + if (!state->snapshot) + return 0; + + for (tommy_node* i = state->disklist; i != 0; i = i->next) { + struct snapraid_disk* disk = i->data; + char root[PATH_MAX]; + char container[PATH_MAX]; + int ret; + + /* check if it supports snapshot */ + if (fssnapshot(disk->mount_point, root, sizeof(root)) != 0) + continue; + + size_t root_len = strlen(root); + + pathcpy(container, sizeof(container), root); + pathcat(container, sizeof(container), SNAPSHOT_CONTAINER); + + /* create the snapshot container directory if not existing */ + ret = mkdir(container, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); + if (ret != 0 && errno != EEXIST) { + log_error(errno, "Error creating '%s'. %s.\n", container, strerror(errno)); + return -1; + } + + /* delete a potential previous pending snapshot */ + if (fssnapshot_delete(container, SNAPSHOT_PENDING) != 0) { + log_fatal(errno, "Failed to delete pending snapshot in '%s' \n", container); + return -1; + } + + /* create a new snapshot */ + if (fssnapshot_create(root, container, SNAPSHOT_PENDING) != 0) { + log_fatal(errno, "Failed to create pending snapshot '%s'\n", container); + return -1; + } + + msg_progress("Created disk %s pending snapshot...\n", disk->name); + + /* setup the snapshot in use */ + pathcpy(disk->dir, sizeof(disk->dir), container); + pathcat(disk->dir, sizeof(disk->dir), "/" SNAPSHOT_PENDING "/"); + pathcat(disk->dir, sizeof(disk->dir), disk->mount_point + root_len); + + /* store the snapshot root */ + pathcpy(disk->snapshot_root, sizeof(disk->snapshot_root), root); + } + + return 0; +} + +int state_snapshot_commit(struct snapraid_state* state) +{ + if (!state->snapshot) + return 0; + + for (tommy_node* i = state->disklist; i != 0; i = i->next) { + struct snapraid_disk* disk = i->data; + char container[PATH_MAX]; + + /* check if it supports snapshot */ + if (disk->snapshot_root[0] == 0) + continue; + + size_t root_len = strlen(disk->snapshot_root); + + pathcpy(container, sizeof(container), disk->snapshot_root); + pathcat(container, sizeof(container), SNAPSHOT_CONTAINER); + + /* for each content file */ + for (tommy_node* j = state->contentlist; j != 0; j = j->next) { + struct snapraid_content* content = j->data; + + /* check if the content file is in the subvolume */ + if (strncmp(content->content, disk->snapshot_root, root_len) == 0) { + /* clone the just written content file into the snapshot */ + char clone[PATH_MAX]; + + pathcpy(clone, sizeof(clone), container); + pathcat(clone, sizeof(clone), "/" SNAPSHOT_PENDING "/"); + pathcat(clone, sizeof(clone), content->content + root_len); + + if (fssnapshot_clone(content->content, clone) != 0) { + log_fatal(errno, "Failed to clone the content file '%s'\n", content->content); + return -1; + } + } + } + + if (fssnapshot_readonly(container, SNAPSHOT_PENDING) != 0) { + log_fatal(errno, "Failed to make pending snapshot read-only in '%s'\n", container); + return -1; + } + + /* delete a potential previous stable snapshot */ + if (fssnapshot_delete(container, SNAPSHOT_STABLE) != 0) { + log_fatal(errno, "Failed to delete stable snapshot in '%s'\n", container); + return -1; + } + + /* rename pending to stable */ + if (fssnapshot_rename(container, SNAPSHOT_PENDING, SNAPSHOT_STABLE) != 0) { + log_fatal(errno, "Failed to rename snapshot in '%s'\n", container); + return -1; + } + + msg_progress("Committed disk %s stable snapshot...\n", disk->name); + } + + return 0; +} + +static int is_dir(const char* path) +{ + struct stat st; + + if (lstat(path, &st) != 0) + return 0; + + return S_ISDIR(st.st_mode); +} + +void state_snapshot_read(struct snapraid_state* state) +{ + if (!state->snapshot) + return; + + for (tommy_node* i = state->disklist; i != 0; i = i->next) { + struct snapraid_disk* disk = i->data; + char root[PATH_MAX]; + char container[PATH_MAX]; + char vol[PATH_MAX]; + + /* check if it supports snapshot */ + if (fssnapshot(disk->mount_point, root, sizeof(root)) != 0) + continue; + + size_t root_len = strlen(root); + + pathcpy(container, sizeof(container), root); + pathcat(container, sizeof(container), SNAPSHOT_CONTAINER); + + pathcpy(vol, sizeof(vol), container); + pathcat(vol, sizeof(vol), "/" SNAPSHOT_PENDING); + if (is_dir(vol)) { + msg_progress("Using disk %s pending snapshot...\n", disk->name); + pathcpy(disk->dir, sizeof(disk->dir), vol); + pathcat(disk->dir, sizeof(disk->dir), "/"); + pathcat(disk->dir, sizeof(disk->dir), disk->mount_point + root_len); + } else { + pathcpy(vol, sizeof(vol), container); + pathcat(vol, sizeof(vol), "/" SNAPSHOT_STABLE); + if (is_dir(vol)) { + msg_progress("Using disk %s stable snapshot...\n", disk->name); + pathcpy(disk->dir, sizeof(disk->dir), vol); + pathcat(disk->dir, sizeof(disk->dir), "/"); + pathcat(disk->dir, sizeof(disk->dir), disk->mount_point + root_len); + } else { + msg_progress("Using disk %s live filesystem...\n", disk->name); + } + } + } +} + +void state_snapshot_write(struct snapraid_state* state, tommy_list* filterlist_disk) +{ + if (!state->snapshot) + return; + + /* if the filter disk is empty, all disks are potentially written */ + if (tommy_list_empty(filterlist_disk)) { + msg_progress("Using live filesystems...\n"); + return; + } + + for (tommy_node* i = state->disklist; i != 0; i = i->next) { + struct snapraid_disk* disk = i->data; + char root[PATH_MAX]; + char container[PATH_MAX]; + char vol[PATH_MAX]; + + /* check if it supports snapshot */ + if (fssnapshot(disk->mount_point, root, sizeof(root)) != 0) + continue; + + size_t root_len = strlen(root); + + /* if the disk is filtered in, it could be written, then we cannot use the snapshot */ + if (filter_path(filterlist_disk, 0, disk->name, 0) == 0) { + msg_progress("Using disk %s live filesystem...\n", disk->name); + continue; + } + + pathcpy(container, sizeof(container), root); + pathcat(container, sizeof(container), SNAPSHOT_CONTAINER); + + pathcpy(vol, sizeof(vol), container); + pathcat(vol, sizeof(vol), "/" SNAPSHOT_PENDING); + if (is_dir(vol)) { + msg_progress("Using disk %s pending snapshot...\n", disk->name); + pathcpy(disk->dir, sizeof(disk->dir), vol); + pathcat(disk->dir, sizeof(disk->dir), "/"); + pathcat(disk->dir, sizeof(disk->dir), disk->mount_point + root_len); + } else { + pathcpy(vol, sizeof(vol), container); + pathcat(vol, sizeof(vol), "/" SNAPSHOT_STABLE); + if (is_dir(vol)) { + msg_progress("Using disk %s stable snapshot...\n", disk->name); + pathcpy(disk->dir, sizeof(disk->dir), vol); + pathcat(disk->dir, sizeof(disk->dir), "/"); + pathcat(disk->dir, sizeof(disk->dir), disk->mount_point + root_len); + } else { + msg_progress("Using disk %s live filesystem...\n", disk->name); + } + } + } +} + diff --git a/cmdline/state.h b/cmdline/state.h index 67679cee..84e8ca93 100644 --- a/cmdline/state.h +++ b/cmdline/state.h @@ -112,6 +112,7 @@ struct snapraid_option { struct snapraid_state { struct snapraid_option opt; /**< Setup options. */ int mapped_device; /**< Devices were already mapped */ + int snapshot; /**< Enable snapshot support */ int filter_hidden; /**< Filter out hidden files. */ uint64_t autosave; /**< Autosave after the specified amount of data. 0 to disable. */ int need_write; /**< If the state is changed. */ @@ -420,6 +421,26 @@ void state_load_ignore_file(tommy_list* filter_list, const char* path, const cha */ void state_attr(struct snapraid_state* state, tommy_list* low); +/** + * Create (or reuse existing) pending snapshots + */ +int state_snapshot_new(struct snapraid_state* state); + +/** + * Make the pending snapshots as stable + */ +int state_snapshot_commit(struct snapraid_state* state); + +/** + * Use the snapshots for reading only commands + */ +void state_snapshot_read(struct snapraid_state* state); + +/** + * Use the snapshots for the reading part (filtered out disks) of writing commands + */ +void state_snapshot_write(struct snapraid_state* state, tommy_list* filterlist_disk); + /****************************************************************************/ /* misc */ diff --git a/cmdline/support.c b/cmdline/support.c index 0cb0523e..f877afd9 100644 --- a/cmdline/support.c +++ b/cmdline/support.c @@ -1194,6 +1194,25 @@ void pathcut(char* dst) dst[0] = 0; } +void pathup(char* dst) +{ + size_t len = strlen(dst); + + if (len == 0) + return; + + /* if ending with slash, remove it */ + if (dst[len - 1] == '/') + --len; + + /* search previous / */ + while (len > 0 && dst[len - 1] != '/') + --len; + + /* cut everything after the slash (if there are no more slash, return the empty string) */ + dst[len] = 0; +} + int pathcmp(const char* a, const char* b) { #ifdef _WIN32 diff --git a/cmdline/support.h b/cmdline/support.h index 2ff8d17b..b662681c 100644 --- a/cmdline/support.h +++ b/cmdline/support.h @@ -369,6 +369,13 @@ void pathslash(char* dst, size_t size); */ void pathcut(char* dst); +/** + * Given a string ending with slash, remove that ending slash and any char up to the previous slash. + * + * If there are not more slash, it returns the empty string. + */ +void pathup(char* dst); + /** * Compare two paths. * In Windows it's case insensitive and assumes \ equal at /. diff --git a/cmdline/thermal.c b/cmdline/thermal.c index f30f90a7..c86e5d3e 100644 --- a/cmdline/thermal.c +++ b/cmdline/thermal.c @@ -131,7 +131,7 @@ void state_thermal(struct snapraid_state* state, time_t now) entry->device = disk->device; device_name_set(entry, disk->name, 0); - pathcpy(entry->mount, sizeof(entry->mount), disk->dir); + pathcpy(entry->mount, sizeof(entry->mount), disk->mount_point); pathcpy(entry->smartctl, sizeof(entry->smartctl), disk->smartctl); memcpy(entry->smartignore, disk->smartignore, sizeof(entry->smartignore)); diff --git a/cmdline/unix.c b/cmdline/unix.c index 4af118c2..c3eba084 100644 --- a/cmdline/unix.c +++ b/cmdline/unix.c @@ -1375,6 +1375,224 @@ int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_har return 0; } +int fssnapshot(const char* path, char* root, size_t root_size) +{ +#if HAVE_LINUX_DEVICE + struct statfs sfs; + struct stat st; + char current_path[PATH_MAX]; + + pathcpy(current_path, sizeof(current_path), path); + + if (statfs(current_path, &sfs) != 0) { + log_error(errno, "Error stating filesystem '%s'. %s.\n", current_path, strerror(errno)); + return -1; + } + + /* only btrfs */ + if (sfs.f_type != BTRFS_SUPER_MAGIC) + return -1; + + /* walk up the directory tree to find the subvolume root (Inode 256) */ + while (1) { + if (stat(current_path, &st) != 0) { + log_error(errno, "Error stating '%s'. %s.\n", current_path, strerror(errno)); + return -1; + } + + /* btrfs reserved inode 256 for subvolume roots */ + if (st.st_ino == 256) { + /* copy the subvol root */ + pathcpy(root, root_size, current_path); + return 0; + } + + /* move to parent directory */ + pathup(current_path); + + if (current_path[0] == 0) { + log_error(ENOENT, "No subvolume root found for '%s'. %s.\n", path, strerror(errno)); + return -1; + } + } +#else + (void)path; + (void)root; + (void)root_size; + return -1; +#endif +} + +int fssnapshot_create(const char* source, const char* parent_dir, const char* name) +{ +#if HAVE_LINUX_DEVICE + struct btrfs_ioctl_vol_args_v2 args; + int fd_source; + int fd_dest_parent; + int ret; + + /* open the source subvolume to get a file descriptor */ + fd_source = open(source, O_RDONLY | O_DIRECTORY); + if (fd_source < 0) { + return -1; + } + + fd_dest_parent = open(parent_dir, O_RDONLY | O_DIRECTORY); + if (fd_dest_parent < 0) { + close(fd_source); + return -1; + } + + memset(&args, 0, sizeof(args)); + args.fd = fd_source; + args.flags = 0; + pathcpy(args.name, BTRFS_SUBVOL_NAME_MAX, name); + + /* issue the snapshot command to the PARENT directory of the destination */ + ret = ioctl(fd_dest_parent, BTRFS_IOC_SNAP_CREATE_V2, &args); + + if (ret < 0) { + close(fd_source); + close(fd_dest_parent); + return -1; + } + + close(fd_source); + close(fd_dest_parent); + + return 0; +#else + (void)source; + (void)parent_dir; + (void)name; + return -1; +#endif +} + +int fssnapshot_readonly(const char* parent_dir, const char* name) +{ +#if HAVE_LINUX_DEVICE + char path[PATH_MAX]; + + pathcpy(path, sizeof(path), parent_dir); + pathcatc(path, sizeof(path), '/'); + pathcat(path, sizeof(path), name); + + int fd = open(path, O_RDONLY | O_DIRECTORY); + if (fd < 0) + return -1; + + /* get existing flags to avoid overwriting other potential flags */ + __u64 flags; + if (ioctl(fd, BTRFS_IOC_SUBVOL_GETFLAGS, &flags) < 0) { + close(fd); + return -1; + } + + flags |= BTRFS_SUBVOL_RDONLY; + + if (ioctl(fd, BTRFS_IOC_SUBVOL_SETFLAGS, &flags) < 0) { + close(fd); + return -1; + } + + close(fd); + return 0; +#else + (void)parent_dir; + (void)name; + return -1; +#endif +} + +int fssnapshot_delete(const char* parent_dir, const char* name) +{ +#if HAVE_LINUX_DEVICE + struct btrfs_ioctl_vol_args args; + int fd_parent; + int ret; + + fd_parent = open(parent_dir, O_RDONLY | O_DIRECTORY); + if (fd_parent < 0) { + return -1; + } + + memset(&args, 0, sizeof(args)); + pathcpy(args.name, BTRFS_PATH_NAME_MAX, name); + + ret = ioctl(fd_parent, BTRFS_IOC_SNAP_DESTROY, &args); + + if (ret < 0 && errno != ENOENT) { + close(fd_parent); + return -1; + } + + close(fd_parent); + return 0; +#else + (void)parent_dir; + (void)name; + return -1; +#endif +} + +int fssnapshot_rename(const char* parent_dir, const char* old_name, const char* new_name) +{ +#if HAVE_LINUX_DEVICE + char old_path[PATH_MAX]; + char new_path[PATH_MAX]; + + pathcpy(old_path, sizeof(old_path), parent_dir); + pathcatc(old_path, sizeof(old_path), '/'); + pathcat(old_path, sizeof(old_path), old_name); + pathcpy(new_path, sizeof(new_path), parent_dir); + pathcatc(new_path, sizeof(new_path), '/'); + pathcat(new_path, sizeof(new_path), new_name); + + if (rename(old_path, new_path) < 0) + return -1; + + return 0; +#else + (void)parent_dir; + (void)old_name; + (void)new_name; + return -1; +#endif +} + +int fssnapshot_clone(const char* source, const char* dest) +{ +#if HAVE_LINUX_DEVICE + int fs, fd; + + fs = open(source, O_RDONLY); + if (fs < 0) + return -1; + + fd = open(dest, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + close(fs); + return 1; + } + + /* perform the reflink copy */ + if (ioctl(fd, FICLONE, fs) < 0) { + close(fd); + close(fs); + return -1; + } + + close(fs); + close(fd); + return 0; +#else + (void)source; + (void)dest; + return -1; +#endif +} + uint64_t os_tick(void) { #if HAVE_MACH_ABSOLUTE_TIME diff --git a/configure.ac b/configure.ac index b2937f3a..220f137f 100644 --- a/configure.ac +++ b/configure.ac @@ -50,7 +50,7 @@ AC_CHECK_HEADERS([fcntl.h stddef.h stdint.h stdlib.h string.h limits.h time.h sy AC_CHECK_HEADERS([unistd.h getopt.h io.h inttypes.h byteswap.h]) AC_CHECK_HEADERS([pthread.h math.h]) AC_CHECK_HEADERS([sys/file.h sys/ioctl.h sys/sysmacros.h sys/mkdev.h sys/mount.h]) -AC_CHECK_HEADERS([linux/fiemap.h linux/fs.h mach/mach_time.h execinfo.h]) +AC_CHECK_HEADERS([linux/fiemap.h linux/fs.h linux/btrfs.h mach/mach_time.h execinfo.h]) dnl Checks for typedefs, structures, and compiler characteristics. AC_C_CONST diff --git a/doc/snapraid.1 b/doc/snapraid.1 index 5db638d0..b6fa214b 100644 --- a/doc/snapraid.1 +++ b/doc/snapraid.1 @@ -1426,6 +1426,21 @@ the boot disk. .PP Note that such disks are not affected by the \`up\` and \`down\` commands because they are expected to be always spinning. +.SS snapshot +Enable the use of filesystem snapshots for the \`sync\`, \`scrub\`, +\`check\`, and \`fix\` commands, provided the underlying filesystem +supports it. +.PP +This option applies exclusively to data disks formatted with the Btrfs +filesystem. Parity disks, or data disks using other filesystems, will +always use the live version of the filesystem. +.PP +When enabled, SnapRAID ensures atomic and consistent operations by +reading from a frozen point\-in\-time image. This prevents errors caused +by file modifications during execution and ensures that deleted files +remain available for parity\-based recovery of other disks. +.PP +See the SNAPSHOTS section for more details. .SS nohidden Excludes all hidden files and directories. In Unix, hidden files are those starting with \`.\`. @@ -1759,6 +1774,130 @@ smartctl 2\-parity \-d areca,2/1 /dev/arcmsr0 .PD 0 .PD .RE +.SH SNAPSHOTS +If the snapshot option is enabled in the configuration, SnapRAID +utilizes filesystem snapshot functionality to ensure atomic and +consistent operations. +.PP +The management of snapshots is completely automatic and transparent. +You can continue to use SnapRAID exactly as before, with the +additional protection provided by snapshots handled entirely in +the background. +.PP +This provides two primary benefits: +.RS 0 +.PD 0 +.HP 4 +.I Consistency +Files modified on the live filesystem during a long\-running +sync or scrub will not cause parity mismatches or aborted +operations, as SnapRAID sees a frozen point\-in\-time image. +.HP 4 +.I Recovery +If a file is deleted from the live filesystem, it remains +preserved in the snapshot. If a disk failure occurs before the +next sync is run, SnapRAID uses the data preserved in the +snapshot to reconstruct the failed disk. Without snapshots, +a deleted file on a healthy disk results in missing data +blocks that may be required to fix other failed disks. +.PD +.RE +.PP +Snapshots are created only for data disks and only if the underlying +filesystem supports this functionality. Parity disks always use +the live filesystem. At present, this is supported exclusively on Btrfs. +.PP +You can mix data disks with different filesystems. Only those that +support snapshots will utilize them, while other disks will continue +to operate directly on the live filesystem. +.SS Command Behavior with Snapshots +The \`sync\` and \`scrub\` commands both operate exclusively on snapshots +to ensure that all data operations are performed against a consistent, +frozen state. By utilizing these snapshots, SnapRAID prevents false +positives and parity mismatches that would otherwise be triggered +by concurrent file modifications on the live filesystem. +During a \`sync\`, the command uses a new snapshot created at the +start of the process to capture the current state for parity computation. +In contrast, the \`scrub\` command utilizes the last snapshot, the one +created during the most recent sync, to maintain a reliable reference +point that matches the existing parity. +.PP +For the \`check\` and \`fix\` commands, the use of the last snapshot +depends on whether specific disks are targeted using the +\-d, \-\-filter\-disk option. +Any disk explicitly selected for checking or fixing will use +the live filesystem. This allows \`check\` to verify the current state of +your files and \`fix\` to physically restore data to the active disk. +.PP +All other data disks not specified by the \-d, \-\-filter\-disk option +will be accessed via their snapshots. These disks act as stable +references, providing the necessary data blocks to solve the parity +equations without interference from live changes. If no \-d option is +provided, SnapRAID assumes the operation applies to the entire array, +in this case, \`check\` and \`fix\` will use the live filesystems exclusively. +.PP +All other commands operate exclusively on the live filesystem. +.SS Sync Safety +To ensure maximum data safety during a sync operation, it is +recommended to place the .content files inside the same subvolume +used for the data, ensuring they are also part of the snapshots. +.PP +When a sync starts, SnapRAID immediately updates the .content file to +reflect the current state of the filesystem. If you have deleted files +on a disk, the new .content file loses the metadata for those +deleted items and their relation to the parity. +.PP +If a disk fails while a \`sync\` is running, the parity for that +failed disk may still depend on the deleted files. +Even if your snapshot physically contains the deleted files, +the new .content file no longer knows how they relate to the +parity blocks. This missing correlation can prevent a full recovery. +.PP +Note that this specific risk only exists if a disk fails during +an active sync. If a disk fails between syncs, the snapshot +already contains data that is perfectly matched with the +last successfully saved content file on the live filesystem, +ensuring a successful recovery. +.PP +By storing the .content file inside the snapshotted subvolume, the +snapshot preserves an older version of the content file, and +the metadata within it, alongside the corresponding version of +the data. +.SS Snapshots Lifecycle +SnapRAID manages two specific snapshots within a hidden \`.snapraid\` +directory at the root of each data subvolume: +.RS 0 +.PD 0 +.HP 4 +.I stable +The reference snapshot representing the state of the last +successfully completed \`sync\`. This snapshot contains the exact +data used to compute the parity and serves as the primary data +source for all \`scrub\`, \`check\`, and \`fix\` commands, provided +that a pending snapshot is not present. +.HP 4 +.I pending +A temporary snapshot created at the start of a sync operation. +Upon the successful completion of a sync, the previous stable +snapshot is deleted, and the pending snapshot is promoted to +stable. +During this promotion, if a content file is stored on the +same subvolume of the data, the final version of the content +file is cloned (via reflink) into the snapshot. +This ensures that the snapshot contains a metadata map that +is perfectly in sync with the data blocks it preserves. +Once promoted, the stable snapshot is set to a read\-only +state to ensure its integrity as a recovery reference. +If the sync fails or is interrupted, the pending snapshot is +preserved in its current state. This allows subsequent \`scrub\`, +\`check\`, and \`fix\` commands to function correctly by using the +exact filesystem state recorded in the live content file, +even if the parity is only partially synchronized. +When sync is restarted after the interruption, the existing +pending snapshot is deleted and a new one is created, +capturing the new state of the live filesystem. +.PD +.RE .SH PATTERN Patterns are used to select a subset of files to exclude or include in the process. Globbing characters can be used to match files and paths diff --git a/doc/snapraid.d b/doc/snapraid.d index b6a6efda..dc72f640 100644 --- a/doc/snapraid.d +++ b/doc/snapraid.d @@ -1146,6 +1146,22 @@ Configuration Note that such disks are not affected by the `up` and `down` commands because they are expected to be always spinning. + snapshot + Enable the use of filesystem snapshots for the `sync`, `scrub`, + `check`, and `fix` commands, provided the underlying filesystem + supports it. + + This option applies exclusively to data disks formatted with the Btrfs + filesystem. Parity disks, or data disks using other filesystems, will + always use the live version of the filesystem. + + When enabled, SnapRAID ensures atomic and consistent operations by + reading from a frozen point-in-time image. This prevents errors caused + by file modifications during execution and ensures that deleted files + remain available for parity-based recovery of other disks. + + See the SNAPSHOTS section for more details. + nohidden Excludes all hidden files and directories. In Unix, hidden files are those starting with `.`. @@ -1383,6 +1399,120 @@ Configuration :smartctl parity -d areca,1/1 /dev/arcmsr0 :smartctl 2-parity -d areca,2/1 /dev/arcmsr0 +Snapshots + If the snapshot option is enabled in the configuration, SnapRAID + utilizes filesystem snapshot functionality to ensure atomic and + consistent operations. + + The management of snapshots is completely automatic and transparent. + You can continue to use SnapRAID exactly as before, with the + additional protection provided by snapshots handled entirely in + the background. + + This provides two primary benefits: + + Consistency - Files modified on the live filesystem during a long-running + sync or scrub will not cause parity mismatches or aborted + operations, as SnapRAID sees a frozen point-in-time image. + Recovery - If a file is deleted from the live filesystem, it remains + preserved in the snapshot. If a disk failure occurs before the + next sync is run, SnapRAID uses the data preserved in the + snapshot to reconstruct the failed disk. Without snapshots, + a deleted file on a healthy disk results in missing data + blocks that may be required to fix other failed disks. + + Snapshots are created only for data disks and only if the underlying + filesystem supports this functionality. Parity disks always use + the live filesystem. At present, this is supported exclusively on Btrfs. + + You can mix data disks with different filesystems. Only those that + support snapshots will utilize them, while other disks will continue + to operate directly on the live filesystem. + + Command Behavior with Snapshots + The `sync` and `scrub` commands both operate exclusively on snapshots + to ensure that all data operations are performed against a consistent, + frozen state. By utilizing these snapshots, SnapRAID prevents false + positives and parity mismatches that would otherwise be triggered + by concurrent file modifications on the live filesystem. + During a `sync`, the command uses a new snapshot created at the + start of the process to capture the current state for parity computation. + In contrast, the `scrub` command utilizes the last snapshot, the one + created during the most recent sync, to maintain a reliable reference + point that matches the existing parity. + + For the `check` and `fix` commands, the use of the last snapshot + depends on whether specific disks are targeted using the + -d, --filter-disk option. + Any disk explicitly selected for checking or fixing will use + the live filesystem. This allows `check` to verify the current state of + your files and `fix` to physically restore data to the active disk. + + All other data disks not specified by the -d, --filter-disk option + will be accessed via their snapshots. These disks act as stable + references, providing the necessary data blocks to solve the parity + equations without interference from live changes. If no -d option is + provided, SnapRAID assumes the operation applies to the entire array, + in this case, `check` and `fix` will use the live filesystems exclusively. + + All other commands operate exclusively on the live filesystem. + + Sync Safety + To ensure maximum data safety during a sync operation, it is + recommended to place the .content files inside the same subvolume + used for the data, ensuring they are also part of the snapshots. + + When a sync starts, SnapRAID immediately updates the .content file to + reflect the current state of the filesystem. If you have deleted files + on a disk, the new .content file loses the metadata for those + deleted items and their relation to the parity. + + If a disk fails while a `sync` is running, the parity for that + failed disk may still depend on the deleted files. + Even if your snapshot physically contains the deleted files, + the new .content file no longer knows how they relate to the + parity blocks. This missing correlation can prevent a full recovery. + + Note that this specific risk only exists if a disk fails during + an active sync. If a disk fails between syncs, the snapshot + already contains data that is perfectly matched with the + last successfully saved content file on the live filesystem, + ensuring a successful recovery. + + By storing the .content file inside the snapshotted subvolume, the + snapshot preserves an older version of the content file, and + the metadata within it, alongside the corresponding version of + the data. + + Snapshots Lifecycle + SnapRAID manages two specific snapshots within a hidden `.snapraid` + directory at the root of each data subvolume: + + stable - The reference snapshot representing the state of the last + successfully completed `sync`. This snapshot contains the exact + data used to compute the parity and serves as the primary data + source for all `scrub`, `check`, and `fix` commands, provided + that a pending snapshot is not present. + pending - A temporary snapshot created at the start of a sync operation. + Upon the successful completion of a sync, the previous stable + snapshot is deleted, and the pending snapshot is promoted to + stable. + During this promotion, if a content file is stored on the + same subvolume of the data, the final version of the content + file is cloned (via reflink) into the snapshot. + This ensures that the snapshot contains a metadata map that + is perfectly in sync with the data blocks it preserves. + Once promoted, the stable snapshot is set to a read-only + state to ensure its integrity as a recovery reference. + If the sync fails or is interrupted, the pending snapshot is + preserved in its current state. This allows subsequent `scrub`, + `check`, and `fix` commands to function correctly by using the + exact filesystem state recorded in the live content file, + even if the parity is only partially synchronized. + When sync is restarted after the interruption, the existing + pending snapshot is deleted and a new one is created, + capturing the new state of the live filesystem. + Pattern Patterns are used to select a subset of files to exclude or include in the process. Globbing characters can be used to match files and paths diff --git a/doc/snapraid.txt b/doc/snapraid.txt index 4e29c167..395ccfad 100644 --- a/doc/snapraid.txt +++ b/doc/snapraid.txt @@ -1222,14 +1222,32 @@ the boot disk. Note that such disks are not affected by the `up` and `down` commands because they are expected to be always spinning. -7.7 nohidden +7.7 snapshot +------------ + +Enable the use of filesystem snapshots for the `sync`, `scrub`, +`check`, and `fix` commands, provided the underlying filesystem +supports it. + +This option applies exclusively to data disks formatted with the Btrfs +filesystem. Parity disks, or data disks using other filesystems, will +always use the live version of the filesystem. + +When enabled, SnapRAID ensures atomic and consistent operations by +reading from a frozen point-in-time image. This prevents errors caused +by file modifications during execution and ensures that deleted files +remain available for parity-based recovery of other disks. + +See the SNAPSHOTS section for more details. + +7.8 nohidden ------------ Excludes all hidden files and directories. In Unix, hidden files are those starting with `.`. In Windows, they are those with the hidden attribute. -7.8 exclude/include PATTERN +7.9 exclude/include PATTERN --------------------------- Defines the file or directory patterns to exclude or include @@ -1247,8 +1265,8 @@ specifications. This option can be used multiple times. -7.9 blocksize SIZE_IN_KIBIBYTES -------------------------------- +7.10 blocksize SIZE_IN_KIBIBYTES +-------------------------------- Defines the basic block size in kibibytes for the parity. One kibibyte is 1024 bytes. @@ -1295,7 +1313,7 @@ This results in about 1.5% extra space, approximately 60 GB for a 4 TB disk, which allows about 460,000 files on each data disk without any wasted space. -7.10 hashsize SIZE_IN_BYTES +7.11 hashsize SIZE_IN_BYTES --------------------------- Defines the hash size in bytes for the saved blocks. @@ -1333,7 +1351,7 @@ Switching to both a hash size of 8 and a block size of 512, you get: RAM = (8 * 4 * 10^12) * (1+8) / (512 * 2^10) = 0.51 GiB -7.11 autosave SIZE_IN_GIGABYTES +7.12 autosave SIZE_IN_GIGABYTES ------------------------------- Automatically saves the state when syncing or scrubbing after the @@ -1341,7 +1359,7 @@ specified amount of GB processed. This option is useful to avoid restarting long `sync` commands from scratch if interrupted by a machine crash or any other event. -7.12 temp_limit TEMPERATURE_CELSIUS +7.13 temp_limit TEMPERATURE_CELSIUS ----------------------------------- Sets the maximum allowed disk temperature in Celsius. When specified, @@ -1372,14 +1390,14 @@ temperature measured at the start of the run as a fallback reference. Normally, SnapRAID shows only the temperature of the hottest disk. To display the temperature of all disks, use the -A or --stats option. -7.13 temp_sleep TIME_IN_MINUTES +7.14 temp_sleep TIME_IN_MINUTES ------------------------------- Sets the standby time, in minutes, when the temperature limit is reached. During this period, the disks remain spun down. The default is 5 minutes. -7.14 pool DIR +7.15 pool DIR ------------- Defines the pooling directory where the virtual view of the disk @@ -1387,7 +1405,7 @@ array is created using the `pool` command. The directory must already exist. -7.15 share UNC_DIR +7.16 share UNC_DIR ------------------ Defines the Windows UNC path required to access the disks remotely. @@ -1403,7 +1421,7 @@ file directory and name. This option is required only for Windows. -7.16 smartctl DISK/PARITY OPTIONS... +7.17 smartctl DISK/PARITY OPTIONS... ------------------------------------ Defines custom smartctl options to obtain the SMART attributes for @@ -1428,7 +1446,7 @@ For example: smartctl parity -d sat %s -7.17 smartignore DISK/PARITY ATTR [ATTR...] +7.18 smartignore DISK/PARITY ATTR [ATTR...] ------------------------------------------- Ignores the specified SMART attribute when computing the probability @@ -1449,7 +1467,7 @@ To ignore it only on the first parity disk: smartignore parity 197 -7.18 Examples +7.19 Examples ------------- An example of a typical configuration for Unix is: @@ -1484,7 +1502,130 @@ An example of a typical configuration for Windows is: smartctl 2-parity -d areca,2/1 /dev/arcmsr0 -8 PATTERN +8 SNAPSHOTS +=========== + +If the snapshot option is enabled in the configuration, SnapRAID +utilizes filesystem snapshot functionality to ensure atomic and +consistent operations. + +The management of snapshots is completely automatic and transparent. +You can continue to use SnapRAID exactly as before, with the +additional protection provided by snapshots handled entirely in +the background. + +This provides two primary benefits: + +Consistency - Files modified on the live filesystem during a long-running + sync or scrub will not cause parity mismatches or aborted + operations, as SnapRAID sees a frozen point-in-time image. +Recovery - If a file is deleted from the live filesystem, it remains + preserved in the snapshot. If a disk failure occurs before the + next sync is run, SnapRAID uses the data preserved in the + snapshot to reconstruct the failed disk. Without snapshots, + a deleted file on a healthy disk results in missing data + blocks that may be required to fix other failed disks. + +Snapshots are created only for data disks and only if the underlying +filesystem supports this functionality. Parity disks always use +the live filesystem. At present, this is supported exclusively on Btrfs. + +You can mix data disks with different filesystems. Only those that +support snapshots will utilize them, while other disks will continue +to operate directly on the live filesystem. + +8.1 Command Behavior with Snapshots +----------------------------------- + +The `sync` and `scrub` commands both operate exclusively on snapshots +to ensure that all data operations are performed against a consistent, +frozen state. By utilizing these snapshots, SnapRAID prevents false +positives and parity mismatches that would otherwise be triggered +by concurrent file modifications on the live filesystem. +During a `sync`, the command uses a new snapshot created at the +start of the process to capture the current state for parity computation. +In contrast, the `scrub` command utilizes the last snapshot, the one +created during the most recent sync, to maintain a reliable reference +point that matches the existing parity. + +For the `check` and `fix` commands, the use of the last snapshot +depends on whether specific disks are targeted using the +-d, --filter-disk option. +Any disk explicitly selected for checking or fixing will use +the live filesystem. This allows `check` to verify the current state of +your files and `fix` to physically restore data to the active disk. + +All other data disks not specified by the -d, --filter-disk option +will be accessed via their snapshots. These disks act as stable +references, providing the necessary data blocks to solve the parity +equations without interference from live changes. If no -d option is +provided, SnapRAID assumes the operation applies to the entire array, +in this case, `check` and `fix` will use the live filesystems exclusively. + +All other commands operate exclusively on the live filesystem. + +8.2 Sync Safety +--------------- + +To ensure maximum data safety during a sync operation, it is +recommended to place the .content files inside the same subvolume +used for the data, ensuring they are also part of the snapshots. + +When a sync starts, SnapRAID immediately updates the .content file to +reflect the current state of the filesystem. If you have deleted files +on a disk, the new .content file loses the metadata for those +deleted items and their relation to the parity. + +If a disk fails while a `sync` is running, the parity for that +failed disk may still depend on the deleted files. +Even if your snapshot physically contains the deleted files, +the new .content file no longer knows how they relate to the +parity blocks. This missing correlation can prevent a full recovery. + +Note that this specific risk only exists if a disk fails during +an active sync. If a disk fails between syncs, the snapshot +already contains data that is perfectly matched with the +last successfully saved content file on the live filesystem, +ensuring a successful recovery. + +By storing the .content file inside the snapshotted subvolume, the +snapshot preserves an older version of the content file, and +the metadata within it, alongside the corresponding version of +the data. + +8.3 Snapshots Lifecycle +----------------------- + +SnapRAID manages two specific snapshots within a hidden `.snapraid` +directory at the root of each data subvolume: + +stable - The reference snapshot representing the state of the last + successfully completed `sync`. This snapshot contains the exact + data used to compute the parity and serves as the primary data + source for all `scrub`, `check`, and `fix` commands, provided + that a pending snapshot is not present. +pending - A temporary snapshot created at the start of a sync operation. + Upon the successful completion of a sync, the previous stable + snapshot is deleted, and the pending snapshot is promoted to + stable. + During this promotion, if a content file is stored on the + same subvolume of the data, the final version of the content + file is cloned (via reflink) into the snapshot. + This ensures that the snapshot contains a metadata map that + is perfectly in sync with the data blocks it preserves. + Once promoted, the stable snapshot is set to a read-only + state to ensure its integrity as a recovery reference. + If the sync fails or is interrupted, the pending snapshot is + preserved in its current state. This allows subsequent `scrub`, + `check`, and `fix` commands to function correctly by using the + exact filesystem state recorded in the live content file, + even if the parity is only partially synchronized. + When sync is restarted after the interruption, the existing + pending snapshot is deleted and a new one is created, + capturing the new state of the live filesystem. + + +9 PATTERN ========= Patterns are used to select a subset of files to exclude or include in @@ -1602,8 +1743,8 @@ In Unix, when using globbing characters on the command line, you must quote them to prevent the shell from expanding them. -9 IGNORE FILE -============= +10 IGNORE FILE +============== In addition to the global rules in the configuration file, you can place `.snapraidignore` files in any directory within the array to @@ -1647,7 +1788,7 @@ For example, if you have a `.snapraidignore` in `/mnt/disk1/projects/`: *.tmp -10 CONTENT +11 CONTENT ========== SnapRAID stores the list and checksums of your files in the content file. @@ -1659,7 +1800,7 @@ This file is read and written by the `sync` and `scrub` commands and read by the `fix`, `check`, and `status` commands. -11 PARITY +12 PARITY ========= SnapRAID stores the parity information of your array in the parity @@ -1672,7 +1813,7 @@ These files are read and written by the `sync` and `fix` commands and only read by the `scrub` and `check` commands. -12 ENCODING +13 ENCODING =========== SnapRAID in Unix ignores any encoding. It reads and stores the @@ -1689,7 +1830,7 @@ redirect the console output to a file, the resulting file is always in UTF-8 format. -13 EXIT CODE +14 EXIT CODE ============ SnapRAID terminates with the following error codes: @@ -1700,13 +1841,13 @@ SnapRAID terminates with the following error codes: needed. -14 COPYRIGHT +15 COPYRIGHT ============ This file is Copyright (C) 2026 Andrea Mazzoleni -15 SEE ALSO +16 SEE ALSO =========== snapraid_log(1), snapraidd(1), rsync(1) diff --git a/snapraid.conf.example b/snapraid.conf.example index f6539449..2c8a8660 100644 --- a/snapraid.conf.example +++ b/snapraid.conf.example @@ -44,6 +44,26 @@ data d3 /mnt/disk3/ # Format: "other DISK_NAME DISK_MOUNT_POINT" extra boot / +# Enable the use of Btrfs snapshots for sync, scrub, check, and fix commands. +# +# When enabled, SnapRAID creates a read-only snapshot of your data at the +# start of a 'sync'. This ensures a consistent, point-in-time view of your +# files, preventing errors caused by concurrent file modifications. +# +# This significantly improves recovery: if a file is deleted from the live +# filesystem, it remains preserved in the snapshot. This prevents the parity +# from becoming "broken" for that block, ensuring you can still successfully +# recover data if another disk fails. +# +# Only Btrfs is supported. Snapshots are stored in a hidden '.snapraid' +# directory at the root of each data subvolume. +# +# Two snapshots are managed per disk: +# .snapraid/pending - The snapshot currently being processed. +# .snapraid/stable - The last successfully completed sync state. +# +#snapshot + # Excludes hidden files and directories (uncomment to enable). #nohidden @@ -56,6 +76,7 @@ extra boot / exclude *.unrecoverable exclude /tmp/ exclude /lost+found/ +exclude .snapraid/ # Defines the block size in kibi bytes (1024 bytes) (uncomment to enable). # WARNING: Changing this value is for experts only!