|
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 |