File Coverage

blib/lib/Dackup.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Dackup;
2 1     1   234657 use Moose;
  0            
  0            
3             use MooseX::StrictConstructor;
4             use MooseX::Types::Path::Class;
5             use Dackup::Cache;
6             use Dackup::Entry;
7             use Dackup::Target::CloudFiles;
8             use Dackup::Target::Filesystem;
9             use Dackup::Target::S3;
10             use Dackup::Target::SSH;
11             use Data::Stream::Bulk::Path::Class;
12             use DBI;
13             use Devel::CheckOS qw(os_is);
14             use File::HomeDir;
15             use List::Util qw(sum);
16             use Number::DataRate;
17             use Path::Class;
18             use Term::ProgressBar::Simple;
19              
20             our $VERSION = '0.44';
21              
22             has 'directory' => (
23             is => 'ro',
24             isa => 'Path::Class::Dir',
25             required => 0,
26             coerce => 1,
27             default => sub {
28             my $self = shift;
29             return dir( File::HomeDir->my_data,
30             ( os_is('MicrosoftWindows') ? 'Perl' : '.perl' ), 'Dackup' );
31             },
32             );
33             has 'source' => (
34             is => 'ro',
35             isa => 'Dackup::Target',
36             required => 1,
37             );
38             has 'destination' => (
39             is => 'ro',
40             isa => 'Dackup::Target',
41             required => 1,
42             );
43             has 'delete' => (
44             is => 'rw',
45             isa => 'Bool',
46             required => 1,
47             );
48             has 'dry_run' => (
49             is => 'rw',
50             isa => 'Bool',
51             default => 0,
52             );
53             has 'verbose' => (
54             is => 'rw',
55             isa => 'Bool',
56             default => 0,
57             );
58             has 'cache' => (
59             is => 'rw',
60             isa => 'Dackup::Cache',
61             );
62             has 'throttle' => (
63             is => 'ro',
64             isa => 'Str',
65             );
66              
67             __PACKAGE__->meta->make_immutable;
68              
69             sub BUILD {
70             my $self = shift;
71             my $directory = $self->directory;
72              
73             unless ( -d $directory ) {
74             $directory->mkpath
75             || confess "Unable to create directory $directory: $!";
76             }
77              
78             my $filename = file( $directory, 'dackup.db' );
79             my $cache = Dackup::Cache->new( filename => $filename );
80             $self->cache($cache);
81             $self->source->dackup($self);
82             $self->destination->dackup($self);
83             }
84              
85             sub backup {
86             my $self = shift;
87             my $source = $self->source;
88             my $destination = $self->destination;
89             my $delete = $self->delete;
90             my $dry_run = $self->dry_run;
91             my $verbose = $self->verbose;
92              
93             my $source_entries = $source->entries;
94             my $destination_entries = $destination->entries;
95              
96             my ( $entries_to_update, $entries_to_delete )
97             = $self->_calc( $source_entries, $destination_entries );
98              
99             my $total = sum map { $_->size } @$entries_to_update;
100             $total += scalar(@$entries_to_delete) if $delete;
101             $total = 0 unless defined $total;
102              
103             my $progress = Term::ProgressBar::Simple->new($total);
104             $progress->message(
105             'Updating ' . scalar(@$entries_to_update) . ' files' );
106             $progress->message( 'Deleting ' . scalar(@$entries_to_delete) . ' files' )
107             if $delete;
108              
109             foreach my $entry (@$entries_to_update) {
110             if ($verbose) {
111             my $source_name = $source->name($entry);
112             my $destination_name = $destination->name($entry);
113             $progress->message("$source_name -> $destination_name");
114             }
115             $destination->update( $source, $entry ) unless $dry_run;
116             $progress += $entry->size;
117             }
118              
119             if ($delete) {
120             foreach my $entry (@$entries_to_delete) {
121             if ($verbose) {
122             my $name = $destination->name($entry);
123             $progress->message("Deleting $name");
124             }
125             $destination->delete($entry) unless $dry_run;
126             $progress++;
127             }
128             }
129              
130             $progress->message( 'Updated ' . scalar(@$entries_to_update) . ' files' );
131             $progress->message( 'Deleted ' . scalar(@$entries_to_delete) . ' files' )
132             if $delete;
133              
134             return scalar(@$entries_to_update);
135             }
136              
137             sub _calc {
138             my ( $self, $source_entries, $destination_entries ) = @_;
139             my %source_entries;
140             my %destination_entries;
141              
142             $source_entries{ $_->key } = $_ foreach @$source_entries;
143             $destination_entries{ $_->key } = $_ foreach @$destination_entries;
144              
145             my @entries_to_update;
146             my @entries_to_delete;
147              
148             foreach my $key ( sort keys %source_entries ) {
149             my $source_entry = $source_entries{$key};
150             my $destination_entry = $destination_entries{$key};
151             if ($destination_entry) {
152             if ( $source_entry->md5_hex eq $destination_entry->md5_hex ) {
153              
154             # warn "$key same";
155             } else {
156              
157             # warn "$key different";
158             push @entries_to_update, $source_entry;
159             }
160             } else {
161              
162             # warn "$key missing";
163             push @entries_to_update, $source_entry;
164             }
165             }
166              
167             foreach my $key ( sort keys %destination_entries ) {
168             my $source_entry = $source_entries{$key};
169             my $destination_entry = $destination_entries{$key};
170             unless ($source_entry) {
171              
172             # warn "$key to delete";
173             push @entries_to_delete, $destination_entry;
174             }
175             }
176              
177             return \@entries_to_update, \@entries_to_delete;
178             }
179              
180             1;
181              
182             __END__
183              
184             =head1 NAME
185              
186             Dackup - Flexible file backup
187              
188             =head1 SYNOPSIS
189              
190             use Dackup;
191              
192             my $source = Dackup::Target::Filesystem->new(
193             prefix => '/home/acme/important/' );
194              
195             my $destination = Dackup::Target::Filesystem->new(
196             prefix => '/home/acme/backup/' );
197              
198             my $dackup = Dackup->new(
199             source => $source,
200             destination => $destination,
201             delete => 0,
202             dry_run => 0,
203             verbose => 1,
204             throttle => '1Mbps',
205             );
206             $dackup->backup;
207              
208             =head1 DESCRIPTION
209              
210             This module is an attempt at a flexible file backup. It supports
211             copying to and from filesystems, remote hosts via SSH, Amazon's
212             Simple Storage Service and Mosso's CloudFiles. At all stages,
213             it checks the MD5 hash of the source and destination files.
214              
215             It uses an MD5 cache to speed up operations, which it stores by
216             default in your home directory (you can pass it as a directory
217             parameter). It's just a cache, so you can delete it, but the next
218             time you sync it might be a little slower.
219              
220             It will update new and changed files. If you pass in
221             delete => 1 then it will also delete removed files.
222              
223             =head1 AUTHOR
224              
225             Leon Brocard <acme@astray.com>
226              
227             =head1 COPYRIGHT
228              
229             Copyright (C) 2009, Leon Brocard.
230              
231             =head1 LICENSE
232              
233             This module is free software; you can redistribute it or
234             modify it under the same terms as Perl itself.