File Coverage

blib/lib/Backup/Duplicity/YADW.pm
Criterion Covered Total %
statement 54 201 26.8
branch 0 40 0.0
condition 0 3 0.0
subroutine 18 41 43.9
pod 5 6 83.3
total 77 291 26.4


line stmt bran cond sub pod time code
1             package Backup::Duplicity::YADW;
2             $Backup::Duplicity::YADW::VERSION = '0.12';
3             $Backup::Duplicity::YADW::VERSION = '0.11';
4 3     3   167736 use Modern::Perl;
  3         14381  
  3         21  
5 3     3   8609 use Moose;
  3         2130401  
  3         30  
6 3     3   95619 use namespace::autoclean;
  3         5302  
  3         20  
7 3     3   191 use warnings FATAL => 'all';
  3         6  
  3         140  
8 3     3   77295 use Smart::Args;
  3         102042  
  3         276  
9 3     3   46 use Carp;
  3         7  
  3         191  
10 3     3   4349 use Config::ApacheFormat;
  3         339823  
  3         131  
11 3     3   37 use File::Basename;
  3         8  
  3         339  
12 3     3   3317 use String::Util 'crunch', 'trim';
  3         10897  
  3         311  
13 3     3   9284 use IPC::Run3;
  3         180510  
  3         346  
14 3     3   44 use File::Path;
  3         7  
  3         171  
15 3     3   4328 use Data::Dumper;
  3         35987  
  3         275  
16 3     3   5178 use Sys::Syslog;
  3         85555  
  3         377  
17 3     3   4157 use PID::File;
  3         25708  
  3         126  
18              
19 3     3   29 use constant CONF_DIR => '/etc/yadw';
  3         6  
  3         166  
20 3     3   19 use constant CONF_FILE => 'default.conf';
  3         7  
  3         371  
21              
22 3     3   55 use constant PID_EXISTS => 10;
  3         9  
  3         385  
23              
24 3     3   22 use vars qw($ErrCode $ErrStr);
  3         7  
  3         10098  
