backuppc-deletefile : peaufinage (bis)
[auf-serveur.git] / backuppc-deletefile / BackupPC_deleteFile
1 #!/usr/bin/perl
2 #============================================================= -*-perl-*-
3 #
4 # BackupPC_deleteFile.pl: Delete one or more files/directories from
5 #                         a range of hosts, backups, and shares
6 #
7 # DESCRIPTION
8 #   See below for detailed description of what it does and how it works
9 #   
10 # AUTHOR
11 #   Jeff Kosowsky
12 #
13 # COPYRIGHT
14 #   Copyright (C) 2008, 2009  Jeff Kosowsky
15 #
16 #   This program is free software; you can redistribute it and/or modify
17 #   it under the terms of the GNU General Public License as published by
18 #   the Free Software Foundation; either version 2 of the License, or
19 #   (at your option) any later version.
20 #
21 #   This program is distributed in the hope that it will be useful,
22 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
23 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24 #   GNU General Public License for more details.
25 #
26 #   You should have received a copy of the GNU General Public License
27 #   along with this program; if not, write to the Free Software
28 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29 #
30 #========================================================================
31 #
32 # Version 0.1.5, released Dec 2009
33 #
34 #========================================================================
35 # CHANGELOG
36 #     0.1 (Nov 2008)   - First public release
37 #     0.1.5 (Dec 2009) - Minor bug fixes
38 #                        Ability to abort/skip/force hard link deletion 
39 #========================================================================
40 # Program logic is as follows:
41 #
42 # 1. First construct a hash of hashes of 3 arrays and 2 hashes that
43 #    encapsulates the structure of the full and incremental backups
44 #    for each host. This hash is called:
45 #    %backupsHoHA{<hostname>}{<key>} 
46 #    where the keys are: "ante", "post", "baks", "level", "vislvl"
47 #    with the first 3 keys having arrays as values and the final 2
48 #    keys having hashes as values. This pre-step is done since this
49 #    same structure can be re-used when deleting multiple files and
50 #    dirs (with potential wilcards) across multiple shares, backups,
51 #    and hosts. The component arrays and hashes which are unique per
52 #    host are constructed as folows:
53 #     
54 #    - Start by constructing the simple hash %LevelH whose keys map
55 #      backup numbers to incremental backup levels based on the
56 #      information in the corresponding backupInfo file.
57 #
58 #    - Then, for each host selected, determine the list (@Baks) of
59 #      individual backups from which files are to be deleted based on
60 #      bakRange and the actual existing backups.
61 #  
62 #    - Based on this list determine the list of direct antecedent
63 #      backups (@Ante) that have strictly increasing backup levels
64 #      starting with the previous level 0 backup. This list thus
65 #      begins with the previous level zero backup and ends with the
66 #      last backup before @Baks that has a lower incremental level
67 #      than the first member of @Baks. Note: this list may be empty if
68 #      @Baks starts with a full (level 0) backup. Note: there is at
69 #      most one (and should in general be exactly one) incremental
70 #      backup per level in this list starting with level 0.
71 #
72 #    - Similarly, constuct the list of direct descendants (@Post) of
73 #      the elements of @Baks that have strictly decreasing backup
74 #      levels starting with the first incremental backup after @Baks
75 #      and continuing until we reach a backup whose level is less than
76 #      or equal to the level of the lowest incremental backup in @Baks
77 #      (which may or may not be a level 0 backup). Again this list may
78 #      be empty if the first backup after @Baks is lower than the
79 #      level of all backups in @Baks. Also, again, there is at most
80 #      one backup per level.
81 #
82 #    - Note that by construction, @Ante is stored in ascending order
83 #      and furthermore each backup number has a strictly ascending
84 #      incremental level. Similarly, @Post is stored in strictly
85 #      ascending order but its successive elements have monotonically
86 #      non-increasing incremental levels. Also, the last element of
87 #      @Ante has an incremental level lower than the first element of
88 #      @Baks and the the last element of @Post has an incremental
89 #      level higher than the lowest level of @Baks. This is all
90 #      because anything else neither affects nor is affected by
91 #      deletions in @Baks. In contrast, note that @Baks can have any
92 #      any pattern of increasing, decreasing, or repeated incremental
93 #      levels.
94 #   
95 #    - Finally, create the second hash (%VislvlH) which has keys equal
96 #      to levels and values equal to the most recent backup with that
97 #      level in @Baks or @Ante that could potentially still be visible
98 #      in @Post. So, since we need to keep @Post unchanged, we need to
99 #      make sure that whatever showed through into @Post before the
100 #      deletions still shows through after deletion. Specifically, we
101 #      may need to move/copy files (or directories) and set delete
102 #      attributes to make sure that nothing more or less is visible in
103 #      @Post after the deletions.
104 #
105 # 2. Second, for each host, combine the share names (and/or shell
106 #    regexs) and list of file names (and/or shell regexs) with the
107 #    backup ranges @Ante and @Baks to glob for all files that need
108 #    either to be deleted from @Baks or blocked from view by setting a
109 #    type=10 delete attribute type.  If a directory is on the list and
110 #    the remove directory flag (-r) is not set, then directories are
111 #    skipped (and an error is logged). If any of these files (or dirs)
112 #    are or contain hard links (either type hard link or a hard link
113 #    "target") then they are skipped and logged since hard links
114 #    cannot easily be deleted/copied/moved (since the other links will
115 #    be affected). Duplicate entries and entries that are a subtree of
116 #    another entry are rationalized and combined.
117 #
118 # 3. Third, for each host and for each relevant candidate file
119 #    deletion, start going successively through the @Ante, @Baks, and
120 #    @Post chains to determine which files and attributes need to be
121 #    deleted, cleared, or copied/linked to @Post.
122 #
123 #    - Start by going through, @Ante, in ascending order to construct
124 #      two visibility hashes. The first hash, %VisibleAnte, is used to
125 #      mark whether or not a file in @Ante may be visible from @Baks
126 #      from a higher incremental level. The presence of a file sets
127 #      the value of the hash while intervening delete type=10 or the
128 #      lack of a parent directory resets the value to invisible
129 #      (-1). Later, when we get to @Baks, we will need to make these
130 #      invisible to complete our deletion effect
131 #
132 #      The second hash, %VisibleAnteBaks, (whose construction
133 #      continues when we iterate through @Baks) determines whether or
134 #      not a file from @Ante or @Baks was originally visible from
135 #      @Post. And if a file was visible, then the backup number of
136 #      that file is stored in the value of the hash. Later, we will
137 #      use this hash to copy/link files from @Ante and @Baks into
138 #      @Post to preserve its pre-deletion state.
139 #
140 #      Note that at each level, there is at *most* one backup from
141 #      @Ante that is visible from @Baks (coded by %VisibleAnte) and
142 #      similarly there is at *most* one backup from @Ante and @Baks
143 #      combined that is visible from @Post (coded by
144 #      @VisibleAnteBaks).
145 #
146 #   - Next, go through @Baks to mark for deletion any instances of the
147 #     file that are present. Then set the attrib type to type=10
148 #     (delete) if %VisibleAnte indicates that a file from @Ante would
149 #     otherwise be visible at that level. Otherwise, clear the attrib
150 #     and mark it for deletion. Similarly, once the type=10 type has
151 #     been set, all higher level element of @Baks can have their file
152 #     attribs cleared whether they originally indicated a file type or
153 #     a delete type (i.e. no need for 2 layers of delete attribs).
154 #
155 #   - Finally, go through the list of @Post in ascending order. If
156 #     there is no file and no delete flag present, then use the
157 #     information coded in %VisibleAnteBaks to determine whether we
158 #     need to link/copy over a version of the file previously stored
159 #     in @Ante and/or @Baks (along with the corresponding file attrib
160 #     entry) or whether we need to set a type=10 delete
161 #     attribute. Conversely, if originally, there was a type=10 delete
162 #     attribute, then by construction of @Post, the delete type is no
163 #     longer needed since the deletion will now occur in one of its
164 #     antecedents in @Baks, so we need to clear the delete type from
165 #     the attrib entry.
166 #
167 # 4. Finally, after all the files for a given host have been marked
168 #    for deletion, moving/copying or attribute changes, loop through
169 #    and execute the changes. Deletions are looped first by host and
170 #    then by backup number and then alphabetically by filepath.
171 #
172 #     Files are deleted by unlinking (recursively via rmtree for
173 #    directories). Files are "copied" to @Post by first attempting to
174 #    link to pool (either using an existing link or by creating a new
175 #    pool entry) and if not successful then by copying. Directories
176 #    are done recursively. Attributes are either cleared (deleted) or
177 #    set to type=10 delete or copied over to @Post. Whenever an
178 #    attribute file needs to be written, first an attempt is made to
179 #    link to pool (or create a new pool entry and link if not
180 #    present). Otherwise, the attribute is just written. Empty
181 #    attribute files are deleted. The attribute writes to filesystem
182 #    are done once per directory per backup (except for the moves).
183 #
184 # 5. As a last step, optionally BackupPC_nightly is called to clean up
185 #    the pool, provided you set the -c flag and that the BackupPC
186 #    daemon is running. Note that this routine itself does NOT touch
187 #    the pool.
188
189 # Debugging & Verification:
190
191 # This program is instrumented to give you plenty of "output" to see
192 # all the subtleties of what is being deleted (or moved) and what is
193 # not. The seemingly simple rules of "inheritance" of incrementals
194 # hide a lot of complexity (and special cases) when you try to delete
195 # a file in the middle of a backup chain.
196 #
197 # To see what is happening during the "calculate_deletes" stage which
198 # is the heart of the algorithm in terms of determining what happens
199 # to what, it is best to use DEBUG level 2 or higher (-d 2). Then for
200 # every host and for every (unique) top-level file or directory
201 # scheduled for deletion, you will see the complete chain of how the
202 # program walks sequentially through @Ante, @Baks, and @Post.
203 # For each file, you first see a line of form:
204 #    LOOKING AT: [hostname] [@Ante chain] [@Baks chain] [@Post chain] <file name>
205 #
206 # Followed by a triad of lines for each of the backups in the chain of form:
207 #     ANTE[baknum](baklevel) <file path including host> [file code] [attribute code]
208 #     BAKS[baknum](baklevel) <file path including host> [file code] [attribute code] [action flag]
209 #     POST[baknum](baklevel) <file path including host> [file code] [attribute code] [action flag]
210 #
211 #  where the file code is one of:
212 #     F = file present at that baklevel and to be deleted (if in @Baks)
213 #         (or f if in @Ante or @Post and potentially visible)
214 #     D = Dnir present at that baklevel and to be deleted (if in @Baks)
215 #             (or f if in @Ante or @Post and potentially visible)
216 #     - = File not present at that baklevel
217 #     X = Parent directory not present at that baklevel 
218 #         (or x if in @Ante or @Post)
219 #  and the attribute code is one of:
220 #     n = Attribute type key (if present)
221 #     - = If no attribute for the file (implies no file)
222 #  and the action flag is one of the following: (only applies to @Baks & @Post)
223 #     C = Clear attribute (if attribute was previously present)
224 #     D = Set to type 10 delete (if not already set)
225 #     Mn = Move file/dir here from level 'n' (@Post only)
226 #
227 # More detail on the individual actions can be obtained by increasing
228 # the debugging level.
229 #
230 # The other interesting output is the result of the "execute_deletes"
231 # stage which shows what actually happens. Here, for every host and
232 # every backup of that host, you see what happens on a file by file
233 # level. The output is of form:
234 #   [hostname][@Ante chain] [@Baks chain] [@Post chain]
235 #   **BACKUP: [hostname][baknum](baklevel)
236 #       [hostname][baknum] <file name> [file code][attribute code]<move>
237 #
238 #  where the file code is one of:
239 #     F = Single file deleted
240 #     D(n) = Directory deleted with total of 'n' file/dir deletes
241 #             (including the directory)
242 #     - = Nothing deleted
243 #  and the attribute code is one of:
244 #     C = Attribute cleared
245 #     D = Attribute set to type 10 delete
246 #     d = Attribute left alone with type 10 delete
247 #     - = Attrib (otherwise) unchanged [shouldn't happen]
248 #  and the (optional) move code is: (applies only to @Post)
249 #     n->m  = File/dir moved by *linking* to pool from backup 'n' to 'm'
250 #     n=>   = File/dir moved by *copying* from backup 'n' to 'm'
251 # Finally, since the files are sorted alphabetically by name and
252 # directory, we only need to actually write the attribute folder after
253 # we finish making all the delete/clear changes in a directory.
254 # This is coded as:
255 #       [hostname][baknum] <dir>/attrib [-][attribute code]
256 #
257 #  where the attribute code is one of:
258 #     W = Attribute file *linked* to pool successfully
259 #     w = Attribute file *copied* to filesystem successfully
260 #     R = Empty attribute file removed from filesystem
261 #     X = Error writing attribute file
262 #========================================================================
263
264 use strict;
265 use warnings;
266
267 use File::Find;
268 use File::Glob ':glob';
269 use Data::Dumper;  #Just used for debugging...
270
271 use lib "/usr/share/BackupPC/lib";
272 use BackupPC::Lib;
273 use BackupPC::jLib;
274 use BackupPC::Attrib qw(:all);
275 use BackupPC::FileZIO;
276 use Getopt::Std;
277
278 use constant S_HLINK_TARGET => 0400000;    # this file is hardlink target
279
280 my $DeleteAttribH = {  #Hash reference to attribute entry for deleted file
281         type  => BPC_FTYPE_DELETED,  #10
282         mode  => 0,
283         uid   => 0,
284         gid   => 0,
285         size  => 0,
286         mtime => 0,
287 };
288
289 my %filedelsHoH;
290 # Hash has following structure:
291 # $filedelsHoH{$host}{$baknum}{$file} = <mask for what happened to file & attribute>
292 #                                       where the mask is one of the following elements
293
294 use constant FILE_ATTRIB_COPY  => 0000001;  # File and corresponding attrib copied/linked to new backup in @Post
295 use constant FILE_DELETED       => 0000002;  # File deleted (not moved)
296 use constant ATTRIB_CLEARED     => 0000010;  # File attrib cleared
297 use constant ATTRIB_DELETETYPE  => 0000020;  # File attrib deleted
298
299
300 my $DEBUG; #Note setting here will override options value
301
302 die("BackupPC::Lib->new failed\n") if ( !(my $bpc = BackupPC::Lib->new) );
303 my $TopDir = $bpc->TopDir();
304 chdir($TopDir); #Do this because 'find' will later try to return to working
305             #directory which may not be accessible if you are su backuppc
306
307
308 (my $pc = "$TopDir/pc") =~ s|//*|/|g;
309 %Conf   = $bpc->Conf();  #Global variable defined in jLib.pm
310
311 my %opts;
312 if ( !getopts("h:n:s:lrH:mF:qtcd:u", \%opts) || defined($opts{u}) ||
313          !defined($opts{h}) || !defined($opts{n}) || 
314          (!defined($opts{s}) && defined($opts{m})) || 
315          (defined $opts{H} && $opts{H} !~ /^(0|abort|1|skip|2|force)$/) ||
316          (!$opts{l} && !$opts{F} && @ARGV < 1)) {
317     print STDERR <<EOF;
318 usage: $0 [options] files/directories...
319
320   Required options:
321     -h <host>     Host (or - for all) from which path is offset
322     -n <bakRange> Range of successive backup numbers to delete.
323                     N   delete files from backup N (only)
324                     M-N delete files from backups M-N (inclusive)
325                     -M  delete files from all backups up to M (inclusive)
326                     M-  delete files from all backups up from M (inlusive)
327                     -   delete files from ALL backups
328                    {N}  if one of the numbers is in braces, then  interpret
329                         as the N\'th backup counting from the *beginning*
330                    [N]  if one of the numbers is in braces, then  interpret
331                         as the N\'th backup counting from the *end*
332     -s <share>    Share name (or - for all) from which path is offset
333                   (don\'t include the 'f' mangle)
334                   NOTE: if unmangle option (-m) is not set then the share name
335                   is optional and if not specified then it must instead be 
336                   included in mangled form as part of the file/directory names.
337
338   Optional options:
339     -l            Just list backups by host (with level noted in parentheses)
340     -r            Allow directories to be removed too (otherwise skips over directories)
341     -H <action>   Treatment of hard links contained in deletion tree:
342                     0|abort  abort with error=2 if hard links in tree [default]
343                     1|skip   Skip hard links or directories containing them
344                     2|force  delete anyway (BE WARNED: this may affect backup
345                              integrity if hard linked to files outside tree)
346     -m            Paths are unmangled (i.e. apply mangle to paths; doesn\'t apply to shares)
347     -F <file>     Read files/directories from <file> (or stdin if <file> = -)
348     -q            Don\'t show deletions
349     -t            Trial run -- do everything but deletions
350     -c            Clean up pool - schedule BackupPC_nightly to run (requires server running)
351                   Only runs if files were deleted
352     -d level      Turn on debug level
353     -u            Print this usage message...
354 EOF
355 exit(1);
356 }
357
358 my $hostopt = $opts{h};
359 my $numopt = $opts{n};
360 my $shareopt = $opts{s} || '';
361 my $listopt = $opts{l} || 0;
362 my $mangleopt = $opts{m} || 0;
363 my $rmdiropt = $opts{r} || 0;
364 my $fileopt = $opts{F} || 0;
365 my $quietopt = $opts{q} || 0;
366 $dryrun = $opts{t} || 0; #global variable jLib.pm
367 my $runnightlyopt = $opts{c} || 0;
368
369 my $hardopt = $opts{H} || 0;
370 my $hardaction;
371 if($hardopt =~ /^(1|skip)$/) {
372         $hardopt = 1;
373         $hardaction = "SKIPPING";
374 }
375 elsif($hardopt =~ /^(2|force)$/) {
376         $hardopt = 2;
377 }
378 else{
379         $hardopt = 0;
380         $hardaction = "ABORTING";
381 }
382
383 $DEBUG = ($opts{d} || 0 ) unless defined $DEBUG; #Override hard-coded definition unless set explicitly
384 #$DEBUG && ($dryrun=1);  #Uncomment if you want DEBUG to imply dry run
385 #$dryrun=1; #JJK: Uncomment to hard-wire to always dry-run (paranoia)
386 my $DRYRUN = ($dryrun == 0 ? "" : " DRY-RUN");
387
388
389 # Fill hash with backup structure by host
390 my %backupsHoHA;
391 get_allhostbackups($hostopt, $numopt, \%backupsHoHA);
392 if($listopt) {
393         print_backup_list(\%backupsHoHA);
394         exit;
395 }
396
397 my $shareregx_sh = my $shareregx_pl = $shareopt;
398 if($shareopt eq '-') {
399         $shareregx_pl = "f[^/]+";
400         $shareregx_sh = "f*"; # For shell globbing
401 }
402 elsif($shareopt ne '') {
403         $shareregx_pl =~ s|//*|%2f|g; #Replace (one or more) '/' with %2f
404     $shareregx_sh = $shareregx_pl = "f" . $shareregx_pl;
405 }
406
407 #Combine share and file arg regexps
408 my (@filelist, @sharearglist);
409 if($fileopt) {
410         @filelist = read_file($fileopt);
411 }
412 else {
413         @filelist = @ARGV;
414 }
415 foreach my $file (@filelist) {
416         $file = $bpc->fileNameMangle($file) if $mangleopt; #Mangle filename
417         my $sharearg = "$shareregx_sh/$file";
418         $sharearg =~ s|//*|/|g;  $sharearg =~ s|^/*||g; $sharearg =~ s|/*$||g;
419             # Remove double, leading, and trailing slashes
420         die "Error: Can't delete root share directory: $sharearg\n"
421                 if ("$sharearg" =~ m|^[^/]*$|); #Avoid because dangerous...
422         push(@sharearglist, $sharearg);
423 }
424
425 my $filesdeleted = my $totfilesdeleted = my $filescopied = 0;
426 my $attrsdeleted = my $attrscleared = my $atfilesdeleted = 0;
427
428 my $hrdlnkflg;
429 foreach my $Host (keys %backupsHoHA) { #Loop through each host
430         $hrdlnkflg=0;
431         unless(defined @{$backupsHoHA{$Host}{baks}}) { #@baks is empty
432                 print "[$Host] ***NO BACKUPS FOUND IN DELETE RANGE***\n" unless $quietopt;
433                 next;
434         }
435         my @Ante = @{$backupsHoHA{$Host}{ante}};
436         my @Baks = @{$backupsHoHA{$Host}{baks}};
437         my @Post = @{$backupsHoHA{$Host}{post}};
438
439         print "[$Host][" . join(" ", @Ante) . "][" . 
440                 join(" ", @Baks) . "][" . join(" ", @Post) . "]\n" unless $quietopt;
441
442 $DEBUG > 1 && (print "  ANTE[$Host]: " . join(" ", @Ante) ."\n");
443 $DEBUG > 1 && (print "  BAKS[$Host]: " . join(" ", @Baks) ."\n");
444 $DEBUG > 1 && (print "  POST[$Host]: " . join(" ", @Post) ."\n");
445
446         #We need to glob files that occur both in the delete list (@Baks) and
447         #in the antecedent list (@Ante) since antecedents affect presence of
448         #later incrementals.
449         my $numregx_sh = "{" . join(",", @Ante, @Baks) . "}";
450         my $pcHost = "$pc/$Host";
451         my @filepathlist;
452
453         foreach my $sharearg (@sharearglist) {
454                 #Glob for all (relevant) file paths for host across @Baks & @Ante backups
455 #JJK            @filepathlist = (@filepathlist, <$pcHost/$numregx_sh/$sharearg>);
456                 @filepathlist = (@filepathlist, bsd_glob("$pcHost/$numregx_sh/$sharearg"));
457         }
458     #Now use a hash to collapse into unique file keys (with host & backup number stripped off)
459         my %fileH;
460         foreach my $filepath (@filepathlist) {
461                 next unless -e $filepath; #Skip non-existent files (note if no wildcard in path, globbing
462                                           #will always return the file name even if doesn't exist)
463                 $filepath =~ m|^$pcHost/[0-9]+/+(.*)|;
464                 $fileH{$1}++;  #Note ++ used to set the keys
465         }
466         unless(%fileH) {
467 $DEBUG && print "  LOOKING AT: [$Host] [" . join(" ", @Ante) . "][" . join(" ", @Baks) . "][" . join(" ", @Post) . "] **NO DELETIONS ON THIS HOST**\n\n";
468                         next;
469         }
470         my $lastfile="///"; #dummy starting point since no file can have this name since eliminated dup '/'s
471         foreach my $File (sort keys %fileH) { #Iterate through sorted files
472                 # First build an array of filepaths based on ascending backup numbers in
473                 # @Baks. Also, do a quick check for directories.
474                 next if $File =~ m|^$lastfile/|; # next if current file is in a subdirectory of previous file
475         $lastfile = $File;
476                 #Now create list of paths to search for hardlinks
477                 my @Pathlist = ();
478                 foreach my $Baknum (@Ante) { #Need to include @Ante in hardlink search
479                         my $Filepath = "$pc/$Host/$Baknum/$File";
480                         next unless -e $Filepath;
481                         push (@Pathlist, $Filepath);
482                 }
483                 my $dirflag=0;
484                 foreach my $Baknum (@Baks) {
485                         my $Filepath = "$pc/$Host/$Baknum/$File";
486                         next unless -e $Filepath;
487                         if (-d $Filepath && !$rmdiropt) {
488                                 $dirflag=1; #Only enforce directory check in @Baks because only deleting there
489                                 printerr "Skipping directory `$Host/*/$File` since -r flag not set\n\n";
490                                 last;
491                         }
492                         push (@Pathlist, $Filepath);
493                 }
494                 next if $dirflag;
495                 next unless(@Pathlist); #Probably shouldn't get here since by construction a path should exist 
496                                         #for at least one of the elements of @Ante or @Baks
497                 #Now check to see if any hard-links in the @Pathlist
498                 find(\&find_is_hlink, @Pathlist ) unless $hardopt == 2; #Unless force
499                 exit 2 if $hrdlnkflg && $hardopt == 0; #abort
500                 next if $hrdlnkflg;
501 $DEBUG && print "  LOOKING AT: [$Host] [" . join(" ", @Ante) . "][" . join(" ", @Baks) . "][" . join(" ", @Post) . "] $File\n";
502                 calculate_deletes($Host, $File, \$backupsHoHA{$Host}, \$filedelsHoH{$Host}, !$quietopt);
503 $DEBUG && print "\n";
504         }
505         execute_deletes($Host, \$backupsHoHA{$Host}, \$filedelsHoH{$Host}, !$quietopt);
506 }
507
508 print "\nFiles/directories deleted: $filesdeleted($totfilesdeleted)     Files/directories copied: $filescopied\n" unless $quietopt;
509 print "Delete attrib set: $attrsdeleted                Attributes cleared: $attrscleared\n" unless $quietopt;
510 print "Empty attrib files deleted: $atfilesdeleted       Errors: $errorcount\n" unless $quietopt;
511 run_nightly($bpc) if (!$dryrun && $runnightlyopt);
512 exit;
513
514 #Set $hrdlnkflg=1 if find a hard link (including "targets")
515 # Short-circuit/prune find as soon as hard link found.
516 sub find_is_hlink
517 {
518         if($hrdlnkflg) {
519                 $File::Find::prune = 1; #Prune search if hard link already found
520         #i.e. don't go any deeper (but still will finish the current level)
521         }
522         elsif($File::Find::name eq $File::Find::topdir  #File
523                   && -f && m|f.*|
524                   &&( get_jtype($File::Find::name) & S_HLINK_TARGET)) {
525         # Check if file has type hard link (or hard link target) Note: we
526         # could have used this test recursively on all files in the
527         # directory tree, but it would be VERY SLOW since we would need to
528         # read the attrib file for every file in every
529         # subdirectory. Instead, we only use this method when we are
530         # searching directly for a file at the top leel
531         # (topdir). Otherwise, we use the method below that just
532         # recursively searches for the attrib file and reads that
533         # directly.
534                 $hrdlnkflg = 1;
535                 print relpath($File::Find::name) . ": File is a hard link. $hardaction...\n\n";
536         }
537         elsif (-d && -e  attrib($File::Find::name)) { #Directory
538     # Read through attrib file hash table in each subdirectory in tree to
539         # find files that are hard links (including 'targets'). Fast
540         # because only need to open attrib file once per subdirectory to test
541         # all the files in the directory.
542                 read_attrib(my $attr, $File::Find::name);
543                 foreach my $file (keys (%{$attr->get()})) { #Look through all file hash entries
544                         if (${$attr->get($file)}{type} == 1 || #Hard link
545                                 (${$attr->get($file)}{mode} & S_HLINK_TARGET)) { #Hard link target
546                                 $hrdlnkflg = 1;
547                                 $File::Find::topdir =~ m|^$pc/([^/]+)/([0-9]+)/(.*)|;
548 #                               print relpath($File::Find::topdir) .
549 #                               print relpath($File::Find::name) .
550 #                                       ": Directory contains hard link: $file'. $hardaction...\n\n";
551                                 print "[$1][$2] $3: Directory contains hard link: " .
552                                         substr($File::Find::name, length($File::Find::topdir)) .
553                                     "/f$file ... $hardaction...\n\n";
554
555                                 last; #Stop readin attrib file...hard link found
556                         }
557                 }
558         }
559 }               
560
561 # Main routine for figuring out what files/dirs in @baks get deleted
562 # and/or copied/linked to @post along with which attrib entries are
563 # cleared or set to delete type in both the @baks and @post backupchains.
564 sub calculate_deletes
565 {
566         my ($hostname, $filepath, $backupshostHAref, $filedelsHref, $verbose) = @_;
567         my @ante = @{$$backupshostHAref->{ante}};
568         my @baks = @{$$backupshostHAref->{baks}};
569         my @post = @{$$backupshostHAref->{post}};
570         my %Level = %{$$backupshostHAref->{level}};
571         my %Vislvl = %{$$backupshostHAref->{vislvl}};
572         my $pchost = "$pc/$hostname";
573
574         #We first need to look down the direct antecedent chain in @ante
575         #to determine whether earlier versions of the file exist and if so
576         #at what level of incrementals will they be visible. A file in the
577         #@ante chain is potentially visible later in the @baks chain at
578         #the given level (or higher) if there is no intervening type=10
579         #(delete) attrib in the chain. If there is already a type=10
580         #attrib in the @ante chain then the file will be invisible in the
581         #@baks chain at the same level or higher of incrmental backups.
582
583         #Recall that the elements of @ante by construction have *strictly*
584         #increasing backup levels. So, that the visibility scope decreases
585         #as you go down the chain.
586
587     #We first iterate up the @ante chain and construct a hash
588         #(%VisibleLvl) that is either 1 or 0 depending on whether there is
589         #a file or type=10 delete attrib at that level. For any level at
590         #which there is no antecedent, the corresponding entry of
591         #%VisibleLvl remains undef
592
593         my %VisibleAnte;  # $VisibleAnte{$level} is equal to -1 if nothing visible from @Ante at the given level.
594                           # i.e. if either there was a type=delete at that level or if that level was blank but 
595                           # there was a type=delete at at a lower level without an intervening file.
596                           # Otherwise, it is set to the backup number of the file that was visible at that level.
597                           # This hash is used to determine where we need to add type=10 delete attributes to 
598                           # @baks to keep the files still present in @ante from poking through into the 
599                       # deleted @baks region.
600
601         my %VisibleAnteBaks;  # This hash is very similar but now we construct it all the way through @Baks to 
602                      # determine what was ORIGINALLY visible to the elements of @post since we may
603                      # need to copy/link files forward to @post if they have been deleted from @baks or
604                      # if they are now blocked by a new type=delete attribute in @baks.
605
606         $VisibleAnte{0} = $VisibleAnteBaks{0} = -1; #Starts as invisible until first file appears
607         $filepath =~ m|(.*)/|;
608         foreach my $prevbaknum (@ante) {        
609                 my $prevbakfile = "$pchost/$prevbaknum/$filepath";
610                 my $level = $Level{$prevbaknum};
611                 my $type = get_attrib_type($prevbakfile);
612                 my $nodir = ($type == -3 ? 1 : 0);      #Note type = -3 if dir non-existent
613                 printerr "Attribute file unreadable: $prevbaknum/$filepath\n" if $type == -4;
614
615                 #Determine what is visible to @Baks and to @Post
616                 if($type == BPC_FTYPE_DELETED || $nodir) {  #Not visible if deleted type or no parent dir
617                         $VisibleAnte{$level} = $VisibleAnteBaks{$level} = -1; #always update
618                         $VisibleAnteBaks{$level} = -1 
619                                 if defined($Vislvl{$level}) && $Vislvl{$level} == $prevbaknum; 
620                         #only update if this is the most recent backup at this level visible from @post
621                 }
622                 elsif (-r $prevbakfile) { #File exists so visible at this level
623                         $VisibleAnte{$level} = $prevbaknum; # always update because @ante is strictly increasing order
624                         $VisibleAnteBaks{$level} = $prevbaknum 
625                                 if defined($Vislvl{$level}) && $Vislvl{$level} == $prevbaknum;
626                                 #Only update if this will be visible from @post (may be blocked later by @baks)
627                 }
628
629 $DEBUG > 1 && print "    ANTE[$prevbaknum]($level) $hostname/$prevbaknum/$filepath [" . (-f $prevbakfile ? "f" : (-d $prevbakfile ? "d": ($nodir ? "x" : "-"))) . "][" . ($type >=0 ? $type : "-") . "]\n";
630         }
631
632     #Next, iterate down @baks to schedule file/dirs for deletion
633     #and/or for clearing/changing file attrib entry based on the
634     #status of the visibility flag at that level (or below) and the
635     #presence of $filepath in the backup.
636         #The status of what we do to the file and what we do to the attribute is stored in
637         #the hash ref %filedelsHref
638         my $minbaklevel = $baks[0];
639         foreach my $currbaknum (@baks) {
640                 my $currbakfile = "$pchost/$currbaknum/$filepath";
641                 my $level = $Level{$currbaknum};
642                 my $type = get_attrib_type($currbakfile); 
643                 my $nodir = ($type == -3 ? 1 : 0);      #Note type = -3 if dir non-existent
644                 printerr "Attribute file unreadable: $currbaknum/$filepath\n" if $type == -4;
645                 my $actionflag = "-"; my $printstring = "";#Used for debugging statements only 
646
647                 #Determine what is visible to @Post; also set file for deletion if present
648                 if($type == BPC_FTYPE_DELETED || $nodir) {  #Not visible if deleted type or no parent dir
649                         $VisibleAnteBaks{$level} = -1 
650                                 if defined $Vislvl{$level} && $Vislvl{$level} == $currbaknum;  #update if visible from @post
651                 }
652         elsif (-r $currbakfile ) {
653                         $VisibleAnteBaks{$level} = $currbaknum 
654                                 if defined($Vislvl{$level}) && $Vislvl{$level} == $currbaknum; #update if visible
655                         $$filedelsHref->{$currbaknum}{$filepath} |= FILE_DELETED;
656 $DEBUG > 2 && ($printstring .= "      [$currbaknum] Adding to delete list: $hostname/$currbaknum/$filepath\n");
657                 }
658
659                 #Determine whether deleted file attribs should be cleared or set to type 10=delete
660                 if(!$nodir && $level <= $minbaklevel && last_visible_backup($level, \%VisibleAnte) >= 0) {
661                         #Existing file in @ante will shine through since nothing in @baks is blocking
662                         #Note if $level > $minbaklevel then we will already be shielding it with a previous @baks element
663                         $minbaklevel = $level;
664                         if ($type != BPC_FTYPE_DELETED) { # Set delete type if not already of type delete
665                                 $$filedelsHref->{$currbaknum}{$filepath} |= ATTRIB_DELETETYPE;
666                                 $actionflag="D";
667 $DEBUG > 2 &&  ($printstring .=  "      [$currbaknum] Set attrib to type=delete: $hostname/$currbaknum/$filepath\n");
668                         }
669                 }
670                 elsif ($type >=0) { #No antecedent from @Ante will shine through since already blocked.
671                                         #So if there is an attribute type there, we should clear the attribute since
672                                         #nothing need be there
673                         $$filedelsHref->{$currbaknum}{$filepath} |= ATTRIB_CLEARED;
674                         $actionflag="C";
675 $DEBUG > 2 && ($printstring .= "      [$currbaknum] Clear attrib file entry: $hostname/$currbaknum/$filepath\n");
676                 }
677 $DEBUG > 1 && print "    BAKS[$currbaknum]($level) $hostname/$currbaknum/$filepath [" . (-f $currbakfile ? "F" : (-d $currbakfile ? "D": ($nodir ? "X" : "-"))) . "][" . ($type>=0 ? $type : "-") . "][$actionflag]\n";
678 $DEBUG >3 && print $printstring;
679         }
680
681 #Finally copy over files as necessary to make them appropriately visible to @post
682 #Recall again that successive elements of @post are strictly lower in level.
683 #Therefore, each element of @post either already has a file entry or it
684 #inherits its entry from the previously deleted backups.
685         foreach my $nextbaknum (@post) { 
686                 my $nextbakfile = "$pchost/$nextbaknum/$filepath";
687                 my $level = $Level{$nextbaknum};
688                 my $type = get_attrib_type($nextbakfile);
689                 my $nodir = ($type == -3 ? 1 : 0);      #Note type = -3 if dir non-existent
690                 printerr "Attribute file unreadable: $nextbaknum/$filepath\n" if $type == -4;
691                 my $actionflag = "-"; my $printstring = ""; #Used for debugging statements only 
692
693                 #If there is a previously visible file from @Ante or @Post that used to shine through (but won't now
694         #because either in @Ante and blocked by @Post deletion or deleted from @Post) and if nothing in @Post
695                 # is blocking (i.e directory exists, no file there, and no delete type), then we need to copy/link
696                 #the file forward
697                 if ((my $delnum = last_visible_backup($level, \%VisibleAnteBaks)) >= 0 &&
698                         $type != BPC_FTYPE_DELETED && !$nodir &&  !(-r $nextbakfile)) {
699                         #First mark that last visible source file in @Ante or @Post gets copied
700                         $$filedelsHref->{$delnum}{$filepath} |= FILE_ATTRIB_COPY;
701             #Note still keep the FILE_DELETED attrib because we may still need to delete the source 
702             #after moving if the source was in @baks
703                         #Second tell the target where it gets its source
704                         $$filedelsHref->{$nextbaknum}{$filepath} = ($delnum+1) << 6; #
705                         #Store the source in higher bit numbers to avoid overlapping with our flags. Add 1 so as to
706                         #be able to distinguish empty (non stored) path from backup #0.
707 $DEBUG > 2 && ($printstring .= "      [$nextbaknum] Moving file and attrib from backup $delnum: $filepath\n");
708                         $actionflag = "M$delnum";
709                 }
710                 elsif ($type == BPC_FTYPE_DELETED) {
711                         # File has a delete attrib that is now no longer necessary since
712                         # every element of @post by construction has a deleted immediate predecessor in @baks
713                         $$filedelsHref->{$nextbaknum}{$filepath} |= ATTRIB_CLEARED;
714 $DEBUG > 2 && ($printstring .= "      [$nextbaknum] Clear attrib file entry:  $hostname/$nextbaknum/$filepath\n");
715                         $actionflag = "C";
716                 }
717 $DEBUG >1 && print "    POST[$nextbaknum]($level) $hostname/$nextbaknum/$filepath [" . (-f $nextbakfile ? "f" : (-d $nextbakfile ? "d": ($nodir ? "x" : "-"))) . "][" . ($type >= 0 ? $type : "-") . "][$actionflag]\n";
718 $DEBUG >3 && print $printstring;
719         }
720 }
721
722 sub execute_deletes
723 {
724         my ($hostname, $backupshostHAref, $filedelsHref, $verbose) = @_;
725         my @ante = @{$$backupshostHAref->{ante}};
726         my @baks = @{$$backupshostHAref->{baks}};
727         my @post = @{$$backupshostHAref->{post}};
728         my %Level = %{$$backupshostHAref->{level}};
729
730         my $pchost = "$pc/$hostname";
731         foreach my $backnum (@ante, @baks, @post) {
732         #Note the only @ante action is copying over files
733         #Note the only @post action is clearing the file attribute
734                 print "**BACKUP: [$hostname][$backnum]($Level{$backnum})\n";
735                 my $prevdir=0;
736                 my ($attr, $dir, $file);
737                 foreach my $filepath (sort keys %{$$filedelsHref->{$backnum}}) {
738                         my $VERBOSE = ($verbose ? "" : "[$hostname][$backnum] $filepath:");
739                         my $delfilelist;
740                         my $filestring = my $attribstring = '-';
741                         my $movestring = my $printstring = '';
742                         $filepath =~ m|(.*)/f(.*)|;
743                         $dir = "$pchost/$backnum/$1";
744                         my $dirstem = $1;
745                         $file = $2;
746                         if($dir ne $prevdir) { #New directory - we only need to read/write the atrrib file once per dir
747                                 write_attrib_out($bpc, $attr, $prevdir, $verbose)
748                                         if $prevdir; #Write out previous $attr
749                                 die "Error: can't write attribute file to directory: $dir" unless -w $dir;
750                                 read_attrib($attr, $dir); #Read in new attribute
751                                 $prevdir = $dir;
752                         }
753
754                         my $action = $$filedelsHref->{$backnum}{$filepath};
755                         if($action & FILE_ATTRIB_COPY) {
756                                 my %sourceattr;
757                                 get_file_attrib("$pchost/$backnum/$filepath", \%sourceattr);
758                                 my $checkpoollinks = 1; #Don't just blindly copy or link - make sure linked to pool
759                                 foreach my $nextbaknum (@post) {
760                                         my ($ret1, $ret2);
761                                         next unless (defined($$filedelsHref->{$nextbaknum}{$filepath}) &&
762                                                                  ($$filedelsHref->{$nextbaknum}{$filepath} >> 6) - 1 == $backnum);
763                                                 #Note: >>6 followed by decrement of 1 recovers the backup number encoding
764                                                 #Note: don't delete or clear/delete source attrib now because we may need to move
765                                                 #several copies - so file deletion and attribute clear/delete is done after moving
766
767                                         
768                                         if(($ret1=link_recursively_topool ($bpc, "$pchost/$backnum/$filepath", 
769                                                                                                           "$pchost/$nextbaknum/$filepath",
770                                                                                                            $checkpoollinks, 1)) >= 0
771                                            && ($ret2=write_file_attrib($bpc, "$pchost/$nextbaknum/$dirstem", $file, \%sourceattr, 1)) > 0){
772                                                 #First move files by linking them to pool recursively and then copy attributes
773                                                 $checkpoollinks = 0 if $ret1 > 0; #No need to check pool links next time if all ok now
774                                                 $movestring .= "," unless $movestring eq '';
775                                                 $movestring .= "$backnum" . ($ret1 == 1 ? "->" :  "=>") . "$nextbaknum\n";
776                                                 $filescopied++;
777                                         }
778                                         else {
779                                                 $action = 0; #If error moving, cancel the subsequent file and attrib deletion/clearing
780                                                 junlink("$pchost/$nextbaknum/$filepath"); #undo partial move
781                                                 if($ret1 <0) {
782                                                         $printstring .= "$VERBOSE      FAILED TO MOVE FILE/DIR: $backnum-->$nextbaknum -- UNDOING PARTIAL MOVE\n";
783                                                 }
784                                                 else {
785                                                         $printstring .= "$VERBOSE      FAILED TO WRITE NEW ATTRIB FILE IN $nextbaknum AFTER MOVING FILE/DIR: $backnum-->$nextbaknum FROM $backnum -- UNDOING MOVE\n";
786                                                 }
787                                                 next; # Skip to next move
788                                         }
789                                 }
790                         }
791                         if ($action & FILE_DELETED) { #Note delete follows moving
792                                 my $isdir = (-d "$pchost/$backnum/$filepath" ? 1 : 0);
793                                 my $numdeletes = delete_files("$pchost/$backnum/$filepath", \$delfilelist);
794                                 if($numdeletes > 0) {
795                                         $filestring = ($isdir ? "D$numdeletes" : "F" );
796                                         $filesdeleted++;
797                                         $totfilesdeleted +=$numdeletes;
798                                         if($delfilelist) {
799                                                 $delfilelist =~ s!(\n|^)(unlink|rmdir ) *$pchost/$backnum/$filepath(\n|$)!!g; #Remove top directory
800                                                 $delfilelist =~ s!^(unlink|rmdir ) *$pc/!       !gm; #Remove unlink/rmdir prefix
801                                         }
802                                 }
803                                 else {
804                                         $printstring .= "$VERBOSE      FILE FAILED TO DELETE ($numdeletes)\n";
805                                 }
806                         }
807                         if ($action & ATTRIB_CLEARED) { #And attrib changing follows file moving & deletion...
808                                 $attr->delete($file);
809                                 $attribstring = "C";
810                                 $attrscleared++;
811
812                         }
813                         elsif($action & ATTRIB_DELETETYPE) {
814                                  if (defined($attr->get($file)) && ${$attr->get($file)}{type} == BPC_FTYPE_DELETED) {
815                                          $attribstring = "d";
816                                  }
817                                  else {
818                                          $attr->set($file, $DeleteAttribH);  # Set file to deleted type (10)
819                                          $attribstring = "D";
820                                          $attrsdeleted++;
821                                  }
822                         }
823                         print "    [$hostname][$backnum]$filepath [$filestring][$attribstring] $movestring$DRYRUN\n" 
824                                 if $verbose && ($filestring ne '-' || $attribstring ne '-' || $movestring ne '');
825                         print $delfilelist . "\n" if $verbose && $delfilelist;
826                         print $printstring;
827                 }
828                 write_attrib_out($bpc, $attr, $dir, $verbose)
829                         if $prevdir; #Write out last attribute
830         }
831 }
832
833 sub write_attrib_out 
834 {
835         my ($bpc, $attr, $dir, $verbose) = @_;
836         my $ret;
837         my $numattribs = count_file_attribs($attr);
838         die "Error writing to attrib file for $dir\n" 
839                 unless ($ret =write_attrib ($bpc, $attr, $dir, 1, 1)) > 0;
840         $dir =~ m|^$pc/([^/]*)/([^/]*)/(.*)|;
841         $atfilesdeleted++ if $ret==4;
842         print "    [$1][$2]$3/attrib [-]" . 
843                 ($ret==4 ? "[R]" : ($ret==3 ? "[w]" : ($ret > 0 ? "[W]" : "[X]")))
844                  ."$DRYRUN\n" if $verbose;
845         return $ret;
846 }
847
848 #If earlier file is visible at this level, return the backup number where a file was last present
849 #Otherwise return -1 (occurs if there was an intervening type=10 or if a file never existed)
850 sub last_visible_backup
851 {
852         my ($numlvl, $Visiblebackref) = @_;
853         my $lvl = --$numlvl; #For visibility look at one less than current level and lower
854
855         return -1 unless $lvl >= 0;
856         do {
857                 return ($Visiblebackref->{$numlvl} = $Visiblebackref->{$lvl}) #Set & return
858                         if defined($Visiblebackref->{$lvl});
859         } while($lvl--);
860         return -1;  #This shouldn't happen since we initialize $Visiblebackref->{0} = -1;
861 }
862
863 # Get the modified type from the attrib file.
864 # Which I define as:
865 #    type + (type == BPC_FTYPE_HARDLINK => 1; ? S_HLINK_TARGET : (mode & S_HLINK_TARGET) )
866 # i.e. you get both the type and whether it is either an hlink 
867 # or an hlink-target
868 sub get_jtype
869 {
870         my ($fullfilename) = @_;
871         my %fileattrib;
872
873         return 100 if  get_file_attrib($fullfilename, \%fileattrib) <= 0;
874         my $type = $fileattrib{type};
875         my $mode = $fileattrib{mode};
876         $type + ($type == BPC_FTYPE_HARDLINK ? 
877                          S_HLINK_TARGET : ($mode & S_HLINK_TARGET));
878 }
879
880 #Set elements of the hash backupsHoHA which is a mixed HoHA and HoHoH
881 #containing backup structure for each host in hostregex_sh
882
883 # Elements are:
884 #   backupsHoHA{$host}{baks} - chain (array) of consecutive backups numbers
885 #         whose selected files we will be deleting
886 #   backupsHoHA{$host}{ante} - chain (array) of backups directly antecedent
887 #         to those in 'baks' - these are all "parents" of all elemenst 
888 #         of 'baks' [in descending numerical order and strictly descending
889 #         increment order]
890 #   backupsHoHA{$host}{post} - chain (array) of backups that are incremental
891 #         backups of elements of 'baks' - these must all be "children" of 
892 #         all element of 'baks' [in ascending numerical order and strictly
893 #         descending increment order]
894 #   backupsHoHA{$host}{level}{$n}  - level of backup $n
895 #   backupsHoHA{$host}{vislvl}{$level}  - highest (most recent) backup number in (@ante, @baks) with $level
896 #         Note: this determines which backups from (@ante, @baks) are potentially visible from @post
897
898 sub get_allhostbackups
899 {
900         my ($hostregx_sh, $numregx, $backupsHoHAref) = @_;
901
902
903         die "$0: bad host name '$hostregx_sh'\n"
904                 if ( $hostregx_sh !~ m|^([-\w\.\s*]+)$| || $hostregx_sh =~ m{(^|/)\.\.(/|$)} );
905         $hostregx_sh = "*" if ($hostregx_sh eq '-'); # For shell globbing
906
907         die "$0: bad backup number range '$numopt'\n" 
908                 if ( $numregx !~ m!^((\d*)|{(\d+)}|\[(\d+)\])-((\d*)|{(\d+)}|\[(\d+)\])$|(\d+)$! );
909
910         my $startnum=0;
911         my $endnum = 99999999;
912         if(defined $2 && $2 ne '') {$startnum = $2;}
913         elsif(defined $9) {$startnum = $endnum = $9;}
914         if(defined $6 && $6 ne ''){$endnum=$6};
915         die "$0: bad dump range '$numopt'\n"
916                 if ( $startnum < 0 || $startnum > $endnum);
917         my $startoffsetbeg = $3;
918         my $endoffsetbeg = $7;
919         my $startoffsetend = $4;
920         my $endoffsetend = $8;
921
922         my @allbaks = bsd_glob("$pc/$hostregx_sh/[0-9]*/backupInfo");
923        #Glob for list of valid backup paths
924         for (@allbaks) { #Convert glob to hash of backups and levels
925                 m|.*/(.*)/([0-9]+)/backupInfo$|; # $1=host $2=baknum
926                 my $level = get_bakinfo("$pc/$1/$2", "level");
927                 $backupsHoHAref->{$1}{level}{$2} = $level 
928                         if defined($level) && $level >=0; # Include if backup level defined
929         }
930
931         foreach my $hostname (keys %{$backupsHoHAref}) { #Loop through each host
932                 #Note: need to initialize the following before we assign reference shortcuts
933                 #Note {level} already defined
934                 @{$backupsHoHAref->{$hostname}{ante}} = ();
935                 @{$backupsHoHAref->{$hostname}{baks}} = ();
936                 @{$backupsHoHAref->{$hostname}{post}} = ();
937                 %{$backupsHoHAref->{$hostname}{vislvl}} = ();
938
939                 #These are all references
940                 my $anteA= $backupsHoHAref->{$hostname}{ante};
941                 my $baksA= $backupsHoHAref->{$hostname}{baks};
942                 my $postA= $backupsHoHAref->{$hostname}{post};
943                 my $levelH= $backupsHoHAref->{$hostname}{level};
944                 my $vislvlH= $backupsHoHAref->{$hostname}{vislvl};
945
946                 my @baklist =  (sort {$a <=> $b} keys %{$levelH}); #Sorted list of backups for current host
947                 $startnum = $baklist[$startoffsetbeg-1] || 99999999 if defined $startoffsetbeg;
948                 $endnum = $baklist[$endoffsetbeg-1] || 99999999 if defined $endoffsetbeg;
949                 $startnum = $baklist[$#baklist - $startoffsetend +1] || 0 if defined $startoffsetend;
950                 $endnum = $baklist[$#baklist - $endoffsetend +1] || 0 if defined $endoffsetend;
951
952                 my $minbaklevel = my $minvislevel = 99999999;
953                 my @before = my @after = ();
954                 #NOTE: following written for clarity, not speed
955                 foreach my $baknum (reverse @baklist) { #Look backwards through list of backups
956         #Loop through reverse sorted list of backups for current host
957                         my $level = $$levelH{$baknum};
958                         if($baknum <= $endnum) {
959                                 $$vislvlH{$level} = $baknum if $level < $minvislevel;
960                                 $minvislevel = $level if $level < $minvislevel;
961                         }
962                         if($baknum >= $startnum && $baknum <= $endnum) {
963                                 unshift(@{$baksA}, $baknum); #sorted in increasing order
964                                 $minbaklevel = $level if $level < $minbaklevel;
965                         }
966                         push (@before, $baknum) if $baknum < $startnum; #sorted in decreasing order
967                         unshift(@after, $baknum) if $baknum > $endnum; #sorted in increasing order
968                 }
969                 next unless defined @{$baksA}; # Nothing to backup on this host
970
971                 my $oldlevel = $$levelH{$$baksA[0]}; # i.e. level of first backup in baksA
972                 for (@before) { 
973                         #Find all direct antecedents until the preceding level 0 and push on anteA
974                         if ($$levelH{$_} < $oldlevel) { 
975                                 unshift(@{$anteA}, $_); #Antecedents are in increasing order with strictly increasing level
976                                 last if $$levelH{$_} == 0;
977                                 $oldlevel = $$levelH{$_};
978                         }
979                 }
980                 $oldlevel = 99999999;
981                 for (@after) {
982                         # Find all successors that are immediate children of elements of @baks
983                         if ($$levelH{$_} <= $oldlevel) { # Can have multiple descendants at the same level
984                                 last if $$levelH{$_} <= $minbaklevel; #Not a successor because dips below minimum
985                                 push(@{$postA}, $_); #Descendants are increasing order with non-increasing level
986                                 $oldlevel = $$levelH{$_};
987                         }
988                 }
989         }
990 }
991
992 # Print the @Baks list along with the level of each backup in parentheses
993 sub print_backup_list
994 {
995         my ($backupsHoHAref) = @_;      
996
997         foreach my $hostname (sort keys %{$backupsHoHAref}) { #Loop through each host
998                 print "$hostname: ";
999                 foreach my $baknum (@{$backupsHoHAref->{$hostname}{baks}}) {
1000                         print "$baknum($backupsHoHAref->{$hostname}{level}{$baknum}) ";
1001                 }
1002                 print "\n";
1003         }
1004 }
1005
1006 #Read in external file and return list of lines of file
1007 sub read_file
1008 {
1009         my ($file) = @_;
1010         my $fh;
1011         my @lines;
1012
1013         if($file eq '-') {
1014                 $fh = *STDIN;
1015         }
1016         else {
1017                 die "ERROR: Can't open: $file\n" unless open($fh, "<", $file);
1018         }
1019         while(<$fh>) {
1020                 chomp;
1021                 next if m|^\s*$| || m|^#|;
1022                 push(@lines, $_);
1023         }
1024         close $fh if $file eq '-';
1025         return @lines;
1026
1027 }
1028                 
1029                 
1030 # Strip off the leading $TopDir/pc portion of path
1031 sub relpath
1032 {
1033         substr($_[0],1+length($pc));
1034 }
1035
1036
1037 sub min
1038 {
1039         $_[0] < $_[1] ? $_[0] : $_[1];
1040 }
1041