bin/imager.fuse
changeset 137 dd11d1262b6c
parent 136 a5d087334439
child 138 790ac145bccc
equal deleted inserted replaced
136:a5d087334439 137:dd11d1262b6c
     1 #! /usr/bin/perl
       
     2 
       
     3 use 5.010;
       
     4 use strict;
       
     5 use warnings;
       
     6 use autodie qw(:all);
       
     7 use Getopt::Long;
       
     8 use Fuse;
       
     9 use POSIX qw(setpgid :errno_h);
       
    10 use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
       
    11 use Pod::Usage;
       
    12 use Hash::Util qw(lock_keys);
       
    13 use File::Temp;
       
    14 use DB_File;
       
    15 use File::Basename;
       
    16 use Imager;
       
    17 use Smart::Comments;
       
    18 
       
    19 my %o = (
       
    20     debug  => undef,
       
    21     detach => 1,
       
    22     tmp    => undef,
       
    23     pass   => undef,
       
    24 );
       
    25 lock_keys %o;
       
    26 
       
    27 use constant ME     => basename $0;
       
    28 use constant BS     => 4 * 1024;
       
    29 use constant CIPHER => "aes-128-cbc";
       
    30 
       
    31 my ($DATA, $IDX);
       
    32 
       
    33 sub tie_vars;
       
    34 
       
    35 sub min {
       
    36     (sort { $a <=> $b } @_)[0];
       
    37 }
       
    38 
       
    39 sub max {
       
    40     (sort { $a <=> $b } @_)[-1];
       
    41 }
       
    42 
       
    43 sub nop_debug { }
       
    44 sub yes_debug { print STDERR @_ }
       
    45 
       
    46 *{main::debug} = \&nop_debug;
       
    47 
       
    48 #$SIG{INT} = sub { warn "Got ^C or INT signal\n"; exit 1; };
       
    49 
       
    50 MAIN: {
       
    51 
       
    52     GetOptions(
       
    53         "d|debug!" => \$o{debug},
       
    54         "detach!"   => \$o{detach},
       
    55         "tmp:s" => sub { $o{tmp} = length $_[1] ? $_[1] : $ENV{TMP} // "/tmp" },
       
    56         "h|help"   => sub { pod2usage(-verbose => 1, -exit => 0) },
       
    57         "p|pass=s" => \$o{pass},
       
    58         "m|man"    => sub {
       
    59             pod2usage(
       
    60                 -verbose   => 2,
       
    61                 -exit      => 0,
       
    62                 -noperlpod => system("perldoc -V 1>/dev/null 2>&1")
       
    63             );
       
    64         },
       
    65       )
       
    66       and @ARGV == 2
       
    67       or pod2usage;
       
    68 
       
    69     if ($o{debug}) {
       
    70         undef &{main::debug};
       
    71         *{main::debug} = \&yes_debug;
       
    72     }
       
    73 
       
    74     my ($src, $mp) = @ARGV;
       
    75 
       
    76     $DATA = "$src/data";
       
    77     $IDX  = "$src/idx";
       
    78 
       
    79     die ME . ": $DATA: $!" if not -d $DATA;
       
    80     die ME . ": $IDX: $!"  if not -d $IDX;
       
    81 
       
    82     if (!$o{debug} and $o{detach}) {
       
    83         fork() and exit;
       
    84         $0 = "FUSE $src $mp";
       
    85         open(STDOUT => ">/dev/null");
       
    86         open(STDIN  => "/dev/null");
       
    87         setpgid($$ => $$);
       
    88     }
       
    89 
       
    90     tie_vars $o{tmp};
       
    91 
       
    92     Fuse::main(
       
    93         mountpoint => $mp,
       
    94 
       
    95         #   debug      => $o{debug} // 0,
       
    96         getattr => \&getattr,
       
    97         getdir  => \&getdir,
       
    98         open    => \&openfile,
       
    99         read    => \&readbuffer,
       
   100         write   => \&writebuffer,
       
   101         release => \&release,
       
   102     );
       
   103 
       
   104     exit;
       
   105 
       
   106 }
       
   107 
       
   108 # not the fuse functions
       
   109 
       
   110 {
       
   111     my (%IMAGE, %DIRTY);
       
   112 
       
   113     sub tie_vars {
       
   114         return if not defined $_[0];
       
   115         my $file =
       
   116           -d $_[0]
       
   117           ? File::Temp->new(DIR => shift, TEMPLATE => "tmp.fuse.XXXXXX")
       
   118           ->filename
       
   119           : shift;
       
   120         tie %DIRTY, "DB_File" => $file
       
   121           or die "Can't tie to $file: $!\n";
       
   122     }
       
   123 
       
   124     sub getattr {
       
   125         my $path = $IDX . shift;
       
   126         return stat $path if -d $path;
       
   127         my @attr = stat $path or return -(ENOENT);
       
   128         my %meta = _get_meta($path);
       
   129         $attr[7] = $meta{devsize};
       
   130         $attr[9] = $meta{timestamp};
       
   131         $attr[2] &= ~0222;    # r/o
       
   132         return @attr;
       
   133     }
       
   134 
       
   135     sub getdir {
       
   136         my $path = $IDX . shift;
       
   137         opendir(my $dh, $path) or return 0;
       
   138         return (readdir($dh), 0);
       
   139     }
       
   140 
       
   141     sub openfile {
       
   142         my $path = $IDX . shift;
       
   143         return 0 if exists $IMAGE{$path};
       
   144         $IMAGE{$path}{meta}      = { _get_meta($path) };
       
   145         $IMAGE{$path}{blocklist} = {};
       
   146 
       
   147         # skip the file header
       
   148         open(my $fh => $path);
       
   149         { local $/ = ""; scalar <$fh> }
       
   150 
       
   151         # should check for the format
       
   152         # $IMAGE{$path}{meta}{format}
       
   153 
       
   154         # now read the block list
       
   155         while (<$fh>) {
       
   156             /^#/ and last;
       
   157             my ($block, $cs, $file) = split;
       
   158             $IMAGE{$path}{blocklist}{$block} = $file;
       
   159         }
       
   160         close $fh;
       
   161         return 0;
       
   162     }
       
   163 
       
   164     sub release {
       
   165         my $path = $IDX . shift;
       
   166         return 0 if not exists $IMAGE{$path};
       
   167         debug("Currently we have " . keys(%DIRTY) . " dirty blocks\n");
       
   168         return 0;
       
   169     }
       
   170 
       
   171     sub readbuffer {
       
   172         my $path = $IDX . shift;
       
   173         my ($size, $offset) = @_;
       
   174         my $finfo = $IMAGE{$path} or die "File $path is not opened!";
       
   175         return "" if $offset >= $finfo->{meta}{devsize};
       
   176 
       
   177 	debug("<<< REQUESTED: offset:$offset size:$size\n");
       
   178 
       
   179         my $buffer = "";
       
   180         for (my $need = $size ; $need > 0 ; $need = $size - length($buffer)) {
       
   181 	    debug("<<<            offset:@{[$offset + length($buffer)]} size:$need\n");
       
   182             $buffer .= _readblock($finfo, $need, $offset + length($buffer));
       
   183 	    debug("<<< missing: ", $size - length($buffer), "\n") if $size - length($buffer);
       
   184         }
       
   185 
       
   186 	debug("<<< SENDING " . length($buffer) . "\n\n");
       
   187         return $buffer;
       
   188     }
       
   189 
       
   190     sub _readblock {
       
   191         my ($finfo, $size, $offset) = @_;
       
   192         my ($block, $blockoffset, $length);
       
   193 
       
   194         debug("<<< requested: offset:$offset size:$size"
       
   195 	    . "\tfrom $finfo->{meta}{filesystem}\n");
       
   196         debug("    mapped to:  block:@{[int($offset/BS)]}+@{[$offset % BS]}\n");
       
   197 
       
   198         # first check if it's an dirty block
       
   199         $block = int($offset / BS);
       
   200         if (exists $DIRTY{ $finfo . $block }) {
       
   201             $blockoffset = $offset % BS;
       
   202             $length = min(BS - $blockoffset, $size);
       
   203 
       
   204             debug(
       
   205                 "+++ dirty offset:$block*@{[BS]} + $blockoffset size:$length\n"
       
   206             );
       
   207             return substr $DIRTY{ $finfo . $block }, $blockoffset, $length;
       
   208         }
       
   209 
       
   210         # if not dirty, we've to find it on disk
       
   211 
       
   212         $block       = int($offset / $finfo->{meta}{blocksize});
       
   213         $blockoffset = $offset % $finfo->{meta}{blocksize};
       
   214         $length      = min($finfo->{meta}{blocksize} - $blockoffset, $size);
       
   215 
       
   216         # find the max length we can satisfy w/o colliding
       
   217         # with dirty blocks
       
   218         for (my $l = BS ; $l < $length ; $l += BS) {
       
   219             my $b = int(($offset + $l) / BS);
       
   220             if ($DIRTY{ $finfo . $b }) {
       
   221                 $length = $l;
       
   222                 last;
       
   223             }
       
   224         }
       
   225 
       
   226         debug("+++ disk  offset:$block*$finfo->{meta}{blocksize}"
       
   227             . " + $blockoffset size:$length\n");
       
   228 
       
   229 #    die if $blockoffset == 417792;
       
   230 
       
   231         my $fn = "$DATA/" . $finfo->{blocklist}{$block};
       
   232 	debug("    fn:$fn\n");
       
   233 
       
   234         state %cache;
       
   235         if (not defined $cache{fn}
       
   236             or ($cache{fn} ne $fn))
       
   237         {
       
   238             Imager::get_block("$fn*" => \$cache{data});
       
   239             $cache{fn} = $fn;
       
   240         }
       
   241 
       
   242         return substr($cache{data}, $blockoffset, $length);
       
   243 
       
   244     }
       
   245 
       
   246     sub writebuffer {
       
   247         my $path = $IDX . shift;
       
   248         my ($buffer, $offset) = @_;
       
   249         my $size = length($buffer);
       
   250         my $finfo = $IMAGE{$path} or die "File $path is not opened!";
       
   251 
       
   252         for (my $written = 0 ; $written < $size ;) {
       
   253 
       
   254             # OPTIMIZE: we should not ask for writing more than the
       
   255             # blocksize
       
   256             my $n =
       
   257               _writeblock($finfo, substr($buffer, $written), $offset + $written)
       
   258               or return $written;
       
   259             $written += $n;
       
   260         }
       
   261         return $size;
       
   262     }
       
   263 
       
   264     sub _writeblock {
       
   265         my ($finfo, $buffer, $offset) = @_;
       
   266         my ($block, $blockoffset, $length);
       
   267         my $size = length($buffer);
       
   268 
       
   269         $block       = int($offset / BS);
       
   270         $blockoffset = $offset % BS;
       
   271         $length      = min(BS - $blockoffset, $size);
       
   272 
       
   273         debug(">>> offset:$offset size:$length of $size\n");
       
   274         debug("    block @{[int($offset/BS)]} + @{[$offset % BS]}\n");
       
   275 
       
   276         if (not exists $DIRTY{ $finfo . $block }) {
       
   277             debug("+++ missing $block+$blockoffset\n");
       
   278             $DIRTY{ $finfo . $block } = _readblock($finfo, BS, $block * BS);
       
   279         }
       
   280 
       
   281         substr($DIRTY{ $finfo . $block }, $blockoffset, $length) =
       
   282           substr($buffer, 0, $length);
       
   283 
       
   284         return $length;
       
   285     }
       
   286 
       
   287     sub _get_meta {
       
   288         my $path = shift;
       
   289         my %meta;
       
   290         open(my $fh => $path);
       
   291         while (<$fh>) {
       
   292             last if /^$/;
       
   293             /^(?<k>\S+):\s+(?:(?<n>\d+)|(?<v>.*?))\s*$/
       
   294               and do {
       
   295 
       
   296           # na sowas, die Zeitstempel dürfen nicht als Zeichenkette reinkommen!
       
   297                 $meta{ $+{k} } = defined $+{n} ? (0 + $+{n}) : $+{v};
       
   298                 next;
       
   299               };
       
   300         }
       
   301         return %meta;
       
   302     }
       
   303 
       
   304 }
       
   305 
       
   306 __END__
       
   307 
       
   308 =head1 NAME
       
   309 
       
   310     imager.fuse - the fuse mount helper for imagers backups
       
   311 
       
   312 =head1 SYNOPSIS
       
   313 
       
   314     imager.fuse [options] {src} {mount point}
       
   315 
       
   316 =head1 DESCRIPTION
       
   317 
       
   318 B<imager.fuse> mounts the src directory (containing F<data/> and F<idx/>
       
   319 directories) the the specified mount point.
       
   320 
       
   321 =head1 OPTIONS
       
   322 
       
   323 =over 4
       
   324 
       
   325 =item B<-d> | B<--debug>
       
   326 
       
   327 Enables debugging output from B<Fuse>. When using this option,
       
   328 B<Fuse> does not detach from the terminal. (default: off)
       
   329 
       
   330 =item B<-->I<[no]>B<detach> 
       
   331 
       
   332 Detach or don't detach from the terminal. (default: detach)
       
   333 
       
   334 =item B<-p>|B<--pass> I<pass>
       
   335 
       
   336 Password to be used for decryption. For the format please refer
       
   337 L<openssl(1)>. (default: not set)
       
   338 
       
   339 =item B<--tmp> [I<dir/>]
       
   340 
       
   341 Write dirty blocks into a buffer file in the specified tmp directory.
       
   342 If no directory is specified, the system default (usually F</tmp>) will
       
   343 be used. (default: no temp file)
       
   344 
       
   345 =item B<-h>|B<--help>
       
   346 
       
   347 =item B<-m>|B<--man>
       
   348 
       
   349 The common help and man options.
       
   350 
       
   351 =back
       
   352 
       
   353 =cut