File Coverage

blib/lib/Brackup/Root.pm
Criterion Covered Total %
statement 91 131 69.4
branch 10 34 29.4
condition 11 17 64.7
subroutine 25 31 80.6
pod 6 18 33.3
total 143 231 61.9


line stmt bran cond sub pod time code
1             package Brackup::Root;
2 13     13   85 use strict;
  13         27  
  13         561  
3 13     13   80 use warnings;
  13         31  
  13         464  
4 13     13   73 use Carp qw(croak);
  13         30  
  13         786  
5 13     13   78 use File::Find;
  13         25  
  13         2074  
6 13     13   7329 use Brackup::DigestCache;
  13         43  
  13         3213  
7 13     13   100 use Brackup::Util qw(io_print_to_fh);
  13         28  
  13         717  
8 13     13   15741 use IPC::Open2;
  13         78466  
  13         876  
9 13     13   118 use Symbol;
  13         30  
  13         30118  
10              
11             sub new {
12 8     8 0 258 my ($class, $conf) = @_;
13 8         422 my $self = bless {}, $class;
14              
15 8 50       45 ($self->{name}) = $conf->name =~ m/^SOURCE:(.+)$/
16             or die "No backup-root name provided.";
17 8 50       87 die "Backup-root name must be only a-z, A-Z, 0-9, and _." unless $self->{name} =~ /^\w+/;
18              
19 8         65 $self->{dir} = $conf->path_value('path');
20 8   50     47 $self->{gpg_path} = $conf->value('gpg_path') || "gpg";
21 8         72 $self->{gpg_rcpt} = [ $conf->values('gpg_recipient') ];
22 8         46 $self->{chunk_size} = $conf->byte_value('chunk_size');
23 8         123 $self->{ignore} = [];
24              
25 8         80 $self->{smart_mp3_chunking} = $conf->bool_value('smart_mp3_chunking');
26              
27 8         29 $self->{merge_files_under} = $conf->byte_value('merge_files_under');
28 8   100     51 $self->{max_composite_size} = $conf->byte_value('max_composite_chunk_size') || 2**20;
29              
30 8 50       103 die "'max_composite_chunk_size' must be greater than 'merge_files_under'\n" unless
31             $self->{max_composite_size} > $self->{merge_files_under};
32              
33 8         109 $self->{gpg_args} = []; # TODO: let user set this. for now, not possible
34              
35 8         113 $self->{digcache} = Brackup::DigestCache->new($self, $conf);
36 8         257 $self->{digcache_file} = $self->{digcache}->backing_file; # may be empty, if digest cache doesn't use a file
37              
38 8         1049 $self->{noatime} = $conf->value('noatime');
39 8         54 return $self;
40             }
41              
42 8     8 1 36 sub merge_files_under { $_[0]{merge_files_under} }
43 2     2 0 10 sub max_composite_size { $_[0]{max_composite_size} }
44 0     0 1 0 sub smart_mp3_chunking { $_[0]{smart_mp3_chunking} }
45              
46             sub gpg_path {
47 3     3 0 19 my $self = shift;
48 3         43 return $self->{gpg_path};
49             }
50              
51             sub gpg_args {
52 11     11 0 36 my $self = shift;
53 11         25 return @{ $self->{gpg_args} };
  11         163396  
54             }
55              
56             sub gpg_rcpts {
57 582     582 0 1290 my $self = shift;
58 582         1736 return @{ $self->{gpg_rcpt} };
  582         5864  
59             }
60              
61             # returns Brackup::DigestCache object
62             sub digest_cache {
63 121     121 0 361 my $self = shift;
64 121         919 return $self->{digcache};
65             }
66              
67             sub chunk_size {
68 90     90 1 192 my $self = shift;
69 90   100     1332 return $self->{chunk_size} || (64 * 2**20); # default to 64MB
70             }
71              
72             sub publicname {
73             # FIXME: let users define the public (obscured) name of their roots. s/porn/media/, etc.
74             # because their metafile key names (which contain the root) aren't encrypted.
75 8     8 0 110 return $_[0]{name};
76             }
77              
78             sub name {
79 186     186 0 9526 return $_[0]{name};
80             }
81              
82             sub ignore {
83 41     41 1 69 my ($self, $pattern) = @_;
84 41         54 push @{ $self->{ignore} }, qr/$pattern/;
  41         311  
85             }
86              
87             sub path {
88 167     167 1 4409 return $_[0]{dir};
89             }
90              
91             sub noatime {
92 110     110 1 4069 return $_[0]{noatime};
93             }
94              
95             sub foreach_file {
96 8     8 0 28 my ($self, $cb) = @_;
97              
98 8 50       229 chdir $self->{dir} or die "Failed to chdir to $self->{dir}";
99              
100 8         18 my %statcache; # file -> statobj
101              
102             find({
103             no_chdir => 1,
104             preprocess => sub {
105 20     20   2376 my $dir = $File::Find::dir;
106 20         252 my @good_dentries;
107             DENTRY:
108 20         297 foreach my $dentry (@_) {
109 142 100 100     1089 next if $dentry eq "." || $dentry eq "..";
110              
111 102         1010 my $path = "$dir/$dentry";
112 102         458 $path =~ s!^\./!!;
113              
114             # skip the digest database file. not sure if this is smart or not.
115             # for now it'd be kinda nice to have, but it's re-creatable from
116             # the backup meta files later, so let's skip it.
117 102 50 33     793 next if $self->{digcache_file} && $path eq $self->{digcache_file};
118              
119             # GC: seems to work fine as of at least gpg 1.4.5, so commenting out
120             # gpg seems to barf on files ending in whitespace, blowing
121             # stuff up, so we just skip them instead...
122             #if ($self->gpg_rcpts && $path =~ /\s+$/) {
123             # warn "Skipping file ending in whitespace: <$path>\n";
124             # next;
125             #}
126              
127 102         316 my $statobj = File::stat::lstat($path);
128 102         20827 my $is_dir = -d _;
129              
130 102         408 foreach my $pattern (@{ $self->{ignore} }) {
  102         363  
131 524 50       3102 next DENTRY if $path =~ /$pattern/;
132 524 50 66     2018 next DENTRY if $is_dir && "$path/" =~ /$pattern/;
133 524 50       1608 next DENTRY if $path =~ m!(^|/)\.brackup-digest\.db(-journal)?$!;
134             }
135              
136 102         375 $statcache{$path} = $statobj;
137 102         238 push @good_dentries, $dentry;
138             }
139              
140             # to let it recurse into the good directories we didn't
141             # already throw away:
142 20         796 return sort @good_dentries;
143             },
144              
145             wanted => sub {
146 110     110   3774 my $path = $_;
147 110         481 $path =~ s!^\./!!;
148              
149 110         726 my $stat_obj = delete $statcache{$path};
150 110         641 my $file = Brackup::File->new(root => $self,
151             path => $path,
152             stat => $stat_obj,
153             );
154 110         351 $cb->($file);
155             },
156 8         1016 }, ".");
157             }
158              
159             sub as_string {
160 81     81 0 180 my $self = shift;
161 81         3183 return $self->{name} . "($self->{dir})";
162             }
163              
164             sub du_stats {
165 0     0 0   my $self = shift;
166              
167 0           my $show_all = $ENV{BRACKUP_DU_ALL};
168 0           my @dir_stack;
169             my %dir_size;
170             my $pop_dir = sub {
171 0     0     my $dir = pop @dir_stack;
172 0   0       printf("%-20d%s\n", $dir_size{$dir} || 0, $dir);
173 0           delete $dir_size{$dir};
174 0           };
175             my $start_dir = sub {
176 0     0     my $dir = shift;
177 0 0         unless ($dir eq ".") {
178 0           my @parts = (".", split(m!/!, $dir));
179 0           while (@dir_stack >= @parts) {
180 0           $pop_dir->();
181             }
182             }
183 0           push @dir_stack, $dir;
184 0           };
185             $self->foreach_file(sub {
186 0     0     my $file = shift;
187 0           my $path = $file->path;
188 0 0         if ($file->is_dir) {
189 0           $start_dir->($path);
190 0           return;
191             }
192 0 0         if ($file->is_file) {
193 0           my $size = $file->size;
194 0 0         my $kB = int($size / 1024) + ($size % 1024 ? 1 : 0);
195 0 0         printf("%-20d%s\n", $kB, $path) if $show_all;
196 0           $dir_size{$_} += $kB foreach @dir_stack;
197             }
198 0           });
199              
200 0           $pop_dir->() while @dir_stack;
201             }
202              
203             # given filehandle to data, returns encrypted data
204             sub encrypt {
205 0     0 0   my ($self, $data_fh, $outfn) = @_;
206 0 0         my @gpg_rcpts = $self->gpg_rcpts
207             or Carp::confess("Encryption not setup for this root");
208              
209 0           my $cout = Symbol::gensym();
210 0           my $cin = Symbol::gensym();
211              
212 0           my @recipients = map {("--recipient", $_)} @gpg_rcpts;
  0            
213 0           my $pid = IPC::Open2::open2($cout, $cin,
214             $self->gpg_path, $self->gpg_args,
215             @recipients,
216             "--trust-model=always",
217             "--batch",
218             "--encrypt",
219             "--output", $outfn,
220             "--yes",
221             "-" # read from stdin
222             );
223              
224             # send data to gpg
225 0           binmode $cin;
226 0 0         my $bytes = io_print_to_fh($data_fh, $cin)
227             or die "Sending data to gpg failed: $!";
228              
229 0           close $cin;
230 0           close $cout;
231              
232 0           waitpid($pid, 0);
233 0 0         die "GPG failed: $!" if $? != 0; # If gpg return status is non-zero
234             }
235              
236             1;
237              
238             __END__