]> git.ipfire.org Git - thirdparty/snapper.git/commitdiff
Unify snbk snapshot transfer and restore (#1086)
authorJames Lai <jamesljlster@gmail.com>
Thu, 22 Jan 2026 08:09:57 +0000 (16:09 +0800)
committerGitHub <noreply@github.com>
Thu, 22 Jan 2026 08:09:57 +0000 (08:09 +0000)
* Unify TheBigThing::transfer and TheBigThing::restore

* Use pair for copy specification

* Use const and reference for copy specification

* Use const for pair elements

* Make snapshot copy-related declarations private

client/snbk/TheBigThing.cc
client/snbk/TheBigThing.h

index 5e3ec0a4443aa875b9a199feaabe6be4b41758b4..31a597ac6658526db0f05f60751fb544e60cbd4e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) [2024-2025] SUSE LLC
+ * Copyright (c) [2024-2026] SUSE LLC
  *
  * All Rights Reserved.
  *
@@ -41,6 +41,7 @@
 
 namespace snapper
 {
+    using namespace std;
 
     namespace
     {
@@ -111,10 +112,19 @@ namespace snapper
            return nodes;
        }
 
-    }
+       const string source_snapshot_dir(const BackupConfig& backup_config,
+                                        unsigned int num)
+       {
+           return backup_config.source_path + "/" SNAPSHOTS_NAME "/" + to_string(num);
+       }
 
+       const string target_snapshot_dir(const BackupConfig& backup_config,
+                                        unsigned int num)
+       {
+           return backup_config.target_path + "/" + to_string(num);
+       }
 
-    using namespace std;
+    }
 
 
     const vector<string> EnumInfo<TheBigThing::SourceState>::names({
@@ -127,26 +137,18 @@ namespace snapper
     });
 
 
-    void
-    TheBigThing::transfer(const BackupConfig& backup_config, TheBigThings& the_big_things,
-                         bool quiet)
+    void TheBigThing::copy(const BackupConfig& backup_config,
+                           TheBigThings& the_big_things,
+                           const pair<CopySpec, CopySpec>& copy_specs)
     {
-       if (!quiet)
-           cout << sformat(_("Transferring snapshot %d."), num) << '\n';
-
-       if (source_state == SourceState::MISSING)
-           SN_THROW(Exception(_("Snapshot not on source.")));
-
-       if (target_state != TargetState::MISSING)
-           SN_THROW(Exception(_("Snapshot already on target.")));
-
-       const string num_string = to_string(num);
-
-       // Create directory on target. No failure if it already exists (option --parents).
-
-       SystemCmd::Args cmd1_args = { backup_config.target_mkdir_bin, "--parents", "--", backup_config.target_path +
-           "/" + num_string };
-       SystemCmd cmd1(shellify(backup_config.get_target_shell(), cmd1_args));
+       // Unpack copy specification
+       const CopySpec& src_spec = copy_specs.first;
+       const CopySpec& dst_spec = copy_specs.second;
+
+       // Create the snapshot directory on the destination.
+       SystemCmd::Args cmd1_args = { dst_spec.mkdir_bin, "--parents", "--",
+                                     dst_spec.snapshot_dir };
+       SystemCmd cmd1(shellify(dst_spec.shell, cmd1_args));
        if (cmd1.retcode() != 0)
        {
            y2err("command '" << cmd1.cmd() << "' failed: " << cmd1.retcode());
@@ -158,15 +160,15 @@ namespace snapper
            SN_THROW(Exception(_("'mkdir' failed.")));
        }
 
-       // Copy info.xml to target.
-
+       // Copy info.xml to the destination.
        switch (backup_config.target_mode)
        {
            case BackupConfig::TargetMode::LOCAL:
            {
-               SystemCmd::Args cmd2_args = { CP_BIN, "--", backup_config.source_path + "/" SNAPSHOTS_NAME "/" +
-                   num_string + "/info.xml", backup_config.target_path + "/" + num_string + "/" };
-               SystemCmd cmd2(shellify(backup_config.get_target_shell(), cmd2_args));
+               SystemCmd::Args cmd2_args = { CP_BIN, "--",
+                                             src_spec.snapshot_dir + "/info.xml",
+                                             dst_spec.snapshot_dir + "/" };
+               SystemCmd cmd2(shellify(src_spec.shell, cmd2_args));
                if (cmd2.retcode() != 0)
                {
                    y2err("command '" << cmd2.cmd() << "' failed: " << cmd2.retcode());
@@ -187,9 +189,9 @@ namespace snapper
                    cmd2_args << "-P" << to_string(backup_config.ssh_port);
                if (!backup_config.ssh_identity.empty())
                    cmd2_args << "-i" << backup_config.ssh_identity;
-               cmd2_args << "--" << backup_config.source_path + "/" SNAPSHOTS_NAME "/" + num_string +
-                   "/info.xml" << (backup_config.ssh_user.empty() ? "" : backup_config.ssh_user + "@") +
-                   backup_config.ssh_host + ":" + backup_config.target_path + "/" + num_string + "/";
+               cmd2_args << "--"
+                         << src_spec.remote_host + src_spec.snapshot_dir + "/info.xml"
+                         << dst_spec.remote_host + dst_spec.snapshot_dir + "/";
 
                SystemCmd cmd2(cmd2_args);
                if (cmd2.retcode() != 0)
@@ -204,33 +206,33 @@ namespace snapper
                }
            }
            break;
-       }
-
-       // Copy snapshot to target.
+       };
 
