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