File Coverage

blib/lib/Backup/Snapback.pm
Criterion Covered Total %
statement 303 546 55.4
branch 85 296 28.7
condition 27 94 28.7
subroutine 29 34 85.2
pod 12 22 54.5
total 456 992 45.9


line stmt bran cond sub pod time code
1             #!/usr/local/bin/perl
2              
3             # Backup::Snapback - routines for Snapback2 rsync backup system
4             #
5             # $Id: Snapback.pm,v 1.5 2006/08/23 14:58:10 mike Exp $
6             #
7             # Copyright (C) 2004 Mike Heins, Perusion
8             # Copyright (C) 2002 Art Mulder
9             # Copyright (C) 2002-2003 Mike Rubel
10             #
11             # This program was originally based on Mike Rubel's rsync snapshot
12             # research and Art Mulder's snapback perl script
13             #
14             # This program is free software; you can redistribute it and/or modify
15             # it under the terms of the GNU General Public License as published by
16             # the Free Software Foundation; either version 2 of the License, or
17             # (at your option) any later version.
18             #
19             # This program is distributed in the hope that it will be useful,
20             # but WITHOUT ANY WARRANTY; without even the implied warranty of
21             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22             # GNU General Public License for more details.
23             #
24             # You should have received a copy of the GNU General Public
25             # License along with this program; if not, write to the Free
26             # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
27             # MA 02111-1307 USA.
28              
29             package Backup::Snapback;
30 1     1   122276 use Sys::Hostname;
  1         1367  
  1         49  
31 1     1   6 use File::Path;
  1         2  
  1         52  
32 1     1   16526 use File::Temp;
  1         28317  
  1         95  
33 1     1   8 use Config::ApacheFormat;
  1         2  
  1         21  
34 1     1   5 use Symbol;
  1         2  
  1         58  
35 1     1   1304 use Data::Dumper;
  1         9311  
  1         82  
36             $Data::Dumper::Terse = 1;
37 1     1   8 use Carp;
  1         2  
  1         62  
38 1     1   1109 use POSIX qw/strftime/;
  1         8491  
  1         10  
39 1     1   1409 use strict;
  1         3  
  1         50  
40              
41 1     1   7 use vars qw/$VERSION $ERROR $errstr %Defaults/;
  1         3  
  1         90  
42 1     1   5 no warnings qw/ uninitialized /;
  1         2  
  1         10382  
