mirror of https://github.com/digint/btrbk
btrbk: use sidecar file "*.info" instead of encoding uuids into filename for raw targets
pros: - better forward compatibility, e.g. symmetrical encryption - better readability of files cons: - two files per backuppull/204/head
parent
571dae4428
commit
cd8d7e3a0a
|
@ -4,6 +4,7 @@ btrbk-current
|
|||
line option (which is now deprecated).
|
||||
* Add "snapshot" command (close #150).
|
||||
* Add "--preserve-snapshots" and "--preserve-backups" options.
|
||||
* Change raw backup format (sidecar file instead of uuid in file).
|
||||
* Do not run in "perl taint mode" by default: remove "perl -T" in
|
||||
hashbang; hardcode $PATH only if taint mode is enabled.
|
||||
* Remove "duration" column from transaction_log/transaction_syslog.
|
||||
|
|
10
README.md
10
README.md
|
@ -364,12 +364,12 @@ compressed and piped through GnuPG.
|
|||
# incremental no
|
||||
|
||||
This will create a GnuPG encrypted, compressed files on the target
|
||||
host:
|
||||
host. For each backup, two files are created:
|
||||
|
||||
* `/backup/home.YYYYMMDD.btrfs_<received_uuid>.xz.gpg` for
|
||||
non-incremental images,
|
||||
* `/backup/home.YYYYMMDD.btrfs_<received_uuid>@<parent_uuid>.xz.gpg`
|
||||
for subsequent incremenal images.
|
||||
* `/backup/home.YYYYMMDD.btrfs.xz.gpg`: main data file containing
|
||||
the btrfs send-stream,
|
||||
* `/backup/home.YYYYMMDD.btrfs.xz.gpg.info`: sidecar file containing
|
||||
metadata used by btrbk.
|
||||
|
||||
I you are using raw _incremental_ backups, please make sure you
|
||||
understand the implications (see [btrbk.conf(5)], TARGET TYPES).
|
||||
|
|
322
btrbk
322
btrbk
|
@ -61,7 +61,7 @@ my $file_match = qr/[0-9a-zA-Z_@\+\-\.\/]+/; # note: ubuntu uses '@' in the sub
|
|||
my $glob_match = qr/[0-9a-zA-Z_@\+\-\.\/\*]+/; # file_match plus '*'
|
||||
my $uuid_match = qr/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;
|
||||
my $timestamp_postfix_match = qr/\.(?<YYYY>[0-9]{4})(?<MM>[0-9]{2})(?<DD>[0-9]{2})(T(?<hh>[0-9]{2})(?<mm>[0-9]{2})((?<ss>[0-9]{2})(?<zz>(Z|[+-][0-9]{4})))?)?(_(?<NN>[0-9]+))?/; # matches "YYYYMMDD[Thhmm[ss+0000]][_NN]"
|
||||
my $raw_postfix_match = qr/--(?<received_uuid>$uuid_match)(\@(?<parent_uuid>$uuid_match))?\.btrfs?(\.(?<compress>($compress_format_alt)))?(\.(?<encrypt>gpg))?(\.(?<split>split))?(\.(?<incomplete>part))?/; # matches ".btrfs_<received_uuid>[@<parent_uuid>][.gz|bz2|xz][.gpg][.split][.part]"
|
||||
my $raw_postfix_match = qr/\.btrfs(\.($compress_format_alt))?(\.gpg)?/; # matches ".btrfs[.gz|bz2|xz][.gpg]"
|
||||
|
||||
my $group_match = qr/[a-zA-Z0-9_:-]+/;
|
||||
my $ssh_cipher_match = qr/[a-z0-9][a-z0-9@.-]+/;
|
||||
|
@ -216,6 +216,22 @@ my %backend_cmd_map = (
|
|||
},
|
||||
);
|
||||
|
||||
# keys used in raw target sidecar files (.info):
|
||||
my %raw_info_sort = (
|
||||
TYPE => 1,
|
||||
FILE => 2,
|
||||
RECEIVED_UUID => 3,
|
||||
RECEIVED_PARENT_UUID => 4,
|
||||
INCOMPLETE => 5,
|
||||
# disabled for now, as its not very useful and might leak information
|
||||
#source_url => 6,
|
||||
#parent_url => 7,
|
||||
#target_url => 8,
|
||||
compress => 9,
|
||||
split => 10,
|
||||
encrypt => 11,
|
||||
);
|
||||
|
||||
my %url_cache; # map URL to btr_tree node
|
||||
my %fstab_cache; # map HOST to btrfs mount points
|
||||
my %uuid_cache; # map UUID to btr_tree node
|
||||
|
@ -1289,12 +1305,13 @@ sub btrfs_send_receive($$$$)
|
|||
}
|
||||
|
||||
|
||||
sub btrfs_send_to_file($$$$)
|
||||
sub btrfs_send_to_file($$$;$$)
|
||||
{
|
||||
my $source = shift || die;
|
||||
my $target = shift || die;
|
||||
my $parent = shift;
|
||||
my $ret_vol_received = shift;
|
||||
my $ret_raw_info = shift;
|
||||
my $source_path = $source->{PATH} // die;
|
||||
my $target_path = $target->{PATH} // die;
|
||||
my $parent_path = $parent ? $parent->{PATH} : undef;
|
||||
|
@ -1303,9 +1320,17 @@ sub btrfs_send_to_file($$$$)
|
|||
die unless($received_uuid);
|
||||
die if($parent && !$parent_uuid);
|
||||
|
||||
# prepare raw_info (for vinfo_inject_child)
|
||||
my %raw_info = (
|
||||
TYPE => 'raw',
|
||||
RECEIVED_UUID => $received_uuid,
|
||||
INCOMPLETE => 1,
|
||||
# source_url => $source->{URL},
|
||||
);
|
||||
|
||||
my $target_filename = $source->{NAME} || die;
|
||||
$target_filename .= "--$received_uuid";
|
||||
$target_filename .= '@' . $parent_uuid if($parent_uuid);
|
||||
# $target_filename .= "--$received_uuid";
|
||||
# $target_filename .= '@' . $parent_uuid if($parent_uuid);
|
||||
$target_filename .= ".btrfs";
|
||||
|
||||
my $compress = config_compress_hash($target, "raw_target_compress");
|
||||
|
@ -1326,38 +1351,46 @@ sub btrfs_send_to_file($$$$)
|
|||
};
|
||||
add_progress_command(\@cmd_pipe);
|
||||
if($compress) {
|
||||
$raw_info{compress} = $compression{$compress->{key}}->{format} if($compress);
|
||||
$target_filename .= '.' . $compression{$compress->{key}}->{format};
|
||||
push @cmd_pipe, { compress => $compress }; # does nothing if already compressed by rsh_compress_out
|
||||
}
|
||||
if($encrypt) {
|
||||
# NOTE: We set "--no-random-seed-file" since one of the btrbk
|
||||
# design principles is to never create any files unasked. Enabling
|
||||
# "--no-random-seed-file" creates ~/.gnupg/random_seed, and as
|
||||
# such depends on $HOME to be set correctly (which e.g. is set to
|
||||
# "/" by some cron daemons). From gpg2(1) man page:
|
||||
# --no-random-seed-file GnuPG uses a file to store its
|
||||
# internal random pool over invocations This makes random
|
||||
# generation faster; however sometimes write operations are not
|
||||
# desired. This option can be used to achieve that with the cost
|
||||
# of slower random generation.
|
||||
die unless($encrypt->{type} eq "gpg");
|
||||
$target_filename .= '.gpg';
|
||||
my @gpg_options = ( '--batch', '--no-tty', '--no-random-seed-file', '--trust-model', 'always' );
|
||||
push @gpg_options, ( '--compress-algo', 'none' ) if($compress); # NOTE: if --compress-algo is not set, gpg might still compress according to OpenPGP standard.
|
||||
push(@gpg_options, ( '--no-default-keyring', '--keyring', $encrypt->{keyring} )) if($encrypt->{keyring});
|
||||
push(@gpg_options, ( '--default-recipient', $encrypt->{recipient} )) if($encrypt->{recipient});
|
||||
push @cmd_pipe, {
|
||||
cmd => [ 'gpg', @gpg_options, '--encrypt' ],
|
||||
name => 'gpg',
|
||||
compressed_ok => ($compress ? 1 : 0),
|
||||
};
|
||||
$raw_info{encrypt} = $encrypt->{type};
|
||||
|
||||
if($encrypt->{type} eq "gpg") {
|
||||
# NOTE: We set "--no-random-seed-file" since one of the btrbk
|
||||
# design principles is to never create any files unasked. Enabling
|
||||
# "--no-random-seed-file" creates ~/.gnupg/random_seed, and as
|
||||
# such depends on $HOME to be set correctly (which e.g. is set to
|
||||
# "/" by some cron daemons). From gpg2(1) man page:
|
||||
# --no-random-seed-file GnuPG uses a file to store its
|
||||
# internal random pool over invocations This makes random
|
||||
# generation faster; however sometimes write operations are not
|
||||
# desired. This option can be used to achieve that with the cost
|
||||
# of slower random generation.
|
||||
$target_filename .= '.gpg';
|
||||
my @gpg_options = ( '--batch', '--no-tty', '--no-random-seed-file', '--trust-model', 'always' );
|
||||
push @gpg_options, ( '--compress-algo', 'none' ) if($compress); # NOTE: if --compress-algo is not set, gpg might still compress according to OpenPGP standard.
|
||||
push(@gpg_options, ( '--no-default-keyring', '--keyring', $encrypt->{keyring} )) if($encrypt->{keyring});
|
||||
push(@gpg_options, ( '--default-recipient', $encrypt->{recipient} )) if($encrypt->{recipient});
|
||||
push @cmd_pipe, {
|
||||
cmd => [ 'gpg', @gpg_options, '--encrypt' ],
|
||||
name => 'gpg',
|
||||
compressed_ok => ($compress ? 1 : 0),
|
||||
};
|
||||
}
|
||||
else {
|
||||
die "Usupported encryption type (raw_target_encrypt)";
|
||||
}
|
||||
}
|
||||
|
||||
if($split) {
|
||||
$target_filename .= '.split';
|
||||
# NOTE: we do not append a ".split" suffix on $target_filename here, as this propagates to ".info" file
|
||||
$raw_info{split} = $split;
|
||||
push @cmd_pipe, {
|
||||
cmd => [ 'split', '-b', uc($split), '-', "${target_path}/${target_filename}_" ],
|
||||
check_unsafe => [ { unsafe => "${target_path}/${target_filename}_" } ],
|
||||
cmd => [ 'split', '-b', uc($split), '-', "${target_path}/${target_filename}.split_" ],
|
||||
check_unsafe => [ { unsafe => "${target_path}/${target_filename}.split_" } ],
|
||||
rsh => vinfo_rsh($target, disable_compression => $compress || config_compress_hash($target, "stream_compress")),
|
||||
rsh_compress_in => $compress || config_compress_hash($target, "stream_compress"),
|
||||
rsh_rate_limit_in => config_key($target, "rate_limit"),
|
||||
|
@ -1375,9 +1408,9 @@ sub btrfs_send_to_file($$$$)
|
|||
# Another approach would be to always pipe through "cat", which
|
||||
# uses st_blksize from fstat(2) (with a minimum of 128K) to
|
||||
# determine the block size.
|
||||
cmd => [ 'dd', 'status=none', 'bs=' . config_key($target, "raw_target_block_size"), "of=${target_path}/${target_filename}.part" ],
|
||||
check_unsafe => [ { unsafe => "${target_path}/${target_filename}.part" } ],
|
||||
#redirect_to_file => { unsafe => "${target_path}/${target_filename}.part" }, # alternative (use shell redirection), less overhead on local filesystems (barely measurable):
|
||||
cmd => [ 'dd', 'status=none', 'bs=' . config_key($target, "raw_target_block_size"), "of=${target_path}/${target_filename}" ],
|
||||
check_unsafe => [ { unsafe => "${target_path}/${target_filename}" } ],
|
||||
#redirect_to_file => { unsafe => "${target_path}/${target_filename}" }, # alternative (use shell redirection), less overhead on local filesystems (barely measurable):
|
||||
rsh => vinfo_rsh($target, disable_compression => $compress || config_compress_hash($target, "stream_compress")),
|
||||
rsh_compress_in => $compress || config_compress_hash($target, "stream_compress"),
|
||||
rsh_rate_limit_in => config_key($target, "rate_limit"),
|
||||
|
@ -1388,6 +1421,13 @@ sub btrfs_send_to_file($$$$)
|
|||
my $vol_received = vinfo_child($target, $target_filename);
|
||||
$$ret_vol_received = $vol_received if(ref $ret_vol_received);
|
||||
|
||||
$raw_info{FILE} = $target_filename;
|
||||
$raw_info{RECEIVED_PARENT_UUID} = $parent_uuid if($parent_uuid);
|
||||
# disabled for now, as its not very useful and might leak information:
|
||||
# $raw_info{parent_url} = $parent->{URL} if($parent);
|
||||
# $raw_info{target_url} = $vol_received->{URL};
|
||||
$$ret_raw_info = \%raw_info if($ret_raw_info);
|
||||
|
||||
print STDOUT "Creating raw backup: $vol_received->{PRINT}\n" if($show_progress && (not $dryrun));
|
||||
|
||||
INFO "[send-to-raw] source: $source->{PRINT}";
|
||||
|
@ -1399,15 +1439,9 @@ sub btrfs_send_to_file($$$$)
|
|||
vinfo_prefixed_keys("source", $source),
|
||||
vinfo_prefixed_keys("parent", $parent),
|
||||
);
|
||||
my $ret = "";
|
||||
if($split) {
|
||||
DEBUG "Creating empty part file (split enabled)";
|
||||
$ret = run_cmd({
|
||||
cmd => ['touch', { unsafe => "${target_path}/${target_filename}.part" } ],
|
||||
rsh => vinfo_rsh($target),
|
||||
name => "touch",
|
||||
});
|
||||
}
|
||||
my $ret;
|
||||
$ret = system_write_raw_info($vol_received, \%raw_info);
|
||||
|
||||
if(defined($ret)) {
|
||||
$ret = run_cmd(@cmd_pipe);
|
||||
}
|
||||
|
@ -1417,7 +1451,7 @@ sub btrfs_send_to_file($$$$)
|
|||
# can not rely on the exit status of the command pipe, and a shell
|
||||
# redirection as well as "dd" always creates the target file.
|
||||
# Note that "split" does not create empty files.
|
||||
my $test_postfix = ($split ? "_aa" : ".part");
|
||||
my $test_postfix = ($split ? ".split_aa" : "");
|
||||
DEBUG "Testing target data file (non-zero size)";
|
||||
$ret = run_cmd({
|
||||
cmd => ['test', '-s', { unsafe => "${target_path}/${target_filename}${test_postfix}" } ],
|
||||
|
@ -1425,12 +1459,9 @@ sub btrfs_send_to_file($$$$)
|
|||
name => "test",
|
||||
});
|
||||
if(defined($ret)) {
|
||||
DEBUG "Renaming target file (remove postfix '.part')";
|
||||
$ret = run_cmd({
|
||||
cmd => ['mv', { unsafe => "${target_path}/${target_filename}.part" }, { unsafe => "${target_path}/${target_filename}" } ],
|
||||
rsh => vinfo_rsh($target),
|
||||
name => "mv",
|
||||
});
|
||||
# Write raw info file again, this time wihtout incomplete flag
|
||||
delete $raw_info{INCOMPLETE};
|
||||
$ret = system_write_raw_info($vol_received, \%raw_info);
|
||||
}
|
||||
}
|
||||
end_transaction("send-to-raw", ($dryrun ? "DRYRUN" : (defined($ret) ? "success" : "ERROR")));
|
||||
|
@ -1578,6 +1609,115 @@ sub btrfs_mountpoint($)
|
|||
}
|
||||
|
||||
|
||||
sub system_read_raw_info_dir($)
|
||||
{
|
||||
my $droot = shift // die;
|
||||
my $ret = run_cmd(
|
||||
# NOTE: we cannot simply "cat" all files here, as it will fail if no files found
|
||||
cmd => [ 'find', { unsafe => $droot->{PATH} },
|
||||
'-maxdepth', '1',
|
||||
'-type', 'f',
|
||||
'-name', '\*.btrfs.\*info', # match ".btrfs[.gz|bz2|xz][.gpg].info"
|
||||
'-exec', 'echo INFO_FILE=\{\} \;',
|
||||
'-exec', 'cat \{\} \;'
|
||||
],
|
||||
rsh => vinfo_rsh($droot),
|
||||
non_destructive => 1,
|
||||
);
|
||||
unless(defined($ret)) {
|
||||
ABORTED($droot, "Failed to read *.btrfs.*.info files in: $droot->{PATH}");
|
||||
return undef;
|
||||
}
|
||||
|
||||
my @raw_targets;
|
||||
my $cur_target;
|
||||
foreach (split("\n", $ret))
|
||||
{
|
||||
if(/^INFO_FILE=/) {
|
||||
push @raw_targets, $cur_target if($cur_target);
|
||||
$cur_target = {};
|
||||
}
|
||||
next if /^#/; # ignore comments
|
||||
next if /^\s*$/; # ignore empty lines
|
||||
if(/^([a-zA-Z_]+)=(.*)/) {
|
||||
my ($key, $value) = ($1, $2);
|
||||
if($cur_target) {
|
||||
$cur_target->{$key} = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
push @raw_targets, $cur_target if($cur_target);
|
||||
|
||||
# input validation (we need to abort here, or the backups will be resumed)
|
||||
foreach my $raw_info (@raw_targets) {
|
||||
unless($raw_info->{INFO_FILE}) {
|
||||
ABORTED($droot, "Error while parsing command output for: $droot->{PATH}");
|
||||
return undef;
|
||||
}
|
||||
unless($raw_info->{FILE}) {
|
||||
ABORTED($droot, "Missing \"FILE\" in raw info file: " . $raw_info->{INFO_FILE});
|
||||
return undef;
|
||||
}
|
||||
unless(check_file($raw_info->{FILE}, { name_only => 1 })) {
|
||||
ABORTED($droot, "Ambiguous \"file\" in raw info file: " . $raw_info->{INFO_FILE});
|
||||
return undef;
|
||||
}
|
||||
unless($raw_info->{TYPE} && ($raw_info->{TYPE} eq 'raw')) {
|
||||
ABORTED($droot, "Unsupported \"type\" in raw info file: " . $raw_info->{INFO_FILE});
|
||||
return undef;
|
||||
}
|
||||
unless($raw_info->{RECEIVED_UUID} && ($raw_info->{RECEIVED_UUID} =~ /^$uuid_match$/)) {
|
||||
ABORTED($droot, "Missing/Illegal \"received_uuid\" in raw info file: " . $raw_info->{INFO_FILE});
|
||||
return undef;
|
||||
}
|
||||
if(defined $raw_info->{RECEIVED_PARENT_UUID}) {
|
||||
unless(($raw_info->{RECEIVED_PARENT_UUID} eq '-') || ($raw_info->{RECEIVED_PARENT_UUID} =~ /^$uuid_match$/)) {
|
||||
ABORTED($droot, "Illegal \"RECEIVED_PARENT_UUID\" in raw info file: " . $raw_info->{INFO_FILE});
|
||||
return undef;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$raw_info->{RECEIVED_PARENT_UUID} = '-';
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG("Parsed " . @raw_targets . " raw info files in path: $droot->{PATH}");
|
||||
TRACE(Data::Dumper->Dump([\@raw_targets], ["system_read_raw_info_dir($droot->{URL})"])) if($do_dumper);
|
||||
|
||||
return \@raw_targets;
|
||||
}
|
||||
|
||||
|
||||
sub system_write_raw_info($$)
|
||||
{
|
||||
my $vol = shift // die;
|
||||
my $raw_info = shift // die;
|
||||
|
||||
my $info_file = $vol->{PATH} . '.info';
|
||||
my @line;
|
||||
push @line, "#btrbk-v$VERSION";
|
||||
push @line, "# Do not edit this file";
|
||||
|
||||
# sort by %raw_info_sort, then by key
|
||||
foreach(sort { (($raw_info_sort{$a} || 99) <=> ($raw_info_sort{$b} || 99)) || ($a cmp $b) } keys %$raw_info) {
|
||||
push @line, ($_ . '=' . $raw_info->{$_});
|
||||
}
|
||||
|
||||
DEBUG "Creating raw info file " . ($raw_info->{INCOMPLETE} ? "(incomplete)" : "(complete)") . ": $info_file";
|
||||
my $echo_text = (join '\n', @line);
|
||||
TRACE "DUMP INFO_FILE=$info_file\n" . join("\n", @line);
|
||||
|
||||
my $ret = run_cmd(
|
||||
{ cmd => [ 'echo', '-e', '-n', '"' . (join '\n', @line) . '\n"' ] },
|
||||
{ redirect_to_file => { unsafe => $info_file },
|
||||
rsh => vinfo_rsh($vol),
|
||||
});
|
||||
return undef unless(defined($ret));
|
||||
|
||||
return $info_file;
|
||||
}
|
||||
|
||||
|
||||
sub btr_tree($$)
|
||||
{
|
||||
my $vol = shift;
|
||||
|
@ -1886,24 +2026,15 @@ sub vinfo_cmd($$@)
|
|||
sub add_btrbk_filename_info($;$)
|
||||
{
|
||||
my $node = shift;
|
||||
my $btrbk_raw_file = shift;
|
||||
my $raw_info = shift;
|
||||
my $name = $node->{REL_PATH};
|
||||
return undef unless(defined($name));
|
||||
|
||||
# NOTE: unless long-iso file format is encountered, the timestamp is interpreted in local timezone.
|
||||
|
||||
$name =~ s/^(.*)\///;
|
||||
my $btrbk_raw;
|
||||
if($btrbk_raw_file && ($name =~ /^(?<name>$file_match)$timestamp_postfix_match$raw_postfix_match$/)) {
|
||||
$btrbk_raw = {
|
||||
received_uuid => $+{received_uuid} // die,
|
||||
remote_parent_uuid => $+{parent_uuid} // '-',
|
||||
encrypt => $+{encrypt} // "",
|
||||
compress => $+{compress} // "",
|
||||
incomplete => $+{incomplete} ? 1 : 0,
|
||||
};
|
||||
}
|
||||
elsif((not $btrbk_raw_file) && ($name =~ /^(?<name>$file_match)$timestamp_postfix_match$/)) { ; }
|
||||
if($raw_info && ($name =~ /^(?<name>$file_match)$timestamp_postfix_match$raw_postfix_match$/)) { ; }
|
||||
elsif((not $raw_info) && ($name =~ /^(?<name>$file_match)$timestamp_postfix_match$/)) { ; }
|
||||
else {
|
||||
return undef;
|
||||
}
|
||||
|
@ -1943,7 +2074,7 @@ sub add_btrbk_filename_info($;$)
|
|||
|
||||
$node->{BTRBK_BASENAME} = $name;
|
||||
$node->{BTRBK_DATE} = [ $time, $NN ];
|
||||
$node->{BTRBK_RAW} = $btrbk_raw if($btrbk_raw);
|
||||
$node->{BTRBK_RAW} = $raw_info if($raw_info);
|
||||
return $node;
|
||||
}
|
||||
|
||||
|
@ -2110,11 +2241,12 @@ sub vinfo_subvol($$)
|
|||
}
|
||||
|
||||
|
||||
sub vinfo_inject_child($$$)
|
||||
sub vinfo_inject_child($$$;$)
|
||||
{
|
||||
my $vinfo = shift;
|
||||
my $vinfo_child = shift;
|
||||
my $detail = shift;
|
||||
my $raw_info = shift;
|
||||
my $node;
|
||||
my $subvol_list = $vinfo->{SUBVOL_LIST};
|
||||
|
||||
|
@ -2133,7 +2265,7 @@ sub vinfo_inject_child($$$)
|
|||
id => $tree_inject_id,
|
||||
uuid => $uuid,
|
||||
};
|
||||
return undef unless(add_btrbk_filename_info($node, 1));
|
||||
return undef unless(add_btrbk_filename_info($node, $raw_info));
|
||||
|
||||
# NOTE: make sure to have all the flags set by _vinfo_subtree_list()
|
||||
$vinfo_child->{subtree_depth} = 0;
|
||||
|
@ -2576,7 +2708,6 @@ sub config_encrypt_hash($$)
|
|||
my $config_key = shift || die;
|
||||
my $encrypt_type = config_key($config, $config_key);
|
||||
return undef unless($encrypt_type);
|
||||
die unless($encrypt_type eq "gpg");
|
||||
return {
|
||||
type => $encrypt_type,
|
||||
keyring => config_key($config, "gpg_keyring"),
|
||||
|
@ -3000,6 +3131,7 @@ sub macro_send_receive(@)
|
|||
|
||||
my $ret;
|
||||
my $vol_received;
|
||||
my $raw_info;
|
||||
if($target_type eq "send-receive")
|
||||
{
|
||||
$ret = btrfs_send_receive($source, $target, $parent, \$vol_received);
|
||||
|
@ -3017,7 +3149,7 @@ sub macro_send_receive(@)
|
|||
$uuid_cache{$detail->{uuid}} = $source->{node};
|
||||
}
|
||||
}
|
||||
$ret = btrfs_send_to_file($source, $target, $parent, \$vol_received);
|
||||
$ret = btrfs_send_to_file($source, $target, $parent, \$vol_received, \$raw_info);
|
||||
ABORTED($config_target, "Failed to send subvolume to raw file") unless($ret);
|
||||
}
|
||||
else
|
||||
|
@ -3043,7 +3175,7 @@ sub macro_send_receive(@)
|
|||
readonly => 1,
|
||||
TARGET_TYPE => $target_type,
|
||||
FORCE_PRESERVE => 'preserve forced: created just now',
|
||||
});
|
||||
}, $raw_info);
|
||||
}
|
||||
|
||||
# add info to $config->{SUBVOL_RECEIVED}
|
||||
|
@ -4625,42 +4757,34 @@ MAIN:
|
|||
{
|
||||
DEBUG "Creating raw subvolume list: $droot->{PRINT}";
|
||||
$droot->{SUBVOL_LIST} = [];
|
||||
my $ret = run_cmd(
|
||||
cmd => [ 'find', { unsafe => $droot->{PATH} . '/' }, '-maxdepth', '1', '-type', 'f' ],
|
||||
rsh => vinfo_rsh($droot),
|
||||
# note: use something like this to get the real (link resolved) path
|
||||
# cmd => [ "find", $droot->{PATH} . '/', "-maxdepth", "1", "-name", "$snapshot_basename.\*.raw\*", '-printf', '%f\0', '-exec', 'realpath', '-z', '{}', ';' ],
|
||||
non_destructive => 1,
|
||||
);
|
||||
unless(defined($ret)) {
|
||||
ABORTED($droot, "Failed to list files from: $droot->{PATH}");
|
||||
WARN "Skipping target \"$droot->{PRINT}\": $abrt";
|
||||
|
||||
# list and parse *.info
|
||||
my $raw_info_ary = system_read_raw_info_dir($droot); # sets ABORTED on error
|
||||
if(ABORTED($droot)) {
|
||||
WARN "Skipping target \"$droot->{PRINT}\": " . ABORTED($droot);
|
||||
next;
|
||||
}
|
||||
die unless $raw_info_ary;
|
||||
|
||||
my $snapshot_basename = config_key($svol, "snapshot_name") // die;
|
||||
my %child_uuid_list;
|
||||
foreach (split("\n", $ret))
|
||||
foreach my $raw_info (@$raw_info_ary)
|
||||
{
|
||||
unless(/^($file_match)$/) {
|
||||
DEBUG "Skipping non-parseable file: \"$_\"";
|
||||
next;
|
||||
}
|
||||
my $file = $1; # untaint argument
|
||||
unless($file =~ s/^\Q$droot->{PATH}\E\///) {
|
||||
ABORTED($droot, "Unexpected result from 'find': file \"$file\" is not under \"$droot->{PATH}\"");
|
||||
last;
|
||||
}
|
||||
|
||||
# Set btrfs subvolume information (received_uuid, parent_uuid) from filename info.
|
||||
#
|
||||
# NOTE: remote_parent_uuid in BTRBK_RAW is the "parent of the source subvolume", NOT the
|
||||
# NOTE: received_parent_uuid in BTRBK_RAW is the "parent of the source subvolume", NOT the
|
||||
# "parent of the received subvolume".
|
||||
my $subvol = vinfo_child($droot, $file);
|
||||
unless(vinfo_inject_child($droot, $subvol, { TARGET_TYPE => 'raw' }))
|
||||
my $subvol = vinfo_child($droot, $raw_info->{FILE});
|
||||
unless(vinfo_inject_child($droot, $subvol, {
|
||||
TARGET_TYPE => $raw_info->{TYPE},
|
||||
parent_uuid => '-', # NOTE: correct value gets inserted below
|
||||
# Incomplete raw fakes get same semantics as real subvolumes (readonly=0, received_uuid='-')
|
||||
received_uuid => ($raw_info->{INCOMPLETE} ? '-' : $raw_info->{RECEIVED_UUID}),
|
||||
readonly => ($raw_info->{INCOMPLETE} ? 0 : 1),
|
||||
}, $raw_info))
|
||||
{
|
||||
DEBUG "Skipping file (filename scheme mismatch): \"$file\"";
|
||||
next;
|
||||
ABORTED($droot, "Ambiguous file in .info: \"$raw_info->{INFO_FILE}\"");
|
||||
last;
|
||||
}
|
||||
unless(defined($subvol->{node}{BTRBK_RAW}) &&
|
||||
($snapshot_basename eq $subvol->{node}{BTRBK_BASENAME}))
|
||||
|
@ -4670,18 +4794,13 @@ MAIN:
|
|||
# If we don't remove them from the list, they will also
|
||||
# be taken into account for incremental backups!
|
||||
pop @{$droot->{SUBVOL_LIST}};
|
||||
DEBUG "Skipping file (base name != \"$snapshot_basename\"): \"$file\"";
|
||||
DEBUG "Skipping file (base name != \"$snapshot_basename\"): \"$raw_info->{FILE}\"";
|
||||
next;
|
||||
}
|
||||
|
||||
# incomplete raw fakes get same semantics as real subvolumes (readonly=0, received_uuid='-')
|
||||
$subvol->{node}{received_uuid} = ($subvol->{node}{BTRBK_RAW}->{incomplete} ? '-' : $subvol->{node}{BTRBK_RAW}->{received_uuid});
|
||||
$subvol->{node}{parent_uuid} = undef; # correct value gets inserted below
|
||||
$subvol->{node}{readonly} = ($subvol->{node}{BTRBK_RAW}->{incomplete} ? 0 : 1);
|
||||
|
||||
if($subvol->{node}{BTRBK_RAW}->{remote_parent_uuid} ne '-') {
|
||||
$child_uuid_list{$subvol->{node}{BTRBK_RAW}->{remote_parent_uuid}} //= [];
|
||||
push @{$child_uuid_list{$subvol->{node}{BTRBK_RAW}->{remote_parent_uuid}}}, $subvol;
|
||||
if($raw_info->{RECEIVED_PARENT_UUID} ne '-') {
|
||||
$child_uuid_list{$raw_info->{RECEIVED_PARENT_UUID}} //= [];
|
||||
push @{$child_uuid_list{$raw_info->{RECEIVED_PARENT_UUID}}}, $subvol;
|
||||
}
|
||||
}
|
||||
if(ABORTED($droot)) {
|
||||
|
@ -4691,7 +4810,6 @@ MAIN:
|
|||
my @subvol_list = @{vinfo_subvol_list($droot, sort => 'path')};
|
||||
DEBUG "Found " . scalar(@subvol_list) . " raw subvolume backups of: $svol->{PRINT}";
|
||||
|
||||
# Make sure that incremental backup chains are never broken:
|
||||
foreach my $subvol (@subvol_list)
|
||||
{
|
||||
# If restoring a backup from raw btrfs images (using "incremental yes|strict"):
|
||||
|
@ -4704,8 +4822,10 @@ MAIN:
|
|||
# - svol.<timestamp>--<received_uuid-n>[@<received_uuid_n-1>].btrfs : incremental image
|
||||
|
||||
foreach my $child (@{$child_uuid_list{$subvol->{node}{received_uuid}}}) {
|
||||
# Insert correct (i.e. fake) parent UUID
|
||||
$child->{node}{parent_uuid} = $subvol->{node}{uuid};
|
||||
|
||||
# Make sure that incremental backup chains are never broken:
|
||||
DEBUG "Found parent/child partners, forcing preserve of: \"$subvol->{PRINT}\", \"$child->{PRINT}\"";
|
||||
$subvol->{node}{FORCE_PRESERVE} = "preserve forced: parent of another raw target";
|
||||
$child->{node}{FORCE_PRESERVE} ||= "preserve forced: child of another raw target";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.TH "btrbk.conf" "5" "2017-07-30" "btrbk v0.25.1" ""
|
||||
.TH "btrbk.conf" "5" "2017-09-28" "btrbk v0.26.0-dev" ""
|
||||
.\" disable hyphenation
|
||||
.nh
|
||||
.\" disable justification (adjust text to left margin only)
|
||||
|
@ -439,16 +439,14 @@ gpg_recipient <name>
|
|||
.RE
|
||||
.PD
|
||||
.PP
|
||||
Raw targets get an extra file suffix in the format:
|
||||
Raw backups consist of two files: the main data file containing the
|
||||
btrfs send stream, and a sidecar file ".info" containing metadata:
|
||||
.RS 4
|
||||
.PP
|
||||
<received_uuid>[@<parent_uuid>].btrfs[.gz|.bz2|.xz][.gpg]
|
||||
<snapshot\-name>.<timestamp>[_N].btrfs[.gz|.bz2|.xz][.gpg]
|
||||
<snapshot\-name>.<timestamp>[_N].btrfs[.gz|.bz2|.xz][.gpg].info
|
||||
.RE
|
||||
.PP
|
||||
The <parent_uuid> is only set on \fIincremental\fR backups, and points
|
||||
to the <received_uuid> of the previous backup in a incremental backup
|
||||
chain.
|
||||
.PP
|
||||
For \fIincremental\fR backups ("incremental yes"), please note that:
|
||||
.IP 1. 4
|
||||
As soon as a single \fIincremental\fR backup file is lost or
|
||||
|
|
Loading…
Reference in New Issue