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 backup
pull/204/head
Axel Burri 2017-06-16 17:43:17 +02:00
parent 571dae4428
commit cd8d7e3a0a
4 changed files with 232 additions and 113 deletions

View File

@ -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.

View File

@ -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
View File

@ -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";

View File

@ -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