diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index f34131ad..92566130 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -29,6 +29,8 @@ on: env: TERM: xterm + BTRFS_IMG: test/rmw-btrfs-test.img + BCACHEFS_IMG: test/rmw-bcachefs-test.img jobs: build: @@ -180,14 +182,27 @@ jobs: uses: actions/cache@v5 id: btrfs-img-cache with: - path: test/rmw-btrfs-test.img + path: ${{ env.BTRFS_IMG }} key: ${{ hashFiles('test/rmw-btrfs-test.img.sha256sum') }} - if: ${{ steps.btrfs-img-cache.outputs.cache-hit != 'true' }} - run: curl -L -o test/rmw-btrfs-test.img 'https://www.dropbox.com/scl/fi/57g3ixd3w3tuz4qoc2zp1/rmw-btrfs-test.img?rlkey=yc7krtntswsa1bwz0sbugy4gi&st=hkgrht05&dl=0' + run: curl -L -o "$BTRFS_IMG" 'https://www.dropbox.com/scl/fi/57g3ixd3w3tuz4qoc2zp1/rmw-btrfs-test.img?rlkey=yc7krtntswsa1bwz0sbugy4gi&st=hkgrht05&dl=0' - name: Test btrfs image existence - run: test -f test/rmw-btrfs-test.img + run: test -f "$BTRFS_IMG" + + - name: Cache bcachefs Image + uses: actions/cache@v5 + id: bcachefs-img-cache + with: + path: ${{ env.BCACHEFS_IMG }} + key: ${{ hashFiles('test/rmw-bcachefs-test.img.sha256sum') }} + + - if: ${{ steps.bcachefs-img-cache.outputs.cache-hit != 'true' }} + run: curl -L -o "$BCACHEFS_IMG" 'https://www.dropbox.com/scl/fi/key20cj08ca9engqt5kzi/rmw-bcachefs-test.img?rlkey=th6zw1ar7t0dy9m7qzah5wvq9&st=nrwczcqn&dl=0' + + - name: Test bcachefs image existence + run: test -f "$BCACHEFS_IMG" - name: Install Dependencies run: | diff --git a/.gitignore b/.gitignore index 8db05af7..efe5d6c2 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ subprojects/** !subprojects/**/*.wrap /packaging/appimage/.env /test/rmw-btrfs-test.img +/test/rmw-bcachefs-test.img diff --git a/ChangeLog b/ChangeLog index 04b37db8..16ff2c66 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,19 @@ +rmw (in-progress): + * BREAKING: The meson build option `want_btrfs_clone` has been renamed to + `want_ficlone`. Update any build scripts that pass `-Dwant_btrfs_clone=` + to use `-Dwant_ficlone=` instead. + * refactor: Rename btrfs.c/btrfs.h to ficlone.c/ficlone.h; rename + do_btrfs_clone() to do_ficlone() and is_btrfs() to is_ficlone_fs(); + rename HAVE_LINUX_BTRFS macro to HAVE_FICLONE + * Add bcachefs test (test_bcachefs.sh); uses a pre-existing image and + skips if kernel bcachefs support or the image is absent + * docs: Add BTRFS AND BCACHEFS section to man page + * bugfix: Fix "Invalid cross-device link" error on bcachefs when source and + waste folder are on separate subvolumes (#526); use generic FICLONE ioctl + (linux/fs.h) instead of BTRFS_IOC_CLONE, and fall back to 'mv' when + rename() returns EXDEV on a same-device move + 2026-04-07 - rmw 0.9.5: diff --git a/README.md b/README.md index 20151473..d955231e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# rmw-0.9.5 +# rmw-0.10.0-dev ## Description rmw (ReMove to Waste) is a trashcan/recycle bin utility for the command line. diff --git a/man/rmw.1 b/man/rmw.1 index d9304afe..38ac1966 100644 --- a/man/rmw.1 +++ b/man/rmw.1 @@ -192,6 +192,17 @@ directory will not be used for the current run of rmw. With the media mounted, once you manually create the waste directory for that device (e.g. "/mnt/flash/.Trash-$UID") and run rmw, it will automatically create the two required child directories "files" and "info". +.SS BTRFS AND BCACHEFS +rmw supports moving files across subvolumes on btrfs and bcachefs +filesystems. Because the kernel treats subvolumes as separate +filesystems, a cross-subvolume move would normally fail; rmw detects +this case and performs a copy-then-delete instead, preserving the +expected trash semantics. + +To use this feature, define a WASTE directory on the destination +subvolume in your configuration file. For example, if your waste +directory is on a different subvolume than the files you want to +remove, rmw will handle the move transparently. .SH EXAMPLES .SS RESTORING rmw -z ~/.local/share/Waste/files/foo diff --git a/meson.build b/meson.build index 9180b842..6267c18e 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'rmw', 'c', - version: '0.9.5', + version: '0.10.0-dev', meson_version: '>= 0.59.0', default_options: [ 'c_std=gnu99', diff --git a/meson_options.txt b/meson_options.txt index db9047ae..22305d0e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,10 +1,10 @@ option('build_tests', type : 'boolean', value : true, description : 'build tests') option( - 'want_btrfs_clone', + 'want_ficlone', type : 'boolean', value: 'true', - description: 'Include support for cloning files between btrfs root volumes and subvolumes') + description: 'Include support for cloning files between subvolumes using FICLONE (btrfs, bcachefs, xfs, etc.)') option('nls', type : 'boolean', value : true, description : 'include native language support (install translations)') diff --git a/src/config_rmw.c b/src/config_rmw.c index 06798f7a..9081f6fe 100644 --- a/src/config_rmw.c +++ b/src/config_rmw.c @@ -20,7 +20,7 @@ along with this program. If not, see . #include -#include "btrfs.h" +#include "ficlone.h" #include "config_rmw.h" #include "utils.h" #include "main.h" @@ -275,7 +275,7 @@ parse_line_waste(st_waste *waste_curr, struct Canfigger *node, else if (p_state == -1) exit(p_state); - waste_curr->is_btrfs = is_btrfs(waste_curr->parent); + waste_curr->is_ficlone_fs = is_ficlone_fs(waste_curr->parent); // get device number to use later for rename struct stat st, mp_st; diff --git a/src/btrfs.c b/src/ficlone.c similarity index 90% rename from src/btrfs.c rename to src/ficlone.c index 6312c3e8..38b4021a 100644 --- a/src/btrfs.c +++ b/src/ficlone.c @@ -23,22 +23,22 @@ along with this program. If not, see . #include "globals.h" #endif -#ifdef HAVE_LINUX_BTRFS +#ifdef HAVE_FICLONE #include -#include +#include #include #include #include #include #endif -#include "btrfs.h" +#include "ficlone.h" #include "messages.h" bool -is_btrfs(const char *path) +is_ficlone_fs(const char *path) { -#ifdef HAVE_LINUX_BTRFS +#ifdef HAVE_FICLONE struct statfs buf; if (statfs(path, &buf) == -1) @@ -57,9 +57,9 @@ is_btrfs(const char *path) int -do_btrfs_clone(const char *source, const char *dest, int *save_errno) +do_ficlone(const char *source, const char *dest, int *save_errno) { -#ifdef HAVE_LINUX_BTRFS +#ifdef HAVE_FICLONE int src_fd, dest_fd; struct stat src_stat; @@ -91,7 +91,7 @@ do_btrfs_clone(const char *source, const char *dest, int *save_errno) return dest_fd; } - int res = ioctl(dest_fd, BTRFS_IOC_CLONE, src_fd); + int res = ioctl(dest_fd, FICLONE, src_fd); *save_errno = errno; close(src_fd); diff --git a/src/btrfs.h b/src/ficlone.h similarity index 85% rename from src/btrfs.h rename to src/ficlone.h index 9da4430f..27f1bdf0 100644 --- a/src/btrfs.h +++ b/src/ficlone.h @@ -18,15 +18,15 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -#ifndef _INC_BTRFS_H -#define _INC_BTRFS_H +#ifndef _INC_FICLONE_H +#define _INC_FICLONE_H #include #define BTRFS_SUPER_MAGIC 0x9123683E -bool is_btrfs(const char *path); +bool is_ficlone_fs(const char *path); -int do_btrfs_clone(const char *source, const char *dest, int *save_errno); +int do_ficlone(const char *source, const char *dest, int *save_errno); #endif diff --git a/src/main.c b/src/main.c index 0aa4da14..9a48fcf8 100644 --- a/src/main.c +++ b/src/main.c @@ -26,7 +26,7 @@ along with this program. If not, see . #include "purging.h" #include "strings_rmw.h" #include "messages.h" -#include "btrfs.h" +#include "ficlone.h" #include "trashinfo.h" @@ -387,7 +387,7 @@ damage of 5000 hp. You feel satisfied.\n")); while (waste_curr != NULL) { if (waste_curr->dev_num == st_target.dev_num || - (waste_curr->is_btrfs && is_btrfs(argv[file_arg]))) + (waste_curr->is_ficlone_fs && is_ficlone_fs(argv[file_arg]))) { char *tmp_str = join_paths(waste_curr->files, st_target.base_name); // *st_target.waste_dest_name = '\0'; @@ -421,7 +421,7 @@ damage of 5000 hp. You feel satisfied.\n")); if (!S_ISDIR(st_file_arg.st_mode)) { /* attempt btrfs clone if not a directory */ - r_result = do_btrfs_clone(src, dst, &save_errno); + r_result = do_ficlone(src, dst, &save_errno); errno = save_errno; } else @@ -448,7 +448,13 @@ damage of 5000 hp. You feel satisfied.\n")); { /* same device: simple rename */ r_result = rename(src, dst); - /* rename sets errno on failure */ + if (r_result != 0 && errno == EXDEV) + { + /* rename failed with EXDEV even though st_dev matched (e.g., + bcachefs cross-subvolume). Fall back to copy+delete. */ + r_result = safe_mv_via_exec(src, dst, &save_errno); + errno = save_errno; + } } } @@ -702,10 +708,10 @@ main(const int argc, char *const argv[]) printf("PATH_MAX = %d\n", PATH_MAX); if (verbose > 0) -#ifdef HAVE_LINUX_BTRFS - puts("btrfs_clone support: true"); +#ifdef HAVE_FICLONE + puts("ficlone support: true"); #else - puts("btrfs_clone support: false"); + puts("ficlone support: false"); #endif const st_loc *st_location = get_locations(cli_user_options.alt_config_file); diff --git a/src/meson.build b/src/meson.build index d9a5ce5d..8fe94b0b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -23,7 +23,7 @@ endif src = [ 'globals.c', - 'btrfs.c', + 'ficlone.c', 'restore.c', 'config_rmw.c', 'parse_cli_options.c', @@ -35,26 +35,26 @@ src = [ 'utils.c', ] -# Used for btrfs functions +# Used for FICLONE support has_statfs = false -has_btrfs_header = false +has_linux_fs_header = false if host_sys == 'linux' - if get_option('want_btrfs_clone') + if get_option('want_ficlone') has_statfs = cc.has_function( 'statfs', prefix: '#include ', ) - has_btrfs_header = cc.has_header('linux/btrfs.h') - if has_statfs and has_btrfs_header - conf.set('HAVE_LINUX_BTRFS', 1) + has_linux_fs_header = cc.has_header('linux/fs.h') + if has_statfs and has_linux_fs_header + conf.set('HAVE_FICLONE', 1) else error( ''' - : Requirements not met for btrfs clone support. - : If missing linux/btrfs.h, you probably need to install the linux-headers package. - : To build without btrfs clone support and skip this check, add - : "-Dwant_btrfs_clone=false" to the meson setup options.''', + : Requirements not met for reflink clone support. + : If missing linux/fs.h, you probably need to install the linux-headers package. + : To build without reflink clone support and skip this check, add + : "-Dwant_ficlone=false" to the meson setup options.''', ) endif endif diff --git a/src/restore.c b/src/restore.c index 5752b006..4654080c 100644 --- a/src/restore.c +++ b/src/restore.c @@ -30,7 +30,7 @@ along with this program. If not, see . #include #include "parse_cli_options.h" -#include "btrfs.h" +#include "ficlone.h" #include "restore.h" #include "utils.h" #include "messages.h" @@ -108,7 +108,7 @@ move_back(const char *src, const char *dest, bool want_dry_run) else { /* regular file on different device -> try btrfs clone */ - int clone_res = do_btrfs_clone(src, dest, &clone_errno); + int clone_res = do_ficlone(src, dest, &clone_errno); if (clone_res == 0) { errno = 0; diff --git a/src/trashinfo.h b/src/trashinfo.h index 342c8c8a..7f32c9e8 100644 --- a/src/trashinfo.h +++ b/src/trashinfo.h @@ -67,7 +67,7 @@ struct st_waste */ bool removable; - bool is_btrfs; + bool is_ficlone_fs; }; diff --git a/test/COMMON b/test/COMMON index 7eb78c3a..dd3b9bf1 100644 --- a/test/COMMON +++ b/test/COMMON @@ -1,6 +1,8 @@ # shellcheck shell=sh # included by test scripts +SKIP=77 + create_some_files() { mkdir -p somefiles/topdir/dir1/dir2/dir3 diff --git a/test/conf/bcachefs_img.testrc b/test/conf/bcachefs_img.testrc new file mode 100644 index 00000000..72b2c0b0 --- /dev/null +++ b/test/conf/bcachefs_img.testrc @@ -0,0 +1,4 @@ +waste = /tmp/rmw-bcachefs-loop/@two/Waste +WASTE = $HOME/.local/share/Waste + +expire_age = 45 diff --git a/test/meson.build b/test/meson.build index 95f4c7c0..a0cb7c36 100644 --- a/test/meson.build +++ b/test/meson.build @@ -18,8 +18,11 @@ scripts = [ 'test_restore.sh', ] -if has_statfs and has_btrfs_header - scripts += ['test_btrfs_clone.sh'] +if host_sys == 'linux' + if has_statfs and has_linux_fs_header + scripts += ['test_btrfs_clone.sh'] + endif + scripts += ['test_bcachefs.sh'] endif RMW_FAKE_HOME = join_paths(meson.current_build_dir(), 'rmw-tests-home') @@ -54,4 +57,3 @@ foreach s : scripts depends: main_bin, ) endforeach - diff --git a/test/rmw-bcachefs-test.img.sha256sum b/test/rmw-bcachefs-test.img.sha256sum new file mode 100644 index 00000000..e8ad504c --- /dev/null +++ b/test/rmw-bcachefs-test.img.sha256sum @@ -0,0 +1 @@ +bc7bcbd2800230995ba36394f1814e83bee2eeca579691420b7feeff653cbf7c rmw-bcachefs-test.img diff --git a/test/test_bcachefs.sh b/test/test_bcachefs.sh new file mode 100755 index 00000000..5ea5e964 --- /dev/null +++ b/test/test_bcachefs.sh @@ -0,0 +1,110 @@ +#!/bin/sh +set -ve + +if [ -e COMMON ]; then + . ./COMMON +else + . "${MESON_SOURCE_ROOT}/test/COMMON" +fi + +BCACHEFS_MOUNTPOINT="/tmp/rmw-bcachefs-loop" +BCACHEFS_IMAGE="${MESON_SOURCE_ROOT}/test/rmw-bcachefs-test.img" + +if ! command -v bcachefs >/dev/null 2>&1; then + echo "bcachefs userspace tools not found; skipping." + exit $SKIP +fi + +if ! sudo -n true 2>/dev/null; then + echo "sudo not available without password; skipping bcachefs test." + exit $SKIP +fi + +# Load the module if available, then confirm kernel support +sudo modprobe bcachefs 2>/dev/null || true +if ! grep -qw 'bcachefs' /proc/filesystems 2>/dev/null; then + echo "bcachefs not supported by kernel; skipping." + exit $SKIP +fi + +if [ ! -f "$BCACHEFS_IMAGE" ]; then + echo "bcachefs test image not found at $BCACHEFS_IMAGE; skipping." + exit $SKIP +fi + +if [ ! -d "$BCACHEFS_MOUNTPOINT" ]; then + sudo mkdir "$BCACHEFS_MOUNTPOINT" +fi + +if mountpoint -q "$BCACHEFS_MOUNTPOINT" 2>/dev/null; then + echo "bcachefs mountpoint already mounted from a previous run; cleaning up." + sudo umount "$BCACHEFS_MOUNTPOINT" +fi + +STALE_LOOP=$(sudo losetup -j "$BCACHEFS_IMAGE" -O NAME --noheadings 2>/dev/null) +if [ -n "$STALE_LOOP" ]; then + echo "bcachefs image already on loop device $STALE_LOOP from a previous run; cleaning up." + sudo losetup -d "$STALE_LOOP" +fi + +LOOP=$(sudo losetup -f --show "$BCACHEFS_IMAGE") +sudo mount -t bcachefs "$LOOP" "$BCACHEFS_MOUNTPOINT" +if [ ! -d "$BCACHEFS_MOUNTPOINT/@two" ]; then + sudo bcachefs subvolume create "$BCACHEFS_MOUNTPOINT/@two" +fi +sudo chown "$(id -u)" -R "$BCACHEFS_MOUNTPOINT" + +cd "$BCACHEFS_MOUNTPOINT" +BCACHEFS_RMW_CMD="$BIN_DIR/rmw -c ${MESON_SOURCE_ROOT}/test/conf/bcachefs_img.testrc" + +BCACHEFS_SUBVOLUME="$BCACHEFS_MOUNTPOINT/@two" +BCACHEFS_WASTE_DIR="$BCACHEFS_SUBVOLUME/Waste" + +rm -rf "$BCACHEFS_WASTE_DIR" + +# --- Test: move a file across bcachefs subvolumes (issue #526) --- +echo "== Test: move a file across bcachefs subvolumes" +touch foo +$BCACHEFS_RMW_CMD foo +test ! -f foo +test -f "$BCACHEFS_WASTE_DIR/files/foo" +test -f "$BCACHEFS_WASTE_DIR/info/foo.trashinfo" + +echo "== Test: restore the moved file" +$BCACHEFS_RMW_CMD -u +test -f foo +test ! -f "$BCACHEFS_WASTE_DIR/info/foo.trashinfo" + +# --- Test: move a directory across bcachefs subvolumes --- +echo "== Test: move a directory across bcachefs subvolumes" +BCACHEFS_TEST_DIR="$BCACHEFS_MOUNTPOINT/test_dir" +rm -rf "$BCACHEFS_TEST_DIR" +mkdir "$BCACHEFS_TEST_DIR" +touch "$BCACHEFS_TEST_DIR/bar" +$BCACHEFS_RMW_CMD "$BCACHEFS_TEST_DIR" +test ! -d "$BCACHEFS_TEST_DIR" +test -d "$BCACHEFS_WASTE_DIR/files/test_dir" +test -f "$BCACHEFS_WASTE_DIR/files/test_dir/bar" +test -f "$BCACHEFS_WASTE_DIR/info/test_dir.trashinfo" + +echo "== Test: restore the moved directory" +$BCACHEFS_RMW_CMD -u +test -d "$BCACHEFS_TEST_DIR" +test -f "$BCACHEFS_TEST_DIR/bar" +test ! -f "$BCACHEFS_WASTE_DIR/info/test_dir.trashinfo" + +# --- Test: purge an expired file from bcachefs waste --- +echo "== Test: purge an expired file from bcachefs waste" +RMW_FAKE_YEAR=true $BCACHEFS_RMW_CMD foo +test -f "$BCACHEFS_WASTE_DIR/files/foo" +test -f "$BCACHEFS_WASTE_DIR/info/foo.trashinfo" +$BCACHEFS_RMW_CMD -g +test ! -f "$BCACHEFS_WASTE_DIR/files/foo" +test ! -f "$BCACHEFS_WASTE_DIR/info/foo.trashinfo" + +cd + +sudo umount "$BCACHEFS_MOUNTPOINT" +sudo losetup -d "$LOOP" + +exit 0