Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions .github/workflows/c-cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ on:

env:
TERM: xterm
BTRFS_IMG: test/rmw-btrfs-test.img
BCACHEFS_IMG: test/rmw-bcachefs-test.img

jobs:
build:
Expand Down Expand Up @@ -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: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ subprojects/**
!subprojects/**/*.wrap
/packaging/appimage/.env
/test/rmw-btrfs-test.img
/test/rmw-bcachefs-test.img
15 changes: 15 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
11 changes: 11 additions & 0 deletions man/rmw.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
4 changes: 2 additions & 2 deletions meson_options.txt
Original file line number Diff line number Diff line change
@@ -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)')

Expand Down
4 changes: 2 additions & 2 deletions src/config_rmw.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.

#include <unistd.h>

#include "btrfs.h"
#include "ficlone.h"
#include "config_rmw.h"
#include "utils.h"
#include "main.h"
Expand Down Expand Up @@ -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;
Expand Down
16 changes: 8 additions & 8 deletions src/btrfs.c → src/ficlone.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "globals.h"
#endif

#ifdef HAVE_LINUX_BTRFS
#ifdef HAVE_FICLONE
#include <fcntl.h>
#include <linux/btrfs.h>
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <sys/statfs.h>
#include <sys/stat.h>
#include <unistd.h>
#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)
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions src/btrfs.h → src/ficlone.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef _INC_BTRFS_H
#define _INC_BTRFS_H
#ifndef _INC_FICLONE_H
#define _INC_FICLONE_H

#include <stdbool.h>

#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
20 changes: 13 additions & 7 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "purging.h"
#include "strings_rmw.h"
#include "messages.h"
#include "btrfs.h"
#include "ficlone.h"
#include "trashinfo.h"


Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
}
}

Expand Down Expand Up @@ -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);
Expand Down
22 changes: 11 additions & 11 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ endif

src = [
'globals.c',
'btrfs.c',
'ficlone.c',
'restore.c',
'config_rmw.c',
'parse_cli_options.c',
Expand All @@ -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 <sys/statfs.h>',
)
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
Expand Down
4 changes: 2 additions & 2 deletions src/restore.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include <unistd.h>

#include "parse_cli_options.h"
#include "btrfs.h"
#include "ficlone.h"
#include "restore.h"
#include "utils.h"
#include "messages.h"
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/trashinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ struct st_waste
*/
bool removable;

bool is_btrfs;
bool is_ficlone_fs;
};


Expand Down
2 changes: 2 additions & 0 deletions test/COMMON
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# shellcheck shell=sh
# included by test scripts

SKIP=77

create_some_files() {
mkdir -p somefiles/topdir/dir1/dir2/dir3

Expand Down
4 changes: 4 additions & 0 deletions test/conf/bcachefs_img.testrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
waste = /tmp/rmw-bcachefs-loop/@two/Waste
WASTE = $HOME/.local/share/Waste

expire_age = 45
8 changes: 5 additions & 3 deletions test/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -54,4 +57,3 @@ foreach s : scripts
depends: main_bin,
)
endforeach

1 change: 1 addition & 0 deletions test/rmw-bcachefs-test.img.sha256sum
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bc7bcbd2800230995ba36394f1814e83bee2eeca579691420b7feeff653cbf7c rmw-bcachefs-test.img
Loading
Loading