mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-25 18:40:19 -04:00
Add Btrfs snapshot support for atomic sync and recovery consistency
This commit is contained in:
parent
fcf85b4b7c
commit
2fc38017a8
18 changed files with 1094 additions and 39 deletions
18
HISTORY
18
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.
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
241
cmdline/state.c
241
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 /.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
218
cmdline/unix.c
218
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
139
doc/snapraid.1
139
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
|
||||
|
|
|
|||
130
doc/snapraid.d
130
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
|
||||
|
|
|
|||
185
doc/snapraid.txt
185
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)
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue