]> git.ipfire.org Git - thirdparty/snapper.git/commitdiff
Unify Btrfs send parent finding algorithm for snbk (#1080)
authorJames Lai <jamesljlster@gmail.com>
Wed, 14 Jan 2026 08:55:39 +0000 (16:55 +0800)
committerGitHub <noreply@github.com>
Wed, 14 Jan 2026 08:55:39 +0000 (08:55 +0000)
* Added a TreeView module for finding Btrfs send parent

* Added default constructor for TreeView

* Unified Btrfs send parent finding algorithm

* Added copyright header for client/snbk/TreeView.cc

* Moved ProxyNode derivatives to anonymous namespace

* Adopted boost::none to return empty search result

* Fixed indentation for access modifier

* Remove redundant usage of 'this' pointer

* Make VirtualNode::uuid const

* Remove unused headers in TreeView.cc

* Make BaseNode::it const

* Implement ProxyNode::is_virtual() for node virtualization

* Refine the implementation for constructing new search candidates

* Return const string reference for UUID getters

* Reorganize member access permissions for TreeView

* Fix variable shadowing

* Use boost to join strings

* Prevent stream flushing when printing graph

* Reimplement tree graph printing in Graphviz format.

* Fix locale for printing virtual nodes

* Refactor TreeView data structure to remove circular references

* Remove redundant TreeView:: scope qualifier

* Refactor SearchResult to use raw pointers

* Rename TreeView::lookup to TreeView::pool

client/snbk/Makefile.am
client/snbk/TheBigThing.cc
client/snbk/TheBigThing.h
client/snbk/TreeView.cc [new file with mode: 0644]
client/snbk/TreeView.h [new file with mode: 0644]

index 55e03022879c4f71e53b8e845b0dfab674e070cd..d7095150f2c11439fd6556fc5cfecc40b082b099 100644 (file)
@@ -22,7 +22,8 @@ snbk_SOURCES =                                                \
        CmdBtrfs.cc             CmdBtrfs.h              \
        CmdLs.cc                CmdLs.h                 \
        JsonFile.cc             JsonFile.h              \
-       utils.cc                utils.h
+       utils.cc                utils.h                 \
+       TreeView.cc             TreeView.h
 
 snbk_LDADD =                           \
        ../../snapper/libsnapper.la     \
index a61ac4fd4c4535e5be0c5c081da4fb78b481181e..5e3ec0a4443aa875b9a199feaabe6be4b41758b4 100644 (file)
 namespace snapper
 {
 
+    namespace
+    {
+       /** A base class for constructing a node from an iterator. */
+       class BaseNode : public TreeView::ProxyNode
+       {
+       public:
+
+           BaseNode(const TheBigThings::const_iterator& it) : it(it) {}
+           unsigned int get_number() const override { return it->num; }
+           bool is_virtual() const override { return false; }
+
+           bool is_valid() const override
+           {
+               return (it->source_state == TheBigThing::SourceState::READ_ONLY &&
+                       it->target_state == TheBigThing::TargetState::VALID);
+           }
+
+       protected:
+
+           const TheBigThings::const_iterator it;
+       };
+
+       /**
+        * Specialized class for source nodes.
+        * (Used when sending snapshots from source to target.)
+        */
+       class SourceNode : public BaseNode
+       {
+       public:
+
+           SourceNode(const TheBigThings::const_iterator& it) : BaseNode(it) {}
+           const string& get_uuid() const override { return it->source_uuid; }
+           const string& get_parent_uuid() const override
+           {
+               return it->source_parent_uuid;
+           }
+       };
+
+       /**
+        * Specialized class for target nodes.
+        * (Used when sending snapshots from target to source.)
+        */
+       class TargetNode : public BaseNode
+       {
+       public:
+
+           TargetNode(const TheBigThings::const_iterator& it) : BaseNode(it) {}
+           const string& get_uuid() const override { return it->target_uuid; }
+           const string& get_parent_uuid() const override
+           {
+               return it->target_parent_uuid;
+           }
+       };
+
+
+       template <typename NodeType>
+       vector<shared_ptr<TreeView::ProxyNode>>
+       make_nodes(const TheBigThings& the_big_things)
+       {
+           vector<shared_ptr<TreeView::ProxyNode>> nodes;
+           for (TheBigThings::const_iterator it = the_big_things.begin();
+                it != the_big_things.end(); ++it)
+           {
+               nodes.push_back(std::make_shared<NodeType>(it));
+           }
+
+           return nodes;
+       }
+
+    }
+
+
     using namespace std;
 
 
@@ -138,8 +210,6 @@ namespace snapper
 
        const int proto = the_big_things.proto();
 
-       TheBigThings::const_iterator it1 = the_big_things.find_send_parent(*this);
-
        SystemCmd::Args cmd3a_args = { BTRFS_BIN, "send" };
        if (proto >= 2)
            cmd3a_args << "--proto" << to_string(proto);
@@ -147,9 +217,9 @@ namespace snapper
            cmd3a_args << "--compressed-data";
        cmd3a_args << backup_config.send_options;
 
-       if (it1 != the_big_things.end())
+       if (auto parent = the_big_things.source_tree.find_nearest_valid_node(source_uuid))
            cmd3a_args << "-p" << backup_config.source_path + "/" SNAPSHOTS_NAME "/" +
-               to_string(it1->num) + "/" SNAPSHOT_NAME;
+               to_string(parent->node->get_number()) + "/" SNAPSHOT_NAME;
        cmd3a_args << "--" << backup_config.source_path + "/" SNAPSHOTS_NAME "/" + num_string + "/" SNAPSHOT_NAME;
 
        SystemCmd::Args cmd3b_args = { backup_config.target_btrfs_bin, "receive" };
@@ -264,8 +334,6 @@ namespace snapper
 
        const int proto = the_big_things.proto();
 
-       TheBigThings::const_iterator it1 = the_big_things.find_restore_parent(*this);
-
        SystemCmd::Args cmd3a_args = { backup_config.target_btrfs_bin, "send" };
        if (proto >= 2)
            cmd3a_args << "--proto" << to_string(proto);
@@ -273,9 +341,9 @@ namespace snapper
            cmd3a_args << "--compressed-data";
        cmd3a_args << backup_config.send_options;
 
-       if (it1 != the_big_things.end())
-           cmd3a_args << "-p" << backup_config.target_path + "/" + to_string(it1->num) +
-           "/" SNAPSHOT_NAME;
+       if (auto parent = the_big_things.target_tree.find_nearest_valid_node(target_uuid))
+           cmd3a_args << "-p" << backup_config.target_path + "/" +
+           to_string(parent->node->get_number()) + "/" SNAPSHOT_NAME;
        cmd3a_args << "--" << target_snapshot_dir + "/" SNAPSHOT_NAME;
 
        SystemCmd::Args cmd3b_args = { BTRFS_BIN, "receive" };
@@ -399,6 +467,10 @@ namespace snapper
        probe_target(backup_config, verbose);
 
        sort(the_big_things.begin(), the_big_things.end());
+
+       // Construct tree for finding Btrfs send parent
+       source_tree = TreeView(make_nodes<SourceNode>(*this));
+       target_tree = TreeView(make_nodes<TargetNode>(*this));
     }
 
 
@@ -606,113 +678,4 @@ namespace snapper
        });
     }
 
