From cff6c4790062699ef8cc77835db51f0593b382d4 Mon Sep 17 00:00:00 2001 From: Maksim Nikulin Date: Sun, 31 May 2026 20:41:52 +0300 Subject: [PATCH] btrbk: add support for deleting directories in snapshots and set them read-only --- btrbk | 73 +++++++++++++++++++++++++++++++++------ doc/btrbk.conf.5.asciidoc | 10 ++++++ ssh_filter_btrbk.sh | 4 +++ 3 files changed, 76 insertions(+), 11 deletions(-) mode change 100755 => 100644 ssh_filter_btrbk.sh diff --git a/btrbk b/btrbk index ddb2eb4..b7d7edb 100755 --- a/btrbk +++ b/btrbk @@ -152,6 +152,7 @@ my %config_options = ( send_compressed_data => { default => undef, accept => [qw( yes no )] }, # NOTE: implies send_protocol=2 snapshot_qgroup_destroy => { default => undef, accept => [qw( yes no )], context => [qw( global volume subvolume )] }, + snapshot_delete_dir => { default => undef, accept_file => { relative => 1 }, allow_multiple => 1, context => [qw( global volume subvolume )] }, target_qgroup_destroy => { default => undef, accept => [qw( yes no )] }, archive_qgroup_destroy => { default => undef, accept => [qw( yes no )], context => [qw( global )] }, @@ -291,10 +292,12 @@ my @btrfs_cmd = ( "btrfs receive", "btrfs filesystem usage", "btrfs qgroup destroy", + "btrfs property set", ); my @system_cmd = ( "readlink", "test", + "rm", ); my %backend_cmd_map = ( "btrfs-progs-btrbk" => { map +( $_ => [ s/ /-/gr ] ), @btrfs_cmd }, @@ -1365,10 +1368,11 @@ sub btrfs_subvolume_find_new($$;$) # returns $target, or undef on error -sub btrfs_subvolume_snapshot($$) +sub btrfs_subvolume_snapshot($$;$) { my $svol = shift || die; my $target_vol = shift // die; + my $readonly = shift // 1; my $target_path = $target_vol->{PATH} // die; my $src_path = $svol->{PATH} // die; INFO "[snapshot] source: $svol->{PRINT}"; @@ -1377,7 +1381,8 @@ sub btrfs_subvolume_snapshot($$) vinfo_prefixed_keys("target", $target_vol), vinfo_prefixed_keys("source", $svol), ); - my $ret = run_cmd(cmd => vinfo_cmd($svol, "btrfs subvolume snapshot", '-r', { unsafe => $src_path }, { unsafe => $target_path } ), + my @ro_flag = $readonly ? ('-r') : (); + my $ret = run_cmd(cmd => vinfo_cmd($svol, "btrfs subvolume snapshot", @ro_flag, { unsafe => $src_path }, { unsafe => $target_path } ), rsh => vinfo_rsh($svol), filter_stderr => \&_btrfs_filter_stderr, ); @@ -1390,6 +1395,23 @@ sub btrfs_subvolume_snapshot($$) } +sub btrfs_subvolume_set_readonly($) +{ + my $vol = shift // die; + my $path = $vol->{PATH} // die; + INFO "[snapshot] set readonly: $vol->{PRINT}"; + my $ret = run_cmd(cmd => vinfo_cmd($vol, "btrfs property set", { unsafe => $path }, 'ro', 'true'), + rsh => vinfo_rsh($vol), + filter_stderr => \&_btrfs_filter_stderr, + ); + unless(defined($ret)) { + ERROR "Failed to set snapshot read-only: $vol->{PRINT}", @stderr; + return undef; + } + return $vol; +} + + sub btrfs_subvolume_delete($@) { my $vol = shift // die; @@ -6697,16 +6719,45 @@ MAIN: # finally create the snapshot INFO "Creating subvolume snapshot for: $svol->{PRINT}"; my $snapshot = vinfo_child($snaproot, "$snapshot_name"); - if(btrfs_subvolume_snapshot($svol, $snapshot)) + my @delete_dirs = @{config_key($svol, "snapshot_delete_dir") // []}; + my $create_rw = scalar(@delete_dirs) ? 1 : 0; + if(btrfs_subvolume_snapshot($svol, $snapshot, !$create_rw)) { - vinfo_inject_child($snaproot, $snapshot, { - parent_uuid => $svol->{node}{uuid}, - received_uuid => '-', - readonly => 1, - FORCE_PRESERVE => 'preserve forced: created just now', - INJECTED_BY => 'snapshot', - }); - $svol->{SNAPSHOT_CREATED} = $snapshot; + my $snapshot_ok = 1; + if($create_rw) { + foreach my $dir (@delete_dirs) { + my $dir_path = $snapshot->{PATH} . '/' . $dir; + INFO "[snapshot] delete dir: $dir_path"; + my $ret = run_cmd(cmd => vinfo_cmd($snapshot, "rm", '-rf', { unsafe => $dir_path } ), + rsh => vinfo_rsh($snapshot), + ); + unless(defined($ret)) { + ERROR "Failed to delete directory \"$dir\" in snapshot: $snapshot->{PRINT}"; + $snapshot_ok = 0; + last; + } + } + if($snapshot_ok) { + unless(btrfs_subvolume_set_readonly($snapshot)) { + $snapshot_ok = 0; + } + } + } + if($snapshot_ok) { + vinfo_inject_child($snaproot, $snapshot, { + parent_uuid => $svol->{node}{uuid}, + received_uuid => '-', + readonly => 1, + FORCE_PRESERVE => 'preserve forced: created just now', + INJECTED_BY => 'snapshot', + }); + $svol->{SNAPSHOT_CREATED} = $snapshot; + } + else { + btrfs_subvolume_delete($snapshot); + ABORTED($svol, "Failed to process snapshot: $svol->{PRINT} -> $snapshot->{PRINT}"); + WARN "Skipping subvolume section: " . ABORTED_TEXT($svol); + } } else { ABORTED($svol, "Failed to create snapshot: $svol->{PRINT} -> $snapshot->{PRINT}"); diff --git a/doc/btrbk.conf.5.asciidoc b/doc/btrbk.conf.5.asciidoc index 04f8e4d..5e80978 100644 --- a/doc/btrbk.conf.5.asciidoc +++ b/doc/btrbk.conf.5.asciidoc @@ -143,6 +143,16 @@ Note that using ``long-iso'' has implications on the scheduling, see btrbk is taking care of snapshot creation). Defaults to ``always''. +*snapshot_delete_dir* :: + Delete directory '' inside newly created snapshots + (relative to the snapshot root). This option can be set multiple + times, and is available in global, volume and subvolume context. ++ +-- +If set, btrbk creates the snapshot as read-write, deletes all +configured directories, and then sets the snapshot to read-only. +-- + *incremental* yes|no|strict:: If set, incremental backups are created. If set to ``strict'', non-incremental (initial) backups are never created, and diff --git a/ssh_filter_btrbk.sh b/ssh_filter_btrbk.sh old mode 100755 new mode 100644 index 0817563..bf18f61 --- a/ssh_filter_btrbk.sh +++ b/ssh_filter_btrbk.sh @@ -147,6 +147,8 @@ while [ "$#" -ge 1 ]; do -s|--source) allow_cmd "${sudo_prefix}btrfs subvolume snapshot" allow_cmd "${sudo_prefix}btrfs send" + allow_exact_cmd "${sudo_prefix}btrfs property set ${file_arg_match} ro true" # set snapshot read-only + allow_exact_cmd "${sudo_prefix}rm -rf ${file_arg_match}" # delete dirs in snapshot ;; -t|--target) @@ -169,6 +171,8 @@ while [ "$#" -ge 1 ]; do --snapshot) allow_cmd "${sudo_prefix}btrfs subvolume snapshot" + allow_exact_cmd "${sudo_prefix}btrfs property set ${file_arg_match} ro true" # set snapshot read-only + allow_exact_cmd "${sudo_prefix}rm -rf ${file_arg_match}" # delete dirs in snapshot ;; --send)