Commit | Line | Data |
---|---|---|
cf225af2 P |
1 | #!/usr/bin/perl |
2 | #============================================================= -*-perl-*- | |
3 | # | |
0391a023 P |
4 | # BackupPC_deleteFile: Delete one or more files/directories from |
5 | # a range of hosts, backups, and shares | |
cf225af2 P |
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 | ||
0391a023 | 271 | use lib "/usr/share/backuppc/lib"; |
cf225af2 P |
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 |