3f56841e415a1868a1364478ff54ff91cd3cfed7
[BackupPC.git] / View.pm
1 #============================================================= -*-perl-*-
2 #
3 # BackupPC::View package
4 #
5 # DESCRIPTION
6 #
7 #   This library defines a BackupPC::View class for merging of
8 #   incremental backups and file attributes.  This provides the
9 #   caller with a single view of a merged backup, without worrying
10 #   about which backup contributes which files.
11 #
12 # AUTHOR
13 #   Craig Barratt  <cbarratt@users.sourceforge.net>
14 #
15 # COPYRIGHT
16 #   Copyright (C) 2002-2009  Craig Barratt
17 #
18 #   This program is free software; you can redistribute it and/or modify
19 #   it under the terms of the GNU General Public License as published by
20 #   the Free Software Foundation; either version 2 of the License, or
21 #   (at your option) any later version.
22 #
23 #   This program is distributed in the hope that it will be useful,
24 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
25 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
26 #   GNU General Public License for more details.
27 #
28 #   You should have received a copy of the GNU General Public License
29 #   along with this program; if not, write to the Free Software
30 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
31 #
32 #========================================================================
33 #
34 # Version 3.2.0, released 31 Jul 2010.
35 #
36 # See http://backuppc.sourceforge.net.
37 #
38 #========================================================================
39
40 package BackupPC::View;
41
42 use strict;
43
44 use File::Path;
45 use BackupPC::Lib;
46 use BackupPC::Attrib qw(:all);
47 use BackupPC::FileZIO;
48 use Data::Dumper;
49 use Encode qw/from_to/;
50
51 sub new
52 {
53     my($class, $bpc, $host, $backups, $options) = @_;
54     my $m = bless {
55         bpc       => $bpc,      # BackupPC::Lib object
56         host      => $host,     # host name
57         backups   => $backups,  # all backups for this host
58         num       => -1,        # backup number
59         idx       => -1,        # index into backups for backup
60                                 #   we are viewing
61         dirPath   => undef,     # path to current directory
62         dirAttr   => undef,     # attributes of current directory
63         dirOpts   => $options,  # $options is a hash of file attributes we need:
64                                 # type, inode, or nlink.  If set, these parameters
65                                 # are added to the returned hash.
66                                 # See BackupPC::Lib::dirRead().
67     }, $class;
68     $m->{topDir} = $m->{bpc}->TopDir();
69     return $m;
70 }
71
72 sub dirCache
73 {
74     my($m, $backupNum, $share, $dir) = @_;
75     my($i, $level);
76
77     #print STDERR "dirCache($backupNum, $share, $dir)\n";
78     $dir = "/$dir" if ( $dir !~ m{^/} );
79     $dir =~ s{/+$}{};
80     return if ( $m->{num} == $backupNum
81                 && $m->{share} eq $share
82                 && defined($m->{dir})
83                 && $m->{dir} eq $dir );
84     $m->backupNumCache($backupNum) if ( $m->{num} != $backupNum );
85     return if ( $m->{idx} < 0 );
86
87     $m->{files} = {};
88     $level = $m->{backups}[$m->{idx}]{level} + 1;
89
90     #
91     # Remember the requested share and dir
92     #
93     $m->{share} = $share;
94     $m->{dir} = $dir;
95
96     #
97     # merge backups, starting at the requested one, and working
98     # backwards until we get to level 0.
99     #
100     $m->{mergeNums} = [];
101     for ( $i = $m->{idx} ; $level > 0 && $i >= 0 ; $i-- ) {
102         #print(STDERR "Do $i ($m->{backups}[$i]{noFill},$m->{backups}[$i]{level})\n");
103         #
104         # skip backups with the same or higher level
105         #
106         next if ( $m->{backups}[$i]{level} >= $level );
107
108         last if exists $m->{dirOpts}->{only_first} && $i != $m->{idx};
109         # used by bin/BackupPC_updatedb to extract just first increment
110
111         $level = $m->{backups}[$i]{level};
112         $backupNum = $m->{backups}[$i]{num};
113         push(@{$m->{mergeNums}}, $backupNum);
114         my $mangle   = $m->{backups}[$i]{mangle};
115         my $compress = $m->{backups}[$i]{compress};
116         my $path     = "$m->{topDir}/pc/$m->{host}/$backupNum/";
117         my $legacyCharset = $m->{backups}[$i]{version} < 3.0;
118         my $sharePathM;
119         if ( $mangle ) {
120             $sharePathM = $m->{bpc}->fileNameEltMangle($share)
121                         . $m->{bpc}->fileNameMangle($dir);
122         } else {
123             $sharePathM = $share . $dir;
124         }
125         $path .= $sharePathM;
126         #print(STDERR "Opening $path (share=$share, mangle=$mangle)\n");
127
128         my $dirOpts    = { %{$m->{dirOpts} || {} } };
129         my $attribOpts = { compress => $compress };
130         if ( $legacyCharset ) {
131             $dirOpts->{charsetLegacy}
132                     = $attribOpts->{charsetLegacy}
133                     = $m->{bpc}->{Conf}{ClientCharsetLegacy} || "iso-8859-1";
134         }
135
136         my $dirInfo = $m->{bpc}->dirRead($path, $dirOpts);
137         if ( !defined($dirInfo) ) {
138             if ( $i == $m->{idx} ) {
139                 #
140                 # Oops, directory doesn't exist.
141                 #
142                 $m->{files} = undef;
143                 return;
144             }
145             next;
146         }
147         my $attr;
148         if ( $mangle ) {
149             $attr = BackupPC::Attrib->new($attribOpts);
150             if ( !$attr->read($path) ) {
151                 $m->{error} = "Can't read attribute file in $path: " . $attr->errStr();
152                 $attr = undef;
153             }
154         }
155         foreach my $entry ( @$dirInfo ) {
156             my $file = $1 if ( $entry->{name} =~ /(.*)/s );
157             my $fileUM = $file;
158             $fileUM = $m->{bpc}->fileNameUnmangle($fileUM) if ( $mangle );
159             #print(STDERR "Doing $fileUM\n");
160             #
161             # skip special files
162             #
163             next if ( defined($m->{files}{$fileUM})
164                     || $file eq ".."
165                     || $file eq "."
166                     || $file eq "backupInfo"
167                     || $mangle && $file eq "attrib" );
168
169             if ( defined($attr) && defined(my $a = $attr->get($fileUM)) ) {
170                 $m->{files}{$fileUM} = $a;
171                 #
172                 # skip directories in earlier backups (each backup always
173                 # has the complete directory tree).
174                 #
175                 next if ( $i < $m->{idx} && $a->{type} == BPC_FTYPE_DIR );
176                 $attr->set($fileUM, undef);
177             } else {
178                 #
179                 # Very expensive in the non-attribute case when compresseion
180                 # is on.  We have to stat the file and read compressed files
181                 # to determine their size.
182                 #
183                 my $realPath = "$path/$file";
184
185                 from_to($realPath, "utf8", $attribOpts->{charsetLegacy})
186                                 if ( $attribOpts->{charsetLegacy} ne "" );
187
188                 my @s = stat($realPath);
189                 next if ( $i < $m->{idx} && -d _ );
190                 $m->{files}{$fileUM} = {
191                     type  => -d _ ? BPC_FTYPE_DIR : BPC_FTYPE_FILE,
192                     mode  => $s[2],
193                     uid   => $s[4],
194                     gid   => $s[5],
195                     size  => -f _ ? $s[7] : 0,
196                     mtime => $s[9],
197                 };
198                 if ( $compress && -f _ ) {
199                     #
200                     # Compute the correct size by reading the whole file
201                     #
202                     my $f = BackupPC::FileZIO->open($realPath,
203                                                     0, $compress);
204                     if ( !defined($f) ) {
205                         $m->{error} = "Can't open $realPath";
206                     } else {
207                         my($data, $size);
208                         while ( $f->read(\$data, 65636 * 8) > 0 ) {
209                             $size += length($data);
210                         }
211                         $f->close;
212                         $m->{files}{$fileUM}{size} = $size;
213                     }
214                 }
215             }
216             ($m->{files}{$fileUM}{relPath}    = "$dir/$fileUM") =~ s{//+}{/}g;
217             ($m->{files}{$fileUM}{sharePathM} = "$sharePathM/$file")
218                                                                =~ s{//+}{/}g;
219             ($m->{files}{$fileUM}{fullPath}   = "$path/$file") =~ s{//+}{/}g;
220             from_to($m->{files}{$fileUM}{fullPath}, "utf8", $attribOpts->{charsetLegacy})
221                                 if ( $attribOpts->{charsetLegacy} ne "" );
222             $m->{files}{$fileUM}{backupNum}   = $backupNum;
223             $m->{files}{$fileUM}{compress}    = $compress;
224             $m->{files}{$fileUM}{nlink}       = $entry->{nlink}
225                                                     if ( $m->{dirOpts}{nlink} );
226             $m->{files}{$fileUM}{inode}       = $entry->{inode}
227                                                     if ( $m->{dirOpts}{inode} );
228         }
229         #
230         # Also include deleted files
231         #
232         if ( defined($attr) ) {
233             my $a = $attr->get;
234             foreach my $fileUM ( keys(%$a) ) {
235                 next if ( $a->{$fileUM}{type} != BPC_FTYPE_DELETED );
236                 my $file = $fileUM;
237                 $file = $m->{bpc}->fileNameMangle($fileUM) if ( $mangle );
238                 $m->{files}{$fileUM}             = $a->{$fileUM};
239                 $m->{files}{$fileUM}{relPath}    = "$dir/$fileUM";
240                 $m->{files}{$fileUM}{sharePathM} = "$sharePathM/$file";
241                 $m->{files}{$fileUM}{fullPath}   = "$path/$file";
242                 from_to($m->{files}{$fileUM}{fullPath}, "utf8", $attribOpts->{charsetLegacy})
243                                     if ( $attribOpts->{charsetLegacy} ne "" );
244                 $m->{files}{$fileUM}{backupNum}  = $backupNum;
245                 $m->{files}{$fileUM}{compress}   = $compress;
246                 $m->{files}{$fileUM}{nlink}      = 0;
247                 $m->{files}{$fileUM}{inode}      = 0;
248             }
249         }
250     }
251     #
252     # Prune deleted files
253     #
254     foreach my $file ( keys(%{$m->{files}}) ) {
255         next if ( $m->{files}{$file}{type} != BPC_FTYPE_DELETED );
256         delete($m->{files}{$file});
257     }
258     #print STDERR "Returning:\n", Dumper($m->{files});
259 }
260
261 #
262 # Return list of shares for this backup
263 #
264 sub shareList
265 {
266     my($m, $backupNum) = @_;
267     my @shareList;
268
269     $m->backupNumCache($backupNum) if ( $m->{num} != $backupNum );
270     return if ( $m->{idx} < 0 );
271
272     my $mangle = $m->{backups}[$m->{idx}]{mangle};
273     my $path = "$m->{topDir}/pc/$m->{host}/$backupNum/";
274     return if ( !opendir(DIR, $path) );
275     my @dir = readdir(DIR);
276     closedir(DIR);
277     foreach my $file ( @dir ) {
278         $file = $1 if ( $file =~ /(.*)/s );
279         next if ( $file eq "attrib" && $mangle
280                || $file eq "."
281                || $file eq ".."
282                || $file eq "backupInfo"
283             );
284         my $fileUM = $file;
285         $fileUM = $m->{bpc}->fileNameUnmangle($fileUM) if ( $mangle );
286         push(@shareList, $fileUM);
287     }
288     $m->{dir} = undef;
289     return @shareList;
290 }
291
292 sub backupNumCache
293 {
294     my($m, $backupNum) = @_;
295
296     if ( $m->{num} != $backupNum ) {
297         my $i;
298         for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) {
299             last if ( $m->{backups}[$i]{num} == $backupNum );
300         }
301         if ( $i >= @{$m->{backups}} ) {
302             $m->{idx} = -1;
303             return;
304         }
305         $m->{num} = $backupNum;
306         $m->{idx} = $i;
307     }
308 }
309
310 #
311 # Return the attributes of a specific file
312 #
313 sub fileAttrib
314 {
315     my($m, $backupNum, $share, $path) = @_;
316
317     #print(STDERR "fileAttrib($backupNum, $share, $path)\n");
318     if ( $path =~ s{(.*)/+(.+)}{$1}s ) {
319         my $file = $2;
320         $m->dirCache($backupNum, $share, $path);
321         return $m->{files}{$file};
322     } else {
323         #print STDERR "Got empty $path\n";
324         $m->dirCache($backupNum, "", "");
325         my $attr = $m->{files}{$share};
326         return if ( !defined($attr) );
327         $attr->{relPath} = "/";
328         return $attr;
329     }
330 }
331
332 #
333 # Return the contents of a directory
334 #
335 sub dirAttrib
336 {
337     my($m, $backupNum, $share, $dir) = @_;
338
339     $m->dirCache($backupNum, $share, $dir);
340     return $m->{files};
341 }
342
343 #
344 # Return a listref of backup numbers that are merged to create this view
345 #
346 sub mergeNums
347 {
348     my($m) = @_;
349
350     return $m->{mergeNums};
351 }
352
353 #
354 # Return a list of backup indexes for which the directory exists
355 #
356 sub backupList
357 {
358     my($m, $share, $dir) = @_;
359     my($i, @backupList);
360
361     $dir = "/$dir" if ( $dir !~ m{^/} );
362     $dir =~ s{/+$}{};
363
364     for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) {
365         my $backupNum = $m->{backups}[$i]{num};
366         my $mangle = $m->{backups}[$i]{mangle};
367         my $path   = "$m->{topDir}/pc/$m->{host}/$backupNum/";
368         my $sharePathM;
369         if ( $mangle ) {
370             $sharePathM = $m->{bpc}->fileNameEltMangle($share)
371                         . $m->{bpc}->fileNameMangle($dir);
372         } else {
373             $sharePathM = $share . $dir;
374         }
375         $path .= $sharePathM;
376         next if ( !-d $path );
377         push(@backupList, $i);
378     }
379     return @backupList;
380 }
381
382 #
383 # Return the history of all backups for a particular directory
384 #
385 sub dirHistory
386 {
387     my($m, $share, $dir) = @_;
388     my($i, $level);
389     my $files = {};
390
391     $dir = "/$dir" if ( $dir !~ m{^/} );
392     $dir =~ s{/+$}{};
393
394     #
395     # merge backups, starting at the first one, and working
396     # forward.
397     #
398     for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) {
399         $level        = $m->{backups}[$i]{level};
400         my $backupNum = $m->{backups}[$i]{num};
401         my $mangle    = $m->{backups}[$i]{mangle};
402         my $compress  = $m->{backups}[$i]{compress};
403         my $path      = "$m->{topDir}/pc/$m->{host}/$backupNum/";
404         my $legacyCharset = $m->{backups}[$i]{version} < 3.0;
405         my $sharePathM;
406         if ( $mangle ) {
407             $sharePathM = $m->{bpc}->fileNameEltMangle($share)
408                         . $m->{bpc}->fileNameMangle($dir);
409         } else {
410             $sharePathM = $share . $dir;
411         }
412         $path .= $sharePathM;
413         #print(STDERR "Opening $path (share=$share)\n");
414
415         my $dirOpts    = { %{$m->{dirOpts} || {} } };
416         my $attribOpts = { compress => $compress };
417         if ( $legacyCharset ) {
418             $dirOpts->{charsetLegacy}
419                     = $attribOpts->{charsetLegacy}
420                     = $m->{bpc}->{Conf}{ClientCharsetLegacy} || "iso-8859-1";
421         }
422
423         my $dirInfo = $m->{bpc}->dirRead($path, $dirOpts);
424         if ( !defined($dirInfo) ) {
425             #
426             # Oops, directory doesn't exist.
427             #
428             next;
429         }
430         my $attr;
431         if ( $mangle ) {
432             $attr = BackupPC::Attrib->new($attribOpts);
433             if ( !$attr->read($path) ) {
434                 $m->{error} = "Can't read attribute file in $path";
435                 $attr = undef;
436             }
437         }
438         foreach my $entry ( @$dirInfo ) {
439             my $file = $1 if ( $entry->{name} =~ /(.*)/s );
440             my $fileUM = $file;
441             $fileUM = $m->{bpc}->fileNameUnmangle($fileUM) if ( $mangle );
442             #print(STDERR "Doing $fileUM\n");
443             #
444             # skip special files
445             #
446             next if (  $file eq ".."
447                     || $file eq "."
448                     || $mangle && $file eq "attrib"
449                     || defined($files->{$fileUM}[$i]) );
450
451             my $realPath = "$path/$file";
452             from_to($realPath, "utf8", $attribOpts->{charsetLegacy})
453                             if ( $attribOpts->{charsetLegacy} ne "" );
454             my @s = stat($realPath);
455             if ( defined($attr) && defined(my $a = $attr->get($fileUM)) ) {
456                 $files->{$fileUM}[$i] = $a;
457                 $attr->set($fileUM, undef);
458             } else {
459                 #
460                 # Very expensive in the non-attribute case when compresseion
461                 # is on.  We have to stat the file and read compressed files
462                 # to determine their size.
463                 #
464                 $files->{$fileUM}[$i] = {
465                     type  => -d _ ? BPC_FTYPE_DIR : BPC_FTYPE_FILE,
466                     mode  => $s[2],
467                     uid   => $s[4],
468                     gid   => $s[5],
469                     size  => -f _ ? $s[7] : 0,
470                     mtime => $s[9],
471                 };
472                 if ( $compress && -f _ ) {
473                     #
474                     # Compute the correct size by reading the whole file
475                     #
476                     my $f = BackupPC::FileZIO->open("$realPath",
477                                                     0, $compress);
478                     if ( !defined($f) ) {
479                         $m->{error} = "Can't open $path/$file";
480                     } else {
481                         my($data, $size);
482                         while ( $f->read(\$data, 65636 * 8) > 0 ) {
483                             $size += length($data);
484                         }
485                         $f->close;
486                         $files->{$fileUM}[$i]{size} = $size;
487                     }
488                 }
489             }
490             ($files->{$fileUM}[$i]{relPath}    = "$dir/$fileUM") =~ s{//+}{/}g;
491             ($files->{$fileUM}[$i]{sharePathM} = "$sharePathM/$file")
492                                                                 =~ s{//+}{/}g;
493             ($files->{$fileUM}[$i]{fullPath}   = "$path/$file") =~ s{//+}{/}g;
494             $files->{$fileUM}[$i]{backupNum}   = $backupNum;
495             $files->{$fileUM}[$i]{compress}    = $compress;
496             $files->{$fileUM}[$i]{nlink}       = $entry->{nlink}
497                                                     if ( $m->{dirOpts}{nlink} );
498             $files->{$fileUM}[$i]{inode}       = $entry->{inode}
499                                                     if ( $m->{dirOpts}{inode} );
500         }
501
502         #
503         # Flag deleted files
504         #
505         if ( defined($attr) ) {
506             my $a = $attr->get;
507             foreach my $fileUM ( keys(%$a) ) {
508                 next if ( $a->{$fileUM}{type} != BPC_FTYPE_DELETED );
509                 $files->{$fileUM}[$i]{type} = BPC_FTYPE_DELETED;
510             }
511         }
512
513         #
514         # Merge old backups.  Don't merge directories from old
515         # backups because every backup has an accurate directory
516         # tree.
517         #
518         for ( my $k = $i - 1 ; $level > 0 && $k >= 0 ; $k-- ) {
519             next if ( $m->{backups}[$k]{level} >= $level );
520             $level = $m->{backups}[$k]{level};
521             foreach my $fileUM ( keys(%$files) ) {
522                 next if ( !defined($files->{$fileUM}[$k])
523                         || defined($files->{$fileUM}[$i])
524                         || $files->{$fileUM}[$k]{type} == BPC_FTYPE_DIR );
525                 $files->{$fileUM}[$i] = $files->{$fileUM}[$k];
526             }
527         }
528     }
529
530     #
531     # Remove deleted files
532     #
533     for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) {
534         foreach my $fileUM ( keys(%$files) ) {
535             next if ( !defined($files->{$fileUM}[$i])
536                     || $files->{$fileUM}[$i]{type} != BPC_FTYPE_DELETED );
537             $files->{$fileUM}[$i] = undef;
538         }
539     }
540
541     #print STDERR "Returning:\n", Dumper($files);
542     return $files;
543 }
544
545
546 #
547 # Do a recursive find starting at the given path (either a file
548 # or directory).  The callback function $callback is called on each
549 # file and directory.  The function arguments are the attrs hashref,
550 # and additional callback arguments.  The search is depth-first if
551 # depth is set.  Returns -1 if $path does not exist.
552 #
553 sub find
554 {
555     my($m, $backupNum, $share, $path, $depth, $callback, @callbackArgs) = @_;
556
557     #print(STDERR "find: got $backupNum, $share, $path\n");
558     #
559     # First call the callback on the given $path
560     #
561     my $attr = $m->fileAttrib($backupNum, $share, $path);
562     return -1 if ( !defined($attr) );
563     &$callback($attr, @callbackArgs);
564     return if ( $attr->{type} != BPC_FTYPE_DIR );
565
566     #
567     # Now recurse into subdirectories
568     #
569     $m->findRecurse($backupNum, $share, $path, $depth,
570                     $callback, @callbackArgs);
571 }
572
573 #
574 # Same as find(), except the callback is not called on the current
575 # $path, only on the contents of $path.  So if $path is a file then
576 # no callback or recursion occurs.
577 #
578 sub findRecurse
579 {
580     my($m, $backupNum, $share, $path, $depth, $callback, @callbackArgs) = @_;
581
582     my $attr = $m->dirAttrib($backupNum, $share, $path);
583     return if ( !defined($attr) );
584     foreach my $file ( sort(keys(%$attr)) ) {
585         &$callback($attr->{$file}, @callbackArgs);
586         next if ( !$depth || $attr->{$file}{type} != BPC_FTYPE_DIR );
587         #
588         # For depth-first, recurse as we hit each directory
589         #
590         $m->findRecurse($backupNum, $share, "$path/$file", $depth,
591                              $callback, @callbackArgs);
592     }
593     if ( !$depth ) {
594         #
595         # For non-depth, recurse directories after we finish current dir
596         #
597         foreach my $file ( keys(%{$attr}) ) {
598             next if ( $attr->{$file}{type} != BPC_FTYPE_DIR );
599             $m->findRecurse($backupNum, $share, "$path/$file", $depth,
600                             $callback, @callbackArgs);
601         }
602     }
603 }
604
605 1;