From cd8d7e3a0abc23a911c987d025c757ec6fb2453a Mon Sep 17 00:00:00 2001 From: Axel Burri Date: Fri, 16 Jun 2017 17:43:17 +0200 Subject: [PATCH] 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 --- ChangeLog | 1 + README.md | 10 +- btrbk | 322 ++++++++++++++++++++++++++++++++--------------- doc/btrbk.conf.5 | 12 +- 4 files changed, 232 insertions(+), 113 deletions(-) diff --git a/ChangeLog b/ChangeLog index 374f87d..67dc38d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -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. diff --git a/README.md b/README.md index 03bc3ad..bf24735 100644 --- a/README.md +++ b/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_.xz.gpg` for - non-incremental images, - * `/backup/home.YYYYMMDD.btrfs_@.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). diff --git a/btrbk b/btrbk index 7096ba2..c24a1d1 100755 --- a/btrbk +++ b/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/\.(?[0-9]{4})(?[0-9]{2})(?
[0-9]{2})(T(?[0-9]{2})(?[0-9]{2})((?[0-9]{2})(?(Z|[+-][0-9]{4})))?)?(_(?[0-9]+))?/; # matches "YYYYMMDD[Thhmm[ss+0000]][_NN]" -my $raw_postfix_match = qr/--(?$uuid_match)(\@(?$uuid_match))?\.btrfs?(\.(?($compress_format_alt)))?(\.(?gpg))?(\.(?split))?(\.(?part))?/; # matches ".btrfs_[@][.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 =~ /^(?$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 =~ /^(?$file_match)$timestamp_postfix_match$/)) { ; } + if($raw_info && ($name =~ /^(?$file_match)$timestamp_postfix_match$raw_postfix_match$/)) { ; } + elsif((not $raw_info) && ($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.--[@].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"; diff --git a/doc/btrbk.conf.5 b/doc/btrbk.conf.5 index a1e22a8..4697e55 100644 --- a/doc/btrbk.conf.5 +++ b/doc/btrbk.conf.5 @@ -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 .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 -[@].btrfs[.gz|.bz2|.xz][.gpg] +.[_N].btrfs[.gz|.bz2|.xz][.gpg] +.[_N].btrfs[.gz|.bz2|.xz][.gpg].info .RE .PP -The is only set on \fIincremental\fR backups, and points -to the 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