43              
44             $VERSION = '1.001';
45              
46             =head1 NAME
47              
48             Backup::Snapback - routines for support of rsync-based snapshot backup
49              
50             =head1 SYNOPSIS
51              
52             use Backup::Snapback;
53             my $backup = new Backup::Snapback %opts;
54              
55             =head1 DESCRIPTION
56              
57             Snapback2 does backup of systems via ssh and rsync. It creates rolling "snapshots"
58             based on hourly, daily, weekly, and monthly rotations. When it runs for
59             some period of time, you will end up with a target backup directory
60             that looks like:
61              
62             drwx--x--x 81 106 staff 4096 Jan 1 05:54 daily.0
63             drwx--x--x 81 106 staff 4096 Dec 31 05:55 daily.1
64             drwx--x--x 81 106 staff 4096 Dec 30 05:55 daily.2
65             drwx--x--x 81 106 staff 4096 Dec 29 05:54 daily.3
66             drwx--x--x 81 106 staff 4096 Dec 28 05:53 daily.4
67             drwx--x--x 81 106 staff 4096 Dec 27 05:53 daily.5
68             drwx--x--x 81 106 staff 4096 Dec 26 05:53 daily.5
69             drwx--x--x 81 106 staff 4096 Jan 1 05:54 hourly.0
70             drwx--x--x 81 106 staff 4096 Dec 31 17:23 hourly.1
71             drwx--x--x 81 106 staff 4096 Jan 1 05:54 monthly.0
72             drwx--x--x 81 106 staff 4096 Dec 1 05:54 monthly.1
73             drwx--x--x 81 106 staff 4096 Dec 28 05:53 weekly.0
74             drwx--x--x 81 106 staff 4096 Dec 21 05:53 weekly.1
75             drwx--x--x 81 106 staff 4096 Dec 14 05:53 weekly.2
76             drwx--x--x 81 106 staff 4096 Dec 7 05:53 weekly.3
77              
78             You might think this would take up lots of space. However, snapback2
79             hard-links the files to create the images. If the file doesn't change,
80             only a link is necessary, taking very little space. It is possible to
81             create a complete yearly backup in just over 2x the actual
82             storage space consumed by the image.
83              
84             =head1 METHODS
85              
86             The Backup::Snapback module is designed to be front-ended by a script
87             such as the included C. Its methods are:
88              
89             =over 4
90              
91             =cut
92              
93             my %Locale;
94              
95             %Defaults = (
96             AlwaysEmail => 'No',
97             ChargeFile => $> == 0 ? '/var/log/snapback.charges' : "$ENV{HOME}/.snapback/snapback.charges",
98             Compress => 1,
99             cp => "/bin/cp",
100             CreateDir => 'Yes',
101             DailyDir => 'daily',
102             HourlyDir => 'hourly',
103             logfile => $> == 0 ? '/var/log/snapback' : "$ENV{HOME}/.snapback/snapback.log",
104             MonthlyDir => 'monthly',
105             MustExceed => '5 minutes',
106             mv => "/bin/mv",
107             Myhost => hostname(),
108             RsyncShell => 'ssh',
109             IgnoreVanished => 'No',
110             Rsync => 'rsync',
111             RsyncVerbose => 0,
112             RetainPermissions => 1,
113             rm => "/bin/rm",
114             RsyncOpts => "-a --force --delete-excluded --one-file-system --delete",
115             sendmail => "/usr/sbin/sendmail",
116             WeeklyDir => 'weekly',
117             );
118              
119             my %None = qw(
120             Logfile 1
121             ChargeFile 1
122             AdminEmail 1
123             DestinationList 1
124             PingCommand 1
125             );
126              
127             my %Boolean = qw(
128             RsyncStats 1
129             RsyncVerbose 1
130             AlwaysEmail 1
131             AutoTime 1
132             IgnoreVanished 1
133             Compress 1
134             CreateDir 1
135             LiteralDirectory 1
136             ManyFiles 1
137             RetainPermissions 1
138             );
139              
140             my @reset_backup = qw/
141             _directory
142             _directories
143             _client_config
144             _client_cfg
145             /;
146              
147             for(grep /[A-Z]/, keys %Defaults) {
148             $Defaults{lc $_} = $Defaults{$_};
149             }
150              
151             for(grep /[A-Z]/, keys %Boolean) {
152             $Boolean{lc $_} = $Boolean{$_};
153             }
154              
155             for(grep /[A-Z]/, keys %None) {
156             $None{lc $_} = $None{$_};
157             }
158              
159             ## Where log entries go
160             my @log;
161              
162             my @config_tries = qw(
163             /etc/snapback2.conf
164             /etc/snapback/snapback2.conf
165             /etc/snapback.conf
166             /etc/snapback/snapback.conf
167             );
168              
169             if($> != 0) {
170             unshift @config_tries, "$ENV{HOME}/.snapback/snapback.conf";
171             }
172              
173             =item new
174              
175             Constructs a new Backup::Snapback object. Accepts any Snapback config
176             file option, plus the special option C, which supplies the
177             configuration file to read. If the passed C is not set,
178             the standard locations are scanned.
179              
180             Standard locations are C<$HOME/.snapback/snapback.conf> if not executing
181             as root, otherwise always in order:
182              
183             /etc/snapback2.conf
184             /etc/snapback/snapback2.conf
185             /etc/snapback.conf
186             /etc/snapback/snapback.conf
187              
188             Returns the snapback object. If the constructor fails, C will be
189             returned and the error will be available as C<$Backup::Snapback::errstr>.
190              
191             Called as usual for a perl object:
192              
193             ## classic constructor
194             my $snap = new Backup::Snapback configfile => '/tmp/snap.conf';
195              
196             ## standard constructor
197             my $snap = Backup::Snapback->new( ChargeFile => '/tmp/snap.charges') ;
198              
199             =cut
200              
201             sub new {
202 1     1 1 8590 my $class = shift;
203 1         10 my %opt;
204 1 50       17 if(ref $_[0] eq 'HASH') {
205 0         0 %opt = %{shift(@_)};
  0         0  
206             }
207             else {
208 1         11 %opt = @_;
209             }
210              
211 1         5 my $configfile = delete $opt{configfile};
212 1 50       12 if(! $configfile) {
213 0         0 for(@config_tries) {
214 0 0       0 next unless -e $_;
215 0         0 $configfile = $_;
216 0         0 last;
217             }
218             }
219              
220 1         111 my $maincfg = new Config::ApacheFormat
221             duplicate_directives => 'combine',
222             root_directive => 'SnapbackRoot',
223             ;
224              
225 1         1366 $maincfg->read($configfile);
226              
227             #print "maincfg=$maincfg\n";
228 1         5022 my $self = bless {
229             _maincfg => $maincfg,
230             _config => {},
231             _log => [],
232             };
233              
234 1         9 $self->{_cfg} = $self->{_maincfg};
235              
236 1         5 for(keys %opt) {
237 0         0 $self->config($_, $opt{$_});
238             }
239              
240 1 50       16 if($self->config(-debug)) {
241 0   0     0 my $debuglog = $self->config(-debuglog)
242             || $self->config(-debugfile) ### deprecated, remove in 2011
243             ;
244 0         0 my $debugtag = $self->config(-debugtag);
245 0 0       0 $self->{debugtag} = $debugtag ? "$debugtag: " : '';
246            
247 0         0 my $sym = gensym();
248 0 0       0 if($debuglog) {
249 0 0       0 open $sym, ">> $debuglog"
250             or die "Can't append debug log $debuglog: $!\n";
251             }
252             else {
253 0         0 open $sym, ">&STDERR";
254             }
255 0         0 $self->{_debug} = $sym;
256             }
257              
258 1         5 return bless $self, $class;
259             }
260              
261             sub DESTROY {
262 1     1   1067 my $self = shift;
263 1         7 my $ary = $self->{_tmpfiles};
264 1 50       685 unlink @$ary if $ary;
265             }
266              
267             sub time_to_seconds {
268 0     0 0 0 my($str) = @_;
269 0         0 my($n, $dur);
270              
271 0         0 ($n, $dur) = ($str =~ m/(\d+)[\s\0]*(\w+)?/);
272 0 0       0 return undef unless defined $n;
273 0 0       0 if (defined $dur) {
274 0         0 local($_) = $dur;
275 0 0       0 if (m/^s|sec|secs|second|seconds$/i) {
    0          
    0          
    0          
    0          
276             }
277             elsif (m/^m|min|mins|minute|minutes$/i) {
278 0         0 $n *= 60;
279             }
280             elsif (m/^h|hour|hours$/i) {
281 0         0 $n *= 60 * 60;
282             }
283             elsif (m/^d|day|days$/i) {
284 0         0 $n *= 24 * 60 * 60;
285             }
286             elsif (m/^w|week|weeks$/i) {
287 0         0 $n *= 7 * 24 * 60 * 60;
288             }
289             else {
290 0         0 return undef;
291             }
292             }
293              
294 0         0 $n;
295             }
296              
297             # =item error
298             #
299             # Sets the last error, with sprintf if more than one param. An internal method.
300             #
301             # $self->error('It failed! Problem was %s', $problem);
302             #
303             # or as a class method:
304             #
305             # Backup::Snapback::error('It failed! Problem was %s', $problem);
306             #
307             # Returns the formatted error.
308             #
309             # =cut
310              
311             sub error {
312 0     0 0 0 my $self = shift;
313 0         0 my ($msg, @args);
314 0 0       0 if(ref $self) {
315 0         0 ($msg, @args) = @_;
316             }
317             else {
318 0         0 ($msg, @args) = ($self, @_);
319 0         0 undef $self;
320             }
321              
322 0 0       0 $msg = sprintf($msg, @args) if @args;
323              
324 0         0 $ERROR = $errstr = $msg;
325 0 0       0 if($self) {
326 0         0 $self->{_errstr} = $msg;
327             }
328 0         0 return $msg;
329             }
330              
331             =item errstr
332              
333             Called as either an object method:
334              
335             $self->errstr;
336              
337             or as a class method:
338              
339             Backup::Snapback::errstr;
340              
341             Returns the most recent error text.
342              
343             =cut
344              
345             sub errstr {
346 0     0 1 0 my $self = shift;
347 0 0       0 $self and return $self->{_errstr};
348 0         0 return $errstr;
349             }
350              
351             ## Internal
352             sub is_yes {
353 37     37 0 54 my $val = shift;
354 37         88 $val = lc $val;
355 37         74 $val =~ s/\W+//g;
356 37         417 my %true = qw(
357             y 1
358             yes 1
359             on 1
360             true 1
361             1 1
362             );
363 37   100     179 $val = $true{$val} || 0;
364 37         226 return $val;
365             }
366              
367             =item config
368              
369             Gets or sets configuration parameters. The base is set in hardcoded
370             program defaults; it then is overlayed with the configuration file results.
371             If a configuration block is entered, those settings override the parent
372             configuration block. Finally, internal setting can be done, temporarily
373             overriding configuration file settings (because of option dependencies).
374              
375             my $compress = $snap->config(-Compress);
376              
377             # turn off compression
378             $snap->config( Compress => No);
379              
380             Some options are boolean, and some accept the special value 'none' to
381             set them empty.
382              
383             Parameter names are not case-sensitive.
384              
385             =cut
386              
387             sub config {
388 151     151 1 239 my $self = shift;
389 151         461 my $parm = shift;
390 151         183 my $value = shift;
391              
392 151         221 $parm = lc $parm;
393 151         619 $parm =~ s/^-//;
394              
395 151   33     879 my $sc = $self->{_client_config} || $self->{_config};
396 151   33     463 my $cfg = $self->{_cfg} || $self->{_maincfg};
397              
398 151 100       311 if(defined $value) {
399 1         4 $sc->{$parm} = $value;
400 1         2 return $value;
401             }
402              
403 150         187 my @vals;
404              
405 150 100       302 if(defined $sc->{$parm}) {
406 3 50       27 if(ref $sc->{$parm} eq 'ARRAY') {
407 0         0 @vals = @{$sc->{parm}};
  0         0  
408             }
409             else {
410 3         10 @vals = $sc->{$parm};
411             }
412             }
413             else {
414 147         559 @vals = $cfg->get($parm);
415             }
416              
417 150         3568 my $num = scalar(@vals);
418 150         186 my $val;
419              
420 150 100       438 if($num == 0) {
    100          
    50          
421 111         310 $val = $Defaults{$parm};
422             }
423             elsif(@vals == 1) {
424 37         74 $val = $vals[0];
425             }
426             elsif(wantarray) {
427 2         21 return @vals;
428             }
429             else {
430 0         0 $val = \@vals;
431             }
432              
433 148 100 66     839 if($Boolean{$parm}) {
    50          
434 37         101 $val = is_yes($val);
435             }
436             elsif($None{$parm} and lc($val) eq 'none') {
437 0         0 $val = '';
438             }
439 148         682 return $val;
440             }
441              
442             sub build_rsync_opts {
443 4     4 0 8 my $self = shift;
444 4         7 my @opts;
445 4         50 my $main_opts = $self->config(-RsyncOpts);
446              
447             # If user supplies their own -RsyncOpts config returns and array
448             # that needs to be turned into a scalar
449             # -- patch from Jay Strauss
450 4 50       282 if (ref $main_opts eq 'ARRAY') {
451 0         0 $main_opts = join " ", @$main_opts;
452             }
453              
454 4         18 push @opts, $main_opts;
455              
456 4         20 my $rsync_sh = $self->config(-RsyncShell);
457 4         45 $self->log_debug("rsync shell=$rsync_sh");
458 4         13 $rsync_sh =~ s/'/\\'/g;
459              
460 4 50 33     40 if($rsync_sh and lc($rsync_sh) ne 'none' and lc($rsync_sh) ne 'rsync' ) {
      33        
461 0         0 unshift @opts, "-e '$rsync_sh'";
462             }
463              
464 4 50 33     18 if($self->config(-chargefile) and ! $self->config(-RsyncVerbose)) {
465 4 50       24 push @opts, '--stats' unless $main_opts =~ /--stats\b/;
466             }
467              
468 4         13 my $compress = $self->config(-Compress);
469 4         26 $self->log_debug("compress=$compress");
470 4 50       11 unshift @opts, "-z" if $compress;
471              
472 4         28 my $verbose = $self->config(-RsyncVerbose);
473 4         25 $self->log_debug("rsync verbose=$verbose");
474 4 50       12 unshift @opts, "-v" if $verbose;
475              
476 4         14 my $opts = join " ", @opts;
477 4         11 $self->log_debug("build_rsync_opts: $opts");
478 4         9 return $opts;
479             }
480              
481             sub output_timestamp {
482 12     12 0 25 my $self = shift;
483 12         22 my $fh = shift;
484              
485             # retrieve and print the current time stamp to the log file
486 12         346 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
487 12         274 printf $fh "%4d-%02d-%02d %02d:%02d:%02d ",
488             $year+1900,$mon+1,$mday,$hour,$min,$sec;
489             }
490              
491             #---------- ---------- ---------- ---------- ---------- ----------
492             # Set up logging
493              
494             sub log_arbitrary {
495 12     12 0 83 my ($self, $file, $msg) = @_;
496 12 50       32 return unless $file;
497 12   100     63 my $fha = $self->{_fd} ||= {};
498 12 100       51 if(! $fha->{$file}) {
499 2         32 my $sym = gensym();
500 2 50       249 open $sym, ">> $file"
501             or croak("log_arbitrary: cannot log to file $file: $!\n");
502 2         15 $fha->{$file} = $sym;
503             }
504 12         38 my $fh = $fha->{$file};
505 12         74 $self->output_timestamp($fh);
506 12         46 print $fh $msg;
507             }
508              
509             =item log_error
510              
511             Logs an error message to the configured log file. If no log file is
512             specified (default is /var/log/snapback or $HOME/.snapback/snapback.log
513             depending on user ID), then no error is logged.
514              
515             Formats messages with sprintf() if appropriate.
516              
517             $snap->log_error("Backup failed for client: %s.", $client);
518              
519             =cut
520              
521             sub log_error {
522 0     0 1 0 my ($self, $msg, @args) = @_;
523              
524 0         0 my $long = length($msg) > 400;
525 0 0       0 $msg = sprintf($msg, @args) if @args;
526 0 0       0 $msg =~ s/[\r\n]*$/\n/ unless $long;
527              
528 0         0 $self->{_errors}++;
529 0         0 push @{$self->{_log}}, $msg;
  0         0  
530              
531 0 0       0 my $logfile = $self->config(-logfile)
532             or return $msg;
533 0         0 $self->log_arbitrary($logfile, $msg);
534 0         0 return $msg;
535             }
536              
537             =item file_handle
538              
539             Returns the file handle of a file already opened with log_arbitrary
540             or log_error. To open a new file, do $self->log_arbitrary($file);
541              
542             =cut
543              
544             sub file_handle {
545 4     4 1 5 my ($self, $file) = @_;
546 4         14 return $self->{_fd}{$file};
547             }
548              
549             =item get_tmpfile
550              
551             Get a temporary file name which will be unlinked when the object
552             is destroyed.
553              
554             =cut
555              
556             sub get_tmpfile {
557 1     1 1 2 my $self = shift;
558 1   50     16 $self->{_tmpfiles} ||= [];
559 1         16 my $name = File::Temp::tmpnam();
560 1         617 push @{$self->{_tmpfiles}}, $name;
  1         4  
561 1         3 return $name;
562             }
563              
564             sub log_debug {
565 52     52 0 456 my $self = shift;
566 52         67 my $fh;
567 52 50       208 return unless $fh = $self->{_debug};
568 0         0 my $msg = shift;
569 0         0 $msg =~ s/\n*$/\n/;
570              
571 0         0 $self->output_timestamp($fh);
572              
573 0         0 print $fh "$self->{debugtag}$msg";
574             }
575              
576             =item backups
577              
578             Returns the name of all of the backup blocks active in the current
579             configuration file.
580              
581             If the file had:
582              
583            
584             Directory /home/foo
585            
586            
587             BackupHost foo.perusion.org>
588             Directory /home/baz
589            
590            
591             Directory /home/bar
592            
593              
594             The call C<$snap->backups()> would return:
595              
596             ('foo.perusion.org', 'pseudo', 'bar.perusion.org')
597              
598             Returns a reference or list based on call context.
599              
600             =cut
601              
602             sub backups {
603 3     3 1 9 my $self = shift;
604 3         54 my @blocks = $self->{_maincfg}->get('backup');
605 3         142 my @backups;
606 3         9 for(@blocks) {
607 6         15 push @backups, $_->[1];
608             }
609              
610 3 50       13 $self->{_debug} and $self->log_debug("backups=" . Dumper(\@backups));
611              
612 3 50       30 return wantarray ? @backups : \@backups;
613             }
614              
615             =item set_backup
616              
617             Sets a particular block active as the current backup. Returns
618             the passed parameter.
619              
620             =cut
621              
622             sub set_backup {
623 4     4 1 16 my ($self, $client) = @_;
624 4         84 for(@reset_backup) {
625 16         176 delete $self->{$_};
626             }
627 4         58 $self->{_cfg} = $self->{_client_cfg} = $self->{_maincfg}->block('backup', $client);
628 4         316 return $self->{_client} = $client;
629             }
630              
631             =item directories
632              
633             Returns the name of all of the backup blocks active in the current
634             configuration file.
635              
636             Must be preceded by a C<$snap->set_backup($client)> call.
637              
638             If the file had:
639              
640            
641             Directory /home/foo
642             Directory /home/baz
643             Directory /home/bar
644            
645             Hourlies 2
646            
647            
648              
649             The call sequence:
650              
651             $snap->set_backup('foo.perusion.org')
652             or die "No backup configuration!";
653             my @dirs = $snap->directories();
654              
655             would return:
656              
657             ('/home/foo', '/home/baz', '/home/bar', '/home/buz')
658              
659             Returns a reference or list based on call context.
660              
661             =cut
662              
663             sub directories {
664 4     4 1 9 my $self = shift;
665 4         56 my @dirs = $self->config(-directory);
666 4         37 my %dir;
667             my @out;
668 4         30 my $literal = $self->config(-literaldirectory);
669 4         20 for(@dirs) {
670 6         14 my $dirname;
671 6 100       18 unless( ref($_) ) {
672 4         6 $dirname = $_;
673 4 50       29 $dirname =~ s:/+$:: unless $literal;
674 4         55 $dir{$dirname} = $_;
675 4         16 push @out, $dirname;
676             }
677             else {
678 2         10 $dirname = $_->{_block_vals}[0];
679 2 50       17 $dirname =~ s:/+$:: unless $literal;
680 2         12 $dir{$dirname} = $_;
681 2         15 push @out, $dirname;
682             }
683             }
684              
685 4         18 $self->{_directories} = \%dir;
686              
687 4 50       13 $self->{_debug} and $self->log_debug("directories=" . Dumper(\@out));
688              
689 4 100       34 return wantarray ? @out : \@out;
690             }
691              
692              
693             =item set_directory
694              
695             Sets a particular directory as active for backup. Must have set $snap->set_backup()
696             previously, returns undef on error.
697              
698             =cut
699              
700             sub set_directory {
701 5     5 1 28 my ($self, $directory) = @_;
702             my $cfg = $self->{_cfg} = $self->{_client_cfg}
703 5 50       48 or do {
704 0         0 $self->log_error("Can't set directory without client.");
705 0         0 $self->error("Can't set directory without client.");
706 0         0 return undef;
707             };
708              
709 5         47 my $literal = $self->config(-literaldirectory);
710 5 50       49 $directory =~ s:/+$:: unless $literal;
711 5         13 my $dhash = $self->{_directories};
712 5 100       15 unless($dhash) {
713 1         11 $self->directories();
714 1         2 $dhash = $self->{_directories};
715             }
716              
717 5 50       29 my $d = $dhash->{$directory}
718             or return undef;
719              
720 5 100       23 if(ref $d) {
721 2         6 $self->{_cfg} = $d;
722             }
723              
724 5 50       31 $self->{_directory} = "$directory/" unless $literal;
725 5         14 return $self->{_directory};
726             }
727              
728             sub rotate {
729 4     4 0 6 my $self = shift;
730 4 50       14 if($self->config(-ManyFiles)) {
731 0         0 return $self->do_rotate_reuse(@_);
732             }
733             else {
734 4         41 return $self->do_rotate(@_);
735             }
736             }
737              
738             ## ---------- ---------- ---------- ---------- ---------- ----------
739             # Age/rotate the old backup directories.
740             # -- the backup dirs are named like: back.0, back.1, back.2
741             # -- so the count is 3 (3 backups)
742             # -- we deleted the oldest (back.2) and move the next-oldest up
743             # so back.2 becomes back.3, back.1 becomes, back.2, etc.
744             # -- then make a hard link from back.0 to back.1
745             # $maxbackups = number of copies they keep, we count from Zero,
746             # so for 4 copies, we'd have 0,1,2,3. In the comments below
747             # we'll give examples assuming a $maxbackup of 4.
748              
749             sub do_rotate {
750 4     4 0 14 my ($self, $maxbackups, $dir, $rotate_all) = @_;
751            
752             ## Step 1: nothing to do if they're only keeping 1 copy
753 4 50 33     16 if (($maxbackups == 1) && ($rotate_all==0)) { return ; }
  0         0  
754              
755             ## Step 2: delete the oldest copy. (eg: $dir.3)
756 4         10 my $count = $maxbackups - 1;
757 4         6 my $countplus = $maxbackups - 1;
758              
759 4         24 my $rm = $self->config(-rm);
760 4         14 my $mv = $self->config(-mv);
761 4         17 my $cp = $self->config(-cp);
762              
763 4 50       108 if (-d "$dir.$count") {
764 0         0 $self->log_debug("$rm -rf $dir.$count\n");
765 0 0       0 system("$rm -rf $dir.$count") == 0
766             or die "FAILED: $rm -rf $dir.$count";
767             }
768 4         8 $count--;
769              
770             ## Step 3: rotate/rename the "middle" copies (eg: $dir.1,2,3)
771             ## DO NOTHING with the most recent backup (eg: $dir.0) of hourlies.
772             ## Rotate same as the rest for dailies/weeklies/etc.
773              
774 4         6 my $smallest;
775              
776 4 50       19 if ($rotate_all) { $smallest = 0 } else {$smallest = 1};
  4         7  
  0         0  
777              
778 4         16 while ($count >= $smallest) {
779 21 100       915 if (-d "$dir.$count") {
780 8         1117 $self->log_debug("$mv $dir.$count $dir.$countplus\n");
781 8 50       61363 system("$mv $dir.$count $dir.$countplus" ) == 0
782             or die "FAILED: $mv $dir.$count $dir.$countplus";
783             }
784 21         116 $count--; $countplus--;
  21         535  
785             }
786             }
787              
788             sub do_rotate_reuse {
789 0     0 0 0 my ($self, $maxbackups, $dir, $rotate_all) = @_;
790            
791             ## Step 1: nothing to do if they're only keeping 1 copy
792 0 0 0     0 if (($maxbackups == 1) && ($rotate_all==0)) { return ; }
  0         0  
793              
794             ## Step 2: move the oldest copy to .TMP. (eg: $dir.3)
795 0         0 my $count = $maxbackups - 1;
796 0         0 my $countplus = $maxbackups - 1;
797              
798 0         0 my $rm = $self->config(-rm);
799 0         0 my $mv = $self->config(-mv);
800 0         0 my $cp = $self->config(-cp);
801              
802 0 0       0 if (-d "$dir.TMP") {
803 0         0 $self->log_error("$dir.TMP directory existed, removing.\n");
804 0         0 $self->log_debug("$rm -rf $dir.TMP\n");
805 0 0       0 system("$rm -rf $dir.TMP") == 0
806             or die "FAILED: $rm -rf $dir.$count";
807             }
808              
809 0         0 $self->log_debug("called do_rotate with maxbackups=$maxbackups rotate_all=$rotate_all");
810              
811             ## Now using John Pelan's suggestion to rotate least-recent to
812             ## .0 for hourlies
813 0 0       0 if(-d "$dir.$count") {
814 0 0       0 if (! $rotate_all) {
815 0         0 $self->log_debug("$mv $dir.$count $dir.TMP\n");
816 0 0       0 system("$mv $dir.$count $dir.TMP") == 0
817             or die "FAILED: $mv $dir.$count $dir.TMP";
818             }
819             else {
820 0         0 $self->log_debug("$rm -rf $dir.$count\n");
821 0 0       0 system("$rm -rf $dir.$count") == 0
822             or die "FAILED: $rm -rf $dir.$count";
823             }
824             }
825 0         0 $count--;
826              
827             ## Step 3: rotate/rename the "middle" copies (eg: $dir.1,2,3)
828             ## Now using Jean Phelan's suggestion to move an expired
829             ## copy to .0 so linking is reduced.
830              
831 0         0 my $smallest = 0;
832              
833 0         0 while ($count >= $smallest) {
834 0         0 $self->log_debug("do_rotate count=$count countplus=$countplus");
835 0 0       0 if (-d "$dir.$count") {
836 0         0 $self->log_debug("$mv $dir.$count $dir.$countplus\n");
837 0 0       0 system("$mv $dir.$count $dir.$countplus" ) == 0
838             or die "FAILED: $mv $dir.$count $dir.$countplus";
839             }
840 0         0 $count--; $countplus--;
  0         0  
841             }
842              
843 0 0       0 if(! $rotate_all) {
844 0 0       0 if(-d "$dir.TMP") {
    0          
845 0         0 $self->log_debug("$mv $dir.TMP $dir.0\n");
846 0 0       0 system("$mv $dir.TMP $dir.0") == 0
847             or die "FAILED: $mv $dir.TMP $dir.0";
848             }
849             elsif (-d "$dir.1") {
850             ## 3.2: Hard link from the newest backup:
851 0         0 $self->log_debug("Hard Link newest backup: $cp -al $dir.1 $dir.0\n");
852 0 0       0 system("$cp -al $dir.1 $dir.0") == 0
853             or die "FAILED: $cp -al $dir.0 $dir.1";
854             }
855             }
856              
857             }
858              
859             =item backup_directory
860              
861             Performs a directory backup after C
862             and C have been called.
863              
864             =cut
865              
866             sub backup_directory {
867 4     4 1 11 my($self, $dir, %opt) = @_; ## Long form of hostname
868              
869 4         79 my $client = $self->{_client};
870 4   33     22 my $host = $self->config(-backuphost) || $client;
871 4   33     54 $dir ||= $self->{_directory};
872 4         28 my @excl = $self->config(-exclude);
873              
874 4         24 my $rsh = lc $self->config(-RsyncShell);
875              
876 4         12 my $spacer = '';
877              
878 4 50       20 if($dir !~ m{^/}) {
879 0 0       0 $spacer = '/' if $rsh eq 'rsync';
880             }
881              
882 4         37 $self->log_debug("directory=$dir host=$host client=$client");
883 4         7 my $rotate_all = 0; ## flag for do_rotate routine
884 4         11 my $hr_dir = $self->config(-HourlyDir);
885 4         23 my $daily_dir = $self->config(-DailyDir);
886 4         13 my $weekly_dir = $self->config(-WeeklyDir);
887 4         12 my $monthly_dir = $self->config(-MonthlyDir);
888              
889 4         13 my $hr_backup = $self->config(-Hourlies);
890              
891 4 50       28 if($hr_backup == 1) {
892 0         0 $self->log_error("Hourly backup must be zero or two, one is not valid.");
893 0         0 return;
894             }
895              
896 4 50       10 if(! $hr_backup) {
897 0         0 $hr_dir = $self->config(-DailyDir);
898             }
899              
900 4         7 my $dest;
901 4         14 my @destlist = $self->config(-DestinationList);
902              
903 4 50 33     18 if( @destlist = $self->config(-DestinationList)
      33        
904             and $destlist[0]
905             and lc($destlist[0]) ne 'none'
906             )
907             {
908 0         0 $self->log_debug("DestinationList is " . join(" ", @destlist));
909 0         0 my $pdir = $dir;
910 0 0       0 $pdir = "/$pdir" unless $pdir =~ m{^/};
911 0         0 my %dest;
912 0         0 foreach my $prospect (@destlist) {
913 0         0 my $prefix = $prospect . "/" . $client . $pdir ;
914 0         0 my $backupdir = $prefix . $hr_dir;
915 0   0     0 my $mtime = (stat "$backupdir.0")[9] || 0;
916 0         0 $dest{$prospect} = $mtime;
917             }
918              
919 0         0 my $actual;
920             my $min;
921 0         0 for (keys %dest) {
922 0 0       0 if(! defined $min) {
    0          
923 0         0 $min = $dest{$_};
924 0         0 $actual = $_;
925             }
926             elsif($min > $dest{$_}) {
927 0         0 $min = $dest{$_};
928 0         0 $actual = $_;
929             }
930             }
931 0         0 $dest = $actual;
932 0         0 $self->log_debug("Selected DestinationList destination $dest");
933             }
934             else {
935 4         25 $dest = $self->config(-Destination);
936 4         17 $self->log_debug("destination from Destination is $dest");
937             }
938              
939 4 50       17 if(! $dest) {
940 0         0 $self->log_error("Refuse to do backup for %s%s without destination.", $client, $dir);
941 0         0 return;
942             }
943              
944 4         13 my $prefix = $dest . "/" . $client . $spacer . $dir ;
945 4         12 my $backupdir = $prefix . $hr_dir;
946              
947             ## ----------
948             ## STEP 1: check the clock and verify if we are just doing
949             ## the hourly backups, or also the daily/weekly/monthlies.
950              
951             ## If the timestamp on the current backup dir does not match
952             ## todays date, then this must be the first run after midnight,
953             ## so we check the dailies/weeklies/monthlies also.
954             ## Not very efficient, since we check this for each backup set
955             ## that we run, instead of just once for all. Oh well.
956              
957             ## Regularize hourly directories to check for holes if necessary
958 4 50       19 if($hr_backup > 0) {
959 4         26 for my $x (0 .. ($hr_backup - 1) ) {
960 25 100       712 next if -d "$backupdir.$x";
961 17 50       36 last if $x >= $hr_backup;
962 17         30 for my $y (($x + 1) .. $hr_backup) {
963 50 50       1144 next unless -d "$backupdir.$y";
964 0         0 $self->log_debug(qq{rename $backupdir.$y --> $backupdir.$x to plug hole.});
965 0 0       0 rename "$backupdir.$y", "$backupdir.$x"
966             or warn "Tried to rename $backupdir.$y --> $backupdir.$x: $!\n";
967 0         0 last;
968             }
969             }
970             }
971              
972             ## Check the directories
973             ## - hourly backup
974 4   50     121 my $mtime = (stat "$backupdir.0")[9] || 0;
975 4         286 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($mtime);
976 4         8 my $backup_date = $yday;
977             ## - weekly backup
978 4         11 my $backupdir_weekly = $prefix . $weekly_dir;
979 4   50     155 my $mtime_weekly = (stat "$backupdir_weekly.0")[9] || 0;
980 4         78 ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($mtime_weekly);
981 4         12 my $backup_date_weekly = $yday;
982             ## - monthly backup
983 4         9 my $backupdir_monthly = $prefix . $monthly_dir;
984 4   50     115 my $mtime_monthly = (stat "$backupdir_monthly.0")[9] || 0;
985 4         87 ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($mtime_monthly);
986 4         11 my $backup_date_monthly = $yday;
987              
988             ## Check to see if we have a Before statement and don't backup
989             ## if it is not in that time
990 4         7 my $between;
991 4 50 33     37 if(! $self->config(-force)
      33        
992             and
993             ( $self->config(-Before) or $self->config(-After) )
994             )
995             {
996 0         0 my $before = $self->config(-Before);
997 0         0 my $after = $self->config(-After);
998 0         0 for(\$before, \$after) {
999 0         0 my $hr;
1000             my $min;
1001 0         0 my $adder = 0;
1002 0         0 my $orig = $$_;
1003 0 0       0 next unless $$_;
1004 0         0 $$_ =~ s/[\s.]+//g;
1005 0 0       0 if($$_ =~ s/([ap])m?$//i) {
1006 0         0 my $mod = $1;
1007 0 0       0 $adder = 12 if $mod =~ /p/;
1008             }
1009 0 0       0 if($$_ =~ /:/) {
1010 0         0 ($hr, $min) = split /:/, $$_;
1011 0         0 $hr =~ s/^0+//;
1012 0         0 $min =~ s/^0+//;
1013             }
1014             else {
1015 0         0 $$_ =~ s/\D+//g;
1016 0 0       0 if($$_ =~ /^(\d\d?)(\d\d)$/) {
    0          
1017 0         0 $hr = $1;
1018 0         0 $min = $2;
1019             }
1020             elsif($$_ =~ /^(\d\d?)$/) {
1021 0         0 $hr = $1;
1022 0         0 $min = 0;
1023             }
1024             else {
1025 0         0 my $msg = sprintf(
1026             "Time of %s not parseable for Before or After",
1027             $orig);
1028 0         0 $self->log_debug($msg);
1029 0         0 $$_ = '';
1030             }
1031             }
1032 0         0 $hr += $adder;
1033 0         0 $$_ = sprintf('%02d:%02d', $hr, $min);
1034             }
1035              
1036 0         0 my $current = strftime('%H:%M', localtime());
1037 0         0 my $stop;
1038              
1039             my @msg;
1040 0 0       0 if($after) {
1041 0 0       0 $stop = 1 unless $current ge $after;
1042             }
1043 0 0       0 if($before) {
1044 0 0       0 $stop = 1 unless $current lt $before;
1045             }
1046              
1047 0 0       0 if($stop) {
1048 0         0 my $constr = '';
1049 0 0       0 if($before) {
1050 0         0 $constr = "before $before";
1051             }
1052 0 0       0 if($after) {
1053 0 0       0 $constr .= ' or ' if $constr;
1054 0         0 $constr .= "after $after";
1055             }
1056 0 0       0 my $msg = sprintf(
1057             "Skipping backup of %s%s%s, must be %s.",
1058             $client, ($rsh eq 'rsync' ? '::' : ''), $dir, $constr,
1059             );
1060 0         0 $self->log_debug($msg);
1061 0         0 return;
1062             }
1063             }
1064              
1065             ## This mode doesn't back up unless the formula
1066             ##
1067             ## (24 / $hr_backup - 1) * 60 * 60 > time() - $mtime
1068             ##
1069             ## is satisfied.
1070 4 50 33     28 if(! $self->config(-force) and $self->config(-AutoTime)) {
1071 0   0     0 my $must_hours = ( 24 / ($hr_backup || 1) ) - 0.5;
1072 0         0 my $must_exceed = $must_hours * 60 * 60;
1073 0 0       0 if(my $min_exceed = $self->config(-MustExceed)) {
1074 0         0 $min_exceed = time_to_seconds($min_exceed);
1075 0 0       0 if($min_exceed > $must_exceed) {
1076 0         0 $must_hours = sprintf "%.1f", $min_exceed / 60 / 60;
1077 0         0 $must_exceed = $min_exceed;
1078 0         0 $self->log_debug("Setting minimum exceed time $must_hours hours.");
1079             }
1080             }
1081 0         0 my $interval = time() - $mtime;
1082 0 0       0 unless ($interval > $must_exceed) {
1083 0         0 my $real_hours = sprintf "%.1f", $interval / 60 / 60;
1084 0 0       0 my $msg = sprintf(
1085             "Skipping backup of %s%s%s, only %s hours old, want %s hours",
1086             $client, ($rsh eq 'rsync' ? '::' : ''), $dir, $real_hours, $must_hours,
1087             );
1088 0         0 $self->log_debug($msg);
1089 0         0 return;
1090             }
1091             }
1092              
1093 4 50       23 if(my $pc = $self->config(-pingcommand)) {
1094 0 0       0 if(ref $pc eq 'ARRAY') {
1095 0         0 $pc = join " ", @$pc;
1096             }
1097             # Command should return 0 to allow backup
1098 0         0 $pc =~ s/\%h/$host/g;
1099 0         0 $pc =~ s/\%d/$dir/g;
1100 0         0 $pc =~ s/\%c/$client/g;
1101 0         0 system $pc;
1102 0 0       0 if($?) {
1103 0         0 $self->log_debug("Ping command '$pc' returned false, skipping.");
1104 0         0 return;
1105             }
1106             }
1107              
1108 4         41 $self->log_debug("backup_date=$backup_date dir=$backupdir\n");
1109              
1110             ## Check the clock
1111 4         121 ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
1112              
1113 4         23 $self->log_debug("yday=$yday dir=$backupdir\n");
1114              
1115             ## we assume (dangerous I know) that if the timestamp on the directory
1116             ## is not the same date as today, then it must be yesterday. In any
1117             ## case, this is then the first run after midnight today.
1118 4         7 my ($do_dailies, $do_weeklies, $do_monthlies );
1119 4         82 $self->log_debug("backup_date: $backup_date");
1120 4 50       15 if ($backup_date != $yday) {
1121 0 0       0 if($hr_backup) {
1122 0         0 $do_dailies = 1;
1123 0         0 $self->log_debug("do_dailies=true");
1124             }
1125             else {
1126 0         0 $hr_backup = $self->config(-Dailies);
1127             }
1128            
1129             ## do weekly backup if
1130             ## - the last one is more than 7 days in the past
1131             ## yday(today) - yday(last weekly backup) > 7
1132             ## - check for turn of the year
1133             ## yday(today) - yday(last weekly backup) < 0 &&
1134             ## yday(today)+365 - yday(last weekly backup) > 7
1135 0         0 $self->log_debug("backup_date_weekly: $backup_date_weekly");
1136 0 0 0     0 if (($yday - $backup_date_weekly) > 7 ||
      0        
1137             (($yday - $backup_date_weekly) < 0 &&
1138             ($yday+365 - $backup_date_weekly) > 7)
1139             ) {
1140 0         0 $do_weeklies = 1;
1141 0         0 $self->log_debug("do_weeklies=true");
1142             }
1143              
1144             ## do monthly backup if
1145             ## - the last one is more than 30 days in the past
1146             ## yday(today) - yday(last monthly backup) > 30
1147             ## - check for turn of the year
1148             ## yday(today) - yday(last weekly backup) < 0 &&
1149             ## yday(today)+365 - yday(last weekly backup) > 30
1150 0         0 $self->log_debug("backup_date_monthly: $backup_date_monthly");
1151 0 0 0     0 if (($yday - $backup_date_monthly) > 30 ||
      0        
1152             (($yday - $backup_date_monthly) < 0 &&
1153             ($yday+365 - $backup_date_monthly) > 30)
1154             ) {
1155 0         0 $do_monthlies = 1;
1156 0         0 $self->log_debug("do_monthlies=true");
1157             }
1158             }
1159              
1160             ## ----------
1161             ## STEP 2: housekeeping - is the backup destination directory
1162             ## set up? Make it if CreateDir option is set.
1163 4 50       117 unless (-d $prefix) {
1164 0 0       0 if (-e $prefix) {
    0          
1165 0         0 die "Destination $prefix is not a directory\n";
1166             }
1167             elsif( $self->config(-CreateDir) ) {
1168 0 0       0 File::Path::mkpath($prefix)
1169             or die "Unable to make directory $prefix";
1170             }
1171             else {
1172 0         0 die "Missing destination $prefix\n";
1173             }
1174             }
1175              
1176             ## Process the exclusions
1177 4         14 my $e_opts = '';
1178 4 50       11 if(@excl) {
1179 4         5 my @e;
1180 4         15 for(@excl) {
1181 4 50       16 next unless $_;
1182 0         0 push @e, qq{--exclude="$_"};
1183             }
1184 4         16 $e_opts = join " ", @e;
1185             }
1186              
1187 4         41 my $cp = $self->config(-cp);
1188 4         17 my $rsync = $self->config(-rsync);
1189              
1190             ## ----------
1191             ## STEP 3: Process Hourly backups
1192              
1193             ## Figure out which rotation method
1194 4         12 my $many_files = $self->config(-ManyFiles);
1195 4         5 my $retain;
1196              
1197 4 50       14 if($self->config(-RetainPermissions)) {
1198             ## This puts the kibosh on ManyFiles
1199 4 50       9 if($many_files) {
1200 0         0 $self->log_error(
1201             "%s and %s are mutually exclusive, unsetting %s",
1202             'RetainPermissions',
1203             'ManyFiles',
1204             'RetainPermissions',
1205             );
1206             }
1207             else {
1208 4         6 $retain = 1;
1209 4         10 $rotate_all = 1;
1210             }
1211             }
1212              
1213             ## 3.1: Rotate older backups
1214              
1215 4         27 $self->log_debug("do_rotate($hr_backup,$backupdir)");
1216            
1217 4         29 $self->rotate($hr_backup, $backupdir, $rotate_all);
1218              
1219             ## 3.2: Hard link from the newest backup:
1220 4 50 33     159 if (! $many_files and ! $retain and -d "$backupdir.0") {
      33        
1221 0         0 $self->log_debug("Hard Link newest backup\n");
1222 0 0       0 system("$cp -al $backupdir.0 $backupdir.1") == 0
1223             or die "FAILED: $cp -al $backupdir.0 $backupdir.1";
1224             }
1225              
1226 4         40 my $extra_ropts = '';
1227 4 50 33     262 if($retain and -d "$backupdir.1") {
1228 4         49 my $bdir = "$backupdir.1";
1229 4         109 $bdir =~ s:.*/::;
1230 4         18 $e_opts .= " --link-dest=../$bdir";
1231             }
1232              
1233             ## Get the rsync options
1234 4         66 my $r_opts = $self->build_rsync_opts();
1235              
1236 4         6 my $xfer_dir;
1237 4 50 33     40 if (! $rsh or $rsh eq 'none') {
    0 0        
    0          
1238 4         12 $xfer_dir = $dir;
1239             }
1240             elsif ($rsh eq 'rsync' and $host =~ /:\d+$/) {
1241 0         0 $xfer_dir = "rsync://$host/$dir";
1242             }
1243             elsif ($rsh eq 'rsync') {
1244 0         0 $xfer_dir = "${host}::$dir";
1245             }
1246             else {
1247 0         0 $xfer_dir = "$host:$dir";
1248             }
1249              
1250 4         30 my $rsync_log = $self->config(-commandlog);
1251 4 100       15 if(! $rsync_log) {
1252 1         4 $rsync_log = $self->get_tmpfile;
1253 1         8 $self->config(-commandlog, $rsync_log);
1254             }
1255              
1256             ## 3.3:
1257             ## Now rsync from the client dir into the latest snapshot
1258             ## (notice that rsync behaves like cp --remove-destination by
1259             ## default, so the destination is unlinked first. If it were not
1260             ## so, this would copy over the other snapshot(s) too!
1261              
1262 4         20 my $command_line = "$rsync $r_opts $e_opts $xfer_dir $backupdir.0";
1263 4         16 $self->log_debug("$command_line\n");
1264 4         19 $self->log_arbitrary($rsync_log, "client $client\n");
1265 4         24 $self->log_arbitrary($rsync_log, "--\n$command_line\n\n");
1266              
1267             # Cheat and get file handle to avoid subroutine overhead
1268 4         15 my $fh = $self->file_handle($rsync_log);
1269              
1270             # Prep for logging to charge file if necessary
1271 4         15 my $clog = $self->config(-chargefile);
1272 4         9 my ($finished, $bytes_read, $bytes_written, $total_size, $xfer_rate);
1273              
1274 4 50       20330 open BCOMMAND, "$command_line |"
1275             or die "Cannot fork '$command_line': $!\n";
1276 4         209181 while() {
1277 68         182 print $fh $_;
1278 68 50       177 next unless $clog;
1279 68 50       442 if(m/
1280             ^ wrote \s+ (\d+) \s+ bytes
1281             \s+ read \s+ (\d+) \s+ bytes
1282             \s+ (.+) \s+ bytes.sec \s* $
1283             /xi
1284             )
1285             {
1286 0         0 $bytes_written = $1;
1287 0         0 $bytes_read = $2;
1288 0         0 $xfer_rate = $3;
1289 0         0 $finished = 1;
1290             }
1291 68 50       946 next unless $finished;
1292 0 0       0 if(/^total size is (\d+)/) {
1293 0         0 $total_size = $1;
1294 0         0 undef $finished;
1295             }
1296             }
1297              
1298             close BCOMMAND
1299 4 50       301 or do {
1300 0         0 my $stat = $? >> 8;
1301 0 0 0     0 unless ($self->config(-IgnoreVanished) && $stat == 24) {
1302 0         0 my $msg = $self->log_error("FAILED with status %s: %s\ncommand was: %s",
1303             $stat,
1304             $!,
1305             $command_line,
1306             );
1307 0         0 $self->error($msg);
1308 0         0 return undef;
1309             }
1310             };
1311              
1312 4 50       25 if($clog) {
1313 4         856 my $bdate = strftime('%Y%m%d', localtime());
1314 4         50 my $line = join ":",
1315             $client,
1316             $bdate,
1317             $bytes_read,
1318             $bytes_written,
1319             $xfer_rate,
1320             $total_size,
1321             $xfer_dir;
1322 4         124 $self->log_arbitrary($clog, "$line\n");
1323             }
1324              
1325             # update the mtime of hourly.0 to reflect the snapshot time
1326 4         32525 system ("touch $backupdir.0");
1327              
1328             ## ----------
1329             ## STEP 4: Process Daily/Weekly/Monthly backups
1330             ## -- simpler than above, the rsync is already done. We just need
1331             ## to "rotate" the old backups, and then hard link to the
1332             ## newest hourly backup from yesterday. NOTE that will be the
1333             ## .1 version, not the .0 version -- the .0 version is from today.
1334              
1335 4         126 my $yesterdays_hourly = "$backupdir.0";
1336 4         29 $rotate_all=1; ## flag for do_rotate routine
1337              
1338             ## Daily Backups - similar steps to above, rotate, hard link
1339 4 50       114 if ($do_dailies) {
1340 0         0 $backupdir = $prefix . $daily_dir;
1341 0         0 $self->rotate($self->config(-Dailies), $backupdir, $rotate_all);
1342              
1343             ## No rsync necessary, just hard-link from the most-recent hourly.
1344 0 0       0 if (-d "$yesterdays_hourly") {
1345 0 0       0 system("$cp -al $yesterdays_hourly $backupdir.0") == 0
1346             or die "FAILED: $cp -al $yesterdays_hourly $backupdir.0";
1347             }
1348             }
1349              
1350             ## Weekly Backups
1351 4 50       25 if ($do_weeklies) {
1352 0         0 $backupdir = $prefix . $weekly_dir;
1353 0         0 $self->rotate($self->config(-Weeklies), $backupdir, $rotate_all);
1354 0 0       0 if (-d "$yesterdays_hourly") {
1355 0 0       0 system("$cp -al $yesterdays_hourly $backupdir.0") == 0
1356             or die "FAILED: $cp -al $yesterdays_hourly $backupdir.0";
1357             }
1358             }
1359              
1360             ## Monthly Backups
1361 4 50       934 if ($do_monthlies) {
1362 0         0 $backupdir = $prefix . $monthly_dir;
1363 0         0 $self->rotate($self->config(-Monthlies), $backupdir, $rotate_all);
1364 0 0       0 if (-d "$yesterdays_hourly") {
1365 0 0       0 system("$cp -al $yesterdays_hourly $backupdir.0") == 0
1366             or die "FAILED: $cp -al $yesterdays_hourly $backupdir.0";
1367             }
1368             }
1369             }
1370              
1371             =item backup_all
1372              
1373             Iterates through all C blocks in turn, backing up all directories.
1374              
1375             =cut
1376              
1377             sub backup_all {
1378 1     1 1 4 my $self = shift;
1379 1         15 my @bu = $self->backups();
1380 1         15 for my $b ( $self->backups() ) {
1381 2         55 $self->set_backup($b);
1382 2         19 for my $d ($self->directories()) {
1383 3         36 $self->set_directory($d);
1384 3         13 $self->backup_directory();
1385             }
1386             }
1387 1         35 return 1;
1388             }
1389              
1390             =head1 CONFIGURATION
1391              
1392             See L.
1393              
1394             =head1 SEE ALSO
1395              
1396             snapback2(1), snapback_loop(1), snap_charge(1)
1397              
1398             See http://www.mikerubel.org/computers/rsync_snapshots/ for detailed
1399             information on the principles used.
1400              
1401             =cut
1402              
1403              
1404              
1405             1;