+       // Copy snapshot to the destination.
        const int proto = the_big_things.proto();
 
-       SystemCmd::Args cmd3a_args = { BTRFS_BIN, "send" };
+       SystemCmd::Args cmd3a_args = { src_spec.btrfs_bin, "send" };
        if (proto >= 2)
            cmd3a_args << "--proto" << to_string(proto);
        if (backup_config.send_compressed_data && proto >= 2)
            cmd3a_args << "--compressed-data";
        cmd3a_args << backup_config.send_options;
 
-       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(parent->node->get_number()) + "/" SNAPSHOT_NAME;
-       cmd3a_args << "--" << backup_config.source_path + "/" SNAPSHOTS_NAME "/" + num_string + "/" SNAPSHOT_NAME;
+       if (!src_spec.parent_subvol_path.empty())
+       {
+           cmd3a_args << "-p" << src_spec.parent_subvol_path;
+       }
+       cmd3a_args << "--" << src_spec.snapshot_dir + "/" SNAPSHOT_NAME;
 
-       SystemCmd::Args cmd3b_args = { backup_config.target_btrfs_bin, "receive" };
+       SystemCmd::Args cmd3b_args = { dst_spec.btrfs_bin, "receive" };
        cmd3b_args << backup_config.receive_options;
-       cmd3b_args << "--" << backup_config.target_path + "/" + num_string;
+       cmd3b_args << "--" << dst_spec.snapshot_dir;
 
        y2deb("source: " << cmd3a_args.get_values());
-       y2deb("target: " << cmd3b_args.get_values());
+       y2deb("destination: " << cmd3b_args.get_values());
 
-       SystemCmd cmd3(shellify_pipe(backup_config.get_source_shell(), cmd3a_args,
-                                    backup_config.get_target_shell(), cmd3b_args));
+       SystemCmd cmd3(
+           shellify_pipe(src_spec.shell, cmd3a_args, dst_spec.shell, cmd3b_args));
        if (cmd3.retcode() != 0)
        {
            y2err("command '" << cmd3.cmd() << "' failed: " << cmd3.retcode());
@@ -241,6 +243,24 @@ namespace snapper
 
            SN_THROW(Exception(_("'btrfs send | btrfs receive' failed.")));
        }
+    }
+
+    void
+    TheBigThing::transfer(const BackupConfig& backup_config, TheBigThings& the_big_things,
+                         bool quiet)
+    {
+       if (!quiet)
+           cout << sformat(_("Transferring snapshot %d."), num) << '\n';
+
+       if (source_state == SourceState::MISSING)
+           SN_THROW(Exception(_("Snapshot not on source.")));
+
+       if (target_state != TargetState::MISSING)
+           SN_THROW(Exception(_("Snapshot already on target.")));
+
+       // Copy the snapshot from the source to the target
+       copy(backup_config, the_big_things,
+            make_copy_specs(backup_config, the_big_things, CopyMode::SOURCE_TO_TARGET));
 
        target_state = TargetState::VALID;
     }
@@ -259,111 +279,9 @@ namespace snapper
        if (source_state != SourceState::MISSING)
            SN_THROW(Exception(_("Snapshot already on source.")));
 
