From 45deda1dfa29cb7350327c269c64fd29567463e5 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Tue, 21 Apr 2026 14:49:34 +0200 Subject: [PATCH] btrfs: Add bd_btrfs_delete_subvolume_recursive function Add a new function for deleting btrfs subvolumes with an option for recursive deletion using the --recursive flag (requires btrfs-progs >= 6.12). The existing bd_btrfs_delete_subvolume now delegates to the new function with recursive=FALSE. A new BD_BTRFS_TECH_MODE_DELETE_RECURSIVE tech mode is added and checked in bd_btrfs_is_tech_avail for the SUBVOL tech. Fixes: #1107 Co-Authored-By: Claude Opus 4.6 --- docs/libblockdev-sections.txt | 1 + src/lib/plugin_apis/btrfs.api | 28 +++++++++++++++--- src/plugins/btrfs.c | 53 +++++++++++++++++++++++++++++++---- src/plugins/btrfs.h | 10 ++++--- tests/btrfs_test.py | 35 +++++++++++++++++++++++ 5 files changed, 113 insertions(+), 14 deletions(-) diff --git a/docs/libblockdev-sections.txt b/docs/libblockdev-sections.txt index 4a6996fa0..418bcd83d 100644 --- a/docs/libblockdev-sections.txt +++ b/docs/libblockdev-sections.txt @@ -34,6 +34,7 @@ bd_btrfs_add_device bd_btrfs_remove_device bd_btrfs_create_subvolume bd_btrfs_delete_subvolume +bd_btrfs_delete_subvolume_recursive bd_btrfs_get_default_subvolume_id bd_btrfs_set_default_subvolume bd_btrfs_create_snapshot diff --git a/src/lib/plugin_apis/btrfs.api b/src/lib/plugin_apis/btrfs.api index 385ea16ef..7a2b5445d 100644 --- a/src/lib/plugin_apis/btrfs.api +++ b/src/lib/plugin_apis/btrfs.api @@ -216,10 +216,11 @@ typedef enum { } BDBtrfsTech; typedef enum { - BD_BTRFS_TECH_MODE_CREATE = 1 << 0, - BD_BTRFS_TECH_MODE_DELETE = 1 << 1, - BD_BTRFS_TECH_MODE_MODIFY = 1 << 2, - BD_BTRFS_TECH_MODE_QUERY = 1 << 3, + BD_BTRFS_TECH_MODE_CREATE = 1 << 0, + BD_BTRFS_TECH_MODE_DELETE = 1 << 1, + BD_BTRFS_TECH_MODE_MODIFY = 1 << 2, + BD_BTRFS_TECH_MODE_QUERY = 1 << 3, + BD_BTRFS_TECH_MODE_DELETE_RECURSIVE = 1 << 4, } BDBtrfsTechMode; /** @@ -308,6 +309,25 @@ gboolean bd_btrfs_create_subvolume (const gchar *mountpoint, const gchar *name, */ gboolean bd_btrfs_delete_subvolume (const gchar *mountpoint, const gchar *name, const BDExtraArg **extra, GError **error); +/** + * bd_btrfs_delete_subvolume_recursive: + * @mountpoint: mountpoint of the btrfs volume to delete subvolume from + * @name: name of the subvolume + * @recursive: whether to delete child subvolumes recursively + * @extra: (nullable) (array zero-terminated=1): extra options for the subvolume deletion (right now + * passed to the 'btrfs' utility) + * @error: (out) (optional): place to store error (if any) + * + * Returns: whether the @mountpoint/@name subvolume was successfully deleted or not + * + * If @recursive is %TRUE, all child subvolumes will be deleted before deleting + * the subvolume itself. This requires btrfs-progs >= 6.12. + * + * Tech category: %BD_BTRFS_TECH_SUBVOL-%BD_BTRFS_TECH_MODE_DELETE if @recursive is %FALSE, + * %BD_BTRFS_TECH_SUBVOL-%BD_BTRFS_TECH_MODE_DELETE_RECURSIVE if @recursive is %TRUE + */ +gboolean bd_btrfs_delete_subvolume_recursive (const gchar *mountpoint, const gchar *name, gboolean recursive, const BDExtraArg **extra, GError **error); + /** * bd_btrfs_get_default_subvolume_id: * @mountpoint: mountpoint of the volume to get the default subvolume ID of diff --git a/src/plugins/btrfs.c b/src/plugins/btrfs.c index 419088bee..a4f18fc7f 100644 --- a/src/plugins/btrfs.c +++ b/src/plugins/btrfs.c @@ -116,11 +116,14 @@ static volatile guint avail_module_deps = 0; static GMutex deps_check_lock; #define DEPS_BTRFS 0 +#define DEPS_BTRFS_612 1 #define DEPS_BTRFS_MASK (1 << DEPS_BTRFS) -#define DEPS_LAST 1 +#define DEPS_BTRFS_612_MASK (1 << DEPS_BTRFS_612) +#define DEPS_LAST 2 static const UtilDep deps[DEPS_LAST] = { {"btrfs", BTRFS_MIN_VERSION, NULL, "[Bb]trfs.* v([\\d\\.]+)"}, + {"btrfs", "6.12", NULL, "[Bb]trfs.* v([\\d\\.]+)"}, }; #define MODULE_DEPS_BTRFS 0 @@ -165,10 +168,16 @@ void bd_btrfs_close (void) { * Returns: whether the @tech-@mode combination is available -- supported by the * plugin implementation and having all the runtime dependencies available */ -gboolean bd_btrfs_is_tech_avail (BDBtrfsTech tech G_GNUC_UNUSED, guint64 mode G_GNUC_UNUSED, GError **error) { +gboolean bd_btrfs_is_tech_avail (BDBtrfsTech tech, guint64 mode, GError **error) { /* all tech-mode combinations are supported by this implementation of the plugin */ - return check_deps (&avail_deps, DEPS_BTRFS_MASK, deps, DEPS_LAST, &deps_check_lock, error) && - check_module_deps (&avail_module_deps, MODULE_DEPS_BTRFS_MASK, module_deps, MODULE_DEPS_LAST, &deps_check_lock, error); + if (!check_deps (&avail_deps, DEPS_BTRFS_MASK, deps, DEPS_LAST, &deps_check_lock, error) || + !check_module_deps (&avail_module_deps, MODULE_DEPS_BTRFS_MASK, module_deps, MODULE_DEPS_LAST, &deps_check_lock, error)) + return FALSE; + + if (tech == BD_BTRFS_TECH_SUBVOL && (mode & BD_BTRFS_TECH_MODE_DELETE_RECURSIVE)) + return check_deps (&avail_deps, DEPS_BTRFS_612_MASK, deps, DEPS_LAST, &deps_check_lock, error); + + return TRUE; } static BDBtrfsDeviceInfo* get_device_info_from_match (GMatchInfo *match_info) { @@ -423,19 +432,51 @@ gboolean bd_btrfs_create_subvolume (const gchar *mountpoint, const gchar *name, * Tech category: %BD_BTRFS_TECH_SUBVOL-%BD_BTRFS_TECH_MODE_DELETE */ gboolean bd_btrfs_delete_subvolume (const gchar *mountpoint, const gchar *name, const BDExtraArg **extra, GError **error) { + return bd_btrfs_delete_subvolume_recursive (mountpoint, name, FALSE, extra, error); +} + +/** + * bd_btrfs_delete_subvolume_recursive: + * @mountpoint: mountpoint of the btrfs volume to delete subvolume from + * @name: name of the subvolume + * @recursive: whether to delete child subvolumes recursively + * @extra: (nullable) (array zero-terminated=1): extra options for the subvolume deletion (right now + * passed to the 'btrfs' utility) + * @error: (out) (optional): place to store error (if any) + * + * Returns: whether the @mountpoint/@name subvolume was successfully deleted or not + * + * If @recursive is %TRUE, all child subvolumes will be deleted before deleting + * the subvolume itself. This requires btrfs-progs >= 6.12. + * + * Tech category: %BD_BTRFS_TECH_SUBVOL-%BD_BTRFS_TECH_MODE_DELETE if @recursive is %FALSE, + * %BD_BTRFS_TECH_SUBVOL-%BD_BTRFS_TECH_MODE_DELETE_RECURSIVE if @recursive is %TRUE + */ +gboolean bd_btrfs_delete_subvolume_recursive (const gchar *mountpoint, const gchar *name, gboolean recursive, const BDExtraArg **extra, GError **error) { gchar *path = NULL; gboolean success = FALSE; - const gchar *argv[5] = {"btrfs", "subvol", "delete", NULL, NULL}; + const gchar *argv[6] = {"btrfs", "subvol", "delete", NULL, NULL, NULL}; if (!check_deps (&avail_deps, DEPS_BTRFS_MASK, deps, DEPS_LAST, &deps_check_lock, error) || !check_module_deps (&avail_module_deps, MODULE_DEPS_BTRFS_MASK, module_deps, MODULE_DEPS_LAST, &deps_check_lock, error)) return FALSE; + if (recursive) { + if (!check_deps (&avail_deps, DEPS_BTRFS_612_MASK, deps, DEPS_LAST, &deps_check_lock, error)) + return FALSE; + } + if (g_str_has_suffix (mountpoint, "/")) path = g_strdup_printf ("%s%s", mountpoint, name); else path = g_strdup_printf ("%s/%s", mountpoint, name); - argv[3] = path; + + if (recursive) { + argv[3] = "--recursive"; + argv[4] = path; + } else { + argv[3] = path; + } success = bd_utils_exec_and_report_error (argv, extra, error); g_free (path); diff --git a/src/plugins/btrfs.h b/src/plugins/btrfs.h index b3d9ea99c..6d609ef2e 100644 --- a/src/plugins/btrfs.h +++ b/src/plugins/btrfs.h @@ -54,10 +54,11 @@ typedef enum { } BDBtrfsTech; typedef enum { - BD_BTRFS_TECH_MODE_CREATE = 1 << 0, - BD_BTRFS_TECH_MODE_DELETE = 1 << 1, - BD_BTRFS_TECH_MODE_MODIFY = 1 << 2, - BD_BTRFS_TECH_MODE_QUERY = 1 << 3, + BD_BTRFS_TECH_MODE_CREATE = 1 << 0, + BD_BTRFS_TECH_MODE_DELETE = 1 << 1, + BD_BTRFS_TECH_MODE_MODIFY = 1 << 2, + BD_BTRFS_TECH_MODE_QUERY = 1 << 3, + BD_BTRFS_TECH_MODE_DELETE_RECURSIVE = 1 << 4, } BDBtrfsTechMode; /* @@ -78,6 +79,7 @@ gboolean bd_btrfs_add_device (const gchar *mountpoint, const gchar *device, cons gboolean bd_btrfs_remove_device (const gchar *mountpoint, const gchar *device, const BDExtraArg **extra, GError **error); gboolean bd_btrfs_create_subvolume (const gchar *mountpoint, const gchar *name, const BDExtraArg **extra, GError **error); gboolean bd_btrfs_delete_subvolume (const gchar *mountpoint, const gchar *name, const BDExtraArg **extra, GError **error); +gboolean bd_btrfs_delete_subvolume_recursive (const gchar *mountpoint, const gchar *name, gboolean recursive, const BDExtraArg **extra, GError **error); guint64 bd_btrfs_get_default_subvolume_id (const gchar *mountpoint, GError **error); gboolean bd_btrfs_set_default_subvolume (const gchar *mountpoint, guint64 subvol_id, const BDExtraArg **extra, GError **error); gboolean bd_btrfs_create_snapshot (const gchar *source, const gchar *dest, gboolean ro, const BDExtraArg **extra, GError **error); diff --git a/tests/btrfs_test.py b/tests/btrfs_test.py index da3d1f39a..2723ad9a7 100644 --- a/tests/btrfs_test.py +++ b/tests/btrfs_test.py @@ -244,6 +244,41 @@ def test_create_delete_subvolume(self): seen.add(subvol) self.assertTrue(subvol.parent_id == BlockDev.BTRFS_MAIN_VOLUME_ID or any(subvol.parent_id == other.id for other in seen)) +class BtrfsTestDeleteSubvolumeRecursive(BtrfsTestCase): + @tag_test(TestTags.CORE) + def test_delete_subvolume_recursive(self): + """Verify that it is possible to recursively delete subvolumes""" + + succ = BlockDev.btrfs_create_volume([self.loop_dev], "myShinyBtrfs", None, None, None) + self.assertTrue(succ) + + mount(self.loop_dev, TEST_MNT) + + # create a subvolume with nested child subvolumes + succ = BlockDev.btrfs_create_subvolume(TEST_MNT, "subvol1", None) + self.assertTrue(succ) + + succ = BlockDev.btrfs_create_subvolume(os.path.join(TEST_MNT, "subvol1"), "subvol1.1", None) + self.assertTrue(succ) + + succ = BlockDev.btrfs_create_subvolume(os.path.join(TEST_MNT, "subvol1", "subvol1.1"), "subvol1.1.1", None) + self.assertTrue(succ) + + subvols = BlockDev.btrfs_list_subvolumes(TEST_MNT, False) + self.assertEqual(len(subvols), 3) + + version = self._get_btrfs_version() + if version < Version("6.12"): + with self.assertRaisesRegex(GLib.GError, "Too low version of btrfs"): + BlockDev.btrfs_delete_subvolume_recursive(TEST_MNT, "subvol1", True, None) + else: + # recursive delete should remove the subvolume and all its children + succ = BlockDev.btrfs_delete_subvolume_recursive(TEST_MNT, "subvol1", True, None) + self.assertTrue(succ) + + subvols = BlockDev.btrfs_list_subvolumes(TEST_MNT, False) + self.assertEqual(len(subvols), 0) + class BtrfsTestCreateSnapshot(BtrfsTestCase): def test_create_snapshot(self): succ = BlockDev.btrfs_create_volume([self.loop_dev], "myShinyBtrfs", None, None, None)