diff --git a/btrbk b/btrbk
index 96c1a1e..8dfe7f9 100755
--- a/btrbk
+++ b/btrbk
@@ -42,9 +42,9 @@ along with this program. If not, see .
use strict;
use warnings FATAL => qw( all );
-use POSIX qw(strftime);
use File::Path qw(make_path);
use Getopt::Std;
+use Date::Calc qw(Today Delta_Days Add_Delta_Days Day_of_Week Monday_of_Week Week_of_Year);
use Data::Dumper;
our $VERSION = "0.01";
@@ -54,8 +54,6 @@ my $version_info = "btrbk command line client, version $VERSION";
my $default_config = "/etc/btrbk.conf";
my $default_snapdir = "_btrbk_snap";
-#my $default_time_format = "%Y%m%d_%H%M%S";
-my $default_time_format = "%Y%m%d";
my %vol_info;
my %uuid_info;
@@ -77,7 +75,6 @@ sub HELP_MESSAGE
print STDERR " --help display this help message\n";
print STDERR " --version display version information\n";
print STDERR " -s DIR make new source snapshots in subfolder
(defaults to \"$default_snapdir\")\n";
- print STDERR " -t FORMAT time format for snapshot postix, see 'man strftime' (defaults to \"$default_time_format\")\n";
print STDERR " -c FILE config file to be processed on execute command (defaults to \"$default_config\")\n";
print STDERR " -v be verbose (set loglevel=info)\n";
print STDERR " -l LEVEL set loglevel (1=warn, 2=info, 3=debug, 4=trace)\n";
@@ -85,6 +82,7 @@ sub HELP_MESSAGE
print STDERR "commands:\n";
print STDERR " tree shows backup tree\n";
print STDERR " execute perform all backups\n";
+ print STDERR " clean delete old backups\n";
print STDERR " dryrun don't run btrfs commands, just show what would be executed\n";
print STDERR " diff shows new files for subvolume , against subvolume \n";
print STDERR "\n";
@@ -231,6 +229,7 @@ sub parse_config($)
elsif($_ eq "create") { $job{options}->{create} = 1; }
elsif($_ eq "log") { $job{options}->{log} = 1; }
elsif($_ =~ /^log=(\S+)$/) { $job{options}->{log} = 1; $job{options}->{logfile} = $1; }
+ elsif($_ =~ /^preserve=[dD]([0-9]+)[wW]([0-9]+)$/) { $job{options}->{preserve} = {daily => $1, weekly => $2}; }
else {
ERROR "Ambiguous option=\"$_\": $file line $.";
return undef; # be very strict here
@@ -439,6 +438,17 @@ sub btrfs_snapshot($$)
}
+sub btrfs_subvolume_delete(@)
+{
+ my @targets = @_;
+ return 0 unless(scalar(@targets));
+ INFO "--- $_" foreach(@targets);
+ my $ret = run_cmd("/sbin/btrfs subvolume delete " . join(' ', @targets));
+ ERROR "Failed to delete btrfs subvolumes: " . join(' ', @targets) unless(defined($ret));
+ return defined($ret) ? scalar(@targets) : undef;
+}
+
+
sub btrfs_send_receive($$;$$)
{
my $src = shift;
@@ -551,6 +561,7 @@ MAIN:
$ENV{PATH} = '';
$Getopt::Std::STANDARD_HELP_VERSION = 1;
$Data::Dumper::Sortkeys = 1;
+ my @today = Today();
my %opts;
getopts('s:t:c:vl:p', \%opts);
@@ -567,7 +578,6 @@ MAIN:
$loglevel = $opts{v} ? 2 : 0;
}
my $config = $opts{c} || $default_config;
- my $time_format = $opts{t} || $default_time_format;
my $snapdir = $opts{s} || $default_snapdir;
$snapdir =~ s/\/+$//; # remove trailing slash
$snapdir =~ s/^\/+//; # remove leading slash
@@ -581,16 +591,20 @@ MAIN:
}
my $action_execute;
+ my $action_clean;
my $action_tree;
my $action_diff;
if(($command eq "execute") || ($command eq "dryrun")) {
$action_execute = 1;
$dryrun = 1 if($command eq "dryrun");
}
- elsif($command eq "tree") {
+ elsif ($command eq "clean") {
+ $action_clean = 1;
+ }
+ elsif ($command eq "tree") {
$action_tree = 1;
}
- elsif($command eq "diff") {
+ elsif ($command eq "diff") {
$action_diff = 1;
}
else {
@@ -600,11 +614,11 @@ MAIN:
}
- #
- # print snapshot diff
- #
if($action_diff)
{
+ #
+ # print snapshot diff
+ #
my $src_vol = shift @ARGV;
my $target_vol = shift @ARGV;
unless($src_vol && $target_vol) {
@@ -718,6 +732,7 @@ MAIN:
}
TRACE(Data::Dumper->Dump([\%vol_info], ["vol_info"]));
+
if($action_tree)
{
#
@@ -766,12 +781,12 @@ MAIN:
}
- #
- # create snapshots
- #
if($action_execute)
{
- my $timestamp = strftime($time_format, localtime);
+ #
+ # create snapshots
+ #
+ my $timestamp = sprintf("%04d%02d%02d", @today);
my %snapshot_cache;
foreach my $job (@$jobs)
{
@@ -859,37 +874,117 @@ MAIN:
INFO "Creating subvolume backup for: $sroot/$svol";
my $changelog = "";
- if($job_opts->{log})
- {
+ if ($job_opts->{log}) {
# log defaults to sidecar of destination snapshot
$changelog = $job_opts->{logfile} || "$droot/$snapshot_name.btrbk.log";
}
- if($job_opts->{incremental})
- {
+ if ($job_opts->{incremental}) {
INFO "Using previously created snapshot: $snapshot";
# INFO "Attempting incremantal backup (option=incremental)";
my ($latest_common_src, $latest_common_dst) = get_latest_common($sroot, $svol, $droot);
- if($latest_common_src && $latest_common_dst)
- {
+ if ($latest_common_src && $latest_common_dst) {
my $parent_snap = $latest_common_src->{FS_PATH};
INFO "Using parent snapshot: $parent_snap";
btrfs_send_receive($snapshot, $droot, $parent_snap, $changelog);
- }
- elsif($job_opts->{init}) {
+ } elsif ($job_opts->{init}) {
INFO "No common parent snapshots found, creating initial backup (option=init)";
btrfs_send_receive($snapshot, $droot, undef, $changelog);
- }
- else {
+ } else {
WARN "Backup to $droot failed: no common parent subvolume found, and job option \"create\" is not set";
}
- }
- elsif($job_opts->{create})
- {
+ } elsif ($job_opts->{create}) {
INFO "Creating new snapshot copy (option=create))";
btrfs_send_receive($snapshot, $droot, undef, $changelog);
}
}
}
+
+
+ if($action_clean)
+ {
+ $dryrun = 1;
+ #
+ # remove backups following a keep_daily/keep_weekly scheme
+ #
+ foreach my $job (@$jobs)
+ {
+ next if($job->{ABORTED});
+
+ my $sroot = $job->{sroot} || die;
+ my $svol = $job->{svol} || die;
+ my $droot = $job->{droot} || die;
+ my $job_opts = $job->{options} || die;
+
+ unless(ref($job_opts->{preserve})) {
+ INFO "Skip cleaning of subvolume backups (option preserve is not set): $sroot/$svol";
+ next;
+ }
+
+ INFO "Cleaning subvolume backups: $sroot/$svol";
+ my $keep_daily = $job_opts->{preserve}->{daily};
+ my $keep_weekly = $job_opts->{preserve}->{weekly};
+
+ # calculate weekly_threshold
+ my @last_sunday;
+ if(Day_of_Week(@today) == 7) { # today is sunday
+ @last_sunday = @today;
+ }
+ else {
+ @last_sunday = Add_Delta_Days(Monday_of_Week(Week_of_Year(@today)), -1);
+ }
+ my @weekly_threshold = Add_Delta_Days(@last_sunday, (-7 * $keep_weekly));
+ INFO "last sunday: " . join('-', @last_sunday);
+ INFO "weekly_threshold: " . join('-', @weekly_threshold);
+
+
+ my %week;
+ my %month;
+ my @delete_targets;
+ foreach my $vol (sort { $b cmp $a } keys %{$vol_info{$sroot}})
+ {
+ next unless($vol =~ /^$snapdir$svol\.([0-9]{4})([0-9]{2})([0-9]{2})/);
+ my ($vol_y, $vol_m, $vol_d) = ($1, $2, $3);
+ my @vol_date = ($vol_y, $vol_m, $vol_d);
+ my $keep = 0;
+
+ my $dd = Delta_Days(@vol_date, @today);
+ if($dd <= $keep_daily)
+ {
+ $keep = "less than $keep_daily days old (age=$dd days)";
+ DEBUG "$vol: $keep";
+ }
+
+ if(Delta_Days(@vol_date, @weekly_threshold) < 0)
+ {
+ DEBUG "$vol: not older than $keep_weekly weeks";
+ my ($vol_wnr, $vol_wy) = Week_of_Year(@vol_date);
+ unless($week{"$vol_wy-$vol_wnr"})
+ {
+ $week{"$vol_wy-$vol_wnr"} = 1;
+ $keep = "last in week $vol_wy-$vol_wnr";
+ DEBUG "$vol: $keep";
+ }
+ }
+
+ unless($month{"$vol_y-$vol_m"})
+ {
+ $month{"$vol_y-$vol_m"} = 1;
+ $keep = "last in month $vol_y-$vol_m";
+ DEBUG "$vol: $keep";
+ }
+
+ if($keep)
+ {
+ INFO "$vol: keeping: $keep";
+ next;
+ }
+
+ INFO "$vol: DELETE";
+ push @delete_targets, "$sroot/$vol";
+ }
+ btrfs_subvolume_delete(@delete_targets);
+ }
+ }
}
diff --git a/btrbk.conf b/btrbk.conf
index 3127aa7..c2611f6 100644
--- a/btrbk.conf
+++ b/btrbk.conf
@@ -11,12 +11,13 @@
# init create initial (non-incremental) snapshot if needed
# incremental do incremental backups (recommended)
# create always create non-incremental snapshots
+# preserve= keep daily backups for XX days, and weekly backups for YY days (monthly backups are always preserved)
# log log to "sidecar" file for each revision (suffix ".btrfs.log")
# log= append log to specified logfile
#
-/mnt/btr_system root_gentoo /mnt/btr_ext/_btrbk incremental,init
+/mnt/btr_system root_gentoo /mnt/btr_ext/_btrbk incremental,init,preserve=d14w10
/mnt/btr_system root_gentoo /mnt/btr_backup/_btrbk incremental,init,log
/mnt/btr_system kvm /mnt/btr_ext/_btrbk incremental,init
/mnt/btr_system kvm /mnt/btr_backup/_btrbk incremental,init,log