File Coverage

blib/lib/File/Rotate/Backup.pm
Criterion Covered Total %
statement 13 258 5.0
branch 0 74 0.0
condition 0 5 0.0
subroutine 5 51 9.8
pod 8 29 27.5
total 26 417 6.2


line stmt bran cond sub pod time code
1             # -*-perl-*-
2             # Creation date: 2003-03-09 15:38:36
3             # Authors: Don
4             # Change log:
5             # $Id: Backup.pm,v 1.33 2007/12/14 03:37:30 don Exp $
6             #
7             # Copyright (c) 2003-2007 Don Owens. All rights reserved.
8             #
9             # This is free software; you can redistribute it and/or modify it under
10             # the Perl Artistic license. You should have received a copy of the
11             # Artistic license with this distribution, in the file named
12             # "Artistic". You may also obtain a copy from
13             # http://regexguy.com/license/Artistic
14             #
15             # This program is distributed in the hope that it will be
16             # useful, but WITHOUT ANY WARRANTY; without even the implied
17             # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
18             # PURPOSE.
19              
20             =pod
21              
22             =head1 NAME
23              
24             File::Rotate::Backup - Make backups of multiple directories and
25             rotate them on unix.
26              
27             =head1 SYNOPSIS
28              
29             my $params = { archive_copies => 2,
30             dir_copies => 1,
31             backup_dir => '/backups',
32             file_prefix => 'backup_'
33             secondary_backup_dir => '/backups2',
34             secondary_archive_copies => 2,
35             verbose => 1,
36             use_flock => 1,
37             };
38              
39             my $backup = File::Rotate::Backup->new($params);
40              
41             $backup->backup([ [ '/etc/httpd/conf' => 'httpd_conf' ],
42             [ '/var/named' => 'named' ],
43             ]);
44              
45             $backup->rotate;
46              
47             =head1 DESCRIPTION
48              
49             This module will make backups and rotate them according to your
50             specification. It creates a backup directory based on the
51             file_prefix you specify and the current time. It then copies the
52             directories you specified in the call to new() to that backup
53             directory. Then a tar'd and compressed file is created from that
54             directory. By default, bzip2 is used for compression.
55              
56             This module has only been tested on Linux and Solaris.
57              
58             The only external programs used are tar and a compression
59             program. Copies and deletes are implemented internally.
60              
61             =head1 METHODS
62              
63             =cut
64              
65 1     1   6232 use strict;
  1         2  
  1         33  
66 1     1   7 use File::Find ();
  1         1  
  1         32  
