diff --git a/ChangeLog b/ChangeLog index 5488d16..64adc62 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,6 +5,7 @@ btrbk-current * Propagate targets defined in "volume" or "root" context to all "subvolume" sections (close: #78). * Added "{snapshot,target}_preserve NNd NNw NNm NNy" shortcut. + * Added hourly retention policies (close: #36). * Added yearly retention policies (close: #69). * Added configuration option "rate_limit" (close: #72). * Detect interrupted transfers of raw targets (close: #75). diff --git a/btrbk b/btrbk index 40bfb23..74a5af0 100755 --- a/btrbk +++ b/btrbk @@ -43,7 +43,7 @@ use strict; use warnings FATAL => qw( all ); use Carp qw(confess); -use Date::Calc qw(Today_and_Now Delta_Days Day_of_Week); +use Date::Calc qw(Today_and_Now Delta_Days Delta_DHMS Day_of_Week); use Getopt::Long qw(GetOptions); use POSIX qw(strftime); use Data::Dumper; @@ -80,16 +80,10 @@ my %config_options = ( incremental => { default => "yes", accept => [ "yes", "no", "strict" ] }, resume_missing => { default => "yes", accept => [ "yes", "no" ] }, preserve_day_of_week => { default => "sunday", accept => [ (keys %day_of_week_map) ] }, - snapshot_preserve_daily => { default => "all", accept => [ "all" ], accept_numeric => 1, context => [ "root", "volume", "subvolume" ] }, - snapshot_preserve_weekly => { default => 0, accept => [ "all" ], accept_numeric => 1, context => [ "root", "volume", "subvolume" ] }, - snapshot_preserve_monthly => { default => "all", accept => [ "all" ], accept_numeric => 1, context => [ "root", "volume", "subvolume" ] }, - snapshot_preserve_yearly => { default => "0", accept => [ "all" ], accept_numeric => 1, context => [ "root", "volume", "subvolume" ] }, - snapshot_preserve => { shortcut => 1, accept_preserve_matrix => 1, context => [ "root", "volume", "subvolume" ], }, - target_preserve_daily => { default => "all", accept => [ "all" ], accept_numeric => 1 }, - target_preserve_weekly => { default => 0, accept => [ "all" ], accept_numeric => 1 }, - target_preserve_monthly => { default => "all", accept => [ "all" ], accept_numeric => 1 }, - target_preserve_yearly => { default => "0", accept => [ "all" ], accept_numeric => 1 }, - target_preserve => { shortcut => 1, accept_preserve_matrix => 1, }, + snapshot_preserve => { default => undef, accept_preserve_matrix => 1, context => [ "root", "volume", "subvolume" ], }, + snapshot_preserve_all => { default => '12h', accept => [ "forever", "no" ], accept_regexp => qr/^[0-9]+[hdwmy]$/, context => [ "root", "volume", "subvolume" ], }, + target_preserve => { default => undef, accept_preserve_matrix => 1 }, + target_preserve_all => { default => undef, accept => [ "forever", "no" ], accept_regexp => qr/^[0-9]+[hdwmy]$/ }, btrfs_commit_delete => { default => undef, accept => [ "after", "each", "no" ] }, ssh_identity => { default => undef, accept_file => { absolute => 1 } }, ssh_user => { default => "root", accept_regexp => qr/^[a-z_][a-z0-9_-]*$/ }, @@ -110,6 +104,18 @@ my %config_options = ( group => { default => undef, accept_regexp => qr/^$group_match(\s*,\s*$group_match)*$/, split => qr/\s*,\s*/ }, # deprecated options + snapshot_preserve_daily => { default => 'all', accept => [ "all" ], accept_numeric => 1, context => [ "root", "volume", "subvolume" ], + deprecated => { COMPAT_PRESERVE_ALL => 1, DEFAULT => { warn => 'Please use "snapshot_preserve" and/or "snapshot_preserve_all"' } } }, + snapshot_preserve_weekly => { default => 0, accept => [ "all" ], accept_numeric => 1, context => [ "root", "volume", "subvolume" ], + deprecated => { COMPAT_PRESERVE_ALL => 1, DEFAULT => { warn => 'Please use "snapshot_preserve" and/or "snapshot_preserve_all"' } } }, + snapshot_preserve_monthly => { default => 'all', accept => [ "all" ], accept_numeric => 1, context => [ "root", "volume", "subvolume" ], + deprecated => { COMPAT_PRESERVE_ALL => 1, DEFAULT => { warn => 'Please use "snapshot_preserve" and/or "snapshot_preserve_all"' } } }, + target_preserve_daily => { default => 'all', accept => [ "all" ], accept_numeric => 1, + deprecated => { COMPAT_PRESERVE_ALL => 1, DEFAULT => { warn => 'Please use "snapshot_preserve" and/or "snapshot_preserve_all"' } } }, + target_preserve_weekly => { default => 0, accept => [ "all" ], accept_numeric => 1, + deprecated => { COMPAT_PRESERVE_ALL => 1, DEFAULT => { warn => 'Please use "snapshot_preserve" and/or "snapshot_preserve_all"' } } }, + target_preserve_monthly => { default => 'all', accept => [ "all" ], accept_numeric => 1, + deprecated => { COMPAT_PRESERVE_ALL => 1, DEFAULT => { warn => 'Please use "snapshot_preserve" and/or "snapshot_preserve_all"' } } }, snapshot_create_always => { default => undef, accept => [ "yes", "no" ], deprecated => { yes => { warn => "Please use \"snapshot_create always\"", replace_key => "snapshot_create", @@ -153,7 +159,7 @@ my %table_formats = ( schedule => { table => [ qw( action host subvol scheme reason ) ], long => [ qw( action host root_path subvol_path scheme reason ) ], - raw => [ qw( topic action url host path dow d w m y) ], + raw => [ qw( topic action url host path dow min h d w m y) ], }, usage => { table => [ qw( host path size used free ) ], @@ -184,6 +190,7 @@ my $tlog_fh; my $current_transaction; my @transaction_log; my %config_override; +my @today_and_now; $SIG{__DIE__} = sub { @@ -2018,13 +2025,21 @@ sub config_preserve_hash($$) { my $config = shift || die; my $prefix = shift || die; - return ( - preserve_day_of_week => config_key($config, "preserve_day_of_week"), - preserve_daily => config_key($config, "${prefix}_preserve_daily"), - preserve_weekly => config_key($config, "${prefix}_preserve_weekly"), - preserve_monthly => config_key($config, "${prefix}_preserve_monthly"), - preserve_yearly => config_key($config, "${prefix}_preserve_yearly"), - ); + my $ret = config_key($config, $prefix . "_preserve") // {}; + my $preserve_all = config_key($config, $prefix . "_preserve_all"); + if(defined($preserve_all)) { + $ret->{all} = $preserve_all; # used for raw schedule output + if($preserve_all eq 'forever') { + $ret->{all_n} = 'forever'; + } + elsif($preserve_all =~ /^([0-9]+)([hdwmy])$/) { + $ret->{all_n} = $1; + $ret->{all_q} = $2; + } + else { die; } + } + $ret->{dow} = config_key($config, "preserve_day_of_week"); + return $ret; } @@ -2039,8 +2054,6 @@ sub config_dump_keys($;@) foreach my $key (sort keys %config_options) { my $val; - next if($config_options{$key}->{deprecated}); - next if($config_options{$key}->{shortcut}); if($opts{resolve}) { $val = config_key($config, $key); } else { @@ -2055,6 +2068,9 @@ sub config_dump_keys($;@) next; # both undef, skip } } + if($config_options{$key}->{accept_preserve_matrix}) { + $val = format_preserve_matrix($val); + } if(ref($val) eq "ARRAY") { my $val2 = join(',', @$val); $val = $val2; @@ -2097,18 +2113,24 @@ sub append_config_option($$$$;$) } if($opt->{accept_preserve_matrix}) { - # special case: preserve matrix of form: "[NNd] [NNw] [NNm] [NNy]" - my $s = $value; - TRACE "option \"$key=$value\" is preserve matrix, parsing..."; - if($s =~ s/([0-9*][0-9]*)\s*d\s*,?\s*//) { my $v = ($1 eq '*' ? 'all' : $1); $config->{$key . "_daily"} = $v; TRACE "adding option \"${key}_daily=$v\" to $context context"; } - if($s =~ s/([0-9*][0-9]*)\s*w\s*,?\s*//) { my $v = ($1 eq '*' ? 'all' : $1); $config->{$key . "_weekly"} = $v; TRACE "adding option \"${key}_weekly=$v\" to $context context"; } - if($s =~ s/([0-9*][0-9]*)\s*m\s*,?\s*//) { my $v = ($1 eq '*' ? 'all' : $1); $config->{$key . "_monthly"} = $v; TRACE "adding option \"${key}_monthly=$v\" to $context context"; } - if($s =~ s/([0-9*][0-9]*)\s*y\s*,?\s*//) { my $v = ($1 eq '*' ? 'all' : $1); $config->{$key . "_yearly"} = $v; TRACE "adding option \"${key}_yearly=$v\" to $context context"; } + my %preserve; + my $s = ' ' . $value; + while($s =~ s/\s+(\*|[0-9]+)([hdwmyHDWMY])//) { + my $n = $1; + my $q = lc($2); # qw( h d w m y ) + $n = 'all' if($n eq '*'); + if(exists($preserve{$q})) { + ERROR "Value \"$value\" failed input validation for option \"$key\": multiple definitions of '$q'" . $config_file_statement; + return undef; + } + $preserve{$q} = $n; + } unless($s eq "") { ERROR "Value \"$value\" failed input validation for option \"$key\"" . $config_file_statement; return undef; } - TRACE "successfully parsed preserve matrix"; + TRACE "adding preserve matrix $context context:" . Data::Dumper->new([\%preserve], [ $key ])->Indent(0)->Pad(' ')->Quotekeys(0)->Pair('=>')->Dump() if($loglevel >= 4); + $config->{$key} = \%preserve; return $config; } @@ -2164,6 +2186,11 @@ sub append_config_option($$$$;$) $value = $replace_value; WARN "Using \"$key $value\""; } + if($opt->{deprecated}->{COMPAT_PRESERVE_ALL}) { + $config_override{snapshot_preserve_all} = 'forever'; + $config_override{target_preserve_all} = 'forever'; + WARN "Refusing to delete anything!"; + } } TRACE "adding option \"$key=$value\" to $context context"; @@ -2324,7 +2351,6 @@ sub init_config(@) # set defaults foreach (keys %config_options) { next if $config_options{$_}->{deprecated}; # don't pollute hash with deprecated options - next if $config_options{$_}->{shortcut}; # don't pollute hash with shortcuts $config_root{$_} = $config_options{$_}->{default}; } return \%config_root; @@ -2553,40 +2579,44 @@ sub cmp_date($$) sub schedule(@) { my %args = @_; - my $schedule = $args{schedule} || die; - my @today = @{$args{today}}; - my $preserve_day_of_week = $args{preserve_day_of_week} || die; - my $preserve_daily = $args{preserve_daily} // die; - my $preserve_weekly = $args{preserve_weekly} // die; - my $preserve_monthly = $args{preserve_monthly} // die; - my $preserve_yearly = $args{preserve_yearly} // die; - my $preserve_latest = $args{preserve_latest} || 0; - my $results_list = $args{results}; - my $result_hints = $args{result_hints} // {}; + my $schedule = $args{schedule} || die; + my $preserve = $args{preserve} || die; + my $preserve_latest = $args{preserve_latest} || 0; + my $results_list = $args{results}; + my $result_hints = $args{result_hints} // {}; - my %preserve_matrix = ( d => $preserve_daily, - w => $preserve_weekly, - m => $preserve_monthly, - y => $preserve_yearly, - dow => $preserve_day_of_week, - ); - DEBUG "Schedule: " . format_preserve_matrix(%preserve_matrix, format => "debug_text"); + my $preserve_day_of_week = $preserve->{dow} || die; + my $preserve_all_n = $preserve->{all_n}; + my $preserve_all_q = $preserve->{all_q}; + my $preserve_hourly = $preserve->{h}; + my $preserve_daily = $preserve->{d}; + my $preserve_weekly = $preserve->{w}; + my $preserve_monthly = $preserve->{m}; + my $preserve_yearly = $preserve->{y}; + + my @today_ymdhms = ref($args{today_and_now}) ? @{$args{today_and_now}} : @today_and_now; + die unless(scalar(@today_ymdhms) == 6); + + DEBUG "Schedule: " . format_preserve_matrix($preserve, format => "debug_text"); # sort the schedule, ascending by date my @sorted_schedule = sort { cmp_date($a->{btrbk_date}, $b->{btrbk_date} ) } @$schedule; # first, do our calendar calculations # note: our week starts on $preserve_day_of_week + my @today = @today_ymdhms[0..2]; my $delta_days_to_eow_from_today = $day_of_week_map{$preserve_day_of_week} - Day_of_Week(@today) - 1; $delta_days_to_eow_from_today = $delta_days_to_eow_from_today + 7 if($delta_days_to_eow_from_today < 0); TRACE "last day before next $preserve_day_of_week is in $delta_days_to_eow_from_today days"; foreach my $href (@sorted_schedule) { - my @date = @{$href->{btrbk_date}}[0..2]; # Date::Calc takes: @date = ( yy, mm, dd ) - my $delta_days = Delta_Days(@date, @today); + my @date = (@{$href->{btrbk_date}}[0..4], 0); # btrbk_date: (y m d h m NN); @date: (y m d h m s) + my ($dd, $dh, undef, undef) = Delta_DHMS(@date, @today_ymdhms); + my $delta_days = Delta_Days(@date[0..2], @today); my $delta_days_to_eow = $delta_days + $delta_days_to_eow_from_today; { use integer; # do integer arithmetics + $href->{delta_hours} = ($dd * 24) + $dh; $href->{delta_days} = $delta_days; $href->{delta_weeks} = $delta_days_to_eow / 7; $href->{delta_months} = ($today[0] - $date[0]) * 12 + ($today[1] - $date[1]); @@ -2604,33 +2634,65 @@ sub schedule(@) $href->{preserve} ||= $preserve_latest; } - # filter daily, weekly, monthly + my %last_in_delta_hours; + my %last_in_delta_days; my %first_in_delta_weeks; my %last_weekly_in_delta_months; my %last_monthly_in_delta_years; + + # filter "preserve all within N days/weeks/..." foreach my $href (@sorted_schedule) { - if($preserve_daily && (($preserve_daily eq "all") || ($href->{delta_days} <= $preserve_daily))) { - $href->{preserve} ||= "preserved daily: $href->{delta_days} days ago"; + if($preserve_all_n) { + if($preserve_all_n eq 'forever') { + $href->{preserve} ||= "preserve all: forever"; + } elsif($preserve_all_q eq 'h') { + $href->{preserve} ||= "preserve all: $href->{delta_hours} hours ago" if($href->{delta_hours} <= $preserve_all_n); + } elsif($preserve_all_q eq 'd') { + $href->{preserve} ||= "preserve all: $href->{delta_days} days ago" if($href->{delta_days} <= $preserve_all_n); + } elsif($preserve_all_q eq 'w') { + $href->{preserve} ||= "preserve all: $href->{delta_weeks} weeks ago" if($href->{delta_weeks} <= $preserve_all_n); + } elsif($preserve_all_q eq 'm') { + $href->{preserve} ||= "preserve all: $href->{delta_months} months ago" if($href->{delta_months} <= $preserve_all_n); + } elsif($preserve_all_q eq 'y') { + $href->{preserve} ||= "preserve all: $href->{delta_years} years ago" if($href->{delta_years} <= $preserve_all_n); + } + } + $last_in_delta_hours{$href->{delta_hours}} = $href; + } + + # filter hourly, daily, weekly, monthly, yearly + foreach (sort {$b <=> $a} keys %last_in_delta_hours) { + my $href = $last_in_delta_hours{$_} || die; + if($preserve_hourly && (($preserve_hourly eq 'all') || $href->{delta_hours} <= $preserve_hourly)) { + $href->{preserve} ||= "preserved hourly: last present of hour, $href->{delta_hours} hours ago"; + } + $last_in_delta_days{$href->{delta_days}} = $href; + } + foreach (sort {$b <=> $a} keys %last_in_delta_days) { + my $href = $last_in_delta_days{$_} || die; + if($preserve_daily && (($preserve_daily eq 'all') || ($href->{delta_days} <= $preserve_daily))) { + $href->{preserve} ||= "preserved daily: last present of day, $href->{delta_days} days ago"; } $first_in_delta_weeks{$href->{delta_weeks}} //= $href; } + foreach (sort {$b <=> $a} keys %first_in_delta_weeks) { my $href = $first_in_delta_weeks{$_} || die; - if($preserve_weekly && (($preserve_weekly eq "all") || ($href->{delta_weeks} <= $preserve_weekly))) { + if($preserve_weekly && (($preserve_weekly eq 'all') || ($href->{delta_weeks} <= $preserve_weekly))) { $href->{preserve} ||= "preserved weekly: $href->{delta_weeks} weeks ago, $href->{err_days_text}"; } $last_weekly_in_delta_months{$href->{delta_months}} = $href; } foreach (sort {$b <=> $a} keys %last_weekly_in_delta_months) { my $href = $last_weekly_in_delta_months{$_} || die; - if($preserve_monthly && (($preserve_monthly eq "all") || ($href->{delta_months} <= $preserve_monthly))) { + if($preserve_monthly && (($preserve_monthly eq 'all') || ($href->{delta_months} <= $preserve_monthly))) { $href->{preserve} ||= "preserved monthly: last present weekly of month $href->{month} ($href->{delta_months} months ago, $href->{err_days_text})"; } $last_monthly_in_delta_years{$href->{delta_years}} = $href; } foreach (sort {$b <=> $a} keys %last_monthly_in_delta_years) { my $href = $last_monthly_in_delta_years{$_} || die; - if($preserve_yearly && (($preserve_yearly eq "all") || ($href->{delta_years} <= $preserve_yearly))) { + if($preserve_yearly && (($preserve_yearly eq 'all') || ($href->{delta_years} <= $preserve_yearly))) { $href->{preserve} ||= "preserved yearly: last present weekly of year $href->{year} ($href->{delta_years} years ago, $href->{err_days_text})"; } } @@ -2638,8 +2700,8 @@ sub schedule(@) # assemble results my @delete; my @preserve; - my %result_base = ( %preserve_matrix, - scheme => format_preserve_matrix(%preserve_matrix, format => "short"), + my %result_base = ( %$preserve, + scheme => format_preserve_matrix($preserve), %$result_hints, ); my $count_defined = 0; @@ -2671,40 +2733,49 @@ sub schedule(@) } -sub format_preserve_matrix(@) +sub format_preserve_matrix($@) { - my %args = @_; - my $dow = $args{dow} // $args{preserve_day_of_week}; - my $d = $args{d} // $args{preserve_daily}; - my $w = $args{w} // $args{preserve_weekly}; - my $m = $args{m} // $args{preserve_monthly}; - my $y = $args{y} // $args{preserve_yearly}; - my $format = $args{format} // "long"; + my $preserve = shift || die; + my %opts = @_; + my $format = $opts{format} // "short"; if($format eq "debug_text") { - my $s = "preserving all within $d days"; - unless($d eq 'all') { - $s .= "; first in week (starting on $dow), for $w weeks" if($w); - unless($w eq 'all') { - $s .= "; last weekly of month, for $m months" if($m); - unless($m eq 'all') { - $s .= "; last weekly of year, for $y years" if($y); + my @out; + my %trans = ( h => 'hours', d => 'days', w => 'weeks', m => 'months', y => 'years' ); + if($preserve->{all_n} && $preserve->{all_n} eq 'forever') { + push @out, "preserving all forever"; + } + else { + push @out, "preserving all within $preserve->{all_n} $trans{$preserve->{all_q}}" if($preserve->{all_n} && $preserve->{all_q}); + push @out, "last present daily for $preserve->{d} days" if($preserve->{d}); + unless($preserve->{d} && ($preserve->{d} eq 'all')) { + push @out, "first daily in week (starting on $preserve->{dow}) for $preserve->{w} weeks" if($preserve->{w}); + unless($preserve->{w} && ($preserve->{w} eq 'all')) { + push @out, "last weekly of month for $preserve->{m} months" if($preserve->{m}); + unless($preserve->{m} && ($preserve->{m} eq 'all')) { + push @out, "last weekly of year for $preserve->{y} years" if($preserve->{y}); + } } } } - return $s; + return join('; ', @out); } - $d =~ s/^all$/\*/; - $w =~ s/^all$/\*/; - $m =~ s/^all$/\*/; - $y =~ s/^all$/\*/; - if($format eq "short") { - # short format - return sprintf("%2sd %2sw %2sm %2sy", $d, $w, $m, $y); + my $s = ""; + if($preserve->{all_n} && ($preserve->{all_n} eq 'forever')) { + $s = '*d+'; } - # long format - return sprintf("%2sd %2sw %2sm %2sy ($dow)", $d, $w, $m, $y); + else { + $s .= $preserve->{all_n} . $preserve->{all_q} . '+' if($preserve->{all_n} && $preserve->{all_q}); + foreach (qw(h d w m y)) { + my $val = $preserve->{$_} // 0; + next unless($val); + $val = '*' if($val eq 'all'); + $s .= ($s ? ' ' : '') . $val . $_; + } + $s .= " ($preserve->{dow})" if($preserve->{dow} && ($preserve->{w} || $preserve->{m} || $preserve->{y})); + } + return $s; } @@ -2910,12 +2981,11 @@ MAIN: Getopt::Long::Configure qw(gnu_getopt); $Data::Dumper::Sortkeys = 1; + $Data::Dumper::Quotekeys = 0; my $start_time = time; - my @today_and_now = Today_and_Now(); - my @today = @today_and_now[0..2]; + @today_and_now = Today_and_Now(); my %config_override_opts; - my ($config_cmdline, $quiet, $verbose, $preserve_backups, $resume_only, $print_schedule); unless(GetOptions( 'help|h' => sub { VERSION_MESSAGE(); HELP_MESSAGE(0); exit 0; }, @@ -4037,7 +4107,7 @@ MAIN: # find unique snapshot name my $timestamp = ((config_key($svol, "timestamp_format") eq "short") ? - sprintf("%04d%02d%02d", @today) : + sprintf("%04d%02d%02d", @today_and_now[0..2]) : sprintf("%04d%02d%02dT%02d%02d", @today_and_now[0..4])); my @unconfirmed_target_name; my @lookup = map { $_->{SUBVOL_PATH} } @{vinfo_subvol_list($sroot)}; @@ -4133,10 +4203,9 @@ MAIN: }); } my ($preserve, undef) = schedule( - schedule => \@schedule, - today => \@today, - preserve_latest => $preserve_latest, - config_preserve_hash($droot, "target"), + schedule => \@schedule, + preserve => config_preserve_hash($droot, "target"), + preserve_latest => $preserve_latest, ); my @resume = grep defined, @$preserve; # remove entries with no value from list (target subvolumes) $resume_total = scalar @resume; @@ -4231,8 +4300,7 @@ MAIN: # INFO "Cleaning backups of subvolume \"$svol->{PRINT}\": $droot->{PRINT}/$snapshot_basename.*"; unless(macro_delete($droot, "", $snapshot_basename, $droot, - { today => \@today, - config_preserve_hash($droot, "target"), + { preserve => config_preserve_hash($droot, "target"), preserve_latest => $preserve_latest_backup, results => $schedule_results, result_hints => { topic => "backup", root_path => $droot->{PATH} }, @@ -4258,8 +4326,7 @@ MAIN: } INFO "Cleaning snapshots: $sroot->{PRINT}/$snapdir_ts$snapshot_basename.*"; macro_delete($sroot, $snapdir_ts, $snapshot_basename, $svol, - { today => \@today, - config_preserve_hash($svol, "snapshot"), + { preserve => config_preserve_hash($svol, "snapshot"), preserve_latest => $preserve_latest_snapshot, results => $schedule_results, result_hints => { topic => "snapshot", root_path => $sroot->{PATH} }, diff --git a/doc/btrbk.conf.5 b/doc/btrbk.conf.5 index 9939eea..1c18716 100644 --- a/doc/btrbk.conf.5 +++ b/doc/btrbk.conf.5 @@ -72,8 +72,8 @@ space-separated table format. .PP \fBtimestamp_format\fR short|long .RS 4 -Timestamp format used as postfix for new snapshot subvolume names. -Defaults to \[lq]short\[rq]. +Timestamp format used as postfix for new snapshot subvolume +names. Defaults to \[lq]short\[rq]. .PP .IP \fBshort\fR YYYYMMDD[_N] (e.g. "20150825", "20150825_1") @@ -129,58 +129,30 @@ snapshots, and missing backups are created if needed (complying to the target preserve matrix). Defaults to \[lq]yes\[rq]. .RE .PP -\fBtarget_preserve\fR [d] [w] [m] [y] +\fBsnapshot_preserve\fR .RS 4 -Shortcut to set \fItarget_preserve_daily\fR, -\fItarget_preserve_weekly\fR, \fItarget_preserve_monthly\fR and -\fItarget_preserve_yearly\fR options (see below) on a single line. Use -an asterisk for \[lq]all\[rq] (e.g. "target_preserve 60d *m" expands -to "target_preserve_daily 60" and "target_preserve_monthly all"). +Set retention policy for snapshots (see RETENTION POLICY below). .RE .PP -\fBtarget_preserve_daily\fR all| +\fBsnapshot_preserve_all\fR forever|no|{h,d,w,m,y} .RS 4 -How many days of backups should be preserved. Defaults to \[lq]all\[rq]. +Preserve all snapshots for the given amount of hours (h), days (d), +weeks (w), months (m) or years (y), regardless of how many there +are. Defaults to \[lq]12h\[rq]. .RE .PP -\fBtarget_preserve_weekly\fR all| +\fBtarget_preserve\fR .RS 4 -Defines for how many weeks back weekly backups should be -preserved. Every backup created at \fIpreserve_day_of_week\fR (or -the next backup in this week if none was made on the exact day) is -considered as a weekly backup. Defaults to \[lq]0\[rq]. +Set retention policy for backups (see RETENTION POLICY below). .RE .PP -\fBtarget_preserve_monthly\fR all| +\fBtarget_preserve_all\fR forever|no|{h,d,w,m,y} .RS 4 -Defines for how many months back monthly backups should be -preserved. Every last weekly backup in a month is considered a -monthly backup. Defaults to \[lq]all\[rq]. +Preserve all backups for the given amount of hours (h), days (d), +weeks (w), months (m) or years (y), regardless of how many there +are. Defaults to \[lq]no\[rq]. .RE .PP -\fBtarget_preserve_yearly\fR all| -.RS 4 -Defines for how many years back yearly backups should be -preserved. Every last monthly backup in a year is considered a yearly -backup. Defaults to \[lq]0\[rq]. -.RE -.PP -\fBsnapshot_preserve\fR -.PD 0 -.PP -\fBsnapshot_preserve_daily\fR -.PP -\fBsnapshot_preserve_weekly\fR -.PP -\fBsnapshot_preserve_monthly\fR -.PP -\fBsnapshot_preserve_yearly\fR -.RS 4 -Defines retention policy for the snapshots, with same semantics as the -\fItarget_preserve_*\fR options. -.RE -.PD -.PP \fBpreserve_day_of_week\fR monday|tuesday|...|sunday .RS 4 Defines on what day a backup/snapshot is considered as a weekly @@ -253,6 +225,65 @@ manipulated by hand (moved, deleted). .PP Lines that contain a hash character (#) in the first column are treated as comments. +.SH RETENTION POLICY +btrbk uses separate retention policy for snapshots and backups, which +are defined by the \fIsnapshot_preserve_all\fR, +\fIsnapshot_preserve\fR, \fItarget_preserve_all\fR, +\fItarget_preserve\fR, and the \fIpreserve_day_of_week\fR +configuration options. +.PP +Within this section, any statement about "backups" is always valid for +backups as well as snapshots, referring to \fItarget_preserve\fR or +\fIsnapshot_preserve\fR respectively. +.PP +The format for \fI\fR is: +.PP +.RS 4 +[h] [d] [w] [m] [y] +.RE +.PP +With the following semantics: +.PP +.B hourly +.RS 4 +Defines how many hours back hourly backups should be preserved. The +last backup of an hour is considered an hourly backup. Note that if +you use scheduling, make sure to also set +\fItimestamp_format\fR to \[lq]long\[rq], or the scheduler will +interpret the time as "00:00" (midnight). +.RE +.PP +.B daily +.RS 4 +Defines how many days back daily backups should be preserved. The +last backup of a day is considered a daily backup. +.RE +.PP +.B weekly +.RS 4 +Defines how many weeks back weekly backups should be preserved. The +first daily backup created at \fIpreserve_day_of_week\fR (or the first +backup in this week if none was made on the exact day) is considered +as a weekly backup. +.RE +.PP +.B monthly +.RS 4 +Defines how many months back monthly backups should be +preserved. Every last weekly backup in a month is considered a +monthly backup. +.RE +.PP +.B yearly +.RS 4 +Defines for how many years back yearly backups should be +preserved. Every last monthly backup in a year is considered a yearly +backup. +.RE +.PP +Use an asterisk for \[lq]all\[rq] (e.g. "target_preserve 60d *m" +states: "preserve daily backups for 60 days back, and all monthly +backups"). .SH TARGET TYPES .PP \fBsend-receive\fR