From: James Lai Date: Thu, 22 Jan 2026 08:09:57 +0000 (+0800) Subject: Unify snbk snapshot transfer and restore (#1086) X-Git-Tag: v0.13.1~51 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=574d72fe09b8fddb50e3428eb97d7a06628a672b;p=thirdparty%2Fsnapper.git Unify snbk snapshot transfer and restore (#1086) * 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 --- diff --git a/client/snbk/TheBigThing.cc b/client/snbk/TheBigThing.cc index 5e3ec0a4..31a597ac 100644 --- a/client/snbk/TheBigThing.cc +++ b/client/snbk/TheBigThing.cc @@ -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 EnumInfo::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& 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 + 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() diff --git a/client/snbk/TheBigThing.h b/client/snbk/TheBigThing.h index 5b6a570e..bcda7cba 100644 --- a/client/snbk/TheBigThing.h +++ b/client/snbk/TheBigThing.h @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024-2025] SUSE LLC + * Copyright (c) [2024-2026] SUSE LLC * * All Rights Reserved. * @@ -24,6 +24,7 @@ #include +#include #include #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 + 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& copy_specs); + };