From 16b08201a4d5067f9b618b9e5c7a047589d1839c Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Sun, 3 May 2026 21:39:35 +0200 Subject: [PATCH] Add Bcachefs support --- .github/workflows/build.yml | 2 +- HISTORY | 2 +- Makefile.am | 112 +++++++-------- cmdline/elem.c | 1 + cmdline/elem.h | 1 + cmdline/mingw.c | 12 +- cmdline/portable.h | 16 ++- cmdline/state.c | 14 +- cmdline/unix.c | 278 +++++++++++++++++++++++++++++++++--- doc/snapraid.1 | 16 ++- doc/snapraid.d | 16 ++- doc/snapraid.txt | 16 ++- snapraid.conf.example | 12 +- test/test-btrfs.conf | 9 -- test/test-snap.conf | 9 ++ 15 files changed, 387 insertions(+), 129 deletions(-) delete mode 100644 test/test-btrfs.conf create mode 100644 test/test-snap.conf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c9019af0..7d4476d2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v5 - run: autoreconf -i - run: ./configure --enable-warning-as-error - - run: make all btrfscheck + - run: make all snapcheck - run: make distcheck build-win: runs-on: windows-latest diff --git a/HISTORY b/HISTORY index e77349a6..a21c9eb7 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). + completion (currently supported on Btrfs and Bcachefs). 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 c63eea8d..68cec1b5 100644 --- a/Makefile.am +++ b/Makefile.am @@ -154,7 +154,7 @@ EXTRA_DIST = \ $(LIST_D) \ $(LIST_MAN) \ $(LIST_TXT) \ - test/test-btrfs.conf \ + test/test-snap.conf \ test/test-par1.conf \ test/test-par2.conf \ test/test-par3.conf \ @@ -269,8 +269,8 @@ CHECKFLAGS_VERBOSE = $(CHECKFLAGS_ALPHA) -v --gui # Quiet options CHECKFLAGS = $(CHECKFLAGS_ALPHA) -q -q -q -# Btrfs with no parity limit -CHECKFLAGS_BTRFS = --test-skip-device --test-skip-self --test-io-advise-none --test-force-order-alpha -q -q -q +# Snapshots with no parity limit +CHECKFLAGS_SNAP = --test-skip-device --test-skip-self --test-io-advise-none --test-force-order-alpha -q -q -q # Number of files to create if !HAVE_MEMORY_CHECKER @@ -302,7 +302,7 @@ PARZ = $(srcdir)/test/test-parz.conf PAR4 = $(srcdir)/test/test-par4.conf PAR5 = $(srcdir)/test/test-par5.conf PAR6 = $(srcdir)/test/test-par6.conf -BTRFS = $(srcdir)/test/test-btrfs.conf +SNAP = $(srcdir)/test/test-snap.conf MSG = @echo ===== SNAPRAID = ./checker$(EXEEXT) @@ -312,89 +312,89 @@ setupcheck: mkdir bench # Not included in main check because it requires root permissions -btrfscheck: mktest checker setupcheck - sudo $(MAKE) btrfscheck_run; \ +snapcheck: mktest checker setupcheck + sudo $(MAKE) snapcheck_run; \ status=$$?; \ - sudo $(MAKE) btrfscheck_cleanup; \ + sudo $(MAKE) snapcheck_cleanup; \ exit $$status -btrfscheck_run: - mkdir bench/btrfs1 - mkdir bench/btrfs2 - mkdir bench/btrfs3 +snapcheck_run: + mkdir bench/snap1 + mkdir bench/snap2 + mkdir bench/snap3 $(MSG) Create storages - dd bs=1048576 count=32 if=/dev/zero of=bench/btrfs1.img - dd bs=1048576 count=32 if=/dev/zero of=bench/btrfs2.img - dd bs=1048576 count=32 if=/dev/zero of=bench/btrfs3.img - /sbin/mkfs.btrfs --mixed bench/btrfs1.img - /sbin/mkfs.btrfs --mixed bench/btrfs2.img - /sbin/mkfs.btrfs --mixed bench/btrfs3.img - mount -o loop bench/btrfs1.img bench/btrfs1 - mount -o loop bench/btrfs2.img bench/btrfs2 - mount -o loop bench/btrfs3.img bench/btrfs3 + dd bs=1048576 count=32 if=/dev/zero of=bench/snap1.img + dd bs=1048576 count=32 if=/dev/zero of=bench/snap2.img + dd bs=1048576 count=32 if=/dev/zero of=bench/snap3.img + /sbin/mkfs.btrfs --mixed bench/snap1.img + /sbin/mkfs.btrfs --mixed bench/snap2.img + /sbin/mkfs.btrfs --mixed bench/snap3.img + mount -o loop bench/snap1.img bench/snap1 + mount -o loop bench/snap2.img bench/snap2 + mount -o loop bench/snap3.img bench/snap3 $(MSG) Create some files - $(TESTENV) ./mktest$(EXEEXT) generate 1 btrfs 3 $(CHECKCOUNT) $(CHECKSIZE) + $(TESTENV) ./mktest$(EXEEXT) generate 1 snap 3 $(CHECKCOUNT) $(CHECKSIZE) $(MSG) Basic # Setup - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) sync - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) scrub -p full - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) check -d btrfs1 + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) scrub -p full + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) check -d snap1 # Recover test $(MSG) Recover with file deletions from two disk with ONLY one parity OUTSIDE SYNC - rm -rf bench/btrfs1/a - rm -rf bench/btrfs2/a - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) fix -d btrfs1 -l test.log - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) fix -d btrfs2 -l test.log - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) check - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) sync --force-full + rm -rf bench/snap1/a + rm -rf bench/snap2/a + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) fix -d snap1 -l test.log + $(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 # Recover test $(MSG) Recover with file deletions from two disk with ONLY one parity aborted BEFORE SYNC # Changes (including deletion) on disk1 - $(TESTENV) ./mktest$(EXEEXT) change 1 500 bench/btrfs1/a/* bench/btrfs1/b/* + $(TESTENV) ./mktest$(EXEEXT) change 1 500 bench/snap1/a/* bench/snap1/b/* # Sync aborted BEFORE - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) sync --test-kill-before-sync - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) status + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync --test-kill-before-sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) status # Destroy disk2 - rm -rf bench/btrfs2/a bench/btrfs2/b + rm -rf bench/snap2/a bench/snap2/b # Recover disk2 (disk1 is intentionally deleted, so nothing to recover) - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) fix -d btrfs2 -l test.log - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) check - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) sync --force-full + $(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 # Recover test $(MSG) Recover with file deletions from two disk with ONLY one parity aborted AFTER SYNC # Changes (including deletion) on disk1 - $(TESTENV) ./mktest$(EXEEXT) change 2 500 bench/btrfs1/a/* bench/btrfs1/b/* + $(TESTENV) ./mktest$(EXEEXT) change 2 500 bench/snap1/a/* bench/snap1/b/* # Sync aborted AFTER - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) sync --test-kill-after-sync - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) status + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync --test-kill-after-sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) status # Destroy disk2 - rm -rf bench/btrfs2/a bench/btrfs2/b + rm -rf bench/snap2/a bench/snap2/b # Recover disk2 (disk1 is intentionally deleted, so nothing to recover) - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) fix -d btrfs2 -l test.log - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) check - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) sync --force-full + $(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/btrfs3/a/* + $(TESTENV) ./mktest$(EXEEXT) change 1 500 bench/snap3/a/* # Sync abort and sync retry - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) sync --test-kill-after-sync - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) sync - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) check + $(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/btrfs3/a/* + $(TESTENV) ./mktest$(EXEEXT) change 1 500 bench/snap3/a/* # Sync abort and sync retry - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) sync --test-kill-before-sync - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) sync - $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_BTRFS) -c $(BTRFS) check + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync --test-kill-before-sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS_SNAP) -c $(SNAP) check -btrfscheck_cleanup: +snapcheck_cleanup: $(MSG) Unmount storages - umount bench/btrfs1 - umount bench/btrfs2 - umount bench/btrfs3 + umount bench/snap1 + umount bench/snap2 + umount bench/snap3 check-local: setupcheck mkdir bench/disk1 diff --git a/cmdline/elem.c b/cmdline/elem.c index 3427be03..5be7929e 100644 --- a/cmdline/elem.c +++ b/cmdline/elem.c @@ -812,6 +812,7 @@ struct snapraid_disk* disk_alloc(const char* name, const char* dir, uint64_t dev /* no snapshot by default */ disk->snapshot_root[0] = 0; + disk->snapshot_magic = 0; pathcpy(disk->dir, sizeof(disk->dir), disk->mount_point); #if HAVE_THREAD diff --git a/cmdline/elem.h b/cmdline/elem.h index 9c42ce23..445474d7 100644 --- a/cmdline/elem.h +++ b/cmdline/elem.h @@ -383,6 +383,7 @@ struct snapraid_disk { * It always terminates with / */ char snapshot_root[PATH_MAX]; + uint32_t snapshot_magic; /**< Magic number of the snapshot filesystem */ char smartctl[PATH_MAX]; /**< Custom command for smartctl. Empty means auto. */ int smartignore[SMART_IGNORE_MAX]; /**< Smart attributes to ignore for this device. */ diff --git a/cmdline/mingw.c b/cmdline/mingw.c index 4e81fae8..bd9cf015 100644 --- a/cmdline/mingw.c +++ b/cmdline/mingw.c @@ -2062,31 +2062,35 @@ 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) +int fssnapshot(const char* path, char* root, size_t root_size, uint32_t* magic) { (void)path; (void)root; (void)root_size; + (void)magic; return -1; } -int fssnapshot_create(const char* source, const char* parent_dir, const char* name) +int fssnapshot_create(uint32_t magic, const char* source, const char* parent_dir, const char* name) { + (void)magic; (void)source; (void)parent_dir; (void)name; return -1; } -int fssnapshot_delete(const char* parent_dir, const char* name) +int fssnapshot_delete(uint32_t magic, const char* parent_dir, const char* name) { + (void)magic; (void)parent_dir; (void)name; return -1; } -int fssnapshot_rename(const char* parent_dir, const char* old_name, const char* new_name) +int fssnapshot_rename(uint32_t magic, const char* parent_dir, const char* old_name, const char* new_name) { + (void)magic; (void)parent_dir; (void)old_name; (void)new_name; diff --git a/cmdline/portable.h b/cmdline/portable.h index 791cb0e6..82c686fa 100644 --- a/cmdline/portable.h +++ b/cmdline/portable.h @@ -366,32 +366,36 @@ int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_har /** * 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 /. + * @param root Set the full path of the subvolume root that contains the specified path. It will end with /. + * @param magic Set the magic number of the filesystem */ -int fssnapshot(const char* path, char* root, size_t root_size); +int fssnapshot(const char* path, char* root, size_t root_size, uint32_t* magic); /** - * Creates a snapshot of a subvolume. + * Creates a snapshot of a subvolume + * @param magic The magic number of the filesystem. * @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); +int fssnapshot_create(uint32_t magic, const char* source, const char* parent_dir, const char* name); /** * Deletes a subvolume/snapshot. + * @param magic The magic number of the filesystem. * @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); +int fssnapshot_delete(uint32_t magic, const char* parent_dir, const char* name); /** * Renames a subvolume. + * @param magic The magic number of the filesystem. * @return 0 on success, -1 on failure. */ -int fssnapshot_rename(const char* parent_dir, const char* old_name, const char* new_name); +int fssnapshot_rename(uint32_t magic, const char* parent_dir, const char* old_name, const char* new_name); /* * Log file. diff --git a/cmdline/state.c b/cmdline/state.c index 47a044f0..5ec8e4c0 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -5845,7 +5845,7 @@ int state_snapshot_new(struct snapraid_state* state) int ret; /* check if it supports snapshot */ - if (fssnapshot(disk->mount_point, root, sizeof(root)) != 0) + if (fssnapshot(disk->mount_point, root, sizeof(root), &disk->snapshot_magic) != 0) continue; size_t root_len = strlen(root); @@ -5861,13 +5861,13 @@ int state_snapshot_new(struct snapraid_state* state) } /* delete a potential previous pending snapshot */ - if (fssnapshot_delete(container, SNAPSHOT_PENDING) != 0) { + if (fssnapshot_delete(disk->snapshot_magic, container, SNAPSHOT_PENDING) != 0) { log_fatal(errno, "Failed to delete pending snapshot in '%s'. %s.\n", container, strerror(errno)); return -1; } /* create a new snapshot */ - if (fssnapshot_create(root, container, SNAPSHOT_PENDING) != 0) { + if (fssnapshot_create(disk->snapshot_magic, root, container, SNAPSHOT_PENDING) != 0) { log_fatal(errno, "Failed to create pending snapshot '%s'. %s.\n", container, strerror(errno)); return -1; } @@ -5908,13 +5908,13 @@ int state_snapshot_commit(struct snapraid_state* state) pathcat(container, sizeof(container), SNAPSHOT_CONTAINER); /* delete a potential previous stable snapshot */ - if (fssnapshot_delete(container, SNAPSHOT_STABLE) != 0) { + if (fssnapshot_delete(disk->snapshot_magic, container, SNAPSHOT_STABLE) != 0) { log_fatal(errno, "Failed to delete stable snapshot in '%s'. %s.\n", container, strerror(errno)); return -1; } /* rename pending to stable */ - if (fssnapshot_rename(container, SNAPSHOT_PENDING, SNAPSHOT_STABLE) != 0) { + if (fssnapshot_rename(disk->snapshot_magic, container, SNAPSHOT_PENDING, SNAPSHOT_STABLE) != 0) { log_fatal(errno, "Failed to rename snapshot in '%s'. %s.\n", container, strerror(errno)); return -1; } @@ -5937,7 +5937,7 @@ void state_snapshot_read(struct snapraid_state* state) char vol[PATH_MAX]; /* check if it supports snapshot */ - if (fssnapshot(disk->mount_point, root, sizeof(root)) != 0) + if (fssnapshot(disk->mount_point, root, sizeof(root), &disk->snapshot_magic) != 0) continue; size_t root_len = strlen(root); @@ -5982,7 +5982,7 @@ void state_snapshot_write(struct snapraid_state* state, tommy_list* filterlist_d char vol[PATH_MAX]; /* check if it supports snapshot */ - if (fssnapshot(disk->mount_point, root, sizeof(root)) != 0) + if (fssnapshot(disk->mount_point, root, sizeof(root), &disk->snapshot_magic) != 0) continue; size_t root_len = strlen(root); diff --git a/cmdline/unix.c b/cmdline/unix.c index ae9a25e5..e816df06 100644 --- a/cmdline/unix.c +++ b/cmdline/unix.c @@ -29,6 +29,7 @@ int exit_failure = 1; int exit_sync_needed = 2; #define BTRFS_SUPER_MAGIC 0x9123683E +#define BCACHEFS_SUPER_MAGIC 0xCA451A4E #if HAVE_POSIX_FADVISE /** @@ -125,6 +126,7 @@ const char* stat_desc(struct stat* st) return "unknown"; } +#if HAVE_LINUX_DEVICE static const char* smartctl_paths[] = { /* Linux & BSD */ "/usr/sbin/smartctl", @@ -132,22 +134,24 @@ static const char* smartctl_paths[] = { "/usr/local/sbin/smartctl", "/usr/bin/smartctl", "/usr/local/bin/smartctl", +#ifdef __APPLE__ /* macOS (Intel & Apple Silicon) */ "/opt/homebrew/sbin/smartctl", +#endif 0 }; -const char* find_smartctl(void) +static const char* find_smartctl(void) { - int i; - - for (i = 0; smartctl_paths[i]; ++i) { - if (eaccess(smartctl_paths[i], X_OK) == 0) + for (int i = 0; smartctl_paths[i]; ++i) { + if (eaccess(smartctl_paths[i], X_OK) == 0) { return smartctl_paths[i]; + } } return 0; } +#endif /** * Read a file from sys @@ -986,6 +990,66 @@ static int devuuid_btrfs(uint64_t device, const char* dir, char* uuid, size_t uu } #endif +#if HAVE_LINUX_DEVICE +struct bch_ioctl_query_uuid { + __u8 uuid[16]; +}; + +#define BCH_IOCTL_QUERY_UUID _IOR(0xbc, 1, struct bch_ioctl_query_uuid) + +static int devuuid_bcachefs(uint64_t device, const char* dir, char* uuid, size_t uuid_size) +{ + 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 != BCACHEFS_SUPER_MAGIC) { + close(fd); + return -1; + } + + struct bch_ioctl_query_uuid info; + memset(&info, 0, sizeof(info)); + if (ioctl(fd, BCH_IOCTL_QUERY_UUID, &info) < 0) { + /* LCOV_EXCL_START */ + close(fd); + return -1; + /* LCOV_EXCL_STOP */ + } + + close(fd); + + snprintf(uuid, uuid_size, + "%02x%02x%02x%02x-" + "%02x%02x-" + "%02x%02x-" + "%02x%02x-" + "%02x%02x%02x%02x%02x%02x", + info.uuid[0], info.uuid[1], info.uuid[2], info.uuid[3], + info.uuid[4], info.uuid[5], + info.uuid[6], info.uuid[7], + info.uuid[8], info.uuid[9], + info.uuid[10], info.uuid[11], info.uuid[12], info.uuid[13], info.uuid[14], info.uuid[15] + ); + + log_tag("uuid:by-bcachefs:%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; @@ -1001,6 +1065,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_bcachefs(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; @@ -1142,7 +1211,6 @@ int filephy(const char* path, uint64_t size, uint64_t* physical) #define BEFS_SUPER_MAGIC 0x42465331 #define BFS_MAGIC 0x1BADFACE #define BINFMTFS_MAGIC 0x42494e4d -#define BTRFS_SUPER_MAGIC 0x9123683E #define CGROUP_SUPER_MAGIC 0x27e0eb #define CIFS_MAGIC_NUMBER 0xFF534D42 #define CODA_SUPER_MAGIC 0x73757245 @@ -1246,6 +1314,7 @@ struct filesystem_entry { { BFS_MAGIC, "bfs", 0 }, { BINFMTFS_MAGIC, "binfmt_misc", 0 }, { BTRFS_SUPER_MAGIC, "btrfs", 0 }, + { BCACHEFS_SUPER_MAGIC, "bcachefs", 0 }, { CEPH_SUPER_MAGIC, "ceph", 1 }, { CGROUP_SUPER_MAGIC, "cgroupfs", 0 }, { CIFS_MAGIC_NUMBER, "cifs", 1 }, @@ -1465,12 +1534,40 @@ 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 +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 + +int fssnapshot(const char* path, char* root, size_t root_size, uint32_t* magic) { #if HAVE_LINUX_DEVICE struct statfs sfs; struct stat st; char current_path[PATH_MAX]; + uint64_t root_inode; pathcpy(current_path, sizeof(current_path), path); @@ -1481,9 +1578,15 @@ int fssnapshot(const char* path, char* root, size_t root_size) /* LCOV_EXCL_STOP */ } - /* only btrfs */ - if (sfs.f_type != BTRFS_SUPER_MAGIC) + 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) { @@ -1494,10 +1597,10 @@ int fssnapshot(const char* path, char* root, size_t root_size) /* LCOV_EXCL_STOP */ } - /* btrfs reserved inode 256 for subvolume roots */ - if (st.st_ino == 256) { + if (st.st_ino == root_inode) { /* copy the subvol root */ pathcpy(root, root_size, current_path); + *magic = sfs.f_type; return 0; } @@ -1515,13 +1618,14 @@ int fssnapshot(const char* path, char* root, size_t root_size) (void)path; (void)root; (void)root_size; + (void)magic; return -1; #endif } -int fssnapshot_create(const char* source, const char* parent_dir, const char* name) -{ #if HAVE_LINUX_DEVICE +int fssnapshot_btrfs_create(const char* source, const char* parent_dir, const char* name) +{ struct btrfs_ioctl_vol_args_v2 args; int fd_source; int fd_dest_parent; @@ -1562,7 +1666,53 @@ int fssnapshot_create(const char* source, const char* parent_dir, const char* na close(fd_dest_parent); return 0; +} +#endif + +#if HAVE_LINUX_DEVICE +static int fssnapshot_bcachefs_create(const char* source, const char* parent_dir, const char* name) +{ + char dst_path[PATH_MAX]; + char cmd[PATH_MAX * 3 + 64]; + const char* bcachefs; + int ret; + + bcachefs = find_bcachefs(); + if (!bcachefs) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + pathcpy(dst_path, sizeof(dst_path), parent_dir); + pathcatc(dst_path, sizeof(dst_path), '/'); + pathcat(dst_path, sizeof(dst_path), name); + + snprintf(cmd, sizeof(cmd), "%s subvolume snapshot -r %s %s", bcachefs, source, dst_path); + + ret = system(cmd); + if (ret != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + return 0; +} +#endif + +int fssnapshot_create(uint32_t magic, const char* source, const char* parent_dir, const char* name) +{ +#if HAVE_LINUX_DEVICE + if (magic == BTRFS_SUPER_MAGIC) { + return fssnapshot_btrfs_create(source, parent_dir, name); + } else if (magic == BCACHEFS_SUPER_MAGIC) { + return fssnapshot_bcachefs_create(source, parent_dir, name); + } else { + return -1; + } #else + (void)magic; (void)source; (void)parent_dir; (void)name; @@ -1570,9 +1720,9 @@ int fssnapshot_create(const char* source, const char* parent_dir, const char* na #endif } -int fssnapshot_delete(const char* parent_dir, const char* name) -{ #if HAVE_LINUX_DEVICE +int fssnapshot_btrfs_delete(const char* parent_dir, const char* name) +{ struct btrfs_ioctl_vol_args args; int fd_parent; int ret; @@ -1588,7 +1738,6 @@ int fssnapshot_delete(const char* parent_dir, const char* name) pathcpy(args.name, BTRFS_PATH_NAME_MAX, name); ret = ioctl(fd_parent, BTRFS_IOC_SNAP_DESTROY, &args); - if (ret < 0 && errno != ENOENT) { /* LCOV_EXCL_START */ close(fd_parent); @@ -1598,16 +1747,70 @@ int fssnapshot_delete(const char* parent_dir, const char* name) close(fd_parent); return 0; +} +#endif + +#if HAVE_LINUX_DEVICE +static int fssnapshot_bcachefs_delete(const char* parent_dir, const char* name) +{ + char target_path[PATH_MAX]; + char cmd[PATH_MAX + 64]; + const char* bcachefs; + int ret; + + bcachefs = find_bcachefs(); + if (!bcachefs) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + pathcpy(target_path, sizeof(target_path), parent_dir); + pathcatc(target_path, sizeof(target_path), '/'); + pathcat(target_path, sizeof(target_path), name); + + if (access(target_path, F_OK) < 0) { + if (errno == ENOENT) + return 0; + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + 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 */ + } + + return 0; +} +#endif + +int fssnapshot_delete(uint32_t magic, const char* parent_dir, const char* name) +{ +#if HAVE_LINUX_DEVICE + if (magic == BTRFS_SUPER_MAGIC) { + return fssnapshot_btrfs_delete(parent_dir, name); + } else if (magic == BCACHEFS_SUPER_MAGIC) { + return fssnapshot_bcachefs_delete(parent_dir, name); + } else { + return -1; + } #else + (void)magic; (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 +static int fssnapshot_btrfs_rename(const char* parent_dir, const char* old_name, const char* new_name) +{ char old_path[PATH_MAX]; char new_path[PATH_MAX]; @@ -1625,7 +1828,46 @@ int fssnapshot_rename(const char* parent_dir, const char* old_name, const char* } return 0; +} +#endif + +#if HAVE_LINUX_DEVICE +static int fssnapshot_bcachefs_rename(const char* parent_dir, const char* old_name, const char* new_name) +{ + char old_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); + + if (fssnapshot_bcachefs_create(old_path, parent_dir, new_name) != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + if (fssnapshot_bcachefs_delete(parent_dir, old_name) != 0) { + /* LCOV_EXCL_START */ + return -1; + /* LCOV_EXCL_STOP */ + } + + return 0; +} +#endif + +int fssnapshot_rename(uint32_t magic, const char* parent_dir, const char* old_name, const char* new_name) +{ +#if HAVE_LINUX_DEVICE + if (magic == BTRFS_SUPER_MAGIC) { + return fssnapshot_btrfs_rename(parent_dir, old_name, new_name); + } else if (magic == BCACHEFS_SUPER_MAGIC) { + return fssnapshot_bcachefs_rename(parent_dir, old_name, new_name); + } else { + return -1; + } #else + (void)magic; (void)parent_dir; (void)old_name; (void)new_name; diff --git a/doc/snapraid.1 b/doc/snapraid.1 index 8fd8afb6..39e4aaf4 100644 --- a/doc/snapraid.1 +++ b/doc/snapraid.1 @@ -1434,18 +1434,18 @@ 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. .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. +This option applies exclusively to data disks formatted with the +Btrfs or Bcachefs 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 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 -Only Btrfs is supported. Snapshots are stored in a hidden \'.snapraid\' -directory at the root of the Btrfs subvolume containing your data. -SnapRAID will automatically detect the subvolume root for each data path. +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 @@ -1833,7 +1833,9 @@ fix other failed disks. .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. +the live filesystem. +.PP +At present, this is supported exclusively on Btrfs and Bcachefs. .PP You can mix data disks with different filesystems. Only those that support snapshots will utilize them, while other disks will continue diff --git a/doc/snapraid.d b/doc/snapraid.d index ba28f521..2aa5e02e 100644 --- a/doc/snapraid.d +++ b/doc/snapraid.d @@ -1154,18 +1154,18 @@ Configuration 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 - filesystem. Parity disks, or data disks using other filesystems, will - always use the live version of the filesystem. + This option applies exclusively to data disks formatted with the + Btrfs or Bcachefs 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 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 the Btrfs subvolume containing your data. - SnapRAID will automatically detect the subvolume root for each data path. + 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 @@ -1445,7 +1445,9 @@ Snapshots 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. + the live filesystem. + + At present, this is supported exclusively on Btrfs and Bcachefs. You can mix data disks with different filesystems. Only those that support snapshots will utilize them, while other disks will continue diff --git a/doc/snapraid.txt b/doc/snapraid.txt index dcae69ce..a7798e15 100644 --- a/doc/snapraid.txt +++ b/doc/snapraid.txt @@ -1232,18 +1232,18 @@ 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 -filesystem. Parity disks, or data disks using other filesystems, will -always use the live version of the filesystem. +This option applies exclusively to data disks formatted with the +Btrfs or Bcachefs 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 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 the Btrfs subvolume containing your data. -SnapRAID will automatically detect the subvolume root for each data path. +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 @@ -1550,7 +1550,9 @@ Recovery - If a file is updated or deleted from the live filesystem, 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. +the live filesystem. + +At present, this is supported exclusively on Btrfs and Bcachefs. You can mix data disks with different filesystems. Only those that support snapshots will utilize them, while other disks will continue diff --git a/snapraid.conf.example b/snapraid.conf.example index f3d8649f..946b3a06 100644 --- a/snapraid.conf.example +++ b/snapraid.conf.example @@ -50,18 +50,18 @@ extra boot / # 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 -# filesystem. Parity disks, or data disks using other filesystems, will -# always use the live version of the filesystem. +# This option applies exclusively to data disks formatted with the +# Btrfs or Bcachefs 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 # 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 the Btrfs subvolume containing your data. -# SnapRAID will automatically detect the subvolume root for each data path. +# 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 diff --git a/test/test-btrfs.conf b/test/test-btrfs.conf deleted file mode 100644 index 51ab2516..00000000 --- a/test/test-btrfs.conf +++ /dev/null @@ -1,9 +0,0 @@ -blocksize 64 -parity bench/btrfs-parity -content bench/btrfs-content -content bench/btrfs-1-content -disk btrfs1 bench/btrfs1/ -disk btrfs2 bench/btrfs2/ -disk btrfs3 bench/btrfs3/ -snapshot -exclude .snapraid/ diff --git a/test/test-snap.conf b/test/test-snap.conf new file mode 100644 index 00000000..268c4aa8 --- /dev/null +++ b/test/test-snap.conf @@ -0,0 +1,9 @@ +blocksize 64 +parity bench/snap-parity +content bench/snap-content +content bench/snap-1-content +disk snap1 bench/snap1/ +disk snap2 bench/snap2/ +disk snap3 bench/snap3/ +snapshot +exclude .snapraid/