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-2003 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 2.1.0, released 20 Jun 2004. |
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 |
|
50 |
sub new |
51 |
{ |
52 |
my($class, $bpc, $host, $backups, $only_one) = @_; |
53 |
my $m = bless { |
54 |
bpc => $bpc, # BackupPC::Lib object |
55 |
host => $host, # host name |
56 |
backups => $backups, # all backups for this host |
57 |
num => -1, # backup number |
58 |
idx => -1, # index into backups for backup |
59 |
# we are viewing |
60 |
dirPath => undef, # path to current directory |
61 |
dirAttr => undef, # attributes of current directory |
62 |
}, $class; |
63 |
for ( my $i = 0 ; $i < @{$m->{backups}} ; $i++ ) { |
64 |
next if ( defined($m->{backups}[$i]{level}) ); |
65 |
$m->{backups}[$i]{level} = $m->{backups}[$i]{type} eq "incr" ? 1 : 0; |
66 |
} |
67 |
$m->{topDir} = $m->{bpc}->TopDir(); |
68 |
$m->{only_one} = $only_one; |
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 ( $m->{only_one} && $i != $m->{idx} ); |
109 |
|
110 |
$level = $m->{backups}[$i]{level}; |
111 |
$backupNum = $m->{backups}[$i]{num}; |
112 |
push(@{$m->{mergeNums}}, $backupNum); |
113 |
my $mangle = $m->{backups}[$i]{mangle}; |
114 |
my $compress = $m->{backups}[$i]{compress}; |
115 |
my $path = "$m->{topDir}/pc/$m->{host}/$backupNum/"; |
116 |
my $sharePathM; |
117 |
if ( $mangle ) { |
118 |
$sharePathM = $m->{bpc}->fileNameEltMangle($share) |
119 |
. $m->{bpc}->fileNameMangle($dir); |
120 |
} else { |
121 |
$sharePathM = $share . $dir; |
122 |
} |
123 |
$path .= $sharePathM; |
124 |
#print(STDERR "Opening $path (share=$share)\n"); |
125 |
if ( !opendir(DIR, $path) ) { |
126 |
if ( $i == $m->{idx} ) { |
127 |
# |
128 |
# Oops, directory doesn't exist. |
129 |
# |
130 |
$m->{files} = undef; |
131 |
return; |
132 |
} |
133 |
next; |
134 |
} |
135 |
my @dir = readdir(DIR); |
136 |
closedir(DIR); |
137 |
my $attr; |
138 |
if ( $mangle ) { |
139 |
$attr = BackupPC::Attrib->new({ compress => $compress }); |
140 |
if ( -f $attr->fileName($path) && !$attr->read($path) ) { |
141 |
$m->{error} = "Can't read attribute file in $path"; |
142 |
$attr = undef; |
143 |
} |
144 |
} |
145 |
foreach my $file ( @dir ) { |
146 |
$file = $1 if ( $file =~ /(.*)/ ); |
147 |
my $fileUM = $file; |
148 |
$fileUM = $m->{bpc}->fileNameUnmangle($fileUM) if ( $mangle ); |
149 |
#print(STDERR "Doing $fileUM\n"); |
150 |
# |
151 |
# skip special files |
152 |
# |
153 |
next if ( defined($m->{files}{$fileUM}) |
154 |
|| $file eq ".." |
155 |
|| $file eq "." |
156 |
|| $mangle && $file eq "attrib" ); |
157 |
# |
158 |
# skip directories in earlier backups (each backup always |
159 |
# has the complete directory tree). |
160 |
# |
161 |
my @s = stat("$path/$file"); |
162 |
next if ( $i < $m->{idx} && -d _ ); |
163 |
if ( defined($attr) && defined(my $a = $attr->get($fileUM)) ) { |
164 |
$m->{files}{$fileUM} = $a; |
165 |
$attr->set($fileUM, undef); |
166 |
} else { |
167 |
# |
168 |
# Very expensive in the non-attribute case when compresseion |
169 |
# is on. We have to stat the file and read compressed files |
170 |
# to determine their size. |
171 |
# |
172 |
$m->{files}{$fileUM} = { |
173 |
type => -d _ ? BPC_FTYPE_DIR : BPC_FTYPE_FILE, |
174 |
mode => $s[2], |
175 |
uid => $s[4], |
176 |
gid => $s[5], |
177 |
size => -f _ ? $s[7] : 0, |
178 |
mtime => $s[9], |
179 |
}; |
180 |
if ( $compress && -f _ ) { |
181 |
# |
182 |
# Compute the correct size by reading the whole file |
183 |
# |
184 |
my $f = BackupPC::FileZIO->open("$path/$file", |
185 |
0, $compress); |
186 |
if ( !defined($f) ) { |
187 |
$m->{error} = "Can't open $path/$file"; |
188 |
} else { |
189 |
my($data, $size); |
190 |
while ( $f->read(\$data, 65636 * 8) > 0 ) { |
191 |
$size += length($data); |
192 |
} |
193 |
$f->close; |
194 |
$m->{files}{$fileUM}{size} = $size; |
195 |
} |
196 |
} |
197 |
} |
198 |
$m->{files}{$fileUM}{relPath} = "$dir/$fileUM"; |
199 |
$m->{files}{$fileUM}{sharePathM} = "$sharePathM/$file"; |
200 |
$m->{files}{$fileUM}{fullPath} = "$path/$file"; |
201 |
$m->{files}{$fileUM}{backupNum} = $backupNum; |
202 |
$m->{files}{$fileUM}{compress} = $compress; |
203 |
$m->{files}{$fileUM}{nlink} = $s[3]; |
204 |
$m->{files}{$fileUM}{inode} = $s[1]; |
205 |
} |
206 |
# |
207 |
# Also include deleted files |
208 |
# |
209 |
if ( defined($attr) ) { |
210 |
my $a = $attr->get; |
211 |
foreach my $fileUM ( keys(%$a) ) { |
212 |
next if ( $a->{$fileUM}{type} != BPC_FTYPE_DELETED ); |
213 |
my $file = $fileUM; |
214 |
$file = $m->{bpc}->fileNameMangle($fileUM) if ( $mangle ); |
215 |
$m->{files}{$fileUM} = $a->{$fileUM}; |
216 |
$m->{files}{$fileUM}{relPath} = "$dir/$fileUM"; |
217 |
$m->{files}{$fileUM}{sharePathM} = "$sharePathM/$file"; |
218 |
$m->{files}{$fileUM}{fullPath} = "$path/$file"; |
219 |
$m->{files}{$fileUM}{backupNum} = $backupNum; |
220 |
$m->{files}{$fileUM}{compress} = $compress; |
221 |
$m->{files}{$fileUM}{nlink} = 0; |
222 |
$m->{files}{$fileUM}{inode} = 0; |
223 |
} |
224 |
} |
225 |
} |
226 |
# |
227 |
# Prune deleted files |
228 |
# |
229 |
foreach my $file ( keys(%{$m->{files}}) ) { |
230 |
next if ( $m->{files}{$file}{type} != BPC_FTYPE_DELETED ); |
231 |
delete($m->{files}{$file}); |
232 |
} |
233 |
#print STDERR "Returning:\n", Dumper($m->{files}); |
234 |
} |
235 |
|
236 |
# |
237 |
# Return list of shares for this backup |
238 |
# |
239 |
sub shareList |
240 |
{ |
241 |
my($m, $backupNum) = @_; |
242 |
my @shareList; |
243 |
|
244 |
$m->backupNumCache($backupNum) if ( $m->{num} != $backupNum ); |
245 |
return if ( $m->{idx} < 0 ); |
246 |
|
247 |
my $mangle = $m->{backups}[$m->{idx}]{mangle}; |
248 |
my $path = "$m->{topDir}/pc/$m->{host}/$backupNum/"; |
249 |
return if ( !opendir(DIR, $path) ); |
250 |
my @dir = readdir(DIR); |
251 |
closedir(DIR); |
252 |
foreach my $file ( @dir ) { |
253 |
$file = $1 if ( $file =~ /(.*)/ ); |
254 |
next if ( $file eq "attrib" && $mangle |
255 |
|| $file eq "." |
256 |
|| $file eq ".." ); |
257 |
my $fileUM = $file; |
258 |
$fileUM = $m->{bpc}->fileNameUnmangle($fileUM) if ( $mangle ); |
259 |
push(@shareList, $fileUM); |
260 |
} |
261 |
$m->{dir} = undef; |
262 |
return @shareList; |
263 |
} |
264 |
|
265 |
sub backupNumCache |
266 |
{ |
267 |
my($m, $backupNum) = @_; |
268 |
|
269 |
if ( $m->{num} != $backupNum ) { |
270 |
my $i; |
271 |
for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) { |
272 |
last if ( $m->{backups}[$i]{num} == $backupNum ); |
273 |
} |
274 |
if ( $i >= @{$m->{backups}} ) { |
275 |
$m->{idx} = -1; |
276 |
return; |
277 |
} |
278 |
$m->{num} = $backupNum; |
279 |
$m->{idx} = $i; |
280 |
} |
281 |
} |
282 |
|
283 |
# |
284 |
# Return the attributes of a specific file |
285 |
# |
286 |
sub fileAttrib |
287 |
{ |
288 |
my($m, $backupNum, $share, $path) = @_; |
289 |
|
290 |
#print(STDERR "fileAttrib($backupNum, $share, $path)\n"); |
291 |
if ( $path =~ s{(.*)/+(.+)}{$1} ) { |
292 |
my $file = $2; |
293 |
$m->dirCache($backupNum, $share, $path); |
294 |
return $m->{files}{$file}; |
295 |
} else { |
296 |
#print STDERR "Got empty $path\n"; |
297 |
$m->dirCache($backupNum, "", ""); |
298 |
my $attr = $m->{files}{$share}; |
299 |
return if ( !defined($attr) ); |
300 |
$attr->{relPath} = "/"; |
301 |
return $attr; |
302 |
} |
303 |
} |
304 |
|
305 |
# |
306 |
# Return the contents of a directory |
307 |
# |
308 |
sub dirAttrib |
309 |
{ |
310 |
my($m, $backupNum, $share, $dir) = @_; |
311 |
|
312 |
$m->dirCache($backupNum, $share, $dir); |
313 |
return $m->{files}; |
314 |
} |
315 |
|
316 |
# |
317 |
# Return a listref of backup numbers that are merged to create this view |
318 |
# |
319 |
sub mergeNums |
320 |
{ |
321 |
my($m) = @_; |
322 |
|
323 |
return $m->{mergeNums}; |
324 |
} |
325 |
|
326 |
# |
327 |
# Return a list of backup indexes for which the directory exists |
328 |
# |
329 |
sub backupList |
330 |
{ |
331 |
my($m, $share, $dir) = @_; |
332 |
my($i, @backupList); |
333 |
|
334 |
$dir = "/$dir" if ( $dir !~ m{^/} ); |
335 |
$dir =~ s{/+$}{}; |
336 |
|
337 |
for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) { |
338 |
my $backupNum = $m->{backups}[$i]{num}; |
339 |
my $mangle = $m->{backups}[$i]{mangle}; |
340 |
my $path = "$m->{topDir}/pc/$m->{host}/$backupNum/"; |
341 |
my $sharePathM; |
342 |
if ( $mangle ) { |
343 |
$sharePathM = $m->{bpc}->fileNameEltMangle($share) |
344 |
. $m->{bpc}->fileNameMangle($dir); |
345 |
} else { |
346 |
$sharePathM = $share . $dir; |
347 |
} |
348 |
$path .= $sharePathM; |
349 |
next if ( !-d $path ); |
350 |
push(@backupList, $i); |
351 |
} |
352 |
return @backupList; |
353 |
} |
354 |
|
355 |
# |
356 |
# Return the history of all backups for a particular directory |
357 |
# |
358 |
sub dirHistory |
359 |
{ |
360 |
my($m, $share, $dir) = @_; |
361 |
my($i, $level); |
362 |
my $files = {}; |
363 |
|
364 |
$dir = "/$dir" if ( $dir !~ m{^/} ); |
365 |
$dir =~ s{/+$}{}; |
366 |
|
367 |
# |
368 |
# merge backups, starting at the first one, and working |
369 |
# forward. |
370 |
# |
371 |
for ( $i = 0 ; $i < @{$m->{backups}} ; $i++ ) { |
372 |
$level = $m->{backups}[$i]{level}; |
373 |
my $backupNum = $m->{backups}[$i]{num}; |
374 |
my $mangle = $m->{backups}[$i]{mangle}; |
375 |
my $compress = $m->{backups}[$i]{compress}; |
376 |
my $path = "$m->{topDir}/pc/$m->{host}/$backupNum/"; |
377 |
my $sharePathM; |
378 |
if ( $mangle ) { |
379 |
$sharePathM = $m->{bpc}->fileNameEltMangle($share) |
380 |
. $m->{bpc}->fileNameMangle($dir); |
381 |
} else { |
382 |
$sharePathM = $share . $dir; |
383 |
} |
384 |
$path .= $sharePathM; |
385 |
#print(STDERR "Opening $path (share=$share)\n"); |
386 |
if ( !opendir(DIR, $path) ) { |
387 |
# |
388 |
# Oops, directory doesn't exist. |
389 |
# |
390 |
next; |
391 |
} |
392 |
my @dir = readdir(DIR); |
393 |
closedir(DIR); |
394 |
my $attr; |
395 |
if ( $mangle ) { |
396 |
$attr = BackupPC::Attrib->new({ compress => $compress }); |
397 |
if ( -f $attr->fileName($path) && !$attr->read($path) ) { |
398 |
$m->{error} = "Can't read attribute file in $path"; |
399 |
$attr = undef; |
400 |
} |
401 |
} |
402 |
foreach my $file ( @dir ) { |
403 |
$file = $1 if ( $file =~ /(.*)/ ); |
404 |
my $fileUM = $file; |
405 |
$fileUM = $m->{bpc}->fileNameUnmangle($fileUM) if ( $mangle ); |
406 |
#print(STDERR "Doing $fileUM\n"); |
407 |
# |
408 |
# skip special files |
409 |
# |
410 |
next if ( $file eq ".." |
411 |
|| $file eq "." |
412 |
|| $mangle && $file eq "attrib" |
413 |
|| defined($files->{$fileUM}[$i]) ); |
414 |
my @s = stat("$path/$file"); |
415 |
if ( defined($attr) && defined(my $a = $attr->get($fileUM)) ) { |
416 |
$files->{$fileUM}[$i] = $a; |
417 |
$attr->set($fileUM, undef); |
418 |
} else { |
419 |
# |
420 |
# Very expensive in the non-attribute case when compresseion |
421 |
# is on. We have to stat the file and read compressed files |
422 |
# to determine their size. |
423 |
# |
424 |
$files->{$fileUM}[$i] = { |
425 |
type => -d _ ? BPC_FTYPE_DIR : BPC_FTYPE_FILE, |
426 |
mode => $s[2], |
427 |
uid => $s[4], |
428 |
gid => $s[5], |
429 |
size => -f _ ? $s[7] : 0, |
430 |
mtime => $s[9], |
431 |
}; |
432 |
if ( $compress && -f _ ) { |
433 |
# |
434 |
# Compute the correct size by reading the whole file |
435 |
# |
436 |
my $f = BackupPC::FileZIO->open("$path/$file", |
437 |
0, $compress); |
438 |
if ( !defined($f) ) { |
439 |
$m->{error} = "Can't open $path/$file"; |
440 |
} else { |
441 |
my($data, $size); |
442 |
while ( $f->read(\$data, 65636 * 8) > 0 ) { |
443 |
$size += length($data); |
444 |
} |
445 |
$f->close; |
446 |
$files->{$fileUM}[$i]{size} = $size; |
447 |
} |
448 |
} |
449 |
} |
450 |
$files->{$fileUM}[$i]{relPath} = "$dir/$fileUM"; |
451 |
$files->{$fileUM}[$i]{sharePathM} = "$sharePathM/$file"; |
452 |
$files->{$fileUM}[$i]{fullPath} = "$path/$file"; |
453 |
$files->{$fileUM}[$i]{backupNum} = $backupNum; |
454 |
$files->{$fileUM}[$i]{compress} = $compress; |
455 |
$files->{$fileUM}[$i]{nlink} = $s[3]; |
456 |
$files->{$fileUM}[$i]{inode} = $s[1]; |
457 |
} |
458 |
|
459 |
# |
460 |
# Merge old backups. Don't merge directories from old |
461 |
# backups because every backup has an accurate directory |
462 |
# tree. |
463 |
# |
464 |
for ( my $k = $i - 1 ; $level > 0 && $k >= 0 ; $k-- ) { |
465 |
next if ( $m->{backups}[$k]{level} >= $level ); |
466 |
$level = $m->{backups}[$k]{level}; |
467 |
foreach my $fileUM ( keys(%$files) ) { |
468 |
next if ( !defined($files->{$fileUM}[$k]) |
469 |
|| defined($files->{$fileUM}[$i]) |
470 |
|| $files->{$fileUM}[$k]{type} == BPC_FTYPE_DIR ); |
471 |
$files->{$fileUM}[$i] = $files->{$fileUM}[$k]; |
472 |
} |
473 |
} |
474 |
|
475 |
# |
476 |
# Finally, remove deleted files |
477 |
# |
478 |
if ( defined($attr) ) { |
479 |
my $a = $attr->get; |
480 |
foreach my $fileUM ( keys(%$a) ) { |
481 |
next if ( $a->{$fileUM}{type} != BPC_FTYPE_DELETED ); |
482 |
$files->{$fileUM}[$i] = undef if ( defined($files->{$fileUM}) ); |
483 |
} |
484 |
} |
485 |
} |
486 |
#print STDERR "Returning:\n", Dumper($files); |
487 |
return $files; |
488 |
} |
489 |
|
490 |
|
491 |
# |
492 |
# Do a recursive find starting at the given path (either a file |
493 |
# or directory). The callback function $callback is called on each |
494 |
# file and directory. The function arguments are the attrs hashref, |
495 |
# and additional callback arguments. The search is depth-first if |
496 |
# depth is set. Returns -1 if $path does not exist. |
497 |
# |
498 |
sub find |
499 |
{ |
500 |
my($m, $backupNum, $share, $path, $depth, $callback, @callbackArgs) = @_; |
501 |
|
502 |
#print(STDERR "find: got $backupNum, $share, $path\n"); |
503 |
# |
504 |
# First call the callback on the given $path |
505 |
# |
506 |
my $attr = $m->fileAttrib($backupNum, $share, $path); |
507 |
return -1 if ( !defined($attr) ); |
508 |
&$callback($attr, @callbackArgs); |
509 |
return if ( $attr->{type} != BPC_FTYPE_DIR ); |
510 |
|
511 |
# |
512 |
# Now recurse into subdirectories |
513 |
# |
514 |
$m->findRecurse($backupNum, $share, $path, $depth, |
515 |
$callback, @callbackArgs); |
516 |
} |
517 |
|
518 |
# |
519 |
# Same as find(), except the callback is not called on the current |
520 |
# $path, only on the contents of $path. So if $path is a file then |
521 |
# no callback or recursion occurs. |
522 |
# |
523 |
sub findRecurse |
524 |
{ |
525 |
my($m, $backupNum, $share, $path, $depth, $callback, @callbackArgs) = @_; |
526 |
|
527 |
my $attr = $m->dirAttrib($backupNum, $share, $path); |
528 |
return if ( !defined($attr) ); |
529 |
foreach my $file ( sort(keys(%$attr)) ) { |
530 |
&$callback($attr->{$file}, @callbackArgs); |
531 |
next if ( !$depth || $attr->{$file}{type} != BPC_FTYPE_DIR ); |
532 |
# |
533 |
# For depth-first, recurse as we hit each directory |
534 |
# |
535 |
$m->findRecurse($backupNum, $share, "$path/$file", $depth, |
536 |
$callback, @callbackArgs); |
537 |
} |
538 |
if ( !$depth ) { |
539 |
# |
540 |
# For non-depth, recurse directories after we finish current dir |
541 |
# |
542 |
foreach my $file ( keys(%{$attr}) ) { |
543 |
next if ( $attr->{$file}{type} != BPC_FTYPE_DIR ); |
544 |
$m->findRecurse($backupNum, $share, "$path/$file", $depth, |
545 |
$callback, @callbackArgs); |
546 |
} |
547 |
} |
548 |
} |
549 |
|
550 |
1; |