-
-    TheBigThings::const_iterator
-    TheBigThings::find_send_parent(const TheBigThing& the_big_thing) const
-    {
-       typedef vector<TheBigThing>::const_reverse_iterator const_reverse_iterator;
-
-       // Find the direct parent or a previous snapshots with the same parent UUID (more
-       // a sibling).
-
-       for (const_reverse_iterator it1 = the_big_things.rbegin(); it1 != the_big_things.rend(); ++it1)
-       {
-           if (it1->num >= the_big_thing.num)
-               continue;
-
-           if (it1->source_state != TheBigThing::SourceState::READ_ONLY ||
-               it1->target_state != TheBigThing::TargetState::VALID)
-               continue;
-
-           if (it1->source_uuid == the_big_thing.source_parent_uuid)
-               // base() is a bit surprising, compensate that
-               return (it1 + 1).base();
-
-           if (it1->source_parent_uuid == the_big_thing.source_parent_uuid)
-               return (it1 + 1).base();
-       }
-
-       // Find the direct parent of the direct parent. This case can happen after a
-       // rollback.
-
-       for (const_reverse_iterator it1 = the_big_things.rbegin(); it1 != the_big_things.rend(); ++it1)
-       {
-           if (it1->num >= the_big_thing.num)
-               continue;
-
-           // Here the direct parent itself might be read-write and thus not available on
-           // the target at all.
-
-           if (it1->source_uuid == the_big_thing.source_parent_uuid)
-           {
-               for (const_reverse_iterator it2 = the_big_things.rbegin(); it2 != the_big_things.rend(); ++it2)
-               {
-                   if (it2->num >= it1->num)
-                       continue;
-
-                   if (it2->source_state != TheBigThing::SourceState::READ_ONLY ||
-                       it2->target_state != TheBigThing::TargetState::VALID)
-                       continue;
-
-                   if (it2->source_uuid == it1->source_parent_uuid)
-                       return (it2 + 1).base();
-               }
-           }
-       }
-
-       return end();
-    }
-
-    TheBigThings::const_iterator
-    TheBigThings::find_restore_parent(const TheBigThing& the_big_thing) const
-    {
-       typedef vector<TheBigThing>::const_reverse_iterator const_reverse_iterator;
-
-       // Find the direct parent or a previous snapshots with the same parent UUID (more
-       // a sibling).
-
-       for (const_reverse_iterator it1 = the_big_things.rbegin();
-            it1 != the_big_things.rend(); ++it1)
-       {
-           if (it1->num >= the_big_thing.num)
-               continue;
-
-           if (it1->source_state != TheBigThing::SourceState::READ_ONLY ||
-               it1->target_state != TheBigThing::TargetState::VALID)
-               continue;
-
-           if (it1->target_uuid == the_big_thing.target_parent_uuid)
-               return (it1 + 1).base();
-
-           if (it1->target_parent_uuid == the_big_thing.target_parent_uuid)
-               return (it1 + 1).base();
-       }
-
-       // Find the nearest previous snapshot to use as the send parent,
-       // reducing disk usage.
-
-       int distance = std::numeric_limits<int>::max();
-       TheBigThings::const_iterator parent = end();
-
-       for (const_reverse_iterator it1 = the_big_things.rbegin();
-            it1 != the_big_things.rend(); ++it1)
-       {
-           if (it1->num >= the_big_thing.num)
-               continue;
-
-           if (it1->source_state != TheBigThing::SourceState::READ_ONLY ||
-               it1->target_state != TheBigThing::TargetState::VALID)
-               continue;
-
-           int tmp_dist = std::abs((int)it1->num - (int)the_big_thing.num);
-           if (tmp_dist < distance)
-           {
-               distance = tmp_dist;
-               parent = (it1 + 1).base();
-           }
-       }
-
-       return parent;
-    }
-
 }
