Add ZFS support

This commit is contained in:
Andrea Mazzoleni 2026-05-04 11:36:34 +02:00
parent 614513bab5
commit 681683bfd5
12 changed files with 767 additions and 242 deletions

View file

@ -7,7 +7,7 @@ SnapRAID HISTORY
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 'stable' state upon successful
completion (currently supported on Btrfs and Bcachefs).
completion (currently supported on Btrfs, Bcachefs, and ZFS).
This ensures that 'scrub', 'check', and 'fix' operations utilize a
frozen point-in-time reference for data disks, eliminating
mismatches caused by concurrent live filesystem modifications.

View file

@ -355,7 +355,7 @@ snapcheck_run:
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) status
# Destroy disk2
rm -rf bench/snap2/a bench/snap2/b
# Recover disk2 (disk1 is intentionally deleted, so nothing to recover)
# Recover disk2 (disk1 is intentionally modified, so nothing to recover)
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) fix -d snap2 -l test.log
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) check
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync --force-full
@ -368,22 +368,22 @@ snapcheck_run:
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) status
# Destroy disk2
rm -rf bench/snap2/a bench/snap2/b
# Recover disk2 (disk1 is intentionally deleted, so nothing to recover)
# Recover disk2 (disk1 is intentionally modified, so nothing to recover)
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) fix -d snap2 -l test.log
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) check
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync --force-full
# Sync recover
$(MSG) Sync recover aborted AFTER
# Change files in disk3
$(TESTENV) ./mktest$(EXEEXT) change 1 500 bench/snap3/a/*
# Change files in disk1
$(TESTENV) ./mktest$(EXEEXT) change 1 500 bench/snap1/a/*
# Sync abort and sync retry
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync --test-kill-after-sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) check
# Sync recover
$(MSG) Sync recover aborted BEFORE
# Change files in disk3
$(TESTENV) ./mktest$(EXEEXT) change 1 500 bench/snap3/a/*
# Change files in disk1
$(TESTENV) ./mktest$(EXEEXT) change 1 500 bench/snap1/b/*
# Sync abort and sync retry
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync --test-kill-before-sync
$(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync

21
TODO
View file

@ -13,12 +13,18 @@ This is the list of TODO items for SnapRAID.
https://github.com/linuxhw/SMART
https://github.com/linuxhw/EnterpriseDrive
* Implement the use Shadow Copy in Windows for creating snapshots.
See Jens's Windows script at:
http://sourceforge.net/p/snapraid/discussion/1677233/thread/a1707211/
https://sourceforge.net/p/snapraid/discussion/1677233/thread/5c9e670c/
Note that a difference between Windows and Unix is that in Windows old snapshots
are automatically deleted.
* A "dependents" output option to [--test-fmt file|disk|path|dependents] for LIST.
Example, snapraid LIST --test-fmt dependents >FileList_with_dependents.txt
The goal is a filelist that, for every file, includes full paths to the dependent files
on other data disks needed to repair that file (in addition to the parity data).
I created a thread about it here: https://sourceforge.net/p/snapraid/discussion/1677233/thread/6e872fdae0/
See: https://sourceforge.net/p/snapraid/discussion/1677233/thread/6e872fdae0/
- Minor
@ -212,17 +218,6 @@ splitting.
the number of hash for each file to three.
- No way to handle block import.
* Create a filesystem snapshot at every sync, and use it in all the other commands
automatically.
At the next sync, drop the old snapshot and create a new one.
This should help recovering, because we'll have the exact copy used by sync.
This feature can be enabled with a specific option, and available
in Windows using Shadow Copy, and in Linux using Btrfs, and in a generic
Unix using ZFS.
See Jens's Windows script at: http://sourceforge.net/p/snapraid/discussion/1677233/thread/a1707211/
Note that a difference between Windows and Unix is that in Windows old snapshots
are automatically deleted.
* Checks if splitting hash/parity computation in 4K pages
can improve speed in sync. That should increase cache locality,
because we read the data two times for hash and parity,

View file

@ -2044,6 +2044,14 @@ int fssnapshot(const char* path, struct fssnapshot_struct* fss)
return -1;
}
int fssnapshot_stat(struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
(void)fss;
(void)name;
(void)st;
return 0;
}
int fssnapshot_create(const struct fssnapshot_struct* fss, const char* name)
{
(void)fss;

View file

@ -399,16 +399,26 @@ struct fssnapshot_struct {
/**
* Initialize snapshot context from an arbitrary path.
*
* @param path Full path inside the filesystem (must end with '/')
* @param dir Full path inside the filesystem (must end with '/')
* @param fss Output snapshot context
* @return 0 on success, -1 on failure
*/
int fssnapshot(const char* path, struct fssnapshot_struct* fss);
int fssnapshot(const char* dir, struct fssnapshot_struct* fss);
/**
* Stat the snapshot directory.
*
* @param fss Snapshot context
* @param name Snapshot name
* @param st Set the stat info
* @return 0 on success, -1 on failure
*/
int fssnapshot_stat(struct fssnapshot_struct* fss, const char* name, struct stat* st);
/**
* Create a snapshot of the filesystem root/dataset.
*
* @param fss Snapshot context
* @param fss Snapshot context
* @param name Snapshot name
* @return 0 on success, -1 on failure
*/
@ -426,9 +436,9 @@ int fssnapshot_delete(const struct fssnapshot_struct* fss, const char* name);
/**
* Rename a snapshot.
*
* @param fss Snapshot context
* @param old_name Existing snapshot name
* @param new_name New snapshot name
* @param fss Snapshot context
* @param old_name Existing snapshot name
* @param new_name New snapshot name
* @return 0 on success, -1 on failure
*/
int fssnapshot_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name);

