X-Git-Url: http://git.rot13.org/?p=BackupPC.git;a=blobdiff_plain;f=lib%2FBackupPC%2FView.pm;h=c8664af7d745bb1c1718dd6f79864c01c0b014d6;hp=be870962a04f74bc7a07ca0c8ad41cb27b5be727;hb=37c0dca99997f39e8785a94801adf5fcdcb837ba;hpb=3dc33e5f39430031766adf3c5bb2ffc649ee9100 diff --git a/lib/BackupPC/View.pm b/lib/BackupPC/View.pm index be87096..c8664af 100644 --- a/lib/BackupPC/View.pm +++ b/lib/BackupPC/View.pm @@ -13,7 +13,7 @@ # Craig Barratt # # COPYRIGHT -# Copyright (C) 2002 Craig Barratt +# Copyright (C) 2002-2009 Craig Barratt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -31,7 +31,7 @@ # #======================================================================== # -# Version 1.6.0_CVS, released 10 Dec 2002. +# Version 3.2.0, released 31 Jul 2010. # # See http://backuppc.sourceforge.net. # @@ -45,24 +45,26 @@ use File::Path; use BackupPC::Lib; use BackupPC::Attrib qw(:all); use BackupPC::FileZIO; +use Data::Dumper; +use Encode qw/from_to/; sub new { - my($class, $bpc, $host, $backups) = @_; + my($class, $bpc, $host, $backups, $options) = @_; my $m = bless { - bpc => $bpc, # BackupPC::Lib object - host => $host, # host name - backups => $backups, # all backups for this host - num => -1, # backup number - idx => -1, # index into backups for backup - # we are viewing - dirPath => undef, # path to current directory - dirAttr => undef, # attributes of current directory + bpc => $bpc, # BackupPC::Lib object + host => $host, # host name + backups => $backups, # all backups for this host + num => -1, # backup number + idx => -1, # index into backups for backup + # we are viewing + dirPath => undef, # path to current directory + dirAttr => undef, # attributes of current directory + dirOpts => $options, # $options is a hash of file attributes we need: + # type, inode, or nlink. If set, these parameters + # are added to the returned hash. + # See BackupPC::Lib::dirRead(). }, $class; - for ( my $i = 0 ; $i < @{$m->{backups}} ; $i++ ) { - next if ( defined($m->{backups}[$i]{level}) ); - $m->{backups}[$i]{level} = $m->{backups}[$i]{type} eq "full" ? 0 : 1; - } $m->{topDir} = $m->{bpc}->TopDir(); return $m; } @@ -72,22 +74,16 @@ sub dirCache my($m, $backupNum, $share, $dir) = @_; my($i, $level); + #print STDERR "dirCache($backupNum, $share, $dir)\n"; $dir = "/$dir" if ( $dir !~ m{^/} ); $dir =~ s{/+$}{}; return if ( $m->{num} == $backupNum && $m->{share} eq $share + && defined($m->{dir}) && $m->{dir} eq $dir ); - if ( $m->{num} != $backupNum ) { - for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) { - last if ( $m->{backups}[$i]{num} == $backupNum ); - } - if ( $i >= @{$m->{backups}} ) { - $m->{idx} = -1; - return; - } - $m->{num} = $backupNum; - $m->{idx} = $i; - } + $m->backupNumCache($backupNum) if ( $m->{num} != $backupNum ); + return if ( $m->{idx} < 0 ); + $m->{files} = {}; $level = $m->{backups}[$m->{idx}]{level} + 1; @@ -103,7 +99,7 @@ sub dirCache # $m->{mergeNums} = []; for ( $i = $m->{idx} ; $level > 0 && $i >= 0 ; $i-- ) { - #print("Do $i ($m->{backups}[$i]{noFill},$m->{backups}[$i]{level})\n"); + #print(STDERR "Do $i ($m->{backups}[$i]{noFill},$m->{backups}[$i]{level})\n"); # # skip backups with the same or higher level # @@ -114,7 +110,8 @@ sub dirCache push(@{$m->{mergeNums}}, $backupNum); my $mangle = $m->{backups}[$i]{mangle}; my $compress = $m->{backups}[$i]{compress}; - my $path = "$m->{topDir}/pc/$m->{host}/$backupNum/"; + my $path = "$m->{topDir}/pc/$m->{host}/$backupNum/"; + my $legacyCharset = $m->{backups}[$i]{version} < 3.0; my $sharePathM; if ( $mangle ) { $sharePathM = $m->{bpc}->fileNameEltMangle($share) @@ -123,8 +120,18 @@ sub dirCache $sharePathM = $share . $dir; } $path .= $sharePathM; - #print("Opening $path\n"); - if ( !opendir(DIR, $path) ) { + #print(STDERR "Opening $path (share=$share, mangle=$mangle)\n"); + + my $dirOpts = { %{$m->{dirOpts} || {} } }; + my $attribOpts = { compress => $compress }; + if ( $legacyCharset ) { + $dirOpts->{charsetLegacy} + = $attribOpts->{charsetLegacy} + = $m->{bpc}->{Conf}{ClientCharsetLegacy} || "iso-8859-1"; + } + + my $dirInfo = $m->{bpc}->dirRead($path, $dirOpts); + if ( !defined($dirInfo) ) { if ( $i == $m->{idx} ) { # # Oops, directory doesn't exist. @@ -134,35 +141,35 @@ sub dirCache } next; } - my @dir = readdir(DIR); - closedir(DIR); my $attr; if ( $mangle ) { - $attr = BackupPC::Attrib->new({ compress => $compress }); - if ( -f $attr->fileName($path) && !$attr->read($path) ) { - $m->{error} = "Can't read attribute file in $path"; + $attr = BackupPC::Attrib->new($attribOpts); + if ( !$attr->read($path) ) { + $m->{error} = "Can't read attribute file in $path: " . $attr->errStr(); $attr = undef; } } - foreach my $file ( @dir ) { - $file = $1 if ( $file =~ /(.*)/ ); + foreach my $entry ( @$dirInfo ) { + my $file = $1 if ( $entry->{name} =~ /(.*)/s ); my $fileUM = $file; $fileUM = $m->{bpc}->fileNameUnmangle($fileUM) if ( $mangle ); + #print(STDERR "Doing $fileUM\n"); # # skip special files # next if ( defined($m->{files}{$fileUM}) || $file eq ".." || $file eq "." + || $file eq "backupInfo" || $mangle && $file eq "attrib" ); - # - # skip directories in earlier backups (each backup always - # has the complete directory tree). - # - my @s = stat("$path/$file"); - next if ( $i < $m->{idx} && -d _ ); + if ( defined($attr) && defined(my $a = $attr->get($fileUM)) ) { $m->{files}{$fileUM} = $a; + # + # skip directories in earlier backups (each backup always + # has the complete directory tree). + # + next if ( $i < $m->{idx} && $a->{type} == BPC_FTYPE_DIR ); $attr->set($fileUM, undef); } else { # @@ -170,6 +177,13 @@ sub dirCache # is on. We have to stat the file and read compressed files # to determine their size. # + my $realPath = "$path/$file"; + + from_to($realPath, "utf8", $attribOpts->{charsetLegacy}) + if ( $attribOpts->{charsetLegacy} ne "" ); + + my @s = stat($realPath); + next if ( $i < $m->{idx} && -d _ ); $m->{files}{$fileUM} = { type => -d _ ? BPC_FTYPE_DIR : BPC_FTYPE_FILE, mode => $s[2], @@ -182,10 +196,10 @@ sub dirCache # # Compute the correct size by reading the whole file # - my $f = BackupPC::FileZIO->open("$path/$file", + my $f = BackupPC::FileZIO->open($realPath, 0, $compress); if ( !defined($f) ) { - $m->{error} = "Can't open $path/$file"; + $m->{error} = "Can't open $realPath"; } else { my($data, $size); while ( $f->read(\$data, 65636 * 8) > 0 ) { @@ -196,13 +210,18 @@ sub dirCache } } } - $m->{files}{$fileUM}{relPath} = "$dir/$fileUM"; - $m->{files}{$fileUM}{sharePathM} = "$sharePathM/$file"; - $m->{files}{$fileUM}{fullPath} = "$path/$file"; - $m->{files}{$fileUM}{backupNum} = $backupNum; - $m->{files}{$fileUM}{compress} = $compress; - $m->{files}{$fileUM}{nlink} = $s[3]; - $m->{files}{$fileUM}{inode} = $s[1]; + ($m->{files}{$fileUM}{relPath} = "$dir/$fileUM") =~ s{//+}{/}g; + ($m->{files}{$fileUM}{sharePathM} = "$sharePathM/$file") + =~ s{//+}{/}g; + ($m->{files}{$fileUM}{fullPath} = "$path/$file") =~ s{//+}{/}g; + from_to($m->{files}{$fileUM}{fullPath}, "utf8", $attribOpts->{charsetLegacy}) + if ( $attribOpts->{charsetLegacy} ne "" ); + $m->{files}{$fileUM}{backupNum} = $backupNum; + $m->{files}{$fileUM}{compress} = $compress; + $m->{files}{$fileUM}{nlink} = $entry->{nlink} + if ( $m->{dirOpts}{nlink} ); + $m->{files}{$fileUM}{inode} = $entry->{inode} + if ( $m->{dirOpts}{inode} ); } # # Also include deleted files @@ -217,12 +236,16 @@ sub dirCache $m->{files}{$fileUM}{relPath} = "$dir/$fileUM"; $m->{files}{$fileUM}{sharePathM} = "$sharePathM/$file"; $m->{files}{$fileUM}{fullPath} = "$path/$file"; + from_to($m->{files}{$fileUM}{fullPath}, "utf8", $attribOpts->{charsetLegacy}) + if ( $attribOpts->{charsetLegacy} ne "" ); $m->{files}{$fileUM}{backupNum} = $backupNum; $m->{files}{$fileUM}{compress} = $compress; $m->{files}{$fileUM}{nlink} = 0; $m->{files}{$fileUM}{inode} = 0; } } + + last if $m->{dirOpts}->{only_increment}; # XXX ASA Search extension } # # Prune deleted files @@ -231,6 +254,56 @@ sub dirCache next if ( $m->{files}{$file}{type} != BPC_FTYPE_DELETED ); delete($m->{files}{$file}); } + #print STDERR "Returning:\n", Dumper($m->{files}); +} + +# +# Return list of shares for this backup +# +sub shareList +{ + my($m, $backupNum) = @_; + my @shareList; + + $m->backupNumCache($backupNum) if ( $m->{num} != $backupNum ); + return if ( $m->{idx} < 0 ); + + my $mangle = $m->{backups}[$m->{idx}]{mangle}; + my $path = "$m->{topDir}/pc/$m->{host}/$backupNum/"; + return if ( !opendir(DIR, $path) ); + my @dir = readdir(DIR); + closedir(DIR); + foreach my $file ( @dir ) { + $file = $1 if ( $file =~ /(.*)/s ); + next if ( $file eq "attrib" && $mangle + || $file eq "." + || $file eq ".." + || $file eq "backupInfo" + ); + my $fileUM = $file; + $fileUM = $m->{bpc}->fileNameUnmangle($fileUM) if ( $mangle ); + push(@shareList, $fileUM); + } + $m->{dir} = undef; + return @shareList; +} + +sub backupNumCache +{ + my($m, $backupNum) = @_; + + if ( $m->{num} != $backupNum ) { + my $i; + for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) { + last if ( $m->{backups}[$i]{num} == $backupNum ); + } + if ( $i >= @{$m->{backups}} ) { + $m->{idx} = -1; + return; + } + $m->{num} = $backupNum; + $m->{idx} = $i; + } } # @@ -239,12 +312,20 @@ sub dirCache sub fileAttrib { my($m, $backupNum, $share, $path) = @_; - my $dir = $path; - $dir =~ s{(.*)/(.*)}{$1}; - my $file = $2; - $m->dirCache($backupNum, $share, $dir); - return $m->{files}{$file}; + #print(STDERR "fileAttrib($backupNum, $share, $path)\n"); + if ( $path =~ s{(.*)/+(.+)}{$1}s ) { + my $file = $2; + $m->dirCache($backupNum, $share, $path); + return $m->{files}{$file}; + } else { + #print STDERR "Got empty $path\n"; + $m->dirCache($backupNum, "", ""); + my $attr = $m->{files}{$share}; + return if ( !defined($attr) ); + $attr->{relPath} = "/"; + return $attr; + } } # @@ -258,6 +339,9 @@ sub dirAttrib return $m->{files}; } +# +# Return a listref of backup numbers that are merged to create this view +# sub mergeNums { my($m) = @_; @@ -265,6 +349,9 @@ sub mergeNums return $m->{mergeNums}; } +# +# Return a list of backup indexes for which the directory exists +# sub backupList { my($m, $share, $dir) = @_; @@ -286,11 +373,175 @@ sub backupList } $path .= $sharePathM; next if ( !-d $path ); - push(@backupList, $backupNum); + push(@backupList, $i); } return @backupList; } +# +# Return the history of all backups for a particular directory +# +sub dirHistory +{ + my($m, $share, $dir) = @_; + my($i, $level); + my $files = {}; + + $dir = "/$dir" if ( $dir !~ m{^/} ); + $dir =~ s{/+$}{}; + + # + # merge backups, starting at the first one, and working + # forward. + # + for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) { + $level = $m->{backups}[$i]{level}; + my $backupNum = $m->{backups}[$i]{num}; + my $mangle = $m->{backups}[$i]{mangle}; + my $compress = $m->{backups}[$i]{compress}; + my $path = "$m->{topDir}/pc/$m->{host}/$backupNum/"; + my $legacyCharset = $m->{backups}[$i]{version} < 3.0; + my $sharePathM; + if ( $mangle ) { + $sharePathM = $m->{bpc}->fileNameEltMangle($share) + . $m->{bpc}->fileNameMangle($dir); + } else { + $sharePathM = $share . $dir; + } + $path .= $sharePathM; + #print(STDERR "Opening $path (share=$share)\n"); + + my $dirOpts = { %{$m->{dirOpts} || {} } }; + my $attribOpts = { compress => $compress }; + if ( $legacyCharset ) { + $dirOpts->{charsetLegacy} + = $attribOpts->{charsetLegacy} + = $m->{bpc}->{Conf}{ClientCharsetLegacy} || "iso-8859-1"; + } + + my $dirInfo = $m->{bpc}->dirRead($path, $dirOpts); + if ( !defined($dirInfo) ) { + # + # Oops, directory doesn't exist. + # + next; + } + my $attr; + if ( $mangle ) { + $attr = BackupPC::Attrib->new($attribOpts); + if ( !$attr->read($path) ) { + $m->{error} = "Can't read attribute file in $path"; + $attr = undef; + } + } + foreach my $entry ( @$dirInfo ) { + my $file = $1 if ( $entry->{name} =~ /(.*)/s ); + my $fileUM = $file; + $fileUM = $m->{bpc}->fileNameUnmangle($fileUM) if ( $mangle ); + #print(STDERR "Doing $fileUM\n"); + # + # skip special files + # + next if ( $file eq ".." + || $file eq "." + || $mangle && $file eq "attrib" + || defined($files->{$fileUM}[$i]) ); + + my $realPath = "$path/$file"; + from_to($realPath, "utf8", $attribOpts->{charsetLegacy}) + if ( $attribOpts->{charsetLegacy} ne "" ); + my @s = stat($realPath); + if ( defined($attr) && defined(my $a = $attr->get($fileUM)) ) { + $files->{$fileUM}[$i] = $a; + $attr->set($fileUM, undef); + } else { + # + # Very expensive in the non-attribute case when compresseion + # is on. We have to stat the file and read compressed files + # to determine their size. + # + $files->{$fileUM}[$i] = { + type => -d _ ? BPC_FTYPE_DIR : BPC_FTYPE_FILE, + mode => $s[2], + uid => $s[4], + gid => $s[5], + size => -f _ ? $s[7] : 0, + mtime => $s[9], + }; + if ( $compress && -f _ ) { + # + # Compute the correct size by reading the whole file + # + my $f = BackupPC::FileZIO->open("$realPath", + 0, $compress); + if ( !defined($f) ) { + $m->{error} = "Can't open $path/$file"; + } else { + my($data, $size); + while ( $f->read(\$data, 65636 * 8) > 0 ) { + $size += length($data); + } + $f->close; + $files->{$fileUM}[$i]{size} = $size; + } + } + } + ($files->{$fileUM}[$i]{relPath} = "$dir/$fileUM") =~ s{//+}{/}g; + ($files->{$fileUM}[$i]{sharePathM} = "$sharePathM/$file") + =~ s{//+}{/}g; + ($files->{$fileUM}[$i]{fullPath} = "$path/$file") =~ s{//+}{/}g; + $files->{$fileUM}[$i]{backupNum} = $backupNum; + $files->{$fileUM}[$i]{compress} = $compress; + $files->{$fileUM}[$i]{nlink} = $entry->{nlink} + if ( $m->{dirOpts}{nlink} ); + $files->{$fileUM}[$i]{inode} = $entry->{inode} + if ( $m->{dirOpts}{inode} ); + } + + # + # Flag deleted files + # + if ( defined($attr) ) { + my $a = $attr->get; + foreach my $fileUM ( keys(%$a) ) { + next if ( $a->{$fileUM}{type} != BPC_FTYPE_DELETED ); + $files->{$fileUM}[$i]{type} = BPC_FTYPE_DELETED; + } + } + + # + # Merge old backups. Don't merge directories from old + # backups because every backup has an accurate directory + # tree. + # + for ( my $k = $i - 1 ; $level > 0 && $k >= 0 ; $k-- ) { + next if ( $m->{backups}[$k]{level} >= $level ); + $level = $m->{backups}[$k]{level}; + foreach my $fileUM ( keys(%$files) ) { + next if ( !defined($files->{$fileUM}[$k]) + || defined($files->{$fileUM}[$i]) + || $files->{$fileUM}[$k]{type} == BPC_FTYPE_DIR ); + $files->{$fileUM}[$i] = $files->{$fileUM}[$k]; + } + } + } + + # + # Remove deleted files + # + for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) { + foreach my $fileUM ( keys(%$files) ) { + next if ( !defined($files->{$fileUM}[$i]) + || $files->{$fileUM}[$i]{type} != BPC_FTYPE_DELETED ); + $files->{$fileUM}[$i] = undef; + } + } + + #print STDERR "Returning:\n", Dumper($files); + return $files; +} + + # # Do a recursive find starting at the given path (either a file # or directory). The callback function $callback is called on each @@ -302,6 +553,7 @@ sub find { my($m, $backupNum, $share, $path, $depth, $callback, @callbackArgs) = @_; + #print(STDERR "find: got $backupNum, $share, $path\n"); # # First call the callback on the given $path # @@ -328,7 +580,7 @@ sub findRecurse my $attr = $m->dirAttrib($backupNum, $share, $path); return if ( !defined($attr) ); - foreach my $file ( keys(%$attr) ) { + foreach my $file ( sort(keys(%$attr)) ) { &$callback($attr->{$file}, @callbackArgs); next if ( !$depth || $attr->{$file}{type} != BPC_FTYPE_DIR ); #