index 140ed749ea7eb884f796ea21ecfac9977d44e460..5b6a570ef132afaf04636d285894877d606d4a74 100644 (file)
@@ -30,6 +30,7 @@
 #include "../proxy/locker.h"
 
 #include "CmdBtrfs.h"
+#include "TreeView.h"
 
 
 namespace snapper
@@ -109,17 +110,18 @@ namespace snapper
 
        iterator find(unsigned int num);
 
-       /**
-        * Detect a suitable parent for btrfs send. Return end() iff none is found.
-        */
-       const_iterator find_send_parent(const TheBigThing& the_big_thing) const;
-       const_iterator find_restore_parent(const TheBigThing& the_big_thing) const;
-
        CmdBtrfsVersion source_btrfs_version;
        CmdBtrfsVersion target_btrfs_version;
 
        int proto();
 
+       /**
+        * Helper objects for finding a suitable Btrfs send parent when transferring and
+        * restoring snapshots.
+        */
+       TreeView source_tree;
+       TreeView target_tree;
+
     private:
 
        const ProxySnapper* snapper;
diff --git a/client/snbk/TreeView.cc b/client/snbk/TreeView.cc
new file mode 100644 (file)
index 0000000..1c27e59
--- /dev/null
@@ -0,0 +1,314 @@
+/*
+ * Copyright (c) [2024-2025] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, contact Novell, Inc.
+ *
+ * To contact Novell about this file by physical or electronic mail, you may
+ * find current contact information at www.novell.com.
+ */
+
+#include <iostream>
+#include <memory>
+#include <queue>
+#include <set>
+#include <unordered_set>
+#include <vector>
+
+#include <boost/algorithm/string.hpp>
+#include <boost/optional.hpp>
+
+#include <snapper/AppUtil.h>
+
+#include "../utils/text.h"
+
+#include "TreeView.h"
+
+
+using namespace std;
+using namespace snapper;
+
+
+namespace snapper
+{
+
+    namespace
+    {
+       // Comparator for shared pointers to nodes.
+       struct NodePtrComparator
+       {
+           bool operator()(const shared_ptr<TreeView::ProxyNode>& lhs,
+                           const shared_ptr<TreeView::ProxyNode>& rhs) const
+           {
+               return (*lhs) < (*rhs);
+           }
+       };
+
+       string get_node_name(const TreeView::ProxyNode* node)
+       {
+           if (node->is_virtual())
+           {
+               // Use the object's address as the node name to avoid name collisions.
+               stringstream ss;
+               ss.imbue(locale::classic());
+               ss << node;
+               return ss.str();
+           }
+           else
+           {
+               // Use the snapshot number as the node name.
+               return to_string(node->get_number());
+           }
+       }
+
+       string get_node_id(const TreeView::ProxyNode* node)
+       {
+           // Graphviz node IDs cannot start with a digit.
+           // Prefix the name with “n” to guarantee a valid identifier.
+           return "n" + get_node_name(node);
+       }
+
+       string get_node_declaration(const TreeView::ProxyNode* node)
+       {
+           // Get the node's properties.
+           vector<string> properties;
+           if (node->is_virtual())
+           {
+               properties.push_back("virtual");
+           }
+
+           if (node->is_valid())
+           {
+               properties.push_back("valid");
+           }
+
+           // Compose the node declaration.
+           string label = get_node_name(node);
+           if (!properties.empty())
+           {
+               label = label + "\\n" + boost::join(properties, ", ");
+           }
+
+           return sformat(_("%s [ label=\"%s\", style=\"%s\", shape=\"box\"]"),
+                          get_node_id(node).c_str(), label.c_str(),
+                          node->is_virtual() ? "dashed" : "");
+       }
+
+       void print_graph_graphviz_recursive(const TreeView::ProxyNode* node)
+       {
+           cout << "    " << get_node_declaration(node) << '\n';
+
+           for (const TreeView::ProxyNode* child : node->children)
+           {
+               const char* link_style = nullptr;
+               switch (child->parent_type)
+               {
+                   case TreeView::ParentType::DIRECT_PARENT:
+                       link_style = "";
+                       break;
+
+                   case TreeView::ParentType::IMPLICIT_PARENT:
+                       link_style = "dashed";
+                       break;
+
+                   case TreeView::ParentType::NONE:
+                       SN_THROW(Exception("Invalid parent type."));
+               }
+
+               cout << sformat(_("    %s -> %s [style=\"%s\"]"),
+                               get_node_id(node).c_str(), get_node_id(child).c_str(),
+                               link_style)
+                    << '\n';
+
+               print_graph_graphviz_recursive(child);
+           }
+       }
+
+    }
+
+
+    bool TreeView::ProxyNode::operator < (const ProxyNode& other) const
+    {
+       // A node with a greater number (more recent) has a higher priority.
+       return get_number() > other.get_number();
+    }
+
+
+    TreeView::VirtualNode::VirtualNode(const string& uuid) : uuid(uuid), parent_uuid("")
+    {
+    }
+
+    unsigned int TreeView::VirtualNode::get_number() const
+    {
+       // A constant number used for virtual nodes.
+       // Assume that there won't be so many snapshots.
+       return numeric_limits<unsigned int>::max();
+    }
+
+    const string& TreeView::VirtualNode::get_uuid() const { return uuid; }
+    const string& TreeView::VirtualNode::get_parent_uuid() const { return parent_uuid; }
+    bool TreeView::VirtualNode::is_virtual() const { return true; }
+    bool TreeView::VirtualNode::is_valid() const { return false; }
+
+
+    TreeView::TreeView() : virtual_root(make_shared<VirtualNode>("virtual_root")) {}
+
+    TreeView::TreeView(const vector<shared_ptr<ProxyNode>>& nodes) : TreeView()
+    {
+       // Construct a sorted container of source nodes in descending order.
+       set<shared_ptr<ProxyNode>, NodePtrComparator> sorted_nodes;
+       for (const shared_ptr<ProxyNode>& node : nodes)
+       {
+           if (!node->get_uuid().empty())
+           {
+               sorted_nodes.insert(node);
+           }
+       }
+
+       // Insert and take ownership of external nodes into the pool.
+       for (const shared_ptr<ProxyNode>& node : sorted_nodes)
+       {
+           pool[node->get_uuid()] = node;
+       }
+
+       // Construct virtual nodes for unmanaged parent Btrfs subvolumes.
+       for (const shared_ptr<ProxyNode>& node : sorted_nodes)
+       {
+           string uuid = node->get_parent_uuid();
+           if (!uuid.empty())
+           {
+               if (pool.find(uuid) == pool.end())
+               {
+                   // Create a virtual node and insert it into the pool.
+                   shared_ptr<ProxyNode> tmp_node = make_shared<VirtualNode>(uuid);
+                   pool[uuid] = tmp_node;
+
+                   // Make it an implicit child of the virtual root.
+                   set_parent(tmp_node.get(), virtual_root.get(),
+                              ParentType::IMPLICIT_PARENT);
+
+                   y2deb("Added virtual node for unmanaged Btrfs subvolume: " << uuid);
+               }
+           }
+       }
+
+       // Construct the tree.
+       for (const shared_ptr<ProxyNode>& node : sorted_nodes)
+       {
+           // Find its parent.
+           string parent_uuid = node->get_parent_uuid();
+           if (parent_uuid.empty())
+           {
+               // If the parent UUID is missing, add the node as a child of the virtual
+               // root to prevent orphaned nodes.
+               set_parent(node.get(), virtual_root.get(), ParentType::IMPLICIT_PARENT);
+           }
+           else
+           {
+               auto pair = pool.find(parent_uuid);
+               if (pair == pool.end())
+               {
+                   string error =
+                       sformat(_("Parent node %s not found."), parent_uuid.c_str());
+                   SN_THROW(Exception(error));
+               }
+               else
+               {
+                   set_parent(node.get(), pair->second.get(), ParentType::DIRECT_PARENT);
+               }
+           }
+       }
+    }
+
+    boost::optional<TreeView::SearchResult>
+    TreeView::find_nearest_valid_node(const string& start_uuid) const
+    {
+       auto pair = pool.find(start_uuid);
+       if (pair != pool.end())
+       {
+           return find_nearest_valid_node(pair->second.get());
+       }
+
+       SN_THROW(Exception(
+           sformat(_("Cannot find the node of UUID %s"), start_uuid.c_str())));
+       __builtin_unreachable();
+    }
+
+    boost::optional<TreeView::SearchResult>
+    TreeView::find_nearest_valid_node(const ProxyNode* start_node) const
+    {
+       queue<SearchResult> nodes_to_visit;
+       unordered_set<string> visited;
+
+       nodes_to_visit.push(SearchResult(start_node, 0));
+       visited.insert(start_node->get_uuid());
+
+       while (!nodes_to_visit.empty())
+       {
+           SearchResult current = nodes_to_visit.front();
+           nodes_to_visit.pop();
+
+           // Return the current search result if the distance is > 0
+           // (i.e., not the start node) and it is valid.
+           if (current.distance > 0 && current.node->is_valid())
+           {
+               return current;
+           }
+
+           // Append the parent and child nodes to the search queue.
+           vector<const ProxyNode*> new_candidates = current.node->children;
+           if (const ProxyNode* tmp_node = current.node->parent)
+           {
+               new_candidates.insert(new_candidates.begin(), tmp_node);
+           }
+
+           for (const ProxyNode* node : new_candidates)
+           {
+               if (!visited.count(node->get_uuid()))
+               {
+                   visited.insert(node->get_uuid());
+                   nodes_to_visit.push(SearchResult(node, current.distance + 1));
+               }
+           }
+       }
+
+       return boost::none;
+    }
+
+    void TreeView::set_parent(ProxyNode* node, ProxyNode* parent, ParentType parent_type)
+    {
+       parent->children.push_back(node);
+       node->parent = parent;
+       node->parent_type = parent_type;
+    }
+
+    void TreeView::print_graph_graphviz(const ProxyNode* node, const string& rankdir)
+    {
+       cout << "digraph {" << '\n';
+       cout << sformat(_("    rankdir=\"%s\"\n"), rankdir.c_str());
+       print_graph_graphviz_recursive(node);
+       cout << "}" << '\n';
+    }
+
+    void TreeView::print_graph_graphviz(const string& rankdir) const
+    {
+       TreeView::print_graph_graphviz(virtual_root.get(), rankdir);
+    }
+
+
+    const vector<string> EnumInfo<TreeView::ParentType>::names({
+       "none", "direct-parent", "implicit-parent"
+    });
+
+}
diff --git a/client/snbk/TreeView.h b/client/snbk/TreeView.h
new file mode 100644 (file)
index 0000000..f402e3a
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) [2024-2025] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, contact Novell, Inc.
+ *
+ * To contact Novell about this file by physical or electronic mail, you may
+ * find current contact information at www.novell.com.
+ */
+
+#ifndef SNAPPER_SNBK_TREE_VIEW_H
+#define SNAPPER_SNBK_TREE_VIEW_H
+
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <boost/optional.hpp>
+
+#include <snapper/Enum.h>
+
+
+namespace snapper
+{
+    using std::shared_ptr;
+    using std::string;
+    using std::vector;
+
+
+    class TreeView
+    {
+    public:
+
+       /** The type of the parent node. */
+       enum class ParentType
+       {
+           NONE,           /** Not yet specified. */
+           DIRECT_PARENT,  /** An explicit parent Btrfs subvolume is specified. */
+           IMPLICIT_PARENT /** Manually assigned. */
+       };
+
+       /** Proxy class to the nodes (snapshots). */
+       class ProxyNode
+       {
+       public:
+
+           virtual ~ProxyNode() = default;
+
+           /** Get the node number (snapshot number). */
+           virtual unsigned int get_number() const = 0;
+
+           /** Get the UUID on the sender side. */
+           virtual const string& get_uuid() const = 0;
+
+           /** Get the parent UUID on the sender side. */
+           virtual const string& get_parent_uuid() const = 0;
+
+           /** Determine whether the node can be used as a Btrfs send parent. */
+           virtual bool is_valid() const = 0;
+
+           /**
+            * Determine whether the node is a virtual node. If a Btrfs subvolume is not
+            * managed by Snapper, the corresponding node is considered virtual.
+            */
+           virtual bool is_virtual() const = 0;
+
+           /** Provided for ordering. This affects the priority of sibling nodes. */
+           virtual bool operator<(const ProxyNode& other) const;
+
+           ParentType parent_type = ParentType::NONE;
+           const ProxyNode* parent = nullptr;
+           vector<const ProxyNode*> children;
+       };
+
+       struct SearchResult
+       {
+           const ProxyNode* node;
+           unsigned int distance;
+
+           SearchResult(const ProxyNode* node, unsigned int distance)
+               : node(node), distance(distance)
+           {
+           }
+       };
+
+       TreeView();
+       TreeView(const vector<shared_ptr<ProxyNode>>& nodes);
+
+       /** Find the nearest valid node to use as a Btrfs‑send parent. */
+       boost::optional<SearchResult>
+       find_nearest_valid_node(const string& start_uuid) const;
+
+       /** Print the tree graph in Graphviz DOT Language. */
+       void print_graph_graphviz(const string& rankdir = "LR") const;
+
+    private:
+
+       /** Virtual node for Btrfs subvolumes that are not managed by snapper. */
+       class VirtualNode : public ProxyNode
+       {
+       public:
+
+           VirtualNode(const string& uuid);
+
+           unsigned int get_number() const override;
+           const string& get_uuid() const override;
+           const string& get_parent_uuid() const override;
+           bool is_virtual() const override;
+           bool is_valid() const override;
+
+       private:
+
+           const string uuid;
+           const string parent_uuid;
+       };
+
+       /**
+        * Find the nearest valid node to use as a Btrfs‑send parent, starting from the
+        * given node.
+        */
+       boost::optional<SearchResult>
+       find_nearest_valid_node(const ProxyNode* start_node) const;
+
+       /**
+        * Print the tree graph in Graphviz DOT Language, starting from the given node.
+        */
+       static void print_graph_graphviz(const ProxyNode* node,
+                                        const string& rankdir = "LR");
+
+       /**
+        * A static function that sets the parent relationship for the given two nodes.
+        */
+       static void set_parent(ProxyNode* node, ProxyNode* parent,
+                              ParentType parent_type);
+
+       /**
+        * A pool that stores and owns all nodes, including both real and virtual nodes.
+        */
+       std::map<string, shared_ptr<ProxyNode>> pool;
+
+       shared_ptr<ProxyNode> virtual_root;
+    };
+
+    template <> struct EnumInfo<TreeView::ParentType>
+    {
+       static const vector<string> names;
+    };
+
+} // namespace snapper
+
+#endif