25              
26             # ABSTRACT: Yet Another Duplicity Wrapper
27              
28              
29             has conf_dir => ( is => 'rw', isa => 'Str', default => CONF_DIR );
30              
31              
32             has conf_file => ( is => 'rw', isa => 'Str', default => CONF_FILE );
33              
34              
35             has dry_run => ( is => 'rw', isa => 'Bool', default => 0 );
36              
37              
38             has use_syslog => ( is => 'rw', isa => 'Bool' );
39              
40              
41             has verbose => ( is => 'rw', isa => 'Bool', default => 0 );
42              
43             has _conf => ( is => 'rw', isa => 'Config::ApacheFormat' );
44              
45             has _pid => ( is => 'rw', isa => 'PID::File' );
46              
47              
48             sub BUILD {
49 0     0 0   my $self = shift;
50              
51 0           $ErrCode = 0;
52              
53 0           my $conf =
54             Config::ApacheFormat->new( fix_booleans => 1,
55             autoload_support => 0 );
56              
57 0           $conf->read( $self->conf_dir . "/" . $self->conf_file );
58 0           $self->_conf($conf);
59 0           $self->_init_logs;
60 0           $self->_write_pidfile;
61             }
62              
63             sub backup {
64              
65 0     0 1   args_pos
66             my $self,
67             my $type => 'Str';
68              
69 0 0         $type = $type eq 'inc' ? 'incremental' : $type;
70              
71 0 0 0       confess "invalid type: $type"
72             if $type ne 'full' and $type ne 'incremental';
73              
74 0           my @cmd = ( 'duplicity', $type );
75              
76 0           $self->_get_verbosity( \@cmd );
77 0           $self->_get_exclude_device_files( \@cmd );
78 0           $self->_get_incl_excl_list( \@cmd );
79 0           $self->_get_encrypt_key( \@cmd );
80 0           $self->_get_log_file( \@cmd );
81 0           $self->_get_async_upload( \@cmd );
82 0           $self->_get_s3_new( \@cmd );
83 0           $self->_get_sourcedir( \@cmd );
84 0           $self->_get_targetdir( \@cmd );
85              
86 0           $self->_system(@cmd);
87              
88 0           return 1;
89             }
90              
91             sub _get_sourcedir {
92              
93 0     0     args_pos
94             my $self,
95             my $cmds;
96              
97 0           push @$cmds, $self->_conf->get('sourcedir');
98             }
99              
100             sub _get_targetdir {
101              
102 0     0     args_pos
103              
104             # required
105             my $self, my $cmds,
106              
107             # optional
108             my $locaction => { isa => 'Str', optional => 1 };
109              
110 0           my $str = $self->_conf->get('targeturl');
111 0 0         $str .= "/$locaction" if $locaction;
112              
113 0           push( @$cmds, $str );
114             }
115              
116             sub _get_async_upload {
117              
118 0     0     args_pos
119             my $self,
120             my $cmds;
121              
122 0 0         if ( $self->_conf()->get('asyncupload') ) {
123 0           push @$cmds, '--asynchronous-upload';
124             }
125             }
126              
127             sub _get_incl_excl_list {
128              
129 0     0     args_pos
130             my $self,
131             my $cmds;
132              
133 0           my $conf = $self->_conf;
134 0           my $block = $conf->block('inclexcl');
135 0           my @list = $block->get('list');
136              
137 0           for ( my $i = 0; $i < @list; $i += 2 ) {
138              
139 0           my $key = trim $list[$i];
140 0           my $val = trim $list[ $i + 1 ];
141              
142 0 0         if ( $key eq '-' ) {
    0          
143 0           $key = '--exclude';
144             }
145             elsif ( $key eq '+' ) {
146 0           $key = '--include';
147             }
148             else {
149 0           confess "malformed InclExcl section";
150             }
151              
152 0           push @$cmds, $key, $val;
153             }
154             }
155              
156             sub _write_pidfile {
157              
158 0     0     args_pos my $self;
159              
160 0           my $conf = $self->_conf;
161 0           my $pidfile = $conf->get('pidfile');
162              
163 0           $self->_log( 'info', "pidfile=$pidfile" );
164              
165 0           my $pid = PID::File->new( file => $pidfile);
166            
167 0 0         if ( -e $pid->file ) {
168 0 0         if ( $pid->running ) {
169 0           $ErrCode = PID_EXISTS;
170 0           $ErrStr = "yadw is already running";
171 0           confess $ErrStr;
172             }
173             else {
174 0           $self->_log( 'notice', "removing stale pidfile $pidfile" );
175 0 0         unlink $pid->file or confess "failed to remove pidfile: $!";
176             }
177             }
178              
179 0 0         $pid->create or confess "failed to write pidfile: $!";
180 0           $pid->guard; # remove pidfile automatically when it goes out of scope
181 0           $self->_pid($pid);
182             }
183              
184             sub _get_expire_days {
185              
186 0     0     args_pos
187             my $self,
188             my $cmds;
189              
190 0           my $days = $self->_conf->get('days2keepbackups');
191              
192 0 0         if ( !defined $days ) {
    0          
193 0           confess "missing configuration days2keepbackups";
194             }
195             elsif ( !$days ) {
196              
197             # confess "days2keepbackups must be greater than 0";
198             }
199              
200 0           push @$cmds, $days . 'D';
201             }
202              
203              
204             sub expire {
205              
206 0     0 1   args_pos my $self;
207              
208 0           $self->_log( 'info', "removing old backups" );
209              
210 0           my @cmd = ( 'duplicity', 'remove-older-than' );
211              
212 0           $self->_get_expire_days( \@cmd );
213 0           push @cmd, '--force';
214 0           push @cmd, '--extra-clean';
215 0           $self->_get_targetdir( \@cmd );
216              
217 0           $self->_system(@cmd);
218              
219 0           return 1;
220             }
221              
222              
223             sub status {
224 0     0 1   args_pos my $self;
225              
226 0           my @cmd = ( 'duplicity', 'collection-status' );
227              
228 0           $self->_get_encrypt_key( \@cmd );
229 0           $self->_get_s3_new( \@cmd );
230 0           $self->_get_targetdir( \@cmd );
231              
232 0           $self->_system(@cmd);
233              
234 0           return 1;
235             }
236              
237              
238             sub verify {
239              
240 0     0 1   args_pos my $self;
241              
242 0           $self->_log( 'info', "verifying backups" );
243              
244 0           my @cmd = ( 'duplicity', 'verify' );
245              
246 0           $self->_get_verbosity( \@cmd );
247 0           $self->_get_exclude_device_files( \@cmd );
248 0           $self->_get_incl_excl_list( \@cmd );
249 0           $self->_get_encrypt_key( \@cmd );
250 0           $self->_get_log_file( \@cmd );
251 0           $self->_get_s3_new( \@cmd );
252 0           $self->_get_targetdir( \@cmd );
253 0           $self->_get_sourcedir( \@cmd );
254              
255 0           $self->_system(@cmd);
256              
257 0           return 1;
258             }
259              
260             sub _system {
261              
262 0     0     my $self = shift;
263              
264 0           $self->_log( 'info', "@_" );
265              
266 0           my @stderr;
267 0           run3( [@_], undef, undef, \@stderr );
268 0           my $exit = $? >> 8;
269 0 0         if ($exit) {
270 0           $self->_log( 'err', "@stderr" );
271 0           confess "duplicity exited with $exit";
272             }
273              
274 0           $self->_log( 'info', "done" );
275             }
276              
277             sub _log {
278              
279 0     0     args_pos
280             my $self,
281             my $level,
282             my $msg;
283              
284 0 0         if ( $self->use_syslog ) {
285 0           syslog( $level, $msg );
286             }
287              
288 0           $self->_verbose($msg);
289             }
290              
291             sub _get_exclude_device_files {
292              
293 0     0     args_pos
294             my $self,
295             my $cmds;
296              
297 0 0         if ( $self->_conf->get('excludedevicefiles') ) {
298 0           push @$cmds, '--exclude-device-files';
299             }
300             }
301              
302             sub _get_verbosity {
303              
304 0     0     args_pos
305             my $self,
306             my $cmds;
307              
308 0           my $level = $self->_conf->get('verbosity');
309              
310 0           push @$cmds, "-v$level";
311             }
312              
313             sub _get_log_file {
314              
315 0     0     args_pos
316             my $self,
317             my $cmds;
318              
319 0           my $fullpath = $self->_conf->get('logfile');
320              
321 0           mkpath( dirname $fullpath);
322              
323 0           push @$cmds, "--log-file", $fullpath;
324             }
325              
326             sub _get_syslog {
327 0     0     args_pos my $self;
328              
329 0           my $toggle = $self->_conf->get('syslog');
330              
331 0 0         if ($toggle) {
332 0           $self->use_syslog(1);
333             }
334             else {
335 0           $self->use_syslog(0);
336             }
337             }
338              
339             sub _get_s3_new {
340              
341 0     0     args_pos
342             my $self,
343             my $cmds;
344              
345 0 0         if ( $self->_conf->get('s3usenewstyle') ) {
346 0           push @$cmds, '--s3-use-new-style';
347             }
348             }
349              
350             sub _get_encrypt_key {
351              
352 0     0     args_pos
353             my $self,
354             my $cmds;
355              
356 0           my $key = $self->_conf->get('encryptkey');
357              
358 0 0         if ( !$key ) {
359 0           push @$cmds, '--no-encrypt';
360             }
361             else {
362              
363 0           push @$cmds, "--encrypt-key", $key;
364             }
365             }
366              
367              
368             sub restore {
369 0     0 1   args
370              
371             # required
372             my $self => __PACKAGE__,
373             my $location => 'Str',
374              
375             # optional
376             my $days => { isa => 'Int', optional => 1 };
377              
378 0           $self->_log( 'info', "restoring $location" );
379              
380 0           my @cmd = ( 'duplicity', 'restore' );
381              
382 0           $self->_get_verbosity( \@cmd );
383 0           $self->_get_encrypt_key( \@cmd );
384 0           $self->_get_log_file( \@cmd );
385 0           $self->_get_s3_new( \@cmd );
386 0           $self->_get_targetdir( \@cmd );
387 0           push( @cmd, $location );
388              
389 0           $self->_system(@cmd);
390              
391 0           return 1;
392             }
393              
394 0     0     sub _get_dry_run {
395              
396             # TODO
397             }
398              
399             sub _init_logs {
400              
401 0     0     args_pos my $self;
402              
403 0           $self->_get_syslog;
404              
405 0 0         if ( $self->use_syslog ) {
406 0           openlog( 'backups', $$, 'user' );
407             }
408              
409 0           $self->_log( 'info', "$0 @ARGV" );
410             }
411              
412             sub _verbose {
413              
414 0     0     my $self = shift;
415              
416 0 0         print STDERR "[VERBOSE] @_\n" if $self->verbose;
417             }
418              
419             __PACKAGE__->meta->make_immutable;
420              
421             1; # End of Backup::Duplicity::YADW
422              
423             __END__
424              
425             =pod
426              
427             =encoding UTF-8
428              
429             =head1 NAME
430              
431             Backup::Duplicity::YADW - Yet Another Duplicity Wrapper
432              
433             =head1 VERSION
434              
435             version 0.12
436              
437             =head1 SYNOPSIS
438              
439             $yadw = Backup::Duplicity::YADW->new;
440            
441             $yadw = Backup::Duplicity::YADW->new(
442             conf_dir => '/etc/mydir',
443             conf_file => 'other.conf',
444             dry_run => 0,
445             use_syslog => 1,
446             verbose => 0
447             );
448            
449             $yadw->backup();
450             $yadw->verify();
451             $yadw->expire();
452              
453             $yadw->restore("/my/file/location");
454              
455             =head1 DESCRIPTION
456              
457             This is a wrapper for Duplicity. I found my command lines for invoking
458             Duplicity getting quite lengthy and wanted a way to persist my configurations
459             in an intuitive manner. I looked at several other Duplicity wrappers, but
460             none of them quite fit what I wanted. So Backup::Duplicity::YADW was born.
461              
462             =head1 NAME
463              
464             Backup::Duplicity::YADW - Yet Another Duplicity Wrapper
465              
466             =head1 VERSION
467              
468             version 0.11
469              
470             =head1 ATTRIBUTES
471              
472             =head2 conf_dir
473              
474             Config file path. Default is /etc/yadw.
475              
476             =head2 conf_file
477              
478             Config file name. Default is default.conf.
479              
480             =head2 dry_run
481              
482             Do a dry run.
483              
484             =head2 use_syslog
485              
486             Tells the module to write log data using the syslog facility
487              
488             =head2 verbose
489              
490             Print extra messages about whats going on.
491              
492             =head1 METHODS
493              
494             =head2 new( [ %attributes ] )
495              
496             Constructor - 'nuff said
497              
498             =head2 backup( $type )
499              
500             Tell duplicity to do a backup. Requires either 'full' or 'inc' for a type.
501             Returns true on success.
502              
503             =head2 expire( )
504              
505             Tell duplicity to "remove-older-than <days in conf file>".
506              
507             =head2 status( )
508              
509             Equivalent to "collection-status" in duplicity. Returns true on success.
510              
511             =head2 verify( )
512              
513             Tell duplicity to verify backups. Returns true on success.
514              
515             =head2 restore( %args )
516              
517             Tell duplicity to do a restore.
518              
519             Required args:
520              
521             location => $path
522              
523             Optional args:
524              
525             time => $time (see duplicity manpage)
526              
527             Returns true on success.
528              
529             =head1 SEE ALSO
530              
531             yadw (ready to use backup script)
532              
533             =head1 AUTHOR
534              
535             John Gravatt <john@gravatt.org>
536              
537             =head1 COPYRIGHT AND LICENSE
538              
539             This software is copyright (c) 2013 by John Gravatt.
540              
541             This is free software; you can redistribute it and/or modify it under
542             the same terms as the Perl 5 programming language system itself.
543              
544             =head1 AUTHOR
545              
546             John Gravatt <john@gravatt.org>
547              
548             =head1 COPYRIGHT AND LICENSE
549              
550             This software is copyright (c) 2013 by John Gravatt.
551              
552             This is free software; you can redistribute it and/or modify it under
553             the same terms as the Perl 5 programming language system itself.
554              
555             =cut