Add Btrfs snapshot support for atomic sync and recovery consistency

This commit is contained in:
Andrea Mazzoleni 2026-04-21 15:19:25 +02:00
parent 450b826555
commit e9871d65ff
18 changed files with 1094 additions and 39 deletions

18
HISTORY
View file

@ -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.8 2026/06
============
* Avoid spinning up standby disks on Windows in some machines during

View file

@ -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());
}

View file

@ -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. */

View file

@ -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

View file

@ -111,6 +111,10 @@
#include <linux/fs.h>
#endif
#if HAVE_LINUX_BTRFS_H
#include <linux/btrfs.h>
#endif
#if HAVE_LINUX_FIEMAP_H
#include <linux/fiemap.h>
#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.
*

View file

@ -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_error(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_error(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 */

View file

@ -1835,6 +1835,11 @@ int snapraid_main(int argc, char* argv[])
if (opt.gui_touch_before)
state_touch(&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) {
@ -1888,6 +1893,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);
@ -1933,6 +1942,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);
@ -2049,6 +2060,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 */

View file

@ -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);
}
}
}
}

View file

@ -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 */

View file

@ -1165,6 +1165,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

View file

@ -360,6 +360,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 /.

View file

@ -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));

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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!