-       const string num_string = to_string(num);
-
-       const string source_snapshot_dir =
-           backup_config.source_path + "/" SNAPSHOTS_NAME "/" + num_string;
-       const string target_snapshot_dir = backup_config.target_path + "/" + num_string;
-
-       // Create directory on source. No failure if it already exists (option --parents).
-
-       SystemCmd::Args cmd1_args = { MKDIR_BIN, "--parents", "--", source_snapshot_dir };
-       SystemCmd cmd1(shellify(backup_config.get_source_shell(), cmd1_args));
-       if (cmd1.retcode() != 0)
-       {
-           y2err("command '" << cmd1.cmd() << "' failed: " << cmd1.retcode());
-           for (const string& tmp : cmd1.get_stdout())
-               y2err(tmp);
-           for (const string& tmp : cmd1.get_stderr())
-               y2err(tmp);
-
-           SN_THROW(Exception(_("'mkdir' failed.")));
-       }
-
-       // Copy info.xml to source.
-
-       switch (backup_config.target_mode)
-       {
-           case BackupConfig::TargetMode::LOCAL:
-           {
-               SystemCmd::Args cmd2_args = {
-                   CP_BIN, "--", target_snapshot_dir + "/info.xml", source_snapshot_dir
-               };
-               SystemCmd cmd2(shellify(backup_config.get_target_shell(), cmd2_args));
-               if (cmd2.retcode() != 0)
-               {
-                   y2err("command '" << cmd2.cmd() << "' failed: " << cmd2.retcode());
-                   for (const string& tmp : cmd2.get_stdout())
-                       y2err(tmp);
-                   for (const string& tmp : cmd2.get_stderr())
-                       y2err(tmp);
-
-                   SN_THROW(Exception(_("'cp info.xml' failed.")));
-               }
-           }
-           break;
-
-           case BackupConfig::TargetMode::SSH_PUSH:
-           {
-               SystemCmd::Args cmd2_args = { SCP_BIN };
-               if (backup_config.ssh_port != 0)
-                   cmd2_args << "-P" << to_string(backup_config.ssh_port);
-               if (!backup_config.ssh_identity.empty())
-                   cmd2_args << "-i" << backup_config.ssh_identity;
-               cmd2_args << "--" <<
-                   (backup_config.ssh_user.empty() ? "" : backup_config.ssh_user + "@") +
-                   backup_config.ssh_host + ":" + target_snapshot_dir + "/info.xml"
-                   << source_snapshot_dir;
-
-               SystemCmd cmd2(cmd2_args);
-               if (cmd2.retcode() != 0)
-               {
-                   y2err("command '" << cmd2.cmd() << "' failed: " << cmd2.retcode());
-                   for (const string& tmp : cmd2.get_stdout())
-                       y2err(tmp);
-                   for (const string& tmp : cmd2.get_stderr())
-                       y2err(tmp);
-
-                   SN_THROW(Exception(_("'scp info.xml' failed.")));
-               }
-           }
-           break;
-       }
-
-       // Copy snapshot to target.
-
-       const int proto = the_big_things.proto();
-
-       SystemCmd::Args cmd3a_args = { backup_config.target_btrfs_bin, "send" };
-       if (proto >= 2)
-           cmd3a_args << "--proto" << to_string(proto);
-       if (backup_config.send_compressed_data && proto >= 2)
-           cmd3a_args << "--compressed-data";
-       cmd3a_args << backup_config.send_options;
-
-       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" };
-       cmd3b_args << backup_config.receive_options << "--" << source_snapshot_dir;
-
-       y2deb("source: " << cmd3a_args.get_values());
-       y2deb("target: " << cmd3b_args.get_values());
-
-       SystemCmd cmd3(shellify_pipe(backup_config.get_target_shell(), cmd3a_args,
-                                    backup_config.get_source_shell(), cmd3b_args));
-       if (cmd3.retcode() != 0)
-       {
-           y2err("command '" << cmd3.cmd() << "' failed: " << cmd3.retcode());
-           for (const string& tmp : cmd3.get_stdout())
-               y2err(tmp);
-           for (const string& tmp : cmd3.get_stderr())
-               y2err(tmp);
-
-           SN_THROW(Exception(_("'btrfs send | btrfs receive' failed.")));
-       }
+       // Copy the snapshot from the target to the source
+       copy(backup_config, the_big_things,
+            make_copy_specs(backup_config, the_big_things, CopyMode::TARGET_TO_SOURCE));
 
        source_state = SourceState::READ_ONLY;
     }
@@ -431,6 +349,72 @@ namespace snapper
        target_state = TargetState::MISSING;
     }
 
