File Coverage

blib/lib/Brackup/Target/Filesystem.pm
Criterion Covered Total %
statement 141 230 61.3
branch 40 114 35.0
condition 4 23 17.3
subroutine 24 30 80.0
pod 0 17 0.0
total 209 414 50.4


line stmt bran cond sub pod time code
1             package Brackup::Target::Filesystem;
2 5     5   35 use strict;
  5         9  
  5         264  
3 5     5   415 use warnings;
  5         12  
  5         337  
4 5     5   30 use base 'Brackup::Target::Filebased';
  5         11  
  5         6461  
5 5     5   41 use File::Basename;
  5         13  
  5         1217  
6 5     5   75 use File::Find ();
  5         14  
  5         1044  
7 5     5   31 use File::Path;
  5         9  
  5         352  
8 5     5   30 use File::stat ();
  5         13  
  5         215  
9 5     5   30 use Brackup::Util qw(io_print_to_fh);
  5         13  
  5         35870  
10              
11              
12             sub new {
13 7     7 0 17 my ($class, $confsec) = @_;
14 7         107 my $self = $class->SUPER::new($confsec);
15              
16 7         75 $self->{path} = $confsec->path_value("path");
17 7         40 $self->{nocolons} = $confsec->value("no_filename_colons");
18              
19             # LAME: Make it work on Windows
20 7 50       71 $self->{nocolons} = ($^O eq 'MSWin32') unless defined $self->{nocolons};
21              
22             # see if we're operating in a pre-1.06 environment
23 7 50       420 if (opendir(my $dh, $self->{path})) {
24 7         31 $self->{_no_four_hex_dirs_in_root} = 1;
25 7         130 while (my $file = readdir($dh)) {
26 14 50       101 if ($file =~ /^[0-9a-f]{4}$/) {
27 0         0 $self->{_no_four_hex_dirs_in_root} = 0;
28             }
29             }
30             }
31              
32 7 50       45 if ($ENV{BRACKUP_REARRANGE_FS_TARGET}) {
33 0         0 $self->_upgrade_layout;
34             }
35              
36 7         154 return $self;
37             }
38              
39             sub new_from_backup_header {
40 11     11 0 26 my ($class, $header) = @_;
41 11         43 my $self = bless {}, $class;
42 11 50       69 $self->{path} = $header->{"BackupPath"} or
43             die "No BackupPath specified in the backup metafile.\n";
44 11 50       55 $self->{nocolons} = $header->{"NoColons"} or 0;
45 11 50       563 unless (-d $self->{path}) {
46 0         0 die "Restore path $self->{path} doesn't exist.\n";
47             }
48 11         45 return $self;
49             }
50              
51             sub nocolons {
52 384     384 0 1363 my ($self) = @_;
53 384         2671 return $self->{nocolons};
54             }
55              
56             sub backup_header {
57 8     8 0 20 my $self = shift;
58             return {
59 8 50       219 "BackupPath" => $self->{path},
60             "NoColons" => $self->{nocolons}?"1":"0",
61             };
62             }
63              
64             # 1.05 and before stored files on disk as: xxxx/xxxx/xxxxxxxxxx.brackup
65             # (that is, two levels of directories, each 4 hex digits long, or 65536
66             # files per directory, which is 2x what ext3 can store, leading to errors.
67             # in 1.06 and above, xx/xx/xxxxxx is used. that is, two levels of 2 hex
68             # digits. this function
69             sub _upgrade_layout {
70 0     0   0 my $self = shift;
71 0         0 my $clean_limit = shift; # optional; if set, max top-level dirs to clean
72              
73 0         0 my $root = $self->{path};
74              
75 0 0       0 opendir(my $dh, $root) or die "Error opening $root: $!";
76              
77             # read the current state of things in the root directory
78             # (which is presumably maxed out on files, at 32k or whatnot)
79 0         0 my %exist_twodir; # two_dir -> 1 (which two-letter directories exist)
80             my %exist_fourdir; # four_dir -> 1 (which four-letter directories exist)
81 0         0 my %four_of_two; # two_dir -> [ four_dir, four_dir, ... ]
82 0         0 while (my $dir = readdir($dh)) {
83 0 0       0 next unless -d "$root/$dir";
84 0 0       0 if ($dir =~ /^[0-9a-f]{2}$/) {
85 0         0 $exist_twodir{$dir} = 1;
86 0         0 next;
87             }
88 0 0       0 if ($dir =~ /^([0-9a-f]{2})([0-9a-f]{2})$/) {
89 0         0 $exist_fourdir{"$1$2"} = 1;
90 0   0     0 push @{ $four_of_two{$1} ||= [] }, "$1$2";
  0         0  
91             }
92             }
93              
94             # for each 4-digit directory, sorted by number of four-digit directories
95             # that exist for their leading 2-digit prefix (to most quickly free up
96             # a link in root, in 2 iterations),
97             # see if the "01/" directory exists (the leading two bytes).
98             # if not,
99             # move it to some random other 'xxxx' directory,
100             # as, say, "abcd/tmp-was-root-0123".
101             # now, for either the "0123" directory or "tmp-was-root-0123"
102             # directory, file all chunks, and move them to the
103             # right locations "01/23/*.chunk", making "01/23" if needed.
104             # (shouldn't be any out-of-link problems down one level)
105 0         0 my @four_dirs = map {
106 0         0 sort @{ $four_of_two{$_} }
  0         0  
107             }
108             sort {
109 0         0 scalar(@{ $four_of_two{$b} }) <=> scalar(@{ $four_of_two{$a} })
  0         0  
  0         0  
110             } keys %four_of_two;
111 0         0 my $n_done;
112 0         0 while (my $four_dir = shift @four_dirs) {
113 0         0 my $leading_two = substr($four_dir, 0, 2);
114 0         0 my $migrate_source;
115 0 0       0 if ($exist_twodir{$leading_two}) {
    0          
116             # top-level destination already exists. no need for more
117             # links in the top-level
118 0         0 $migrate_source = $four_dir;
119             } elsif (@four_dirs) {
120             # we need to move four_dir away, into another four_dir,
121             # to make room to create a new two_dir in the root
122 0         0 my $holder_four_dir = $four_dirs[0];
123 0         0 $migrate_source = "$holder_four_dir/tmp-was-root-$four_dir";
124 0         0 my $temp_dir = "$root/$migrate_source";
125 0 0       0 rename "$root/$four_dir", $temp_dir
126             or die "Rename of $root/$four_dir -> $temp_dir failed: $!";
127             } else {
128             # no four_dirs left? then I bet we aren't out of links
129             # anymore. just migrate.
130 0         0 $migrate_source = $four_dir;
131             }
132              
133 0         0 $self->_upgrade_chunks_in_directory($four_dir, $migrate_source);
134 0 0       0 if (-e "$root/$four_dir") {
135 0         0 die "Upgrade of $root/$four_dir/* didn't seem to have worked.";
136             }
137 0         0 $n_done++;
138 0 0 0     0 last if $clean_limit && $n_done >= $clean_limit;
139             }
140             }
141              
142             sub _upgrade_chunks_in_directory {
143 0     0   0 my $self = shift;
144 0         0 my $four_dig = shift; # first four hex digits of all files being moved
145 0         0 my $rel_dir = shift; # directory (relative to root) to move files from, and then remove
146 0 0       0 die "not relative" unless $rel_dir =~ m!^[^/]!;
147              
148 0         0 my $root = $self->{path};
149              
150 0 0       0 my ($hex12, $hex34) = $four_dig =~ /^([0-9a-f]{2})([0-9a-f]{2})$/
151             or die "four_dig not four hex digits";
152              
153 0         0 my $dest_dir0 = "$root/$hex12";
154 0         0 my $dest_dir = "$root/$hex12/$hex34";
155 0         0 for ($dest_dir0, $dest_dir) {
156 0 0       0 next if -d $_;
157 0 0       0 mkdir $_ or die "Failed to mkdir $_: $!";
158             }
159              
160 0         0 my @dirs;
161             File::Find::find({wanted => sub {
162 0     0   0 my $name = $File::Find::name;
163 0 0       0 if (-f $name) {
    0          
164 0         0 my $basefile = $_; # stupid File::Find conventions
165 0 0       0 rename $name, "$dest_dir/$basefile" or die
166             "Failed to move $name to $dest_dir: $!";
167             } elsif (-d $name) {
168 0 0 0     0 return if $_ eq "." || $_ eq "..";
169 0         0 push @dirs, $name;
170             }
171 0         0 }}, "$root/$rel_dir");
172              
173 0         0 my $final_dir = "$root/$four_dig";
174 0         0 for my $dir (reverse(@dirs), $final_dir) {
175 0 0 0     0 if (!rmdir($dir) && -d $dir) {
176 0         0 warn "Directory not empty? $dir. Skipping cleanup.\n";
177 0         0 return;
178             }
179             }
180 0         0 warn "Rearranged & removed $four_dig\n";
181             }
182              
183             # version <= 1.05: 0123/4567/89ab/cdef/0123456789abcdef...xxx.chunk
184             # this is totally stupid. 65k files in root (twice ext3's historical/common
185             # maximum), and the leaves were always containing but one file.
186             sub _old_diskpath {
187 77     77   193 my ($self, $dig) = @_;
188 77         146 my @parts;
189 77         145 my $fulldig = $dig;
190 77         988 $dig =~ s/^\w+://; # remove the "hashtype:" from beginning
191 77 50       1742 $fulldig =~ s/:/./g if $self->nocolons; # Convert colons to dots if we've been asked to
192 77   66     706 while (length $dig && @parts < 4) {
193 308 50       1985 $dig =~ s/^([0-9a-f]{4})// or die "Can't get 4 hex digits of $fulldig";
194 308         2207 push @parts, $1;
195             }
196 77         583 return $self->{path} . "/" . join("/", @parts) . "/$fulldig.chunk";
197             }
198              
199             sub chunkpath {
200 273     273 0 996 my ($self, $dig) = @_;
201              
202             # if the old (version <= 1.05) chunk still exists,
203             # just use that, unless we know (from initial scan)
204             # that such paths can't exist, thus avoiding a
205             # bunch of stats()
206 273 100       1842 unless ($self->{_no_four_hex_dirs_in_root}) {
207 77         335 my $old = $self->_old_diskpath($dig);
208 77 50       2570 return $old if -e $old;
209             }
210              
211             # else, use the new (version >= 1.06) location, which
212             # is much more sensible
213 273         6147 return $self->{path} . '/' . $self->SUPER::chunkpath($dig);
214             }
215              
216             sub metapath {
217 16     16 0 62 my ($self, $name) = @_;
218 16         209 return $self->{path} . '/' . $self->SUPER::metapath($name);
219             }
220              
221             sub size {
222 0     0 0 0 my ($self, $path) = @_;
223 0         0 return -s $path;
224             }
225              
226             sub has_chunk_of_handle {
227 0     0 0 0 my ($self, $handle) = @_;
228 0         0 my $dig = $handle->digest; # "sha1:sdfsdf" format scalar
229 0         0 my $path = $self->chunkpath($dig);
230 0         0 return -e $path;
231             }
232              
233             sub load_chunk {
234 184     184 0 771 my ($self, $dig) = @_;
235 184         1279 my $path = $self->chunkpath($dig);
236 184 50       14019 open (my $fh, $path) or die "Error opening $path to load chunk: $!";
237 184         490 my $chunk = do { local $/; <$fh>; };
  184         1027  
  184         5309  
238 184         4611 return \$chunk;
239             }
240              
241             sub has_chunk {
242 0     0 0 0 my ($self, $chunk) = @_;
243 0         0 my $dig = $chunk->backup_digest;
244 0         0 my $blen = $chunk->backup_length;
245 0         0 my $path = $self->chunkpath($dig);
246 0         0 my $exist_size = -s $path;
247 0 0 0     0 if ($exist_size && $exist_size == $blen) {
248 0         0 return 1;
249             }
250 0         0 return 0;
251             }
252              
253             sub store_chunk {
254 79     79 0 248 my ($self, $chunk) = @_;
255 79         388 my $dig = $chunk->backup_digest;
256 79         382 my $blen = $chunk->backup_length;
257              
258 79         849 my $path = $self->chunkpath($dig);
259              
260             # is it already there? then do nothing.
261 79         5296 my $exist_size = -s $path;
262 79 50 33     325 if ($exist_size && $exist_size == $blen) {
263 0         0 return 1;
264             }
265              
266 79         10690 my $dir = dirname($path);
267              
268 79 50       2236 unless (-d $dir) {
269 79 50       223 unless (eval { File::Path::mkpath($dir) }) {
  79         42719  
270 0 0       0 if ($!{EMLINK}) {
271 0         0 warn "Too many directories in one directory; doing partial cleanup before proceeding...\n";
272             # NOTE: 2 directories is key to freeing up one link. imagine upgrading one:
273             # it'd remove "0000" but possibly (likely) create "00". so we do two,
274             # because, following the example, "0001" would also go into "00", so we'd have one
275             # link left in the root. _upgrade_layout orders the directories to clean in
276             # an order such that 2 will succeed or fail, but no higher will succeed when
277             # 2 won't.
278 0         0 $self->_upgrade_layout(2);
279 0 0       0 unless (eval { File::Path::mkpath($dir) }) {
  0         0  
280 0         0 die "Still can't create directory $dir: $!\n";
281             }
282             } else {
283 0         0 die "Failed to mkdir: $dir: $!\n";
284             }
285             }
286             }
287              
288 79         415 my $partial = "$path.partial";
289 79 50       12618 open (my $fh, '>', $partial) or die "Failed to open $partial for writing: $!\n";
290 79         273 binmode($fh);
291 79         613 io_print_to_fh($chunk->chunkref, $fh);
292 79 50       23601 close($fh) or die "Failed to close $path\n";
293              
294 79         4331 unlink $path;
295 79 50       13293 rename $partial, $path or die "Failed to rename $partial to $path: $!\n";
296              
297 79         12527 my $actual_size = -s $path;
298 79         928 my $expected_size = $chunk->backup_length;
299 79 50       276 unless (defined($actual_size)) {
300 0         0 die "Chunk output file $path does not exist. Do you need to set no_filename_colons=1?";
301             }
302 79 50       329 unless ($actual_size == $expected_size) {
303 0         0 die "Chunk $path was written to disk wrong: size is $actual_size, expecting $expected_size\n";
304             }
305              
306 79         1343 return 1;
307             }
308              
309             sub delete_chunk {
310 10     10 0 19 my ($self, $dig) = @_;
311 10         219 my $path = $self->chunkpath($dig);
312 10         5351 unlink $path;
313             }
314              
315              
316             # returns a list of names of all chunks
317             sub chunks {
318 2     2 0 4 my $self = shift;
319              
320 2         6 my @chunks = ();
321             my $found_chunk = sub {
322 112 100   112   7776 m/\.chunk$/ or return;
323 34         1365 my $chunk_name = basename($_);
324 34         123 $chunk_name =~ s/\.chunk$//;
325 34 50       84 $chunk_name =~ s/\./:/g if $self->nocolons;
326 34         637 push @chunks, $chunk_name;
327 2         15 };
328 2         247 File::Find::find({ wanted => $found_chunk, no_chdir => 1}, $self->{path});
329 2         66 return @chunks;
330             }
331              
332             sub store_backup_meta {
333 8     8 0 34 my ($self, $name, $fh) = @_;
334              
335 8         68 my $dir = $self->metapath();
336 8 100       311 unless (-d $dir) {
337 7 50       939 mkdir $dir or die "Failed to mkdir $dir: $!\n";
338             }
339              
340 8         40 my $out_filepath = "$dir/$name.brackup";
341 8 50       1041 open (my $out_fh, '>', $out_filepath)
342             or die "Failed to open metafile '$out_filepath': $!\n";
343 8         263 io_print_to_fh($fh, $out_fh);
344 8 50       783 close $out_fh or die "Failed to close metafile '$out_filepath': $!\n";
345              
346 8         52 return 1;
347             }
348              
349             sub backups {
350 5     5 0 118 my ($self) = @_;
351              
352 5         23 my $dir = $self->metapath();
353 5 50       211 return () unless -d $dir;
354              
355 5 50       187 opendir(my $dh, $dir) or
356             die "Failed to open $dir: $!\n";
357              
358 5         14 my @ret = ();
359 5         104 while (my $fn = readdir($dh)) {
360 17 100       123 next unless $fn =~ s/\.brackup$//;
361 7         46 my $stat = File::stat::stat("$dir/$fn.brackup");
362 7         1322 push @ret, Brackup::TargetBackupStatInfo->new($self, $fn,
363             time => $stat->mtime,
364             size => $stat->size);
365             }
366 5         62 closedir($dh);
367              
368 5         172 return @ret;
369             }
370              
371             # downloads the given backup name to the current directory (with
372             # *.brackup extension) or to the specified location
373             sub get_backup {
374 2     2 0 6 my ($self, $name, $output_file) = @_;
375 2         11 my $file = $self->metapath("$name.brackup");
376              
377 2 50       68 die "File doesn't exist: $file" unless -e $file;
378              
379 2   33     8 $output_file ||= "$name.brackup";
380              
381 2 50       99 open(my $in, $file) or die "Failed to open $file: $!\n";
382 2 50       133 open(my $out, '>', $output_file) or die "Failed to open $output_file: $!\n";
383              
384 2         4 my $buf;
385             my $rv;
386 2         29 while ($rv = sysread($in, $buf, 128*1024)) {
387 2         79 my $outv = syswrite($out, $buf);
388 2 50       24 die "copy error" unless $outv == $rv;
389             }
390 2 50       9 die "copy error" unless defined $rv;
391              
392 2         48 return 1;
393             }
394              
395             sub delete_backup {
396 1     1 0 2 my $self = shift;
397 1         2 my $name = shift;
398              
399 1         7 my $file = $self->metapath("$name.brackup");
400 1 50       27 die "File doesn't exist: $file" unless -e $file;
401 1         120 unlink $file;
402 1         6 return 1;
403             }
404              
405             1;
406              
407              
408             =head1 NAME
409              
410             Brackup::Target::Filesystem - backup to a locally mounted filesystem
411              
412             =head1 DESCRIPTION
413              
414             Back up to an NFS or Samba server, another disk array (external storage), etc.
415              
416             =head1 EXAMPLE
417              
418             In your ~/.brackup.conf file:
419              
420             [TARGET:nfs_in_garage]
421             type = Filesystem
422             path = /mnt/nfs-garage/brackup/
423              
424             =head1 CONFIG OPTIONS
425              
426             =over
427              
428             =item B
429              
430             Must be "B".
431              
432             =item B
433              
434             Path to backup to.
435              
436             =back
437              
438             =head1 SEE ALSO
439              
440             L