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