diff --git a/HISTORY b/HISTORY index c00c1564..26579c59 100644 --- a/HISTORY +++ b/HISTORY @@ -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. diff --git a/Makefile.am b/Makefile.am index 68cec1b5..aa7c6332 100644 --- a/Makefile.am +++ b/Makefile.am @@ -356,7 +356,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 @@ -369,22 +369,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 diff --git a/TODO b/TODO index 4e819032..f953f4e0 100644 --- a/TODO +++ b/TODO @@ -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, diff --git a/cmdline/mingw.c b/cmdline/mingw.c index 28802f24..545d7a42 100644 --- a/cmdline/mingw.c +++ b/cmdline/mingw.c @@ -2069,6 +2069,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; diff --git a/cmdline/portable.h b/cmdline/portable.h index ac0d44d6..3e90f5a2 100644 --- a/cmdline/portable.h +++ b/cmdline/portable.h @@ -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); diff --git a/cmdline/state.c b/cmdline/state.c index 0f7a6090..481475a4 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -5813,18 +5813,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; } @@ -5846,9 +5850,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 */ @@ -5860,13 +5866,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; } } @@ -5887,9 +5888,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 */ @@ -5911,7 +5914,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) @@ -5919,16 +5921,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 */ @@ -5951,7 +5947,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) @@ -5965,30 +5960,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 */ diff --git a/cmdline/unix.c b/cmdline/unix.c index 8720824a..4e9ef955 100644 --- a/cmdline/unix.c +++ b/cmdline/unix.c @@ -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; } diff --git a/doc/snapraid.1 b/doc/snapraid.1 index 39e4aaf4..cb136013 100644 --- a/doc/snapraid.1 +++ b/doc/snapraid.1 @@ -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 diff --git a/doc/snapraid.d b/doc/snapraid.d index 2aa5e02e..7282de8c 100644 --- a/doc/snapraid.d +++ b/doc/snapraid.d @@ -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 diff --git a/doc/snapraid.txt b/doc/snapraid.txt index a7798e15..d55666d9 100644 --- a/doc/snapraid.txt +++ b/doc/snapraid.txt @@ -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 diff --git a/makedist.sh b/makedist.sh index a52797b9..e54d7c2d 100755 --- a/makedist.sh +++ b/makedist.sh @@ -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 diff --git a/snapraid.conf.example b/snapraid.conf.example index 946b3a06..3929f964 100644 --- a/snapraid.conf.example +++ b/snapraid.conf.example @@ -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 -