mirror of https://github.com/digint/btrbk
229 lines
5.9 KiB
Perl
Executable File
229 lines
5.9 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
|
|
=head1 NAME
|
|
|
|
btrbk - backup btrfs volumes at file-system level
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
btrbk --help
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
Backup tool for btrfs (sub-)volumes, taking advantage of btrfs
|
|
specific send-receive mechanism, allowing incremental backups at
|
|
file-system level.
|
|
|
|
The full btrbk documentation is available at L<http://www.digint.ch/btrbk>.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Axel Burri <axel@tty0.ch>
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
Copyright (c) 2014 Axel Burri. All rights reserved.
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
=cut
|
|
|
|
use strict;
|
|
use warnings FATAL => qw( all );
|
|
|
|
use POSIX qw(strftime);
|
|
use File::Path qw(make_path);
|
|
use Getopt::Std;
|
|
use Data::Dumper;
|
|
|
|
our $VERSION = "0.01";
|
|
our $PROJECT_HOME = '<http://www.digint.ch/btrbk>';
|
|
|
|
my $version_info = "btrfs-backup command line client, version $VERSION";
|
|
my $time_format = "%Y%m%d_%H%M%S";
|
|
|
|
my $src_snapshot_dir = "_btrbk";
|
|
|
|
my %vol_info;
|
|
my $dryrun;
|
|
my $verbose = 0;
|
|
|
|
sub VERSION_MESSAGE
|
|
{
|
|
print STDERR $version_info . "\n\n";
|
|
}
|
|
|
|
sub HELP_MESSAGE
|
|
{
|
|
print STDERR "usage: $0 [options] <root_volume> <subvol> <dest> [dest...]\n";
|
|
print STDERR "\n";
|
|
print STDERR "options:\n";
|
|
print STDERR " -h, --help display this help message\n";
|
|
print STDERR " --version display version information\n";
|
|
print STDERR " -i incremental backup\n";
|
|
print STDERR " -v verbose\n";
|
|
print STDERR " -d dryrun\n";
|
|
print STDERR "\n";
|
|
print STDERR "For additional information, see $PROJECT_HOME\n";
|
|
}
|
|
|
|
sub DEBUG
|
|
{
|
|
my $text = shift;
|
|
print STDERR "DEBUG: $text\n" if($verbose);
|
|
}
|
|
|
|
sub run_cmd($;$)
|
|
{
|
|
my $cmd = shift;
|
|
my $always_execute = shift;
|
|
my $ret;
|
|
DEBUG "CMD: $cmd\n";
|
|
if($always_execute || (not $dryrun)) {
|
|
$ret = `$cmd`;
|
|
die("command execution failed: \"$cmd\"") if($?);
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
sub fetch_subvolume_info($)
|
|
{
|
|
my $path = shift;
|
|
my $ret = run_cmd("/sbin/btrfs subvolume list $path", 1); # TODO: use -r for read-only
|
|
# my $ret = 'ID 350 gen 185 top level 349 path _backup_btr_system/root_gentoo.20141130_0
|
|
#ID 363 gen 194 top level 349 path _backup_btr_system/kvm.20141130_0
|
|
#ID 363 gen 194 top level 349 path boot
|
|
#';
|
|
my @list;
|
|
foreach (split(/\n/, $ret)) {
|
|
die("Failed to parse line: \"$_\"") unless(/^ID ([0-9]+) gen ([0-9]+) top level ([0-9]+) path (.+)$/);
|
|
push @list, { ID => $1, gen => $2, top_level => $3, path => $4 };
|
|
}
|
|
return \@list;
|
|
}
|
|
|
|
sub check_vol($$)
|
|
{
|
|
my $root = shift;
|
|
my $vol = shift;
|
|
die("subvolume info not present: $root") unless(exists($vol_info{$root}));
|
|
foreach (@{$vol_info{$root}}) {
|
|
return 1 if($_->{path} eq $vol);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub check_src($$)
|
|
{
|
|
my $root = shift;
|
|
my $vol = shift;
|
|
die("subvolume not found: ${root}/${vol}") unless(check_vol($root, $vol));
|
|
my $dir = "${root}/${src_snapshot_dir}";
|
|
unless(-d $dir) {
|
|
print "creating directory: $dir\n";
|
|
make_path("${root}/${src_snapshot_dir}");
|
|
}
|
|
}
|
|
|
|
sub snapshot($$)
|
|
{
|
|
my $src = shift;
|
|
my $dst = shift;
|
|
run_cmd("/sbin/btrfs subvolume snapshot -r $src $dst");
|
|
}
|
|
|
|
sub send_receive($$;$)
|
|
{
|
|
my $src = shift;
|
|
my $dst = shift;
|
|
my $parent = shift;
|
|
$parent = $parent ? "-p $parent" : "";
|
|
run_cmd("/sbin/btrfs send $parent $src | /sbin/btrfs receive ${dst}/");
|
|
}
|
|
|
|
sub get_latest($$)
|
|
{
|
|
my $vol = shift;
|
|
my $root = shift;
|
|
die("subvolume info not present: $root") unless(exists($vol_info{$root}));
|
|
my $latest;
|
|
foreach (@{$vol_info{$root}}) {
|
|
my $v = $_->{path};
|
|
next unless($v =~ /^$vol\./);
|
|
DEBUG "found snapshot: $v";
|
|
$latest = $v if((not defined($latest)) || ($latest lt $v));
|
|
}
|
|
die("no snapshot matching \"${vol}.*\" present in subvolume: $root") unless($latest);
|
|
return $latest;
|
|
}
|
|
|
|
MAIN:
|
|
{
|
|
$ENV{PATH} = '';
|
|
$Getopt::Std::STANDARD_HELP_VERSION = 1;
|
|
|
|
my %opts;
|
|
getopts('hivd', \%opts);
|
|
my $sroot = shift @ARGV;
|
|
my $svol = shift @ARGV;
|
|
my @droot = @ARGV;
|
|
|
|
if($opts{h} || (not $svol) || (not @droot)) {
|
|
VERSION_MESSAGE();
|
|
HELP_MESSAGE(0);
|
|
exit 0;
|
|
}
|
|
$dryrun = $opts{d};
|
|
$verbose = $opts{v} || $dryrun;
|
|
my $incremental = $opts{i};
|
|
|
|
$sroot =~ s/\/+$//; # sanitize trailing slash
|
|
$svol =~ s/\/+$//; # sanitize trailing slash
|
|
$svol =~ s/^\/+//; # sanitize trailing slash
|
|
die("svol contains slashes: $svol") if($svol =~ /\//);
|
|
|
|
$vol_info{$sroot} = fetch_subvolume_info($sroot);
|
|
|
|
foreach (@droot) {
|
|
s/\/+$//; # sanitize
|
|
die if exists $vol_info{$_};
|
|
$vol_info{$_} = fetch_subvolume_info($_);
|
|
};
|
|
print Data::Dumper->Dump([\%vol_info], ["vol_info"]) if($verbose);
|
|
|
|
my $postfix = '.' . strftime($time_format, localtime);
|
|
|
|
my $ssnap = "${src_snapshot_dir}/${svol}${postfix}";
|
|
check_src($sroot, $svol);
|
|
|
|
# always make snapshot of svol
|
|
die("snapshot source does not exists: ${sroot}/${svol}") unless check_vol($sroot, $svol);
|
|
die("snapshot destination already exists: ${sroot}/${ssnap}") if check_vol($sroot, $ssnap);
|
|
snapshot("${sroot}/${svol}", "${sroot}/${ssnap}");
|
|
|
|
foreach (@droot) {
|
|
die("subvolume already exists at destination: $_") if(check_vol($_, "${svol}${postfix}"));
|
|
if($incremental) {
|
|
my $dest_latest = get_latest($svol, $_);
|
|
die("snapshot parent source does not exists: ${sroot}/${src_snapshot_dir}/${dest_latest}") unless check_vol($sroot, "${src_snapshot_dir}/${dest_latest}");
|
|
send_receive("${sroot}/${ssnap}", $_, "${sroot}/${src_snapshot_dir}/${dest_latest}");
|
|
}
|
|
else {
|
|
send_receive("${sroot}/${ssnap}", $_);
|
|
}
|
|
}
|
|
}
|
|
|
|
1;
|