File Coverage

blib/lib/Sys/RotateBackup.pm
Criterion Covered Total %
statement 11 13 84.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 16 18 88.8


line stmt bran cond sub pod time code
1             package Sys::RotateBackup;
2             {
3             $Sys::RotateBackup::VERSION = '0.12';
4             }
5             BEGIN {
6 1     1   26569 $Sys::RotateBackup::AUTHORITY = 'cpan:TEX';
7             }
8             # ABSTRACT: Rotate numbered backup directories
9              
10 1     1   27 use 5.010_000;
  1         3  
  1         28  
11 1     1   1495 use mro 'c3';
  1         802  
  1         6  
12 1     1   41 use feature ':5.10';
  1         3  
  1         119  
13              
14 1     1   414 use Moose;
  0            
  0            
15             use namespace::autoclean;
16              
17             # use IO::Handle;
18             # use autodie;
19             # use MooseX::Params::Validate;
20              
21             use Sys::FS ;
22             use Sys::Run;
23              
24             has 'fs' => (
25             'is' => 'rw',
26             'isa' => 'Sys::FS',
27             'lazy' => 1,
28             'builder' => '_init_fs',
29             );
30              
31             has 'sys' => (
32             'is' => 'rw',
33             'isa' => 'Sys::Run',
34             'lazy' => 1,
35             'builder' => '_init_sys',
36             );
37              
38             has 'daily' => (
39             'is' => 'ro',
40             'isa' => 'Int',
41             'default' => 10,
42             );
43              
44             has 'weekly' => (
45             'is' => 'ro',
46             'isa' => 'Int',
47             'default' => 4,
48             );
49              
50             has 'monthly' => (
51             'is' => 'ro',
52             'isa' => 'Int',
53             'default' => 12,
54             );
55              
56             has 'yearly' => (
57             'is' => 'ro',
58             'isa' => 'Int',
59             'default' => 10,
60             );
61              
62             has 'vault' => (
63             'is' => 'rw',
64             'isa' => 'Str',
65             'required' => 1,
66             );
67              
68             with qw(Log::Tree::RequiredLogger);
69              
70             sub _init_sys {
71             my $self = shift;
72              
73             my $Sys = Sys::Run::->new( { 'logger' => $self->logger(), } );
74              
75             return $Sys;
76             }
77              
78             sub _init_fs {
79             my $self = shift;
80              
81             my $FS = Sys::FS::->new(
82             {
83             'logger' => $self->logger(),
84             'sys' => $self->sys(),
85             }
86             );
87              
88             return $FS;
89             }
90              
91             sub rotate {
92             my $self = shift;
93             my $time = shift || time();
94              
95             my ( $sec, $min, $hour, $dom, $mon, $year, $dow, $doy, $isdst ) = localtime($time);
96             $year += 1900;
97             $mon++;
98             $dow++;
99             $doy++;
100             # dom is already correct (starting at 1, not 0 like to others ...)
101              
102             # rotate daily
103             $self->_rotate( 'daily', $self->daily() );
104              
105             # rotate weekly if dow == 1 && do_weekly
106             if ( $self->weekly() && $dow == 1 ) {
107             $self->_rotate( 'weekly', $self->weekly() );
108             }
109              
110             # rotate monthly if dom == 1 && do_monthly
111             if ( $self->monthly() && $dom == 1 ) {
112             $self->_rotate( 'monthly', $self->monthly() );
113             }
114              
115             # rotate yearly if dom == 1 && mon == 1 && do_yearly
116             if ( $self->yearly() && $doy == 1 ) {
117             $self->_rotate( 'yearly', $self->yearly() );
118             }
119              
120             return 1;
121             }
122              
123             sub cleanup {
124             my $self = shift;
125              
126             # cleanup old backups copies
127              
128             # clean up dailys
129             $self->_cleanup( 'daily', $self->daily() );
130              
131             # clean up weeklys
132             if( $self->weekly() ) {
133             $self->_cleanup( 'weekly', $self->weekly() );
134             } else {
135             my $weekly_dir = $self->fs()->filename( $self->vault(), 'weekly' );
136             $self->sys()->run_cmd( 'rm -rf ' . $weekly_dir );
137             }
138              
139             # clean up monthlys
140             if( $self->monthly() ) {
141             $self->_cleanup( 'monthly', $self->monthly() );
142             } else {
143             my $monthly_dir = $self->fs()->filename( $self->vault(), 'monthly' );
144             $self->sys()->run_cmd( 'rm -rf ' . $monthly_dir );
145             }
146              
147             # clean up yearlys
148             if( $self->yearly() ) {
149             $self->_cleanup( 'yearly', $self->yearly() );
150             } else {
151             my $yearly_dir = $self->fs()->filename( $self->vault(), 'yearly' );
152             $self->sys()->run_cmd( 'rm -rf ' . $yearly_dir );
153             }
154              
155             return 1;
156             }
157              
158             sub _cleanup {
159             my $self = shift;
160             my $type = shift;
161             my $num = shift || 1;
162              
163             my $basedir = $self->fs()->filename( $self->vault(), $type );
164              
165             if(!-d $basedir) {
166             $self->logger()->log( message => 'Basedir '.$basedir.' not a directory. Aborting!', level => 'error', );
167             return;
168             }
169              
170             if(-d $basedir && opendir(my $DH, $basedir)) {
171             while(my $de = readdir($DH)) {
172             next if $de =~ m/^\./; # skip cur and upper dir
173             next unless $de =~ m/^\d+$/; # skip non-numeric dirs
174             next if $de < $num; # skip any valid rotational
175             my $path = $self->fs()->filename( $basedir, $de );
176             next unless -d $path; # skip any non-dirs
177              
178             if($self->sys()->run_cmd( 'rm -rf ' . $path )) {
179             $self->logger()->log( message => 'Removed superflous rotational directory: '.$path, level => 'debug', );
180             } else {
181             $self->logger()->log( message => 'Failed to remove superflous rotational directory: '.$path, level => 'debug', );
182             }
183             }
184             closedir($DH);
185             }
186              
187             return 1;
188             }
189              
190             sub _rotate {
191             my $self = shift;
192             my $type = shift;
193             my $num = shift;
194              
195             my $basedir = $self->fs()->filename( $self->vault(), $type );
196              
197             if ( $num > 500 ) {
198             $num = 500;
199             }
200             elsif ( $num < 1 ) {
201             $num = 10;
202             }
203              
204             # Create basedir for this type
205             if ( !-d $basedir ) {
206             $self->fs()->makedir($basedir);
207             }
208              
209             # Remove the oldest directory
210             if ( -d $basedir . q{/} . $num ) {
211             $self->sys()->run_cmd( 'rm -rf ' . $basedir . q{/} . $num );
212             }
213              
214             # Rotate the others
215             ## no critic (ProhibitCStyleForLoops)
216             for ( my $i = $num ; $i > 0 ; $i-- ) {
217             my $olddir = $basedir . q{/} . ( $i - 1 );
218             my $newdir = $basedir . q{/} . $i;
219             if ( -d $olddir && -w $basedir ) {
220             $self->logger()->log( message => 'Moving from '.$olddir.' to '.$newdir, level => 'debug', );
221             my $cmd = q{mv -f "} . $olddir . q{" "} . $newdir . q{"};
222             $self->logger()->log( message => 'CMD: ' . $cmd, level => 'debug', );
223             $self->sys()->run_cmd($cmd);
224             }
225             elsif ( -w $basedir ) {
226             my $cmd = q{mkdir -p -m0700 "} . $newdir . q{"};
227             $self->logger()->log( message => 'CMD: ' . $cmd, level => 'debug', );
228             $self->sys()->run_cmd($cmd);
229             if ( $i > 1 ) { # do not create the 0 dir, or mv'ing inprogress will move to wrong destination
230             $cmd = q{mkdir -p -m0700 "}.$olddir.q{"};
231             $self->logger()->log( message => 'CMD: ' . $cmd, level => 'debug', );
232             $self->sys()->run_cmd($cmd);
233             }
234             }
235             }
236             ## use critic
237              
238             # Create the current dir
239             if ( $type eq 'daily' ) {
240              
241             # Move inprogress
242             my $olddir = $basedir . '/inprogress';
243             my $newdir = $basedir . '/0';
244             if ( -d $olddir && -w $basedir ) {
245             $self->logger()->log( message => 'Moving from '.$olddir.' to '.$newdir, level => 'debug', );
246             my $cmd = q{mv -f "} . $olddir . q{" "} . $newdir . q{"};
247             $self->logger()->log( message => 'CMD: ' . $cmd, level => 'debug', );
248             $self->sys()->run_cmd($cmd);
249             }
250             else {
251             $self->logger()->log( message => q{Can't move from }.$olddir.' to '.$newdir.'. '.$olddir.' not found or '.$basedir.' is not writeable.', level => 'debug', );
252             }
253             }
254             else {
255             my $daily = $self->fs()->filename( $self->vault(), 'daily', '0' );
256             my $newdir = $basedir . '/0';
257             my $cmd = q{};
258             if ( -x '/usr/bin/rsync' ) {
259             $cmd = '/usr/bin/rsync -a --whole-file --link-dest=' . $daily . q{/ } . $daily . q{/ } . $newdir . q{/};
260             }
261             else {
262             $cmd = '/bin/cp -al ' . $daily . q{/ } . $newdir . q{/};
263             }
264             $self->logger()->log( message => "CMD: $cmd", level => 'debug', );
265             $self->sys()->run_cmd($cmd);
266             }
267             return 1;
268             }
269              
270             no Moose;
271             __PACKAGE__->meta->make_immutable;
272              
273             1;
274              
275             __END__
276              
277             =pod
278              
279             =encoding utf-8
280              
281             =head1 NAME
282              
283             Sys::RotateBackup - Rotate numbered backup directories
284              
285             =head1 METHODS
286              
287             =head2 rotate
288              
289             Rotate all valid types in vault.
290              
291             =head2 cleanup
292              
293             Remove all unnecessary directores.
294              
295             =cut
296              
297             =head1 NAME
298              
299             Sys::RotateBackup - rotate numiercal directories
300              
301             =head1 AUTHOR
302              
303             Dominik Schulz <dominik.schulz@gauner.org>
304              
305             =head1 COPYRIGHT AND LICENSE
306              
307             This software is copyright (c) 2012 by Dominik Schulz.
308              
309             This is free software; you can redistribute it and/or modify it under
310             the same terms as the Perl 5 programming language system itself.
311              
312             =cut