--- a/bin/catter Thu Jul 28 12:21:00 2011 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,112 +0,0 @@
-#! /usr/bin/perl
-# Eigentlich geht das selbe mit:
-# grep '^[[:space:]]*[[:digit:]]' IDX-file | tr -d | cut -f4 -d' ' | while read f; do
-# cat DATA/$f || zcat DATA/$f.gz
-# done
-# ODER
-# perl -ne '/^\s*\d/ and print "DATA/" . (split)[2] . "\n"' IDX-File | while read f; do
-# cat DATA/$f || zcat DATA/$f.gz
-# done
-
-use 5.010;
-use strict;
-use warnings;
-use File::Basename;
-use Cwd qw(abs_path);
-use autodie qw(:all);
-use Pod::Usage;
-use Getopt::Long;
-use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
-
-use constant KiB => 1024;
-use constant MiB => 1024 * KiB;
-use constant GiB => 1024 * MiB;
-use constant ME => basename $0;
-
-sub find_data_dir;
-
-MAIN: {
-
- Getopt::Long::Configure(qw(Bundling));
- GetOptions(
- "h|help" => sub { pod2usage(-verbose => 1, -exit => 0) },
- "m|man" => sub {
- pod2usage(
- -verbose => 2,
- -exit => 0,
- -noperldoc => system(
- "perldoc -V 1>/dev/null
- 2>&1"
- )
- );
- },
- )
- and @ARGV == 2
- or pod2usage;
-
- my $idx = shift;
- my $dst = shift;
- my $blocksize = undef;
- my $data = find_data_dir($idx);
-
- open(my $fh => $idx);
- { local $/ = ""; $_ = <$fh>; }
- /^format:\s*1$/m or die ME . ": expected index format 1\n";
- ($blocksize) = /^blocksize:\s*(\d+)/m or die ME . ": no blocksize found\n";
-
- my $out;
- if ($dst eq "-") { open($out => ">&STDOUT") }
- else { open($out => ">", $dst) }
-
- while (<$fh>) {
- next if /^#/;
- my ($blk, $hash, $path) = split;
- my ($in, $buffer);
-
- if (-f "$data/$path") {
- open($in => "$data/$path");
- binmode($in);
- local $/ = \$blocksize;
- $buffer = <$in>;
- }
- elsif (-f "$data/$path.gz") {
- open($in => "$data/$path.gz");
- binmode($in);
- gunzip($in => \$buffer)
- or die $GunzipError;
- }
- else {
- die ME . ": Can't open $data/$path: $!\n";
- }
- print {$out} $buffer;
- close($in);
- }
- close($out);
- close($fh);
-}
-
-sub find_data_dir {
- for (my $dir = shift ; $dir ne "/" ; $dir = abs_path("$dir/..")) {
- return "$dir/data" if -d "$dir/data" and -d "$dir/idx";
- }
- die ME . ": no data directory found!\n";
-}
-
-__END__
-
-=head1 NAME
-
- catter - cats the blocks of the imager
-
-=head1 SYNOPSIS
-
- catter {idx} {destination}
-
-=head1 DESCRIPTION
-
-The B<catter> takes all the blocks from the IDX file and
-cats them as one data stream. The destination can be any block device,
-a file name or even B<-> (STDOUT).
-
-
-=cut
--- a/bin/checker Thu Jul 28 12:21:00 2011 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,265 +0,0 @@
-#! /usr/bin/perl
-
-use 5.010;
-use strict;
-use warnings;
-use Pod::Usage;
-use Hash::Util qw(lock_keys);
-use File::Find;
-use File::Temp;
-use DB_File;
-use File::Basename;
-use autodie qw(:all);
-use Cwd qw(abs_path);
-
-use Getopt::Long;
-sub get_block_list;
-sub purge_unused;
-sub check_images;
-
-my %o = (
- yes => undef,
- verbose => undef,
- check => undef,
-);
-lock_keys(%o);
-
-MAIN: {
- Getopt::Long::Configure qw(Bundling);
- GetOptions(
- "y|yes!" => \$o{yes},
- "v|verbose!" => \$o{verbose},
- "c|check" => \$o{check},
- "h|help" => sub { pod2usage(-verbose => 1, -exit 0) },
- "m|man" => sub {
- pod2usage(
- -verbose => 2,
- -exit 0,
- -noperldoc => system(
- "perldoc -V 1>/dev/null
- 2>&1"
- )
- );
- },
- )
- and @ARGV
- or pod2usage;
- my $dir = shift;
- my $tmp = File::Temp->new;
-
- # load the index files, remember the latest
- # timestamp we see
- #tie %idx, "DB_File" => $tmp->filename;
- my %block = get_block_list($dir);
-
- verbose("# indexed: "
- . scalar(@{ $block{""} // [] })
- . " images with "
- . (grep !/^\.idx$/ => keys(%block))
- . " blocks");
-
- purge_unused($dir => %block);
- check_images($dir => %block);
-}
-
-sub verbose { say @_ if $o{verbose} }
-
-sub get_file_list {
- my ($list) = @_;
- my @files = ();
-
- open(my $fh => $list);
- while (<$fh>) {
- push @files, (split)[2];
- }
- return grep /^[a-z\d.\/]+$/ => @files;
-}
-
-sub get_block_list {
- my $dir = shift;
- my %block;
- find(
- sub {
- (-f) or return; # we need to include the tmp files!
- push @{ $block{""} }, abs_path $_;
- foreach my $f (get_file_list($_)) {
- push @{ $block{$f} } => $#{ $block{""} };
- }
- },
- "$dir/idx"
- );
- return %block;
-}
-
-sub purge_unused {
- my ($dir, %block) = @_;
-
- my ($total, $done);
- verbose("# pass 1 - checking for unused blocks");
- verbose("# pass 1 - estimating file count");
-
- # calculate the number of files we expect
- find(
- sub {
- -d or return;
- opendir(my $dh => $_);
- map { $total++ if not $_ ~~ [qw<. ..>] and length > 8 } readdir $dh;
- closedir($dh);
- },
- "$dir/data"
- );
-
- # progress
- local $SIG{ALRM} = sub {
- return alarm 1 if not $done;
- my $speed = $done / (time - $^T + 1);
- verbose sprintf "# pass 1 done %5.1f%% | %25s (%*d of %d blocks)",
- 100 * ($done / $total),
- scalar(localtime($^T + $speed * ($total - $done))),
- length($total) => $done,
- $total;
- alarm 5;
- };
- $SIG{ALRM}->();
-
- my @unused;
- find(
- sub {
- $done++ if -f;
- (-f _) and ((-M _) > 0) or return; # don't process the fresh blocks
-
- # we don't need uncompressed files if an compressed version
- # exists
- unlink $_ and return if -f "$_.gz";
-
- # cut away the first part of the filename and
- # some optional extension
- (my $rn = $File::Find::name) =~ s/^$dir\/data\/(.*?)(?:\..+)?$/$1/;
- exists $block{$rn} and return;
- push @unused, abs_path $File::Find::name;
- return;
-
- },
- "$dir/data"
- );
- $SIG{ALRM}->();
- alarm 0;
-
- return if not @unused;
-
- say sprintf "found %d (%.1f%%) unused files",
- 0 + @unused,
- 100 * (@unused / $total);
-
- if ($o{yes}) {
- verbose("# deleting " . @unused . " files");
- unlink @unused;
- return;
- }
-
- if (-t) {
- while (1) {
- print "delete? [y/N/v]: ";
- given (<STDIN>) {
- when (/^y(?:es)?$/i) { unlink @unused; last }
- when (/^v/) { say join "\n", @unused; next }
- default { last }
- }
- }
- }
-
-}
-
-sub check_images {
- my ($dir, %block) = @_;
-
- my $total = grep { $_ ne "" } keys(%block);
- my $done = 0;
-
- verbose("# pass 2 - checking image completeness");
-
- # progress
- local $SIG{ALRM} = sub {
- return alarm 1 if not $done;
- my $speed = $done / (time - $^T + 1);
- verbose sprintf "# pass 2 done %5.1f%% | %25s (%*d of %d blocks)",
- 100 * $done / $total,
- scalar(localtime($^T + ($total - $done) * $speed)),
- length($total) => $done,
- $total;
- alarm 5;
- };
- $SIG{ALRM}->();
-
- my %invalid;
- foreach my $k (keys %block) {
- my $i = $block{$k};
- next if $k eq "";
- ++$done;
-
- next
- if -f "$dir/data/$k"
- or -f "$dir/data/$k.gz";
- say "missing $k @$i";
- @invalid{@$i} = ();
- }
- $SIG{ALRM}->();
- alarm 0;
-
- # invalid now contains the numbers of the idx files beiing
- # invalid
- my @invalid = sort @{ $block{""} }[keys %invalid];
-
- return if not @invalid;
-
- say sprintf "found %d (%.1f%%) invalid images:",
- 0 + @invalid,
- 100 * (@invalid / $total);
-
- if ($o{yes}) {
- unlink @invalid;
- return;
- }
-
- while (-t) {
- print "delete? [y/N/v] ";
- given (<STDIN>) {
- when (/^y(?:es)?$/i) { unlink @invalid; last }
- when (/^v/i) { say join "\n" => @invalid; next }
- default { last }
- }
- }
-}
-__END__
-
-=head1 NAME
-
- checker - checks the imager data and index files
-
-=head1 SYNOPSIS
-
- checker [options] {directory}
-
-=head1 DESCRIPTION
-
-This tool loads all the index files from I<directory>F</idx/>,
-checks if all mentioned files are existing and optionally purges
-unreferenced files.
-
-=head1 OPTIONS
-
-=over
-
-=item B<-y>|B<--yes>
-
-Assume "yes" for all questions (dangerous!). (default: no)
-
-=item B<-h>|B<--help>
-
-=item B<-m>|B<--man>
-
-The short and longer help.
-
-=back
-
-=cut
--- a/bin/fuse-imager Thu Jul 28 12:21:00 2011 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,284 +0,0 @@
-#! /usr/bin/perl
-
-use 5.010;
-use strict;
-use warnings;
-use autodie qw(:all);
-use Getopt::Long;
-use Fuse;
-use POSIX qw(setpgid :errno_h);
-use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
-use Pod::Usage;
-use Hash::Util qw(lock_keys);
-use File::Temp;
-use DB_File;
-use File::Basename;
-
-my %o = (
- debug => undef,
- detach => 1,
- tmp => undef,
-);
-lock_keys %o;
-
-use constant ME => basename $0;
-my ($DATA, $IDX);
-
-sub tie_vars;
-
-MAIN: {
-
- GetOptions(
- "d|debug!" => \$o{debug},
- "detach!" => \$o{detach},
- "tmp:s" => sub { $o{tmp} = length $_[1] ? $_[1] : $ENV{TMP} // "/tmp" },
- "h|help" => sub { pod2usage(-verbose => 1, -exit => 0) },
- "m|man" => sub {
- pod2usage(
- -verbose => 2,
- -exit => 0,
- -noperlpod => system("perldoc -V 1>/dev/null 2>&1")
- );
- },
- )
- and @ARGV == 2
- or pod2usage;
-
- my ($src, $mp) = @ARGV;
-
- $DATA = "$src/data";
- $IDX = "$src/idx";
-
- die ME . ": $DATA: $!" if not -d $DATA;
- die ME . ": $IDX: $!" if not -d $IDX;
-
- if (!$o{debug} and $o{detach}) {
- fork() and exit;
- $0 = "FUSE $src $mp";
- open(STDOUT => ">/dev/null");
- open(STDIN => "/dev/null");
-
- setpgid($$ => $$);
- }
-
- tie_vars $o{tmp};
-
- Fuse::main(
- mountpoint => $mp,
- debug => $o{debug} // 0,
- getattr => \&getattr,
- getdir => \&getdir,
- open => \&openfile,
- read => \&readbuffer,
- write => \&writebuffer,
- );
-
- exit;
-
-}
-
-# not the fuse functions
-
-{
- my (%IMAGE, %DIRTY);
-
- sub tie_vars {
- return if not defined $_[0];
- my $file =
- -d $_[0]
- ? File::Temp->new(DIR => shift, TEMPLATE => "tmp.fuse.XXXXXX")
- ->filename
- : shift;
- tie %DIRTY, "DB_File" => $file
- or die "Can't tie to $file: $!\n";
- }
-
- sub getattr {
- my $path = $IDX . shift;
- return stat $path if -d $path;
- my @attr = stat $path or return -(ENOENT);
- my %meta = _get_meta($path);
- $attr[7] = $meta{devsize};
- $attr[9] = $meta{timestamp};
- $attr[2] &= ~0222; # r/o
- return @attr;
- }
-
- sub getdir {
- my $path = $IDX . shift;
- opendir(my $dh, $path) or return 0;
- return (readdir($dh), 0);
- }
-
- sub openfile {
- my $path = $IDX . shift;
- return 0 if exists $IMAGE{$path};
- $IMAGE{$path}{meta} = { _get_meta($path) };
- $IMAGE{$path}{blocklist} = {};
-
- # skip the file header
- open(my $fh => $path);
- { local $/ = ""; scalar <$fh> }
-
- # should check for the format
- # $IMAGE{$path}{meta}{format}
-
- # now read the block list
- while (<$fh>) {
- /^#/ and last;
- my ($block, $cs, $file) = split;
- $IMAGE{$path}{blocklist}{$block} = $file;
- }
- close $fh;
- return 0;
- }
-
- sub readbuffer {
- my $path = $IDX . shift;
- my ($size, $offset) = @_;
- my $finfo = $IMAGE{$path} or die "File $path is not opened!";
- return "" if $offset >= $finfo->{meta}{devsize};
-
- my $buffer = "";
- for (my $need = $size ; $need > 0 ; $need = $size - length($buffer)) {
- $buffer .= _readblock($finfo, $need, $offset + length($buffer));
- }
-
- return $buffer;
- }
-
- sub _readblock {
- my ($finfo, $size, $offset) = @_;
-
- my $block = int($offset / $finfo->{meta}{blocksize});
- my $blockoffset = $offset % $finfo->{meta}{blocksize};
-
- my $length = $finfo->{meta}{blocksize} - $blockoffset;
- $length = $size if $size <= $length;
-
- if (exists $DIRTY{ $finfo . $block }) {
- return substr $DIRTY{ $finfo . $block }, $blockoffset, $length;
- }
-
- my $fn = "$DATA/" . $finfo->{blocklist}{$block};
- if (-e $fn) {
- open(my $fh => $fn);
- binmode($fh);
- seek($fh => $blockoffset, 0) or die "seek: $!";
- local $/ = \$length;
- return scalar <$fh>;
- }
- elsif (-e "$fn.gz") {
- open(my $fh => "$fn.gz");
- binmode($fh);
- my $buffer;
- gunzip($fh => \$buffer)
- or die $GunzipError;
- close($fh);
- return substr($buffer, $blockoffset, $size);
- }
-
- die "$fn: $!\n";
- }
-
- sub writebuffer {
- my $path = $IDX . shift;
- my ($buffer, $offset) = @_;
- my $size = length($buffer);
- my $finfo = $IMAGE{$path} or die "File $path is not opened!";
-
- for (my $written = 0 ; $written < $size ;) {
-
- # OPTIMIZE: we should not ask for writing more than the
- # blocksize
- my $n =
- _writeblock($finfo, substr($buffer, $written), $offset + $written)
- or return $written;
- $written += $n;
- }
- return $size;
- }
-
- sub _writeblock {
- my ($finfo, $buffer, $offset) = @_;
- my $size = length($buffer);
-
- my $block = int($offset / $finfo->{meta}{blocksize});
- my $blockoffset = $offset % $finfo->{meta}{blocksize};
-
- if (not exists $DIRTY{ $finfo . $block }) {
- $DIRTY{ $finfo . $block } = _readblock(
- $finfo,
- $finfo->{meta}{blocksize},
- $block * $finfo->{meta}{blocksize}
- );
- }
-
- my $length = $finfo->{meta}{blocksize} - $blockoffset;
- $length = $size if $size < $length;
-
- substr($DIRTY{ $finfo . $block }, $blockoffset, $length) =
- substr($buffer, 0, $length);
-
- return $length;
- }
-
- sub _get_meta {
- my $path = shift;
- my %meta;
- open(my $fh => $path);
- while (<$fh>) {
- last if /^$/;
- /^(?<k>\S+):\s+(?<v>.*?)\s*$/
- and do { $meta{ $+{k} } = $+{v}; next; };
- }
- return %meta;
- }
-
-}
-
-__END__
-
-=head1 NAME
-
- fuse-imager - the fuse mount helper for imagers backups
-
-=head1 SYNOPSIS
-
- fuse-imager [options] {src} {mount point}
-
-=head1 DESCRIPTION
-
-B<fuse-imager> mounts the src directory (containing F<data/> and F<idx/>
-directories) the the specified mount point.
-
-=head1 OPTIONS
-
-=over 4
-
-=item B<--tmp> [I<dir/>]
-
-Write dirty blocks into a buffer file in the specified tmp directory.
-If no directory is specified, the system default (usually F</tmp>) will
-be used. (default: no temp file)
-
-B<Beware>: The temporary file may get B<HUUGE>.
-
-=item B<-d>|B<--debug>
-
-Enables debugging output from B<Fuse>. When using this option,
-B<Fuse> does not detach from the terminal. (default: off)
-
-=item B<-->I<[no]>B<detach>
-
-Detach or don't detach from the terminal. (default: detach)
-
-=item B<-h>|B<--help>
-
-=item B<-m>|B<--man>
-
-The common help and man options.
-
-=back
-
-=cut
--- a/bin/imager Thu Jul 28 12:21:00 2011 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,228 +0,0 @@
-#! /usr/bin/perl
-
-use 5.010;
-use strict;
-use warnings;
-use POSIX qw(strftime);
-use autodie qw(:all);
-use Digest::MD5 qw(md5_hex);
-use File::Path qw(mkpath);
-use File::Basename;
-use File::Temp;
-use Sys::Hostname;
-use IO::Compress::Gzip qw(gzip $GzipError :level :strategy);
-use Hash::Util qw(lock_keys);
-use Getopt::Long;
-use Pod::Usage;
-
-use constant KiB => 1024;
-use constant MiB => 1024 * KiB;
-use constant GiB => 1024 * MiB;
-use constant NOW => time();
-use constant DATETIME => strftime("%Y-%m-%dT%H:%M:%SZ" => gmtime(NOW));
-
-sub get_devsize;
-sub get_devname;
-
-$SIG{INT} = sub { die "Got INT\n" };
-
-my %o = (
- compress => undef,
- verbose => undef,
- blocksize => 2 * MiB,
-);
-lock_keys(%o);
-
-my $NOW = time();
-
-MAIN: {
- my ($src, $dst);
-
- my $idx = "{DIR}/idx/{HOSTNAME}/{DEVICE}/";
- my $data = "{DIR}/data";
- my $size;
-
- GetOptions(
- "h|help" => sub { pod2usage(-verbose => 1, exit => 0) },
- "m|man" => sub {
- pod2usage(
- -verbose => 2,
- exit => 0,
- -noperldoc => system("perldoc -V >/dev/null 2>&1")
- );
- },
- "z|compress:i" => sub { $o{compress} = $_[1] ? $_[1] : Z_BEST_SPEED },
- "b|blocksize=s" => sub {
- given ($_[1]) {
- when (/(\d+)G/i) { $o{blocksize} = $1 * GiB };
- when (/(\d+)M/i) { $o{blocksize} = $1 * MiB };
- when (/(\d+)K/i) { $o{blocksize} = $1 * KiB };
- when (/^(\d+)$/) { $o{blocksize} = $1 };
- default {
- die "Blocksize $_[1] is incorrect!\n"
- };
- }
- },
- )
- and @ARGV == 2
- or pod2usage;
- ($src, $dst) = @ARGV;
-
- foreach ($idx, $data) {
- s/{DIR}/$dst/g;
- s/{HOSTNAME}/hostname/eg;
- s/{DEVICE}/get_devname($src)/eg;
- }
- $size = get_devsize($src);
-
- -d $dst or die "$0: $dst: $!\n";
- mkpath([$data, $idx]);
-
- my $index = File::Temp->new(DIR => $idx);
- print {$index} <<__EOT;
-# imager
-format: 1
-host: @{[hostname]}
-filesystem: $src
-blocksize: $o{blocksize}
-devsize: $size
-timestamp: @{[NOW]}
-datetime: @{[DATETIME]}
-
-__EOT
-
- open(my $in => $src);
- binmode($in);
- local $/ = \(my $bs = $o{blocksize});
- local $| = 1;
-
- my %stats = (
- written => 0,
- skipped => 0,
- todo => 1 + int($size / $o{blocksize}),
- );
-
- local $SIG{ALRM} = sub {
- my $speed = ($stats{written} + $stats{skipped}) / (time - $^T + 1);
- say sprintf
-"# done %5.1f%% | %24s (%*d of $stats{todo}, written %*d, skipped %*d)",
- 100 * (($stats{written} + $stats{skipped}) / $stats{todo}),
- ($speed ? (scalar localtime($^T + $stats{todo} / $speed)) : ""),
- length($stats{todo}) => $stats{written} + $stats{skipped},
- length($stats{todo}) => $stats{written},
- length($stats{todo}) => $stats{skipped};
- alarm(5);
- };
- $SIG{ALRM}->();
-
- while (my $buffer = <$in>) {
- my ($file, $ext, $cs);
- $file = $cs = md5_hex($buffer);
- $file =~ s/(?<fn>(?<prefix>...).*)/$+{prefix}\/$+{fn}/g;
- $ext = $o{compress} ? ".gz" : "";
-
- # the extension we do not put into the index
- my $log = sprintf "%12d %s %s" => ($. - 1), $cs, $file;
-
- if (not(-e "$data/$file" or -e "$data/$file.gz")) {
- mkpath dirname("$data/$file.gz");
- my $out = File::Temp->new(
- TEMPLATE => ".XXXXXXX",
- DIR => dirname("$data/$file")
- );
- binmode($out);
- if ($o{compress}) {
- gzip(
- \$buffer => $out,
- -Minimal => 1,
- -Level => Z_BEST_SPEED,
- -Strategy => Z_FILTERED
- ) or die $GzipError;
- }
- else { print {$out} $buffer }
- close($out);
- rename($out => "$data/$file$ext");
- $log .= " *";
- $stats{written}++;
- }
- else {
- $log .= " ";
- $stats{skipped}++;
- }
-
- say {$index} $log;
- }
- $SIG{ALRM}->();
- alarm 0;
-
- say {$index} "# DONE (runtime " . (time() - $^T) . "s)";
-
- say "# DONE (runtime " . (time() - $^T) . "s)";
- say "# WRITTEN $stats{written}, SKIPPED $stats{skipped} blocks";
- say "# SAVINGS "
- . sprintf "%3d%%" => 100 *
- ($stats{skipped} / ($stats{written} + $stats{skipped}));
-
- rename $index->filename => "$idx/" . DATETIME;
- close $index;
-
-}
-
-sub get_devsize {
- my ($devname) = @_;
- open(my $fh => $devname);
- seek($fh, 0, 2);
- return tell($fh);
-}
-
-sub get_devname {
- my $_ = shift;
- s/^\/dev\///;
- s/_/__/g;
- s/\//_/g;
- return $_;
-}
-
-__END__
-
-=head1 NAME
-
- imager - create a block device snapshot
-
-=head1 SYNOPSIS
-
- imager [options] {device} {destination}
-
-=head1 DESCRIPTION
-
-This tool creates a snapshot of a blockdevice.
-Just call it like
-
- imager /dev/sda1 /media/backup
-
-This will create F</media/backup/{data,idx}>, if not already existing.
-The index (blocklist) goes to
-I<destination>F</idx/>I<hostname>F</>I<devicename>. The data goes to
-I<destination>/F<data/>.
-
-=head1 OPTIONS
-
-=over
-
-=item B<-z> [I<level>]|B<--compress>[=I<level>]
-
-Use compression when writing the blocks to disk. (default: off)
-
-=item B<-b> I<blocksize>|B<--blocksize>=I<blocksize>
-
-The blocksize used. (may be suffixed with K, M, G). (default: 2MiB)
-
-=item B<-h>|B<--help>
-
-=item B<-m>|B<--man>
-
-The short and longer help.
-
-=back
-
-=cut
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/imager.check Fri Jul 29 10:53:14 2011 +0200
@@ -0,0 +1,265 @@
+#! /usr/bin/perl
+
+use 5.010;
+use strict;
+use warnings;
+use Pod::Usage;
+use Hash::Util qw(lock_keys);
+use File::Find;
+use File::Temp;
+use DB_File;
+use File::Basename;
+use autodie qw(:all);
+use Cwd qw(abs_path);
+
+use Getopt::Long;
+sub get_block_list;
+sub purge_unused;
+sub check_images;
+
+my %o = (
+ yes => undef,
+ verbose => undef,
+ check => undef,
+);
+lock_keys(%o);
+
+MAIN: {
+ Getopt::Long::Configure qw(Bundling);
+ GetOptions(
+ "y|yes!" => \$o{yes},
+ "v|verbose!" => \$o{verbose},
+ "c|check" => \$o{check},
+ "h|help" => sub { pod2usage(-verbose => 1, -exit 0) },
+ "m|man" => sub {
+ pod2usage(
+ -verbose => 2,
+ -exit 0,
+ -noperldoc => system(
+ "perldoc -V 1>/dev/null
+ 2>&1"
+ )
+ );
+ },
+ )
+ and @ARGV
+ or pod2usage;
+ my $dir = shift;
+ my $tmp = File::Temp->new;
+
+ # load the index files, remember the latest
+ # timestamp we see
+ #tie %idx, "DB_File" => $tmp->filename;
+ my %block = get_block_list($dir);
+
+ verbose("# indexed: "
+ . scalar(@{ $block{""} // [] })
+ . " images with "
+ . (grep !/^\.idx$/ => keys(%block))
+ . " blocks");
+
+ purge_unused($dir => %block);
+ check_images($dir => %block);
+}
+
+sub verbose { say @_ if $o{verbose} }
+
+sub get_file_list {
+ my ($list) = @_;
+ my @files = ();
+
+ open(my $fh => $list);
+ while (<$fh>) {
+ push @files, (split)[2];
+ }
+ return grep /^[a-z\d.\/]+$/ => @files;
+}
+
+sub get_block_list {
+ my $dir = shift;
+ my %block;
+ find(
+ sub {
+ (-f) or return; # we need to include the tmp files!
+ push @{ $block{""} }, abs_path $_;
+ foreach my $f (get_file_list($_)) {
+ push @{ $block{$f} } => $#{ $block{""} };
+ }
+ },
+ "$dir/idx"
+ );
+ return %block;
+}
+
+sub purge_unused {
+ my ($dir, %block) = @_;
+
+ my ($total, $done);
+ verbose("# pass 1 - checking for unused blocks");
+ verbose("# pass 1 - estimating file count");
+
+ # calculate the number of files we expect
+ find(
+ sub {
+ -d or return;
+ opendir(my $dh => $_);
+ map { $total++ if not $_ ~~ [qw<. ..>] and length > 8 } readdir $dh;
+ closedir($dh);
+ },
+ "$dir/data"
+ );
+
+ # progress
+ local $SIG{ALRM} = sub {
+ return alarm 1 if not $done;
+ my $speed = $done / (time - $^T + 1);
+ verbose sprintf "# pass 1 done %5.1f%% | %25s (%*d of %d blocks)",
+ 100 * ($done / $total),
+ scalar(localtime($^T + $speed * ($total - $done))),
+ length($total) => $done,
+ $total;
+ alarm 5;
+ };
+ $SIG{ALRM}->();
+
+ my @unused;
+ find(
+ sub {
+ $done++ if -f;
+ (-f _) and ((-M _) > 0) or return; # don't process the fresh blocks
+
+ # we don't need uncompressed files if an compressed version
+ # exists
+ unlink $_ and return if -f "$_.gz";
+
+ # cut away the first part of the filename and
+ # some optional extension
+ (my $rn = $File::Find::name) =~ s/^$dir\/data\/(.*?)(?:\..+)?$/$1/;
+ exists $block{$rn} and return;
+ push @unused, abs_path $File::Find::name;
+ return;
+
+ },
+ "$dir/data"
+ );
+ $SIG{ALRM}->();
+ alarm 0;
+
+ return if not @unused;
+
+ say sprintf "found %d (%.1f%%) unused files",
+ 0 + @unused,
+ 100 * (@unused / $total);
+
+ if ($o{yes}) {
+ verbose("# deleting " . @unused . " files");
+ unlink @unused;
+ return;
+ }
+
+ if (-t) {
+ while (1) {
+ print "delete? [y/N/v]: ";
+ given (<STDIN>) {
+ when (/^y(?:es)?$/i) { unlink @unused; last }
+ when (/^v/) { say join "\n", @unused; next }
+ default { last }
+ }
+ }
+ }
+
+}
+
+sub check_images {
+ my ($dir, %block) = @_;
+
+ my $total = grep { $_ ne "" } keys(%block);
+ my $done = 0;
+
+ verbose("# pass 2 - checking image completeness");
+
+ # progress
+ local $SIG{ALRM} = sub {
+ return alarm 1 if not $done;
+ my $speed = $done / (time - $^T + 1);
+ verbose sprintf "# pass 2 done %5.1f%% | %25s (%*d of %d blocks)",
+ 100 * $done / $total,
+ scalar(localtime($^T + ($total - $done) * $speed)),
+ length($total) => $done,
+ $total;
+ alarm 5;
+ };
+ $SIG{ALRM}->();
+
+ my %invalid;
+ foreach my $k (keys %block) {
+ my $i = $block{$k};
+ next if $k eq "";
+ ++$done;
+
+ next
+ if -f "$dir/data/$k"
+ or -f "$dir/data/$k.gz";
+ say "missing $k @$i";
+ @invalid{@$i} = ();
+ }
+ $SIG{ALRM}->();
+ alarm 0;
+
+ # invalid now contains the numbers of the idx files beiing
+ # invalid
+ my @invalid = sort @{ $block{""} }[keys %invalid];
+
+ return if not @invalid;
+
+ say sprintf "found %d (%.1f%%) invalid images:",
+ 0 + @invalid,
+ 100 * (@invalid / $total);
+
+ if ($o{yes}) {
+ unlink @invalid;
+ return;
+ }
+
+ while (-t) {
+ print "delete? [y/N/v] ";
+ given (<STDIN>) {
+ when (/^y(?:es)?$/i) { unlink @invalid; last }
+ when (/^v/i) { say join "\n" => @invalid; next }
+ default { last }
+ }
+ }
+}
+__END__
+
+=head1 NAME
+
+ imager.check - checks the imager data and index files
+
+=head1 SYNOPSIS
+
+ imager.check [options] {directory}
+
+=head1 DESCRIPTION
+
+This tool loads all the index files from I<directory>F</idx/>,
+checks if all mentioned files are existing and optionally purges
+unreferenced files.
+
+=head1 OPTIONS
+
+=over
+
+=item B<-y>|B<--yes>
+
+Assume "yes" for all questions (dangerous!). (default: no)
+
+=item B<-h>|B<--help>
+
+=item B<-m>|B<--man>
+
+The short and longer help.
+
+=back
+
+=cut
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/imager.fuse Fri Jul 29 10:53:14 2011 +0200
@@ -0,0 +1,284 @@
+#! /usr/bin/perl
+
+use 5.010;
+use strict;
+use warnings;
+use autodie qw(:all);
+use Getopt::Long;
+use Fuse;
+use POSIX qw(setpgid :errno_h);
+use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
+use Pod::Usage;
+use Hash::Util qw(lock_keys);
+use File::Temp;
+use DB_File;
+use File::Basename;
+
+my %o = (
+ debug => undef,
+ detach => 1,
+ tmp => undef,
+);
+lock_keys %o;
+
+use constant ME => basename $0;
+my ($DATA, $IDX);
+
+sub tie_vars;
+
+MAIN: {
+
+ GetOptions(
+ "d|debug!" => \$o{debug},
+ "detach!" => \$o{detach},
+ "tmp:s" => sub { $o{tmp} = length $_[1] ? $_[1] : $ENV{TMP} // "/tmp" },
+ "h|help" => sub { pod2usage(-verbose => 1, -exit => 0) },
+ "m|man" => sub {
+ pod2usage(
+ -verbose => 2,
+ -exit => 0,
+ -noperlpod => system("perldoc -V 1>/dev/null 2>&1")
+ );
+ },
+ )
+ and @ARGV == 2
+ or pod2usage;
+
+ my ($src, $mp) = @ARGV;
+
+ $DATA = "$src/data";
+ $IDX = "$src/idx";
+
+ die ME . ": $DATA: $!" if not -d $DATA;
+ die ME . ": $IDX: $!" if not -d $IDX;
+
+ if (!$o{debug} and $o{detach}) {
+ fork() and exit;
+ $0 = "FUSE $src $mp";
+ open(STDOUT => ">/dev/null");
+ open(STDIN => "/dev/null");
+
+ setpgid($$ => $$);
+ }
+
+ tie_vars $o{tmp};
+
+ Fuse::main(
+ mountpoint => $mp,
+ debug => $o{debug} // 0,
+ getattr => \&getattr,
+ getdir => \&getdir,
+ open => \&openfile,
+ read => \&readbuffer,
+ write => \&writebuffer,
+ );
+
+ exit;
+
+}
+
+# not the fuse functions
+
+{
+ my (%IMAGE, %DIRTY);
+
+ sub tie_vars {
+ return if not defined $_[0];
+ my $file =
+ -d $_[0]
+ ? File::Temp->new(DIR => shift, TEMPLATE => "tmp.fuse.XXXXXX")
+ ->filename
+ : shift;
+ tie %DIRTY, "DB_File" => $file
+ or die "Can't tie to $file: $!\n";
+ }
+
+ sub getattr {
+ my $path = $IDX . shift;
+ return stat $path if -d $path;
+ my @attr = stat $path or return -(ENOENT);
+ my %meta = _get_meta($path);
+ $attr[7] = $meta{devsize};
+ $attr[9] = $meta{timestamp};
+ $attr[2] &= ~0222; # r/o
+ return @attr;
+ }
+
+ sub getdir {
+ my $path = $IDX . shift;
+ opendir(my $dh, $path) or return 0;
+ return (readdir($dh), 0);
+ }
+
+ sub openfile {
+ my $path = $IDX . shift;
+ return 0 if exists $IMAGE{$path};
+ $IMAGE{$path}{meta} = { _get_meta($path) };
+ $IMAGE{$path}{blocklist} = {};
+
+ # skip the file header
+ open(my $fh => $path);
+ { local $/ = ""; scalar <$fh> }
+
+ # should check for the format
+ # $IMAGE{$path}{meta}{format}
+
+ # now read the block list
+ while (<$fh>) {
+ /^#/ and last;
+ my ($block, $cs, $file) = split;
+ $IMAGE{$path}{blocklist}{$block} = $file;
+ }
+ close $fh;
+ return 0;
+ }
+
+ sub readbuffer {
+ my $path = $IDX . shift;
+ my ($size, $offset) = @_;
+ my $finfo = $IMAGE{$path} or die "File $path is not opened!";
+ return "" if $offset >= $finfo->{meta}{devsize};
+
+ my $buffer = "";
+ for (my $need = $size ; $need > 0 ; $need = $size - length($buffer)) {
+ $buffer .= _readblock($finfo, $need, $offset + length($buffer));
+ }
+
+ return $buffer;
+ }
+
+ sub _readblock {
+ my ($finfo, $size, $offset) = @_;
+
+ my $block = int($offset / $finfo->{meta}{blocksize});
+ my $blockoffset = $offset % $finfo->{meta}{blocksize};
+
+ my $length = $finfo->{meta}{blocksize} - $blockoffset;
+ $length = $size if $size <= $length;
+
+ if (exists $DIRTY{ $finfo . $block }) {
+ return substr $DIRTY{ $finfo . $block }, $blockoffset, $length;
+ }
+
+ my $fn = "$DATA/" . $finfo->{blocklist}{$block};
+ if (-e $fn) {
+ open(my $fh => $fn);
+ binmode($fh);
+ seek($fh => $blockoffset, 0) or die "seek: $!";
+ local $/ = \$length;
+ return scalar <$fh>;
+ }
+ elsif (-e "$fn.gz") {
+ open(my $fh => "$fn.gz");
+ binmode($fh);
+ my $buffer;
+ gunzip($fh => \$buffer)
+ or die $GunzipError;
+ close($fh);
+ return substr($buffer, $blockoffset, $size);
+ }
+
+ die "$fn: $!\n";
+ }
+
+ sub writebuffer {
+ my $path = $IDX . shift;
+ my ($buffer, $offset) = @_;
+ my $size = length($buffer);
+ my $finfo = $IMAGE{$path} or die "File $path is not opened!";
+
+ for (my $written = 0 ; $written < $size ;) {
+
+ # OPTIMIZE: we should not ask for writing more than the
+ # blocksize
+ my $n =
+ _writeblock($finfo, substr($buffer, $written), $offset + $written)
+ or return $written;
+ $written += $n;
+ }
+ return $size;
+ }
+
+ sub _writeblock {
+ my ($finfo, $buffer, $offset) = @_;
+ my $size = length($buffer);
+
+ my $block = int($offset / $finfo->{meta}{blocksize});
+ my $blockoffset = $offset % $finfo->{meta}{blocksize};
+
+ if (not exists $DIRTY{ $finfo . $block }) {
+ $DIRTY{ $finfo . $block } = _readblock(
+ $finfo,
+ $finfo->{meta}{blocksize},
+ $block * $finfo->{meta}{blocksize}
+ );
+ }
+
+ my $length = $finfo->{meta}{blocksize} - $blockoffset;
+ $length = $size if $size < $length;
+
+ substr($DIRTY{ $finfo . $block }, $blockoffset, $length) =
+ substr($buffer, 0, $length);
+
+ return $length;
+ }
+
+ sub _get_meta {
+ my $path = shift;
+ my %meta;
+ open(my $fh => $path);
+ while (<$fh>) {
+ last if /^$/;
+ /^(?<k>\S+):\s+(?<v>.*?)\s*$/
+ and do { $meta{ $+{k} } = $+{v}; next; };
+ }
+ return %meta;
+ }
+
+}
+
+__END__
+
+=head1 NAME
+
+ imager.fuse - the fuse mount helper for imagers backups
+
+=head1 SYNOPSIS
+
+ imager.fuse [options] {src} {mount point}
+
+=head1 DESCRIPTION
+
+B<imager.fuse> mounts the src directory (containing F<data/> and F<idx/>
+directories) the the specified mount point.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<--tmp> [I<dir/>]
+
+Write dirty blocks into a buffer file in the specified tmp directory.
+If no directory is specified, the system default (usually F</tmp>) will
+be used. (default: no temp file)
+
+B<Beware>: The temporary file may get B<HUUGE>.
+
+=item B<-d>|B<--debug>
+
+Enables debugging output from B<Fuse>. When using this option,
+B<Fuse> does not detach from the terminal. (default: off)
+
+=item B<-->I<[no]>B<detach>
+
+Detach or don't detach from the terminal. (default: detach)
+
+=item B<-h>|B<--help>
+
+=item B<-m>|B<--man>
+
+The common help and man options.
+
+=back
+
+=cut
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/imager.restore Fri Jul 29 10:53:14 2011 +0200
@@ -0,0 +1,112 @@
+#! /usr/bin/perl
+# Eigentlich geht das selbe mit:
+# grep '^[[:space:]]*[[:digit:]]' IDX-file | tr -d | cut -f4 -d' ' | while read f; do
+# cat DATA/$f || zcat DATA/$f.gz
+# done
+# ODER
+# perl -ne '/^\s*\d/ and print "DATA/" . (split)[2] . "\n"' IDX-File | while read f; do
+# cat DATA/$f || zcat DATA/$f.gz
+# done
+
+use 5.010;
+use strict;
+use warnings;
+use File::Basename;
+use Cwd qw(abs_path);
+use autodie qw(:all);
+use Pod::Usage;
+use Getopt::Long;
+use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
+
+use constant KiB => 1024;
+use constant MiB => 1024 * KiB;
+use constant GiB => 1024 * MiB;
+use constant ME => basename $0;
+
+sub find_data_dir;
+
+MAIN: {
+
+ Getopt::Long::Configure(qw(Bundling));
+ GetOptions(
+ "h|help" => sub { pod2usage(-verbose => 1, -exit => 0) },
+ "m|man" => sub {
+ pod2usage(
+ -verbose => 2,
+ -exit => 0,
+ -noperldoc => system(
+ "perldoc -V 1>/dev/null
+ 2>&1"
+ )
+ );
+ },
+ )
+ and @ARGV == 2
+ or pod2usage;
+
+ my $idx = shift;
+ my $dst = shift;
+ my $blocksize = undef;
+ my $data = find_data_dir($idx);
+
+ open(my $fh => $idx);
+ { local $/ = ""; $_ = <$fh>; }
+ /^format:\s*1$/m or die ME . ": expected index format 1\n";
+ ($blocksize) = /^blocksize:\s*(\d+)/m or die ME . ": no blocksize found\n";
+
+ my $out;
+ if ($dst eq "-") { open($out => ">&STDOUT") }
+ else { open($out => ">", $dst) }
+
+ while (<$fh>) {
+ next if /^#/;
+ my ($blk, $hash, $path) = split;
+ my ($in, $buffer);
+
+ if (-f "$data/$path") {
+ open($in => "$data/$path");
+ binmode($in);
+ local $/ = \$blocksize;
+ $buffer = <$in>;
+ }
+ elsif (-f "$data/$path.gz") {
+ open($in => "$data/$path.gz");
+ binmode($in);
+ gunzip($in => \$buffer)
+ or die $GunzipError;
+ }
+ else {
+ die ME . ": Can't open $data/$path: $!\n";
+ }
+ print {$out} $buffer;
+ close($in);
+ }
+ close($out);
+ close($fh);
+}
+
+sub find_data_dir {
+ for (my $dir = shift ; $dir ne "/" ; $dir = abs_path("$dir/..")) {
+ return "$dir/data" if -d "$dir/data" and -d "$dir/idx";
+ }
+ die ME . ": no data directory found!\n";
+}
+
+__END__
+
+=head1 NAME
+
+ imager.restore - cats the blocks of the imager
+
+=head1 SYNOPSIS
+
+ imager.restore {idx} {destination}
+
+=head1 DESCRIPTION
+
+The B<imager.restore> takes all the blocks from the IDX file and
+cats them as one data stream. The destination can be any block device,
+a file name or even B<-> (STDOUT).
+
+
+=cut
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/imager.save Fri Jul 29 10:53:14 2011 +0200
@@ -0,0 +1,228 @@
+#! /usr/bin/perl
+
+use 5.010;
+use strict;
+use warnings;
+use POSIX qw(strftime);
+use autodie qw(:all);
+use Digest::MD5 qw(md5_hex);
+use File::Path qw(mkpath);
+use File::Basename;
+use File::Temp;
+use Sys::Hostname;
+use IO::Compress::Gzip qw(gzip $GzipError :level :strategy);
+use Hash::Util qw(lock_keys);
+use Getopt::Long;
+use Pod::Usage;
+
+use constant KiB => 1024;
+use constant MiB => 1024 * KiB;
+use constant GiB => 1024 * MiB;
+use constant NOW => time();
+use constant DATETIME => strftime("%Y-%m-%dT%H:%M:%SZ" => gmtime(NOW));
+
+sub get_devsize;
+sub get_devname;
+
+$SIG{INT} = sub { die "Got INT\n" };
+
+my %o = (
+ compress => undef,
+ verbose => undef,
+ blocksize => 2 * MiB,
+);
+lock_keys(%o);
+
+my $NOW = time();
+
+MAIN: {
+ my ($src, $dst);
+
+ my $idx = "{DIR}/idx/{HOSTNAME}/{DEVICE}/";
+ my $data = "{DIR}/data";
+ my $size;
+
+ GetOptions(
+ "h|help" => sub { pod2usage(-verbose => 1, exit => 0) },
+ "m|man" => sub {
+ pod2usage(
+ -verbose => 2,
+ exit => 0,
+ -noperldoc => system("perldoc -V >/dev/null 2>&1")
+ );
+ },
+ "z|compress:i" => sub { $o{compress} = $_[1] ? $_[1] : Z_BEST_SPEED },
+ "b|blocksize=s" => sub {
+ given ($_[1]) {
+ when (/(\d+)G/i) { $o{blocksize} = $1 * GiB };
+ when (/(\d+)M/i) { $o{blocksize} = $1 * MiB };
+ when (/(\d+)K/i) { $o{blocksize} = $1 * KiB };
+ when (/^(\d+)$/) { $o{blocksize} = $1 };
+ default {
+ die "Blocksize $_[1] is incorrect!\n"
+ };
+ }
+ },
+ )
+ and @ARGV == 2
+ or pod2usage;
+ ($src, $dst) = @ARGV;
+
+ foreach ($idx, $data) {
+ s/{DIR}/$dst/g;
+ s/{HOSTNAME}/hostname/eg;
+ s/{DEVICE}/get_devname($src)/eg;
+ }
+ $size = get_devsize($src);
+
+ -d $dst or die "$0: $dst: $!\n";
+ mkpath([$data, $idx]);
+
+ my $index = File::Temp->new(DIR => $idx);
+ print {$index} <<__EOT;
+# imager
+format: 1
+host: @{[hostname]}
+filesystem: $src
+blocksize: $o{blocksize}
+devsize: $size
+timestamp: @{[NOW]}
+datetime: @{[DATETIME]}
+
+__EOT
+
+ open(my $in => $src);
+ binmode($in);
+ local $/ = \(my $bs = $o{blocksize});
+ local $| = 1;
+
+ my %stats = (
+ written => 0,
+ skipped => 0,
+ todo => 1 + int($size / $o{blocksize}),
+ );
+
+ local $SIG{ALRM} = sub {
+ my $speed = ($stats{written} + $stats{skipped}) / (time - $^T + 1);
+ say sprintf
+"# done %5.1f%% | %24s (%*d of $stats{todo}, written %*d, skipped %*d)",
+ 100 * (($stats{written} + $stats{skipped}) / $stats{todo}),
+ ($speed ? (scalar localtime($^T + $stats{todo} / $speed)) : ""),
+ length($stats{todo}) => $stats{written} + $stats{skipped},
+ length($stats{todo}) => $stats{written},
+ length($stats{todo}) => $stats{skipped};
+ alarm(5);
+ };
+ $SIG{ALRM}->();
+
+ while (my $buffer = <$in>) {
+ my ($file, $ext, $cs);
+ $file = $cs = md5_hex($buffer);
+ $file =~ s/(?<fn>(?<prefix>...).*)/$+{prefix}\/$+{fn}/g;
+ $ext = $o{compress} ? ".gz" : "";
+
+ # the extension we do not put into the index
+ my $log = sprintf "%12d %s %s" => ($. - 1), $cs, $file;
+
+ if (not(-e "$data/$file" or -e "$data/$file.gz")) {
+ mkpath dirname("$data/$file.gz");
+ my $out = File::Temp->new(
+ TEMPLATE => ".XXXXXXX",
+ DIR => dirname("$data/$file")
+ );
+ binmode($out);
+ if ($o{compress}) {
+ gzip(
+ \$buffer => $out,
+ -Minimal => 1,
+ -Level => Z_BEST_SPEED,
+ -Strategy => Z_FILTERED
+ ) or die $GzipError;
+ }
+ else { print {$out} $buffer }
+ close($out);
+ rename($out => "$data/$file$ext");
+ $log .= " *";
+ $stats{written}++;
+ }
+ else {
+ $log .= " ";
+ $stats{skipped}++;
+ }
+
+ say {$index} $log;
+ }
+ $SIG{ALRM}->();
+ alarm 0;
+
+ say {$index} "# DONE (runtime " . (time() - $^T) . "s)";
+
+ say "# DONE (runtime " . (time() - $^T) . "s)";
+ say "# WRITTEN $stats{written}, SKIPPED $stats{skipped} blocks";
+ say "# SAVINGS "
+ . sprintf "%3d%%" => 100 *
+ ($stats{skipped} / ($stats{written} + $stats{skipped}));
+
+ rename $index->filename => "$idx/" . DATETIME;
+ close $index;
+
+}
+
+sub get_devsize {
+ my ($devname) = @_;
+ open(my $fh => $devname);
+ seek($fh, 0, 2);
+ return tell($fh);
+}
+
+sub get_devname {
+ my $_ = shift;
+ s/^\/dev\///;
+ s/_/__/g;
+ s/\//_/g;
+ return $_;
+}
+
+__END__
+
+=head1 NAME
+
+ imager.save - create a block device snapshot
+
+=head1 SYNOPSIS
+
+ imager.save [options] {device} {destination}
+
+=head1 DESCRIPTION
+
+This tool creates a snapshot of a blockdevice.
+Just call it like
+
+ imager.save /dev/sda1 /media/backup
+
+This will create F</media/backup/{data,idx}>, if not already existing.
+The index (blocklist) goes to
+I<destination>F</idx/>I<hostname>F</>I<devicename>. The data goes to
+I<destination>/F<data/>.
+
+=head1 OPTIONS
+
+=over
+
+=item B<-z> [I<level>]|B<--compress>[=I<level>]
+
+Use compression when writing the blocks to disk. (default: off)
+
+=item B<-b> I<blocksize>|B<--blocksize>=I<blocksize>
+
+The blocksize used. (may be suffixed with K, M, G). (default: 2MiB)
+
+=item B<-h>|B<--help>
+
+=item B<-m>|B<--man>
+
+The short and longer help.
+
+=back
+
+=cut