+    const pair<const TheBigThing::CopySpec, const TheBigThing::CopySpec>
+    TheBigThing::make_copy_specs(const BackupConfig& backup_config,
+                                 const TheBigThings& the_big_things,
+                                 CopyMode copy_mode) const
+    {
+       CopySpec spec_source; // Copy specification for the snapshot on the source.
+       spec_source.shell = backup_config.get_source_shell();
+       spec_source.mkdir_bin = MKDIR_BIN;
+       spec_source.btrfs_bin = BTRFS_BIN;
+       spec_source.snapshot_dir = source_snapshot_dir(backup_config, num);
+
+       CopySpec spec_target; // Copy specification for the snapshot on the target.
+       spec_target.shell = backup_config.get_target_shell();
+       spec_target.mkdir_bin = backup_config.target_mkdir_bin;
+       spec_target.btrfs_bin = backup_config.target_btrfs_bin;
+       spec_target.snapshot_dir = target_snapshot_dir(backup_config, num);
+
+       // Resolve the remote host when using SSH push.
+       if (backup_config.target_mode == BackupConfig::TargetMode::SSH_PUSH)
+       {
+           spec_target.remote_host =
+               (backup_config.ssh_user.empty() ? "" : backup_config.ssh_user + "@") +
+               backup_config.ssh_host + ":";
+       }
+
+       switch (copy_mode)
+       {
+           case CopyMode::SOURCE_TO_TARGET:
+
+               // Resolve Btrfs send parent.
+               if (auto parent =
+                       the_big_things.source_tree.find_nearest_valid_node(source_uuid))
+               {
+                   spec_source.parent_subvol_path =
+                       source_snapshot_dir(backup_config, parent->node->get_number()) +
+                       "/" SNAPSHOT_NAME;
+               }
+
+               // Return copy specification.
+               return {
+                   spec_source, // Snapshot on the source as the copy source.,
+                   spec_target  // Backup target as the copy destination.
+               };
+
+           case CopyMode::TARGET_TO_SOURCE:
+
+               // Resolve Btrfs send parent
+               if (auto parent =
+                       the_big_things.target_tree.find_nearest_valid_node(target_uuid))
+               {
+                   spec_target.parent_subvol_path =
+                       target_snapshot_dir(backup_config, parent->node->get_number()) +
+                       "/" SNAPSHOT_NAME;
+               }
+
+               // Return copy specification
+               return {
+                   spec_target, // Snapshot on the target as the copy source.
+                   spec_source  // Backup source as the copy destination.
+               };
+       };
+
+       SN_THROW(Exception("invalid copy mode"));
+       __builtin_unreachable();
+    }
+
 
     int
     TheBigThings::proto()
index 5b6a570ef132afaf04636d285894877d606d4a74..bcda7cbab51b9a57237231ebf4b175997631e0b3 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) [2024-2025] SUSE LLC
+ * Copyright (c) [2024-2026] SUSE LLC
  *
  * All Rights Reserved.
  *
@@ -24,6 +24,7 @@
 
 
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "../proxy/proxy.h"
@@ -37,6 +38,7 @@ namespace snapper
 {
 
     using std::string;
+    using std::pair;
     using std::vector;
 
 
@@ -76,6 +78,45 @@ namespace snapper
        string target_received_uuid;
        string target_creation_time;
 
+    private:
+
+       /** Snapshot copy mode. */
+       enum class CopyMode
+       {
+           /** Copy the snapshot from the source to the target (i.e., transfer). */
+           SOURCE_TO_TARGET,
+
+           /** Copy the snapshots from the target to the source (i.e., restore). */
+           TARGET_TO_SOURCE
+       };
+
+       /**
+        * Specification for the copy source or the copy destination.
+        * The `source` here is different from the backup `source`. A copy source can be
+        * either a snapshot on the backup source or the backup target.
+        */
+       struct CopySpec
+       {
+           Shell shell;
+           string mkdir_bin;
+           string btrfs_bin;
+           string remote_host;
+           string snapshot_dir;
+           string parent_subvol_path;
+       };
+
+       /**
+        * Create specifications for the copy source and the copy destination according to
+        * the specified `copy_mode`. The function returns a pair containing the source
+        * and destination copy specifications.
+        */
+       const pair<const CopySpec, const CopySpec>
+       make_copy_specs(const BackupConfig& backup_config,
+                       const TheBigThings& the_big_things, CopyMode copy_mode) const;
+
+       void copy(const BackupConfig& backup_config, TheBigThings& the_big_things,
+                 const pair<CopySpec, CopySpec>& copy_specs);
+
     };