67             # use File::Copy ();
68              
69             { package File::Rotate::Backup;
70              
71 1     1   6 use vars qw($VERSION);
  1         5  
  1         50  
72              
73             BEGIN {
74 1     1   15 $VERSION = '0.13'; # update below in POD as well
75             }
76              
77 1     1   1623 use File::Rotate::Backup::Copy;
  1         2  
  1         3127  
78              
79             =pod
80              
81             =head2 new(\%params)
82              
83             my $params = { archive_copies => 2,
84             dir_copies => 1,
85             backup_dir => '/backups',
86             file_prefix => 'backup_'
87             secondary_backup_dir => '/backups2',
88             secondary_archive_copies => 2,
89             verbose => 1,
90             use_flock => 1,
91             dir_regex => '\d+-\d+-\d+_\d+_\d+_\d+',
92             file_regex => '\d+-\d+-\d+_\d+_\d+_\d+',
93             };
94              
95             my $backup = File::Rotate::Backup->new($params);
96              
97             Creates a backup object.
98              
99             =over 4
100              
101             =item archive_copies
102              
103             The number of old archive files to keep.
104              
105             =item no_archive
106              
107             If set to true, then no compressed archive(s) will be created
108             even if archive_copies is set.
109              
110             =item dir_copies
111              
112             The number of old backup directories to keep.
113              
114             =item backup_dir
115              
116             Where backups are placed.
117              
118             =item file_prefix
119              
120             The prefix to use for the backup directories and archive files.
121             When the directories and archive files are created, the name for
122             each is created by appending a timestamp to the end of the file
123             prefix you specify.
124              
125             =item secondary_backup_dir
126              
127             Overflow directory to copy files to before deleting them from the
128             backup directory when rotating.
129              
130             =item secondary_archive_copies
131              
132             The number of archive files to keep in the secondary backup
133             directory.
134              
135             =item verbose
136              
137             If set to a true value, status messages will be printed as the
138             files are being processed.
139              
140             =item use_flock
141              
142             If set to a true value, an attempt will be made to acquire a
143             write lock on any file to be removed during rotation. If a lock
144             cannot be acquired, the file will not be removed. This is useful
145             for concurrency control, e.g., when your backup script gets run
146             at the same time as another script that is writing the backups to
147             tape.
148              
149             =item use_rm
150              
151             If set to a true value, the external program /bin/rm will be used
152             to remove a file in the case where unlink() fails. This may
153             occur on systems where the file being removed is larger than 2GB
154             and such files are not fully supported.
155              
156             =item dir_regex
157              
158             Regular expression used to search for directories to rotate. The
159             file_prefix is prepended to this to create the final regular
160             expression. This is useful for rotating directories that were
161             not created by this module.
162              
163             =item file_regex
164              
165             Regular expression used to search for archive files to rotate.
166             The file_prefix is prepended to this to create the final regular
167             expression. This is useful for rotating files that were not
168             created by this module.
169              
170             =back
171              
172             =cut
173              
174             # BEGIN {
175             # use vars '%Config';
176             # eval 'use Config';
177             # }
178              
179             sub new {
180 0     0 1   my ($proto, $params) = @_;
181              
182 0           my $self = {};
183 0   0       bless $self, ref($proto) || $proto;
184            
185 0 0         $self->setArchiveCopies(defined($$params{archive_copies}) ? $$params{archive_copies} : 1);
186 0 0         $self->setDirCopies(defined($$params{dir_copies}) ? $$params{dir_copies} : 1);
187 0           my $dir = $$params{backup_dir};
188 0 0         $dir = '/tmp' if $dir eq '';
189 0           $self->setBackupDir($dir);
190 0           $self->setSecondaryBackupDir($$params{secondary_backup_dir});
191 0           $self->setSecondaryArchiveCopies($$params{secondary_archive_copies});
192 0           $self->setFilePrefix($$params{file_prefix});
193 0           $self->_setVerbose($$params{verbose});
194 0           $self->_setUseFileLock($$params{use_flock});
195 0           $self->_setUseRm($$params{use_rm});
196 0 0         $self->{_archive_dir_regex} = $params->{dir_regex} if defined $params->{dir_regex};
197 0 0         $self->{_archive_file_regex} = $params->{file_regex} if defined $params->{file_regex};
198 0 0         $self->{_no_archive} = defined $params->{no_archive} ? $params->{no_archive} : 0;
199              
200             # foreach my $exe ('tar', 'gzip', 'bzip2', 'rm', 'mv') {
201             # if (defined($Config{$exe}) and $Config{$exe} ne '') {
202             # $self->{'_' . $exe} = $Config{$exe};
203             # }
204             # }
205 0           return $self;
206             }
207              
208             =pod
209              
210             =head2 backup(\@conf)
211              
212             Makes the backup -- creates the backed up directory and archive
213             file. @conf is an array where each element is either a string or
214             an array. If it is a string, it is expected to be the path to a
215             directory that is to be backed up. If the element is an array,
216             the first element is expected to be a directory that is to be
217             backed up, and the second should be the name the directory is
218             called once it has been copied to the backup directory. The
219             return value is the name of the archive file created; unless
220             'no_archive' is set, then it will return an empty string.
221              
222             =cut
223             sub backup {
224 0     0 1   my ($self, $conf) = @_;
225              
226 0           my $today = $self->_getTimestampForFileName;
227 0           my $file_prefix = $self->getFilePrefix . $today;
228 0           my $backup_dir = $self->getBackupDir;
229 0           my $dst = "$backup_dir/$file_prefix";
230 0           my $dst_file = '';
231 0           mkdir $dst, 0755;
232              
233 0           my $cp = $self->getCpPath;
234 0           foreach my $entry (@$conf) {
235 0 0         if (ref($entry) eq 'ARRAY') {
236 0           my ($dir, $name) = @$entry;
237 0           $self->copy($dir, "$dst/$name");
238             } else {
239 0           $self->copy($entry, "$dst/");
240             }
241             }
242              
243 0 0         unless ( $self->{_no_archive} )
244             {
245 0           my $compress = $self->getCompressProgramPath;
246 0           my $ext = $self->getCompressExtension;
247 0 0         $ext = '.' . $ext unless $ext eq '';
248 0           $dst_file = $dst . '.tar' . $ext;
249 0           my $params = '-p';
250 0 0         $params = '-v ' . $params if $self->_getVerbose;
251 0           my $tar_cmd = $self->getTarPath . " $params -c -f - -C '$backup_dir' '$file_prefix'";
252 0           system "$tar_cmd | $compress > $dst_file";
253             }
254              
255 0           return $dst_file;
256             }
257              
258             =pod
259              
260             =head2 rotate()
261              
262             Rotates the backup directories and archive files. The number of
263             archive files to keep and the number of directories to keep are
264             specified in the new() constructor.
265              
266             =cut
267             sub rotate {
268 0     0 1   my ($self) = @_;
269 0           my $archive_copies = $self->getArchiveCopies;
270 0           my $dir_copies = $self->getDirCopies;
271 0           my $backup_dir = $self->getBackupDir;
272 0           my $secondary_backup_dir = $self->getSecondaryBackupDir;
273              
274 0           $self->_rotate($backup_dir, $archive_copies, $dir_copies, $secondary_backup_dir);
275              
276 0 0         return 1 if $secondary_backup_dir eq '';
277 0           my $secondary_archive_copies = $self->getSecondaryArchiveCopies;
278 0           $self->_rotate($secondary_backup_dir, $secondary_archive_copies, 0, '');
279             }
280              
281             =pod
282              
283             =head2 my $archives = getArchiveDeleteList()
284              
285             Returns a list of archive files that will get deleted if the
286             rotate() method is called.
287              
288             =cut
289             sub getArchiveDeleteList {
290 0     0 1   my ($self) = @_;
291            
292 0           my $backup_dir = $self->getBackupDir;
293 0           my $archives = $self->_getSortedArchives($backup_dir);
294 0           my $num_archives = scalar(@$archives);
295 0           my $archive_copies = $self->getArchiveCopies;
296              
297 0           my @files_to_delete;
298 0 0         if ($num_archives > $archive_copies) {
299 0           my $num_to_delete = $num_archives - $archive_copies;
300 0           @files_to_delete = @$archives[0 .. $num_to_delete - 1];
301             }
302              
303 0           @files_to_delete = map { "$backup_dir/$_" } @files_to_delete;
  0            
304              
305 0           return \@files_to_delete;
306             }
307              
308             =pod
309              
310             =head2 my $dirs = getDirDeleteList()
311              
312             Returns a list of directories that will get deleted if the
313             rotate() method is called.
314              
315              
316             =cut
317              
318             sub getDirDeleteList {
319 0     0 1   my ($self) = @_;
320              
321 0           my $backup_dir = $self->getBackupDir;
322 0           my $dirs = $self->_getSortedArchiveDirs($backup_dir);
323 0           my $num_dirs = scalar(@$dirs);
324 0           my $dir_copies = $self->getDirCopies;
325              
326 0           my @dirs_to_delete;
327 0 0         if ($num_dirs > $dir_copies) {
328 0           my $num_to_delete = $num_dirs - $dir_copies;
329 0           @dirs_to_delete = @$dirs[0 .. $num_to_delete - 1];
330             }
331              
332 0           @dirs_to_delete = map { "$backup_dir/$_" } @dirs_to_delete;
  0            
333            
334 0           return \@dirs_to_delete;
335             }
336              
337             sub _rotate {
338 0     0     my ($self, $backup_dir, $archive_copies, $dir_copies, $secondary_backup_dir) = @_;
339              
340 0           my $archives = $self->_getSortedArchives($backup_dir);
341 0           my $num_archives = scalar(@$archives);
342 0           my $dirs = $self->_getSortedArchiveDirs($backup_dir);
343 0           my $num_dirs = scalar(@$dirs);
344              
345 0 0         if ($num_archives > $archive_copies) {
346 0           my $num_to_delete = $num_archives - $archive_copies;
347 0           my @files_to_delete = @$archives[0 .. $num_to_delete - 1];
348 0           foreach my $file (@files_to_delete) {
349 0           my $path = "$backup_dir/$file";
350 0 0         unless ($secondary_backup_dir eq '') {
351 0           $self->copy($path, "$secondary_backup_dir/");
352             }
353 0           $self->_debugPrint("removing $path\n");
354 0           $self->remove($path);
355             }
356             }
357              
358 0 0         if ($num_dirs > $dir_copies) {
359 0           my $num_to_delete = $num_dirs - $dir_copies;
360 0           my @dirs_to_delete = @$dirs[0 .. $num_to_delete - 1];
361 0           foreach my $dir (@dirs_to_delete) {
362 0           my $path = "$backup_dir/$dir";
363 0           $self->_debugPrint("removing $path\n");
364 0           $self->remove($path);
365             }
366              
367             }
368             }
369              
370             sub _debug {
371 0     0     my ($self) = @_;
372 0           return $$self{_debug};
373             }
374              
375             sub _debugOff {
376 0     0     my ($self) = @_;
377 0           undef $$self{_debug};
378 0           undef $$self{_debug_fh};
379             }
380              
381             sub _debugOn {
382 0     0     my ($self, $fh) = @_;
383 0           $$self{_debug} = 1;
384 0           $$self{_debug_fh} = $fh;
385             }
386              
387             sub _debugPrint {
388 0     0     my ($self, $str) = @_;
389 0 0         return undef unless $$self{_debug};
390 0           my $fh = $$self{_debug_fh};
391 0           print $fh $str;
392             }
393              
394             sub _getVerbose {
395 0     0     my ($self) = @_;
396 0           return $$self{_verbose};
397             }
398              
399             sub _setVerbose {
400 0     0     my ($self, $val) = @_;
401 0           return $$self{_verbose} = $val;
402             }
403              
404             sub _getUseFileLock {
405 0     0     my ($self) = @_;
406 0           return $$self{_use_flock};
407             }
408            
409             sub _setUseFileLock {
410 0     0     my ($self, $val) = @_;
411 0           $$self{_use_flock} = $val;
412             }
413              
414             sub _getUseRm {
415 0     0     my ($self) = @_;
416 0           return $$self{_use_rm};
417             }
418              
419             sub _setUseRm {
420 0     0     my ($self, $val) = @_;
421 0           $$self{_use_rm} = $val;
422             }
423              
424             sub copy {
425 0     0 0   my ($self, $src, $dst) = @_;
426              
427 0           my $copy = $self->_getCopyObject;
428 0           $copy->copy($src, $dst);
429             }
430              
431             sub _getCopyObject {
432 0     0     my ($self) = @_;
433 0           my $copy = $$self{_copy_obj};
434 0 0         unless ($copy) {
435 0           $copy = File::Rotate::Backup::Copy->new({ use_flock => $self->_getUseFileLock,
436             use_rm => $self->_getUseRm
437             });
438 0           $$self{_copy_obj} = $copy;
439             }
440            
441 0 0         if ($$self{_debug}) {
    0          
442 0           $copy->debugOn($$self{_debug_fh}, 1);
443             } elsif ($self->_getVerbose) {
444 0           $copy->debugOn(\*STDERR, 1);
445             } else {
446 0           $copy->debugOff;
447             }
448            
449 0           return $copy;
450             }
451              
452             sub remove {
453 0     0 0   my ($self, $victim) = @_;
454              
455 0           my $remove = $self->_getCopyObject;
456 0           $remove->remove($victim);
457             }
458              
459             sub _getArchiveFileRegex {
460 0     0     my $self = shift;
461 0           my $prefix = quotemeta($self->getFilePrefix);
462 0           my $regex;
463            
464 0 0         if (exists($self->{_archive_file_regex})) {
465 0           $regex = $self->{_archive_file_regex};
466             }
467             else {
468 0           $regex = '\d+-\d+-\d+_\d+_\d+_\d+';
469             }
470              
471 0           $regex = $prefix . $regex;
472              
473 0           return $regex;
474             }
475              
476             sub _getSortedArchives {
477 0     0     my ($self, $dir) = @_;
478             # my $prefix = quotemeta($self->getFilePrefix);
479 0 0         $dir = $self->getBackupDir if $dir eq '';
480 0           local(*DIR);
481 0 0         opendir(DIR, $dir) or return undef;
482 0           my $regex = $self->_getArchiveFileRegex;
483 0 0         my @files = grep { m/^$regex/ and not -d "$dir/$_" } readdir DIR;
  0            
484 0           closedir DIR;
485              
486 0           @files = sort { $a cmp $b } @files;
  0            
487 0           return \@files;
488             }
489              
490             sub _getArchiveDirRegex {
491 0     0     my $self = shift;
492 0           my $prefix = quotemeta($self->getFilePrefix);
493              
494 0           my $regex;
495 0 0         if (exists($self->{_archive_dir_regex})) {
496 0           $regex = $self->{_archive_dir_regex};
497 0 0         $regex = '' unless defined $regex;
498             }
499             else {
500 0           $regex = '\d+-\d+-\d+_\d+_\d+_\d+';
501             }
502              
503 0           $regex = $prefix . $regex;
504              
505 0           return $regex;
506             }
507              
508             sub _getSortedArchiveDirs {
509 0     0     my ($self, $dir) = @_;
510             # my $prefix = quotemeta($self->getFilePrefix);
511 0 0         $dir = $self->getBackupDir if $dir eq '';
512 0           local(*DIR);
513 0 0         opendir(DIR, $dir) or return undef;
514             # my @files = grep { m/^$prefix\d+-\d+-\d+_\d+_\d+_\d+/ and -d "$dir/$_" } readdir DIR;'
515 0           my $regex = $self->_getArchiveDirRegex;
516 0 0         my @files = grep { m/^$regex/ and -d "$dir/$_" } readdir DIR;
  0            
517 0           closedir DIR;
518              
519 0           @files = sort { $a cmp $b } @files;
  0            
520 0           return \@files;
521             }
522              
523             sub _getTimestampForFileName {
524 0     0     my ($self, $time) = @_;
525              
526 0 0         $time = time() unless $time;
527              
528 0           my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time);
529 0           $mon += 1;
530 0           $year += 1900;
531 0           my $date = sprintf "%04d-%02d-%02d_%02d_%02d_%02d", $year, $mon, $mday,
532             $hour, $min, $sec;
533              
534 0           return $date;
535             }
536              
537            
538             #################
539             # getters/setters
540            
541             sub getCompressProgramPath {
542 0     0 0   my ($self) = @_;
543 0           my $path = $$self{_compress_program_path};
544 0 0         if ($path eq '') {
545 0   0       return $self->{_bzip2_path} || 'bzip2';
546             }
547              
548 0           return $path;
549             }
550              
551             =pod
552              
553             =head2 setCompressProgramPath($path)
554              
555             Set the path to the compression program you want to use when
556             creating the archive files in the call to backup(). The given
557             compression program must provide the same API as gzip and bzip2,
558             at least to the extent that it will except input from stdin and
559             will write output to stdout when no file names are provided.
560             This defaults to 'bzip2' (no explicit path).
561              
562             =cut
563             sub setCompressProgramPath {
564 0     0 1   my ($self, $path) = @_;
565 0           $$self{_compress_program_path} = $path;
566             }
567              
568             sub getCompressExtension {
569 0     0 0   my ($self) = @_;
570            
571 0 0         if (exists($$self{_compress_ext})) {
572 0           return $$self{_compress_ext};
573             }
574              
575 0           my $compress_prog_path = $self->getCompressProgramPath;
576 0           my $prog;
577 0 0         if ($compress_prog_path =~ m{(?:\A|/)([^/\s]+)([^/]*)$}) {
578 0           $prog = $1;
579             }
580              
581 0           my $ext = { 'bzip2' => 'bz2',
582             'gzip' => 'gz',
583             }->{$prog};
584              
585 0           return $ext;
586             }
587              
588             =pod
589              
590             =head2 setCompressExtension($ext)
591              
592             This sets the extension given to the archive name after the .tar.
593             This defaults to .bz2 if bzip2 is used for compression, and .gz
594             if gzip is used.
595              
596             =cut
597             sub setCompressExtension {
598 0     0 1   my ($self, $ext) = @_;
599 0 0         $ext =~ s/^\.// unless $ext eq '.';
600 0           $$self{_compress_ext} = $ext;
601             }
602              
603             sub getTarPath {
604 0     0 0   my ($self) = @_;
605 0           my $path = $$self{_tar_path};
606 0 0         if ($path eq '') {
607 0           return 'tar';
608             }
609            
610 0           return $path;
611             }
612              
613             =pod
614              
615             =head2 setTarPath($path)
616              
617             Set the path to the tar program. This defaults to 'tar' (no
618             explicit path).
619              
620             =cut
621             sub setTarPath {
622 0     0 1   my ($self, $path) = @_;
623 0           $$self{_tar_path} = $path;
624             }
625              
626             sub getRmPath {
627 0     0 0   my ($self) = @_;
628 0           my $path = $$self{_rm_path};
629 0 0         if ($path eq '') {
630 0           return '/bin/rm';
631             }
632              
633 0           return $path;
634             }
635              
636             sub setRmPath {
637 0     0 0   my ($self, $path) = @_;
638 0           $$self{_rm_path} = $path;
639             }
640              
641             sub getCpPath {
642 0     0 0   my ($self) = @_;
643 0           my $path = $$self{_cp_path};
644 0 0         if ($path eq '') {
645 0           return 'cp';
646             }
647              
648 0           return $path;
649             }
650              
651             sub setCpPath {
652 0     0 0   my ($self, $path) = @_;
653 0           $$self{_cp_path} = $path;
654             }
655              
656             sub getArchiveCopies {
657 0     0 0   my ($self) = @_;
658 0           return $$self{_archive_copies};
659             }
660              
661             sub setArchiveCopies {
662 0     0 0   my ($self, $num) = @_;
663 0           $$self{_archive_copies} = $num;
664             }
665              
666             sub getDirCopies {
667 0     0 0   my ($self) = @_;
668 0           return $$self{_dir_copies};
669             }
670              
671             sub setDirCopies {
672 0     0 0   my ($self, $num) = @_;
673 0           $$self{_dir_copies} = $num;
674             }
675              
676             sub getBackupDir {
677 0     0 0   my ($self) = @_;
678 0           return $$self{_backup_dir};
679             }
680              
681             sub setBackupDir {
682 0     0 0   my ($self, $dir) = @_;
683 0           $$self{_backup_dir} = $dir;
684             }
685              
686             # added for v0_02
687             sub getSecondaryBackupDir {
688 0     0 0   my ($self) = @_;
689 0           return $$self{_secondary_backup_dir};
690             }
691              
692             # added for v0_02
693             sub setSecondaryBackupDir {
694 0     0 0   my ($self, $dir) = @_;
695 0           $$self{_secondary_backup_dir} = $dir;
696             }
697              
698             sub getSecondaryArchiveCopies {
699 0     0 0   my ($self) = @_;
700 0           return $$self{_secondary_archive_copies};
701             }
702              
703             sub setSecondaryArchiveCopies {
704 0     0 0   my ($self, $num) = @_;
705 0           $$self{_secondary_archive_copies} = $num;
706             }
707              
708             sub getFilePrefix {
709 0     0 0   my ($self) = @_;
710 0           return $$self{_file_prefix};
711             }
712              
713             sub setFilePrefix {
714 0     0 0   my ($self, $prefix) = @_;
715 0           $$self{_file_prefix} = $prefix;
716             }
717            
718              
719             }
720              
721             1;
722              
723             __END__