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