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 \
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;
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);
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" };
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);
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" };
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));
}
});
}
-
- 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;
- }
-
}
#include "../proxy/locker.h"
#include "CmdBtrfs.h"
+#include "TreeView.h"
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;
--- /dev/null
+/*
+ * 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"
+ });
+
+}
--- /dev/null
+/*
+ * 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