View file

@ -5806,18 +5806,22 @@ void state_load_ignore_file(tommy_list* filter_list, const char* path, const cha
#define SNAPSHOT_PENDING "pending"
#define SNAPSHOT_STABLE "stable"
static int state_snapshot_dir(const char* dir, struct snapraid_disk* disk)
static int state_snapshot_dir(struct fssnapshot_struct* fss, const char* name, const char* dir, struct snapraid_disk* disk)
{
struct stat st;
if (lstat(dir, &st) != 0)
return -1;
if (!S_ISDIR(st.st_mode))
/* fail if it doesn't exists at high level */
if (fssnapshot_stat(fss, name, &st) != 0)
return -1;
if (disk) {
pathcpy(disk->dir, sizeof(disk->dir), dir);
char vol[PATH_MAX];
pathcpy(vol, sizeof(vol), fss->snapshot_dir);
pathcat(vol, sizeof(vol), name);
pathcatc(vol, sizeof(vol), '/');
pathcat(vol, sizeof(vol), dir);
pathcpy(disk->dir, sizeof(disk->dir), vol);
disk->dir_device = st.st_dev;
}
@ -5839,9 +5843,11 @@ int state_snapshot_new(struct snapraid_state* state)
size_t root_len = strlen(disk->fss.root_dir);
/* delete a potential previous pending snapshot */
if (fssnapshot_delete(&disk->fss, SNAPSHOT_PENDING) != 0) {
log_fatal(errno, "Failed to delete pending snapshot in '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
return -1;
if (state_snapshot_dir(&disk->fss, SNAPSHOT_PENDING, 0, 0) == 0) {
if (fssnapshot_delete(&disk->fss, SNAPSHOT_PENDING) != 0) {
log_fatal(errno, "Failed to delete pending snapshot in '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
return -1;
}
}
/* create a new snapshot */
@ -5853,13 +5859,8 @@ int state_snapshot_new(struct snapraid_state* state)
msg_progress("Created disk %s pending snapshot...\n", disk->name);
/* setup the snapshot in use */
char vol[PATH_MAX];
pathcpy(vol, sizeof(vol), disk->fss.snapshot_dir);
pathcat(vol, sizeof(vol), SNAPSHOT_PENDING "/");
pathcat(vol, sizeof(vol), disk->mount_point + root_len);
if (state_snapshot_dir(vol, disk) != 0) {
log_error(errno, "Error stating snapshot dir '%s'. %s.\n", vol, strerror(errno));
if (state_snapshot_dir(&disk->fss, SNAPSHOT_PENDING, disk->mount_point + root_len, disk) != 0) {
log_error(errno, "Error stating pending snapshot '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
return -1;
}
}
@ -5880,9 +5881,11 @@ int state_snapshot_commit(struct snapraid_state* state)
continue;
/* delete a potential previous stable snapshot */
if (fssnapshot_delete(&disk->fss, SNAPSHOT_STABLE) != 0) {
log_fatal(errno, "Failed to delete stable snapshot in '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
return -1;
if (state_snapshot_dir(&disk->fss, SNAPSHOT_STABLE, 0, 0) == 0) {
if (fssnapshot_delete(&disk->fss, SNAPSHOT_STABLE) != 0) {
log_fatal(errno, "Failed to delete stable snapshot in '%s'. %s.\n", disk->fss.snapshot_dir, strerror(errno));
return -1;
}
}
/* rename pending to stable */
@ -5904,7 +5907,6 @@ void state_snapshot_read(struct snapraid_state* state)
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
char vol[PATH_MAX];
/* check if it supports snapshot */
if (fssnapshot(disk->mount_point, &disk->fss) != 0)
@ -5912,16 +5914,10 @@ void state_snapshot_read(struct snapraid_state* state)
size_t root_len = strlen(disk->fss.root_dir);
pathcpy(vol, sizeof(vol), disk->fss.snapshot_dir);
pathcat(vol, sizeof(vol), SNAPSHOT_PENDING "/");
pathcat(vol, sizeof(vol), disk->mount_point + root_len);
if (state_snapshot_dir(vol, disk) == 0) {
if (state_snapshot_dir(&disk->fss, SNAPSHOT_PENDING, disk->mount_point + root_len, disk) == 0) {
msg_progress("Using disk %s pending snapshot...\n", disk->name);
} else {
pathcpy(vol, sizeof(vol), disk->fss.snapshot_dir);
pathcat(vol, sizeof(vol), SNAPSHOT_STABLE "/");
pathcat(vol, sizeof(vol), disk->mount_point + root_len);
if (state_snapshot_dir(vol, disk) == 0) {
if (state_snapshot_dir(&disk->fss, SNAPSHOT_STABLE, disk->mount_point + root_len, disk) == 0) {
msg_progress("Using disk %s stable snapshot...\n", disk->name);
} else {
/* fallback to standard mount point */
@ -5944,7 +5940,6 @@ void state_snapshot_write(struct snapraid_state* state, tommy_list* filterlist_d
for (tommy_node* i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
char vol[PATH_MAX];
/* check if it supports snapshot */
if (fssnapshot(disk->mount_point, &disk->fss) != 0)
@ -5958,30 +5953,26 @@ void state_snapshot_write(struct snapraid_state* state, tommy_list* filterlist_d
continue;
}
pathcpy(vol, sizeof(vol), disk->fss.snapshot_dir);
pathcat(vol, sizeof(vol), SNAPSHOT_PENDING "/");
pathcat(vol, sizeof(vol), disk->mount_point + root_len);
if (state_snapshot_dir(vol, disk) == 0) {
if (state_snapshot_dir(&disk->fss, SNAPSHOT_PENDING, disk->mount_point + root_len, disk) == 0) {
msg_progress("Using disk %s pending snapshot...\n", disk->name);
/* if there is a dealloc list */
if (!tommy_list_empty(&disk->dealloclist)) {
pathcpy(vol, sizeof(vol), disk->fss.snapshot_dir);
pathcat(vol, sizeof(vol), SNAPSHOT_STABLE "/");
pathcat(vol, sizeof(vol), disk->mount_point + root_len);
/* if there is a previous snapshot */
if (state_snapshot_dir(vol, 0) == 0) {
msg_progress("Importing disk %s stable snapshot %" PRIu64 " deallocated files...\n", disk->name, tommy_list_count(&disk->dealloclist));
if (state_snapshot_dir(&disk->fss, SNAPSHOT_STABLE, disk->mount_point + root_len, 0) == 0) {
msg_progress("Importing disk %s stable snapshot %" PRIu64 " deallocated files...\n", disk->name, (uint64_t)tommy_list_count(&disk->dealloclist));
/* set where to find deallocated files */
char vol[PATH_MAX];
pathcpy(vol, sizeof(vol), disk->fss.snapshot_dir);
pathcat(vol, sizeof(vol), SNAPSHOT_STABLE "/");
pathcat(vol, sizeof(vol), disk->mount_point + root_len);
state_dealloc(state, vol, &disk->dealloclist);
}
}
} else {
pathcpy(vol, sizeof(vol), disk->fss.snapshot_dir);
pathcat(vol, sizeof(vol), SNAPSHOT_STABLE "/");
pathcat(vol, sizeof(vol), disk->mount_point + root_len);
if (state_snapshot_dir(vol, disk) == 0) {
if (state_snapshot_dir(&disk->fss, SNAPSHOT_STABLE, disk->mount_point + root_len, disk) == 0) {
msg_progress("Using disk %s stable snapshot...\n", disk->name);
} else {
/* fallback to standard mount point */

View file

@ -30,6 +30,84 @@ int exit_sync_needed = 2;
#define BTRFS_SUPER_MAGIC 0x9123683E
#define BCACHEFS_SUPER_MAGIC 0xCA451A4E
#define ZFS_SUPER_MAGIC 0x2FC12FC1
#if HAVE_LINUX_DEVICE
static const char* const bcachefs_paths[] = {
"/usr/sbin/bcachefs",
"/sbin/bcachefs",
"/usr/local/sbin/bcachefs",
"/usr/bin/bcachefs",
"/bin/bcachefs",
"/usr/local/bin/bcachefs",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/bcachefs",
#endif
0
};
static const char* find_bcachefs(void)
{
for (int i = 0; bcachefs_paths[i]; ++i) {
if (access(bcachefs_paths[i], X_OK) == 0) {
return bcachefs_paths[i];
}
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static const char* const zfs_paths[] = {
"/usr/sbin/zfs",
"/sbin/zfs",
"/usr/local/sbin/zfs",
"/usr/bin/zfs",
"/bin/zfs",
"/usr/local/bin/zfs",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/zfs",
#endif
0
};
static const char* find_zfs(void)
{
for (int i = 0; zfs_paths[i]; ++i) {
if (access(zfs_paths[i], X_OK) == 0) {
return zfs_paths[i];
}
}
return 0;
}
static const char* const zpool_paths[] = {
"/usr/sbin/zpool",
"/sbin/zpool",
"/usr/local/sbin/zpool",
"/usr/bin/zpool",
"/bin/zpool",
"/usr/local/bin/zpool",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/zpool",
#endif
0
};
static const char* find_zpool(void)
{
for (int i = 0; zpool_paths[i]; ++i) {
if (access(zpool_paths[i], X_OK) == 0) {
return zpool_paths[i];
}
}
return 0;
}
#endif
#if HAVE_POSIX_FADVISE
/**
@ -448,10 +526,313 @@ struct dev_struct {
tommy_node node;
};
#if HAVE_LINUX_DEVICE
static int devdereference_btrfs(uint64_t device, const char* dir, int fd, tommy_list* devlist)
{
struct btrfs_ioctl_fs_info_args fs_info;
memset(&fs_info, 0, sizeof(fs_info));
if (ioctl(fd, BTRFS_IOC_FS_INFO, &fs_info) < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (fs_info.max_id == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
for (__u64 i = 1; i <= fs_info.max_id; ++i) {
struct btrfs_ioctl_dev_info_args dev_info;
memset(&dev_info, 0, sizeof(dev_info));
dev_info.devid = i;
if (ioctl(fd, BTRFS_IOC_DEV_INFO, &dev_info) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* get major:minor, use stat on the path returned */
struct stat st;
if (stat((char*)dev_info.path, &st) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = st.st_rdev;
tommy_list_insert_tail(devlist, &dev->node, dev);
log_tag("dereference:btrfs:%s:%u:%u:%u:%u\n", dir, major(device), minor(device), major(dev->device), minor(dev->device));
}
return 0;
}
static int devdereference_bcachefs(uint64_t device, const char* dir, tommy_list* devlist)
{
char resolved[PATH_MAX];
char device_list[PATH_MAX * 8];
if (realpath(dir, resolved) == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
FILE* fd = fopen("/proc/self/mountinfo", "r");
if (!fd) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/*
* mountinfo format
* 0 - mount ID
* 1 - parent ID
* 2 - major:minor
* 3 - root
* 4 - mount point
* 5 - options
* 6 - "-" (separator)
* 7 - fs
* 8 - mount source - /dev/device
*/
size_t best_len = 0;
while (1) {
char buf[PATH_MAX * 2 + 64];
char* id_map[8];
unsigned id_mac;
char* fs_map[8];
unsigned fs_mac;
char* s = fgets(buf, sizeof(buf), fd);
if (s == 0)
break;
/* find the separator position */
char* separator = strstr(s, " - ");
if (!separator)
continue;
/* skip the separator */
*separator = 0;
separator += 3;
/* split the line */
id_mac = strsplit(id_map, 8, s, " \t\n");
fs_mac = strsplit(fs_map, 8, separator, " \t\n");
/* if too short, it's the wrong line */
if (id_mac < 5)
continue;
if (fs_mac < 2)
continue;
/* mount point must contain the directory */
const char* mp = id_map[4];
size_t mp_len = strlen(mp);
if (strncmp(resolved, mp, mp_len) != 0)
continue;
if (mp_len > 1 && resolved[mp_len] != '/' && resolved[mp_len] != 0)
continue;
/* it should be bcachefs */
const char* fs = fs_map[0];
if (strcmp(fs, "bcachefs") != 0)
continue;
/* keep the longest (innermost) match */
if (mp_len > best_len) {
best_len = mp_len;
pathcpy(device_list, sizeof(device_list), fs_map[1]);
}
}
fclose(fd);
if (best_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* split the device list */
char* dev_map[64];
unsigned dev_mac = strsplit(dev_map, 64, device_list, ":");
for (unsigned i = 0; i < dev_mac; ++i) {
/* get major:minor, use stat on the path returned */
struct stat st;
if (stat(dev_map[i], &st) != 0)
continue;
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = st.st_rdev;
tommy_list_insert_tail(devlist, &dev->node, dev);
log_tag("dereference:bcachefs:%s:%u:%u:%u:%u\n", dir, major(device), minor(device), major(dev->device), minor(dev->device));
}
return 0;
}
static int extract_zfs(const char* dir, char* dataset, size_t dataset_size, char* uuid, size_t uuid_size, char* mount_point, size_t mount_point_size)
{
char resolved[PATH_MAX];
char cmd[PATH_MAX * 2 + 64];
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (realpath(dir, resolved) == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* list all ZFS filesystems in one shot */
snprintf(cmd, sizeof(cmd), "%s list -H -o name,guid,mountpoint -t filesystem 2>/dev/null", zfs);
FILE* fp = popen(cmd, "r");
if (!fp) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
size_t best_len = 0;
while (1) {
char buf[PATH_MAX * 2 + 64];
char* map[3];
unsigned mac;
char* s = fgets(buf, sizeof(buf), fp);
if (s == 0)
break;
/* split the line */
mac = strsplit(map, 3, s, " \t\n");
if (mac < 3)
continue;
const char* mp = map[2];
size_t mp_len = strlen(mp);
if (strncmp(resolved, mp, mp_len) != 0)
continue;
if (mp_len > 1 && resolved[mp_len] != '/' && resolved[mp_len] != 0)
continue;
/* keep the longest (innermost) match */
if (mp_len > best_len) {
best_len = mp_len;
if (dataset)
pathcpy(dataset, dataset_size, map[0]);
if (uuid)
pathcpy(uuid, uuid_size, map[1]);
if (mount_point)
pathcpy(mount_point, mount_point_size, map[2]);
}
}
pclose(fp);
if (best_len == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
static int devdereference_zfs(uint64_t device, const char* dir, tommy_list* devlist)
{
char pool[PATH_MAX];
char cmd[PATH_MAX * 2 + 64];
const char* zpool = find_zpool();
if (!zpool) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (extract_zfs(dir, pool, sizeof(pool), 0, 0, 0, 0) != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* extract pool name */
char* slash = strchr(pool, '/');
if (slash)
*slash = 0;
snprintf(cmd, sizeof(cmd), "%s status -P %s 2>/dev/null", zpool, pool);
FILE* fp = popen(cmd, "r");
if (!fp) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
size_t count = 0;
while (1) {
char buf[PATH_MAX * 2 + 64];
char* s = fgets(buf, sizeof(buf), fp);
if (s == 0)
break;
char* file = strstr(s, "/dev/");
if (!file)
continue;
size_t i = 0;
while (file[i] != 0 && file[i] != ' ' && file[i] != '\t' && file[i] != '\n')
++i;
file[i] = 0;
struct stat st;
if (stat(file, &st) != 0)
continue;
if (!S_ISBLK(st.st_mode))
continue;
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = st.st_rdev;
tommy_list_insert_tail(devlist, &dev->node, dev);
log_tag("dereference:zfs:%s:%u:%u:%u:%u\n", dir, major(device), minor(device), major(dev->device), minor(dev->device));
++count;
}
pclose(fp);
if (count == 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
/**
* Get the devices of a virtual device.
*
* This is intended to resolve the case of Btrfs filesystems that
* This is intended to resolve the case of Btrfs/ZFS filesystems that
* create a virtual superblock (major==0) not backed by any low
* level device.
*
@ -459,85 +840,40 @@ struct dev_struct {
* Bug 711881 - too funny btrfs st_dev numbers
* https://bugzilla.redhat.com/show_bug.cgi?id=711881
*/
#if HAVE_LINUX_DEVICE
static int devdereference(uint64_t device, const char* dir, tommy_list* devlist)
{
if (major(device) == 0) {
struct statfs sfs;
int fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
if (fstatfs(fd, &sfs) != 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
if (sfs.f_type != BTRFS_SUPER_MAGIC) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
struct btrfs_ioctl_fs_info_args fs_info;
memset(&fs_info, 0, sizeof(fs_info));
if (ioctl(fd, BTRFS_IOC_FS_INFO, &fs_info) < 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
if (fs_info.max_id == 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
for (__u64 i = 1; i <= fs_info.max_id; ++i) {
struct btrfs_ioctl_dev_info_args dev_info;
memset(&dev_info, 0, sizeof(dev_info));
dev_info.devid = i;
if (ioctl(fd, BTRFS_IOC_DEV_INFO, &dev_info) != 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
/* get major:minor, use stat on the path returned */
struct stat st;
if (stat((char*)dev_info.path, &st) != 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = st.st_rdev;
tommy_list_insert_tail(devlist, &dev->node, dev);
}
close(fd);
return 0;
struct statfs sfs;
int fd = open(dir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
/* insert the device itself in the list */
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = device;
tommy_list_insert_tail(devlist, &dev->node, dev);
if (fstatfs(fd, &sfs) != 0) {
/* LCOV_EXCL_START */
close(fd);
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
int ret;
if (sfs.f_type == BTRFS_SUPER_MAGIC) {
ret = devdereference_btrfs(device, dir, fd, devlist);
} else if (sfs.f_type == BCACHEFS_SUPER_MAGIC) {
ret = devdereference_bcachefs(device, dir, devlist);
} else if (sfs.f_type == ZFS_SUPER_MAGIC) {
ret = devdereference_zfs(device, dir, devlist);
} else {
/* insert the device itself in the list */
struct dev_struct* dev = malloc_nofail(sizeof(struct dev_struct));
dev->device = device;
tommy_list_insert_tail(devlist, &dev->node, dev);
ret = 0;
}
close(fd);
return ret;
}
#endif
@ -1050,6 +1386,18 @@ static int devuuid_bcachefs(uint64_t device, const char* dir, char* uuid, size_t
}
#endif
#if HAVE_LINUX_DEVICE
int devuuid_zfs(uint64_t device, const char* dir, char* uuid, size_t uuid_size)
{
if (extract_zfs(dir, 0, 0, uuid, uuid_size, 0, 0) != 0)
return -1;
log_tag("uuid:by-zfs:%u:%u:%s: found %s\n", major(device), minor(device), uuid, dir);
return 0;
}
#endif
int devuuid(uint64_t device_id, const char* device_path, char* uuid, size_t uuid_size)
{
(void)device_path;
@ -1070,6 +1418,11 @@ int devuuid(uint64_t device_id, const char* device_path, char* uuid, size_t uuid
return 0;
#endif
#if HAVE_LINUX_DEVICE
if (devuuid_zfs(device_id, device_path, uuid, uuid_size) == 0)
return 0;
#endif
#if HAVE_LINUX_DEVICE
if (devuuid_dev(device_id, uuid, uuid_size) == 0)
return 0;
@ -1298,7 +1651,6 @@ int filephy(const char* path, uint64_t size, uint64_t* physical)
#define UFS_BYTESWAPPED_SUPER_MAGIC 0x54190100
#define VMHGFS_SUPER_MAGIC 0xBACBACBC
#define VZFS_SUPER_MAGIC 0x565A4653
#define ZFS_SUPER_MAGIC 0x2FC12FC1
struct filesystem_entry {
unsigned id;
@ -1534,64 +1886,18 @@ int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_har
return 0;
}
#if HAVE_LINUX_DEVICE
static const char* const bcachefs_paths[] = {
"/usr/sbin/bcachefs",
"/sbin/bcachefs",
"/usr/local/sbin/bcachefs",
"/usr/bin/bcachefs",
"/bin/bcachefs",
"/usr/local/bin/bcachefs",
#ifdef __APPLE__
/* macOS (Intel & Apple Silicon) */
"/opt/homebrew/sbin/bcachefs",
#endif
0
};
static const char* find_bcachefs(void)
{
for (int i = 0; bcachefs_paths[i]; ++i) {
if (access(bcachefs_paths[i], X_OK) == 0) {
return bcachefs_paths[i];
}
}
return 0;
}
#endif
#define SNAPSHOT_CONTAINER ".snapraid"
int fssnapshot(const char* path, struct fssnapshot_struct* fss)
{
#if HAVE_LINUX_DEVICE
struct statfs sfs;
struct stat st;
static int fssnapshot_inode(const char* path, uint32_t magic, uint64_t root_inode, struct fssnapshot_struct* fss)
{
char current_path[PATH_MAX];
uint64_t root_inode;
pathcpy(current_path, sizeof(current_path), path);
if (statfs(current_path, &sfs) != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Error stating filesystem '%s'. %s.\n", current_path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
if (sfs.f_type == BTRFS_SUPER_MAGIC) {
/* btrfs reserved inode 256 for subvolume roots */
root_inode = 256;
} else if (sfs.f_type == BCACHEFS_SUPER_MAGIC) {
/* Bcachefs reserves inode 4096 for each subvolume's root directory */
root_inode = 4096;
} else {
return -1;
}
/* walk up the directory tree to find the subvolume root (Inode 256) */
while (1) {
struct stat st;
if (stat(current_path, &st) != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Error stating '%s'. %s.\n", current_path, strerror(errno));
@ -1601,6 +1907,7 @@ int fssnapshot(const char* path, struct fssnapshot_struct* fss)
if (st.st_ino == root_inode) {
/* copy the subvol root */
pathcpy(fss->root_dir, sizeof(fss->root_dir), current_path);
pathcpy(fss->snapshot_dir, sizeof(fss->snapshot_dir), current_path);
pathcat(fss->snapshot_dir, sizeof(fss->snapshot_dir), SNAPSHOT_CONTAINER "/");
@ -1611,8 +1918,7 @@ int fssnapshot(const char* path, struct fssnapshot_struct* fss)
return -1;
}
pathcpy(fss->root_dir, sizeof(fss->root_dir), current_path);
fss->magic = sfs.f_type;
fss->magic = magic;
return 0;
}
@ -1626,6 +1932,53 @@ int fssnapshot(const char* path, struct fssnapshot_struct* fss)
/* LCOV_EXCL_STOP */
}
}
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs(const char* dir, uint32_t magic, struct fssnapshot_struct* fss)
{
if (extract_zfs(dir, fss->dataset, sizeof(fss->dataset), 0, 0, fss->root_dir, sizeof(fss->root_dir)) != 0)
return -1;
pathslash(fss->root_dir, sizeof(fss->root_dir));
pathcpy(fss->snapshot_dir, sizeof(fss->snapshot_dir), fss->root_dir);
pathcat(fss->snapshot_dir, sizeof(fss->snapshot_dir), ".zfs/snapshot/");
/* ensure ZFS snapshot directory is visible */
if (access(fss->snapshot_dir, R_OK | X_OK) != 0) {
log_error(errno, "ZFS snapshot directory not accessible: '%s'. %s.\n", fss->snapshot_dir, strerror(errno));
return -1;
}
fss->magic = magic;
return 0;
}
#endif
int fssnapshot(const char* path, struct fssnapshot_struct* fss)
{
#if HAVE_LINUX_DEVICE
struct statfs sfs;
if (statfs(path, &sfs) != 0) {
/* LCOV_EXCL_START */
log_error(errno, "Error stating filesystem '%s'. %s.\n", path, strerror(errno));
return -1;
/* LCOV_EXCL_STOP */
}
if (sfs.f_type == BTRFS_SUPER_MAGIC) {
/* btrfs reserved inode 256 for subvolume roots */
return fssnapshot_inode(path, sfs.f_type, 256, fss);
} else if (sfs.f_type == BCACHEFS_SUPER_MAGIC) {
/* Bcachefs reserves inode 4096 for each subvolume's root directory */
return fssnapshot_inode(path, sfs.f_type, 4096, fss);
} else if (sfs.f_type == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs(path, sfs.f_type, fss);
} else {
return -1;
}
#else
(void)path;
(void)fss;
@ -1633,6 +1986,109 @@ int fssnapshot(const char* path, struct fssnapshot_struct* fss)
#endif
}
#if HAVE_LINUX_DEVICE
static int fssnapshot_stat_fs(const struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
char target_path[PATH_MAX];
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
if (stat(target_path, st) != 0)
return -1;
/* it must be a directory */
if (!S_ISDIR(st->st_mode))
return -1;
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_stat_zfs(const struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
char target_path[PATH_MAX];
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
/*
* Ensure to get into into the snapshot
*
* This is required by ZFS, otherwise we get a different st_dev
*/
pathcat(target_path, sizeof(target_path), "/.");
if (stat(target_path, st) != 0)
return -1;
/* it must be a directory */
if (!S_ISDIR(st->st_mode))
return -1;
/**
* When verifying if a ZFS snapshot has been successfully deleted, a direct
* path lookup (stat, access, open) is unreliable. A directory listing
* (readdir) of the .zfs/snapshot directory must be used instead.
*
* On Linux, the Virtual File System (VFS) maintains a 'dentry cache'
* (dcache). If a snapshot path was accessed recently, the kernel caches
* the directory entry in RAM. When ZFS destroys the snapshot, the kernel
* dcache is not always immediately synchronized. Consequently, stat()
* may hit the stale cache and report the snapshot still exists.
*
* Unlike stat(), a directory listing (ls/readdir) forces the kernel to
* query the ZFS module for the current set of valid entries, bypassing
* the stale dcache for a "ground truth" check.
*
* This behavior can be confirmed by forcing the kernel to purge its
* dcache manually using the following command:
* sync; echo 2 > /proc/sys/vm/drop_caches
*
* After dropping caches, stat() will correctly return ENOENT. To avoid
* requiring root privileges or impacting system performance with
* drop_caches, always use readdir() for existence checks in this context.
*/
DIR* d = opendir(fss->snapshot_dir);
if (!d) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
while (1) {
struct dirent* entry = readdir(d);
if (!entry)
break;
if (strcmp(entry->d_name, name) == 0) {
closedir(d);
return 0;
}
}
closedir(d);
return -1;
}
#endif
int fssnapshot_stat(struct fssnapshot_struct* fss, const char* name, struct stat* st)
{
#if HAVE_LINUX_DEVICE
if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_stat_zfs(fss, name, st);
} else {
return fssnapshot_stat_fs(fss, name, st);
}
#else
(void)fss;
(void)name;
(void)st;
return 0;
#endif
}
#if HAVE_LINUX_DEVICE
int fssnapshot_btrfs_create(const struct fssnapshot_struct* fss, const char* name)
{
@ -1682,22 +2138,47 @@ int fssnapshot_btrfs_create(const struct fssnapshot_struct* fss, const char* nam
#if HAVE_LINUX_DEVICE
static int fssnapshot_bcachefs_create(const struct fssnapshot_struct* fss, const char* name)
{
char dst_path[PATH_MAX];
char target_path[PATH_MAX];
char cmd[PATH_MAX * 3 + 64];
const char* bcachefs;
int ret;
bcachefs = find_bcachefs();
const char* bcachefs = find_bcachefs();
if (!bcachefs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
pathcpy(dst_path, sizeof(dst_path), fss->snapshot_dir);
pathcat(dst_path, sizeof(dst_path), name);
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
snprintf(cmd, sizeof(cmd), "%s subvolume snapshot -r %s %s", bcachefs, fss->root_dir, dst_path);
snprintf(cmd, sizeof(cmd), "%s subvolume snapshot -r %s %s", bcachefs, fss->root_dir, target_path);
ret = system(cmd);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs_create(const struct fssnapshot_struct* fss, const char* name)
{
char cmd[PATH_MAX * 2 + 64];
int ret;
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(cmd, sizeof(cmd), "%s snapshot %s@%s", zfs, fss->dataset, name);
ret = system(cmd);
if (ret != 0) {
@ -1717,6 +2198,8 @@ int fssnapshot_create(const struct fssnapshot_struct* fss, const char* name)
return fssnapshot_btrfs_create(fss, name);
} else if (fss->magic == BCACHEFS_SUPER_MAGIC) {
return fssnapshot_bcachefs_create(fss, name);
} else if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs_create(fss, name);
} else {
return -1;
}
@ -1745,7 +2228,7 @@ int fssnapshot_btrfs_delete(const struct fssnapshot_struct* fss, const char* nam
pathcpy(args.name, BTRFS_PATH_NAME_MAX, name);
ret = ioctl(fd_parent, BTRFS_IOC_SNAP_DESTROY, &args);
if (ret < 0 && errno != ENOENT) {
if (ret < 0) {
/* LCOV_EXCL_START */
close(fd_parent);
return -1;
@ -1761,11 +2244,10 @@ int fssnapshot_btrfs_delete(const struct fssnapshot_struct* fss, const char* nam
static int fssnapshot_bcachefs_delete(const struct fssnapshot_struct* fss, const char* name)
{
char target_path[PATH_MAX];
char cmd[PATH_MAX + 64];
const char* bcachefs;
char cmd[PATH_MAX * 2 + 64];
int ret;
bcachefs = find_bcachefs();
const char* bcachefs = find_bcachefs();
if (!bcachefs) {
/* LCOV_EXCL_START */
return -1;
@ -1775,15 +2257,33 @@ static int fssnapshot_bcachefs_delete(const struct fssnapshot_struct* fss, const
pathcpy(target_path, sizeof(target_path), fss->snapshot_dir);
pathcat(target_path, sizeof(target_path), name);
if (access(target_path, F_OK) < 0) {
if (errno == ENOENT)
return 0;
snprintf(cmd, sizeof(cmd), "%s subvolume delete %s", bcachefs, target_path);
ret = system(cmd);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(cmd, sizeof(cmd), "%s subvolume delete %s", bcachefs, target_path);
return 0;
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs_delete(const struct fssnapshot_struct* fss, const char* name)
{
char cmd[PATH_MAX * 2 + 64];
int ret;
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(cmd, sizeof(cmd), "%s destroy %s@%s", zfs, fss->dataset, name);
ret = system(cmd);
if (ret != 0) {
@ -1803,6 +2303,8 @@ int fssnapshot_delete(const struct fssnapshot_struct* fss, const char* name)
return fssnapshot_btrfs_delete(fss, name);
} else if (fss->magic == BCACHEFS_SUPER_MAGIC) {
return fssnapshot_bcachefs_delete(fss, name);
} else if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs_delete(fss, name);
} else {
return -1;
}
@ -1853,6 +2355,32 @@ static int fssnapshot_bcachefs_rename(const struct fssnapshot_struct* fss, const
}
#endif
#if HAVE_LINUX_DEVICE
static int fssnapshot_zfs_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
char cmd[PATH_MAX * 2 + 64];
int ret;
const char* zfs = find_zfs();
if (!zfs) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
snprintf(cmd, sizeof(cmd), "%s rename %s@%s %s@%s", zfs, fss->dataset, old_name, fss->dataset, new_name);
ret = system(cmd);
if (ret != 0) {
/* LCOV_EXCL_START */
return -1;
/* LCOV_EXCL_STOP */
}
return 0;
}
#endif
int fssnapshot_rename(const struct fssnapshot_struct* fss, const char* old_name, const char* new_name)
{
#if HAVE_LINUX_DEVICE
@ -1860,6 +2388,8 @@ int fssnapshot_rename(const struct fssnapshot_struct* fss, const char* old_name,
return fssnapshot_btrfs_rename(fss, old_name, new_name);
} else if (fss->magic == BCACHEFS_SUPER_MAGIC) {
return fssnapshot_bcachefs_rename(fss, old_name, new_name);
} else if (fss->magic == ZFS_SUPER_MAGIC) {
return fssnapshot_zfs_rename(fss, old_name, new_name);
} else {
return -1;
}

View file

@ -1435,7 +1435,7 @@ start of a \'sync\'. This ensures a consistent, point\-in\-time view of your
files, preventing errors caused by concurrent file modifications.
.PP
This option applies exclusively to data disks formatted with the
Btrfs or Bcachefs filesystems. Parity disks, or data disks using
Btrfs, Bcachefs or ZFS filesystems. Parity disks, or data disks using
other filesystems, will always use the live version of the filesystem.
.PP
This significantly improves recovery: if a file is deleted from the live
@ -1443,15 +1443,11 @@ filesystem, it remains preserved in the snapshot. This prevents the parity
from becoming \[dq]broken\[dq] for that block, ensuring you can still successfully
recover data if another disk fails.
.PP
Snapshots are stored in a hidden \'.snapraid\' directory at the root
of the subvolume containing your data. SnapRAID will automatically
detect the subvolume root for each data path.
.PP
Snapshot creation and deletion require administrative privileges.
Ensure SnapRAID is run with the necessary permissions (e.g., sudo) when
this option is enabled.
.PP
See the SNAPSHOTSsection for a detailed explanation of the snapshot
See the SNAPSHOTS section for a detailed explanation of the snapshot
lifecycle.
.SS nohidden
Excludes all hidden files and directories.
@ -1835,7 +1831,7 @@ Snapshots are created only for data disks and only if the underlying
filesystem supports this functionality. Parity disks always use
the live filesystem.
.PP
At present, this is supported exclusively on Btrfs and Bcachefs.
At present, this is supported exclusively on Btrfs, Bcachefs, and ZFS.
.PP
You can mix data disks with different filesystems. Only those that
support snapshots will utilize them, while other disks will continue
@ -1877,8 +1873,9 @@ live filesystems exclusively.
All other commands operate exclusively on the live filesystem.
.SH SNAPSHOTS LIFECYCLE
SnapRAID manages two specific snapshots, \`stable\` and \`pending\`,
within a hidden \`.snapraid\` directory at the root of each data
subvolume.
within a hidden directory at the root of each data subvolume.
In Btrfs an Bcachefs it\'s used the \`.snapraid/\` directory, in ZFS
the standard \`.zfs/snapshot/\`.
.PP
The \`stable\` snapshot represents the state of the last
successfully completed \`sync\`, containing the exact data used to

View file

@ -1155,7 +1155,7 @@ Configuration
files, preventing errors caused by concurrent file modifications.
This option applies exclusively to data disks formatted with the
Btrfs or Bcachefs filesystems. Parity disks, or data disks using
Btrfs, Bcachefs or ZFS filesystems. Parity disks, or data disks using
other filesystems, will always use the live version of the filesystem.
This significantly improves recovery: if a file is deleted from the live
@ -1163,15 +1163,11 @@ Configuration
from becoming "broken" for that block, ensuring you can still successfully
recover data if another disk fails.
Snapshots are stored in a hidden '.snapraid' directory at the root
of the subvolume containing your data. SnapRAID will automatically
detect the subvolume root for each data path.
Snapshot creation and deletion require administrative privileges.
Ensure SnapRAID is run with the necessary permissions (e.g., sudo) when
this option is enabled.
See the SNAPSHOTSsection for a detailed explanation of the snapshot
See the SNAPSHOTS section for a detailed explanation of the snapshot
lifecycle.
nohidden
@ -1447,7 +1443,7 @@ Snapshots
filesystem supports this functionality. Parity disks always use
the live filesystem.
At present, this is supported exclusively on Btrfs and Bcachefs.
At present, this is supported exclusively on Btrfs, Bcachefs, and ZFS.
You can mix data disks with different filesystems. Only those that
support snapshots will utilize them, while other disks will continue
@ -1491,8 +1487,9 @@ Snapshots
Snapshots Lifecycle
SnapRAID manages two specific snapshots, `stable` and `pending`,
within a hidden `.snapraid` directory at the root of each data
subvolume.
within a hidden directory at the root of each data subvolume.
In Btrfs an Bcachefs it's used the `.snapraid/` directory, in ZFS
the standard `.zfs/snapshot/`.
The `stable` snapshot represents the state of the last
successfully completed `sync`, containing the exact data used to

View file

@ -1233,7 +1233,7 @@ start of a 'sync'. This ensures a consistent, point-in-time view of your
files, preventing errors caused by concurrent file modifications.
This option applies exclusively to data disks formatted with the
Btrfs or Bcachefs filesystems. Parity disks, or data disks using
Btrfs, Bcachefs or ZFS filesystems. Parity disks, or data disks using
other filesystems, will always use the live version of the filesystem.
This significantly improves recovery: if a file is deleted from the live
@ -1241,15 +1241,11 @@ 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.
Snapshots are stored in a hidden '.snapraid' directory at the root
of the subvolume containing your data. SnapRAID will automatically
detect the subvolume root for each data path.
Snapshot creation and deletion require administrative privileges.
Ensure SnapRAID is run with the necessary permissions (e.g., sudo) when
this option is enabled.
See the SNAPSHOTSsection for a detailed explanation of the snapshot
See the SNAPSHOTS section for a detailed explanation of the snapshot
lifecycle.
7.8 nohidden
@ -1552,7 +1548,7 @@ 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 and Bcachefs.
At present, this is supported exclusively on Btrfs, Bcachefs, and ZFS.
You can mix data disks with different filesystems. Only those that
support snapshots will utilize them, while other disks will continue
@ -1601,8 +1597,9 @@ All other commands operate exclusively on the live filesystem.
=====================
SnapRAID manages two specific snapshots, `stable` and `pending`,
within a hidden `.snapraid` directory at the root of each data
subvolume.
within a hidden directory at the root of each data subvolume.
In Btrfs an Bcachefs it's used the `.snapraid/` directory, in ZFS
the standard `.zfs/snapshot/`.
The `stable` snapshot represents the state of the last
successfully completed `sync`, containing the exact data used to

View file

@ -9,7 +9,7 @@ make distclean
# Reconfigure (with force) to get the latest revision from git
autoreconf -f
if ! ./configure.windows-x86; then
if ! ./configure.windows-x86 --enable-warning-as-error; then
exit 1
fi
@ -21,7 +21,7 @@ if ! make distwindows-x86 distclean; then
exit 1
fi
if ! ./configure.windows-x64; then
if ! ./configure.windows-x64 --enable-warning-as-error; then
exit 1
fi
@ -33,7 +33,7 @@ if ! make distwindows-x64 distclean; then
exit 1
fi
if ! ./configure ; then
if ! ./configure --enable-warning-as-error; then
exit 1
fi

View file

@ -44,14 +44,15 @@ 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.
# Enable the use of filesystem 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 option applies exclusively to data disks formatted with the
# Btrfs or Bcachefs filesystems. Parity disks, or data disks using
# Btrfs, Bcachefs or ZFS filesystems. Parity disks, or data disks using
# other filesystems, will always use the live version of the filesystem.
#
# This significantly improves recovery: if a file is deleted from the live
@ -137,4 +138,3 @@ exclude .snapraid/
#smartctl d2 -d usbjmicron %s
#smartctl parity -d areca,1/1 /dev/sg0
#smartctl 2-parity -d areca,2/1 /dev/sg0