bin/fuse-imager
changeset 26 496ee9b0f488
parent 25 94a50c69de28
child 27 82c0df89b287
equal deleted inserted replaced
25:94a50c69de28 26:496ee9b0f488
     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 
       
    17 my %o = (
       
    18     debug  => undef,
       
    19     detach => 1,
       
    20     tmp    => undef,
       
    21 );
       
    22 lock_keys %o;
       
    23 
       
    24 use constant ME => basename $0;
       
    25 my ($DATA, $IDX);
       
    26 
       
    27 sub tie_vars;
       
    28 
       
    29 MAIN: {
       
    30 
       
    31     GetOptions(
       
    32         "d|debug!" => \$o{debug},
       
    33         "detach!"  => \$o{detach},
       
    34         "tmp:s" => sub { $o{tmp} = length $_[1] ? $_[1] : $ENV{TMP} // "/tmp" },
       
    35         "h|help" => sub { pod2usage(-verbose => 1, -exit => 0) },
       
    36         "m|man"  => sub {
       
    37             pod2usage(
       
    38                 -verbose   => 2,
       
    39                 -exit      => 0,
       
    40                 -noperlpod => system("perldoc -V 1>/dev/null 2>&1")
       
    41             );
       
    42         },
       
    43       )
       
    44       and @ARGV == 2
       
    45       or pod2usage;
       
    46 
       
    47     my ($src, $mp) = @ARGV;
       
    48 
       
    49     $DATA = "$src/data";
       
    50     $IDX  = "$src/idx";
       
    51 
       
    52     die ME . ": $DATA: $!" if not -d $DATA;
       
    53     die ME . ": $IDX: $!"  if not -d $IDX;
       
    54 
       
    55     if (!$o{debug} and $o{detach}) {
       
    56         fork() and exit;
       
    57         $0 = "FUSE $src $mp";
       
    58         open(STDOUT => ">/dev/null");
       
    59         open(STDIN  => "/dev/null");
       
    60 
       
    61         setpgid($$ => $$);
       
    62     }
       
    63 
       
    64     tie_vars $o{tmp};
       
    65 
       
    66     Fuse::main(
       
    67         mountpoint => $mp,
       
    68         debug      => $o{debug} // 0,
       
    69         getattr    => \&getattr,
       
    70         getdir     => \&getdir,
       
    71         open       => \&openfile,
       
    72         read       => \&readbuffer,
       
    73         write      => \&writebuffer,
       
    74     );
       
    75 
       
    76     exit;
       
    77 
       
    78 }
       
    79 
       
    80 # not the fuse functions
       
    81 
       
    82 {
       
    83     my (%IMAGE, %DIRTY);
       
    84 
       
    85     sub tie_vars {
       
    86         return if not defined $_[0];
       
    87         my $file =
       
    88           -d $_[0]
       
    89           ? File::Temp->new(DIR => shift, TEMPLATE => "tmp.fuse.XXXXXX")
       
    90           ->filename
       
    91           : shift;
       
    92         tie %DIRTY, "DB_File" => $file
       
    93           or die "Can't tie to $file: $!\n";
       
    94     }
       
    95 
       
    96     sub getattr {
       
    97         my $path = $IDX . shift;
       
    98         return stat $path if -d $path;
       
    99         my @attr = stat $path or return -(ENOENT);
       
   100         my %meta = _get_meta($path);
       
   101         $attr[7] = $meta{devsize};
       
   102         $attr[9] = $meta{timestamp};
       
   103         $attr[2] &= ~0222;    # r/o
       
   104         return @attr;
       
   105     }
       
   106 
       
   107     sub getdir {
       
   108         my $path = $IDX . shift;
       
   109         opendir(my $dh, $path) or return 0;
       
   110         return (readdir($dh), 0);
       
   111     }
       
   112 
       
   113     sub openfile {
       
   114         my $path = $IDX . shift;
       
   115         return 0 if exists $IMAGE{$path};
       
   116         $IMAGE{$path}{meta}      = { _get_meta($path) };
       
   117         $IMAGE{$path}{blocklist} = {};
       
   118 
       
   119         # skip the file header
       
   120         open(my $fh => $path);
       
   121         { local $/ = ""; scalar <$fh> }
       
   122 
       
   123         # should check for the format
       
   124         # $IMAGE{$path}{meta}{format}
       
   125 
       
   126         # now read the block list
       
   127         while (<$fh>) {
       
   128             /^#/ and last;
       
   129             my ($block, $cs, $file) = split;
       
   130             $IMAGE{$path}{blocklist}{$block} = $file;
       
   131         }
       
   132         close $fh;
       
   133         return 0;
       
   134     }
       
   135 
       
   136     sub readbuffer {
       
   137         my $path = $IDX . shift;
       
   138         my ($size, $offset) = @_;
       
   139         my $finfo = $IMAGE{$path} or die "File $path is not opened!";
       
   140         return "" if $offset >= $finfo->{meta}{devsize};
       
   141 
       
   142         my $buffer = "";
       
   143         for (my $need = $size ; $need > 0 ; $need = $size - length($buffer)) {
       
   144             $buffer .= _readblock($finfo, $need, $offset + length($buffer));
       
   145         }
       
   146 
       
   147         return $buffer;
       
   148     }
       
   149 
       
   150     sub _readblock {
       
   151         my ($finfo, $size, $offset) = @_;
       
   152 
       
   153         my $block       = int($offset / $finfo->{meta}{blocksize});
       
   154         my $blockoffset = $offset % $finfo->{meta}{blocksize};
       
   155 
       
   156         my $length = $finfo->{meta}{blocksize} - $blockoffset;
       
   157         $length = $size if $size <= $length;
       
   158 
       
   159         if (exists $DIRTY{ $finfo . $block }) {
       
   160             return substr $DIRTY{ $finfo . $block }, $blockoffset, $length;
       
   161         }
       
   162 
       
   163         my $fn = "$DATA/" . $finfo->{blocklist}{$block};
       
   164         if (-e $fn) {
       
   165             open(my $fh => $fn);
       
   166             binmode($fh);
       
   167             seek($fh => $blockoffset, 0) or die "seek: $!";
       
   168             local $/ = \$length;
       
   169             return scalar <$fh>;
       
   170         }
       
   171         elsif (-e "$fn.gz") {
       
   172             open(my $fh => "$fn.gz");
       
   173             binmode($fh);
       
   174             my $buffer;
       
   175             gunzip($fh => \$buffer)
       
   176               or die $GunzipError;
       
   177             close($fh);
       
   178             return substr($buffer, $blockoffset, $size);
       
   179         }
       
   180 
       
   181         die "$fn: $!\n";
       
   182     }
       
   183 
       
   184     sub writebuffer {
       
   185         my $path = $IDX . shift;
       
   186         my ($buffer, $offset) = @_;
       
   187         my $size = length($buffer);
       
   188         my $finfo = $IMAGE{$path} or die "File $path is not opened!";
       
   189 
       
   190         for (my $written = 0 ; $written < $size ;) {
       
   191 
       
   192             # OPTIMIZE: we should not ask for writing more than the
       
   193             # blocksize
       
   194             my $n =
       
   195               _writeblock($finfo, substr($buffer, $written), $offset + $written)
       
   196               or return $written;
       
   197             $written += $n;
       
   198         }
       
   199         return $size;
       
   200     }
       
   201 
       
   202     sub _writeblock {
       
   203         my ($finfo, $buffer, $offset) = @_;
       
   204         my $size = length($buffer);
       
   205 
       
   206         my $block       = int($offset / $finfo->{meta}{blocksize});
       
   207         my $blockoffset = $offset % $finfo->{meta}{blocksize};
       
   208 
       
   209         if (not exists $DIRTY{ $finfo . $block }) {
       
   210             $DIRTY{ $finfo . $block } = _readblock(
       
   211                 $finfo,
       
   212                 $finfo->{meta}{blocksize},
       
   213                 $block * $finfo->{meta}{blocksize}
       
   214             );
       
   215         }
       
   216 
       
   217         my $length = $finfo->{meta}{blocksize} - $blockoffset;
       
   218         $length = $size if $size < $length;
       
   219 
       
   220         substr($DIRTY{ $finfo . $block }, $blockoffset, $length) =
       
   221           substr($buffer, 0, $length);
       
   222 
       
   223         return $length;
       
   224     }
       
   225 
       
   226     sub _get_meta {
       
   227         my $path = shift;
       
   228         my %meta;
       
   229         open(my $fh => $path);
       
   230         while (<$fh>) {
       
   231             last if /^$/;
       
   232             /^(?<k>\S+):\s+(?<v>.*?)\s*$/
       
   233               and do { $meta{ $+{k} } = $+{v}; next; };
       
   234         }
       
   235         return %meta;
       
   236     }
       
   237 
       
   238 }
       
   239 
       
   240 __END__
       
   241 
       
   242 =head1 NAME
       
   243 
       
   244     fuse-imager - the fuse mount helper for imagers backups
       
   245 
       
   246 =head1 SYNOPSIS
       
   247 
       
   248     fuse-imager [options] {src} {mount point}
       
   249 
       
   250 =head1 DESCRIPTION
       
   251 
       
   252 B<fuse-imager> mounts the src directory (containing F<data/> and F<idx/>
       
   253 directories) the the specified mount point.
       
   254 
       
   255 =head1 OPTIONS
       
   256 
       
   257 =over 4
       
   258 
       
   259 =item B<--tmp> [I<dir/>]
       
   260 
       
   261 Write dirty blocks into a buffer file in the specified tmp directory.
       
   262 If no directory is specified, the system default (usually F</tmp>) will
       
   263 be used. (default: no temp file)
       
   264 
       
   265 B<Beware>: The temporary file may get B<HUUGE>.
       
   266 
       
   267 =item B<-d>|B<--debug>
       
   268 
       
   269 Enables debugging output from B<Fuse>. When using this option,
       
   270 B<Fuse> does not detach from the terminal. (default: off)
       
   271 
       
   272 =item B<-->I<[no]>B<detach> 
       
   273 
       
   274 Detach or don't detach from the terminal. (default: detach)
       
   275 
       
   276 =item B<-h>|B<--help>
       
   277 
       
   278 =item B<-m>|B<--man>
       
   279 
       
   280 The common help and man options.
       
   281 
       
   282 =back
       
   283 
       
   284 =cut