File Coverage

blib/lib/Brackup/Backup.pm
Criterion Covered Total %
statement 257 287 89.5
branch 61 100 61.0
condition 28 43 65.1
subroutine 32 32 100.0
pod 0 17 0.0
total 378 479 78.9


line stmt bran cond sub pod time code
1             package Brackup::Backup;
2 13     13   69 use strict;
  13         20  
  13         668  
3 13     13   67 use warnings;
  13         26  
  13         368  
4 13     13   63 use Carp qw(croak);
  13         24  
  13         678  
5 13     13   6714 use Brackup::ChunkIterator;
  13         35  
  13         459  
6 13     13   10022 use Brackup::CompositeChunk;
  13         38  
  13         387  
7 13     13   8416 use Brackup::GPGProcManager;
  13         47  
  13         456  
8 13     13   91 use Brackup::GPGProcess;
  13         30  
  13         300  
9 13     13   80 use File::Basename;
  13         28  
  13         1217  
10 13     13   85 use File::Temp qw(tempfile);
  13         25  
  13         63176  
11              
12             sub new {
13 8     8 0 74 my ($class, %opts) = @_;
14 8         35 my $self = bless {}, $class;
15              
16 8         57 $self->{root} = delete $opts{root}; # Brackup::Root
17 8         29 $self->{target} = delete $opts{target}; # Brackup::Target
18 8         30 $self->{dryrun} = delete $opts{dryrun}; # bool
19 8         31 $self->{verbose} = delete $opts{verbose}; # bool
20 8         189 $self->{inventory} = delete $opts{inventory}; # bool
21 8         27 $self->{savefiles} = delete $opts{savefiles}; # bool
22 8         29 $self->{zenityprogress} = delete $opts{zenityprogress}; # bool
23              
24 8         39 $self->{modecounts} = {}; # type -> mode(octal) -> count
25 8         30 $self->{idcounts} = {}; # type -> uid/gid -> count
26              
27 8         20 $self->{_uid_map} = {}; # uid -> username
28 8         22 $self->{_gid_map} = {}; # gid -> group
29              
30 8         24 $self->{saved_files} = []; # list of Brackup::File objects backed up
31 8         23 $self->{unflushed_files} = []; # list of Brackup::File objects not in backup_file
32              
33 8 50       38 croak("Unknown options: " . join(', ', keys %opts)) if %opts;
34              
35 8         32 return $self;
36             }
37              
38             # returns true (a Brackup::BackupStats object) on success, or dies with error
39             sub backup {
40 8     8 0 19 my ($self, $backup_file) = @_;
41              
42 8         25 my $root = $self->{root};
43 8         22 my $target = $self->{target};
44              
45 8         171 my $stats = Brackup::BackupStats->new;
46              
47 8         400 my @gpg_rcpts = $self->{root}->gpg_rcpts;
48              
49 8         21 my $n_kb = 0.0; # num: kb of all files in root
50 8         22 my $n_files = 0; # int: # of files in root
51 8         18 my $n_kb_done = 0.0; # num: kb of files already done with (uploaded or skipped)
52              
53             # if we're pre-calculating the amount of data we'll
54             # actually need to upload, store it here.
55 8         23 my $n_files_up = 0;
56 8         18 my $n_kb_up = 0.0;
57 8         24 my $n_kb_up_need = 0.0; # by default, not calculated/used.
58              
59 8         16 my $n_files_done = 0; # int
60 8         14 my @files; # Brackup::File objs
61              
62 8         54 $self->debug("Discovering files in ", $root->path, "...\n");
63 8         37 $self->report_progress(0, "Discovering files in " . $root->path . "...");
64             $root->foreach_file(sub {
65 110     110   144 my ($file) = @_; # a Brackup::File
66 110         192 push @files, $file;
67 110         293 $self->record_mode_ids($file);
68 110         940 $n_files++;
69 110         319 $n_kb += $file->size / 1024;
70 8         104 });
71              
72 8         558 $self->debug("Number of files: $n_files\n");
73 8         70 $stats->timestamp('File Discovery');
74 8         49 $stats->set('Number of Files' => $n_files);
75 8         164 $stats->set('Total File Size' => sprintf('%0.01f MB', $n_kb / 1024));
76              
77             # calc needed chunks
78 8 50       46 if ($ENV{CALC_NEEDED}) {
79 0         0 my $fn = 0;
80 0         0 foreach my $f (@files) {
81 0         0 $fn++;
82 0 0       0 if ($fn % 100 == 0) { warn "$fn / $n_files ...\n"; }
  0         0  
83 0         0 foreach my $pc ($f->chunks) {
84 0 0       0 if ($target->stored_chunk_from_inventory($pc)) {
85 0         0 $pc->forget_chunkref;
86 0         0 next;
87             }
88 0         0 $n_kb_up_need += $pc->length / 1024;
89 0         0 $pc->forget_chunkref;
90             }
91             }
92 0         0 warn "kb need to upload = $n_kb_up_need\n";
93 0         0 $stats->timestamp('Calc Needed');
94             }
95              
96              
97 8         108 my $chunk_iterator = Brackup::ChunkIterator->new(@files);
98 8         33 undef @files;
99 8         34 $stats->timestamp('Chunk Iterator');
100              
101 8         46 my $gpg_iter;
102             my $gpg_pm; # gpg ProcessManager
103 8 100       33 if (@gpg_rcpts) {
104 3         37 ($chunk_iterator, $gpg_iter) = $chunk_iterator->mux_into(2);
105 3         61 $gpg_pm = Brackup::GPGProcManager->new($gpg_iter, $target);
106             }
107              
108             # begin temp backup_file
109 8         26 my ($metafh, $meta_filename);
110 8 50       47 unless ($self->{dryrun}) {
111 8         1187 ($metafh, $meta_filename) = tempfile(
112             '.' . basename($backup_file) . 'XXXXX',
113             DIR => dirname($backup_file),
114             );
115 8 100       6148 if (! @gpg_rcpts) {
116 5 50       18 if (eval { require IO::Compress::Gzip }) {
  5         11418  
117 5         344671 close $metafh;
118 5 50       47 $metafh = IO::Compress::Gzip->new($meta_filename)
119             or die "Cannot open tempfile with IO::Compress::Gzip: $IO::Compress::Gzip::GzipError";
120             }
121             }
122 8         18982 print $metafh $self->backup_header;
123             }
124              
125 8         933 my $cur_file; # current (last seen) file
126             my @stored_chunks;
127 8         20 my $file_has_shown_status = 0;
128              
129 8         46 my $merge_under = $root->merge_files_under;
130 8         20 my $comp_chunk = undef;
131              
132             my $end_file = sub {
133 118 100   118   404 return unless $cur_file;
134 110 100 100     721 if ($merge_under && $comp_chunk) {
135             # defer recording to backup_file until CompositeChunk finalization
136 28         312 $self->add_unflushed_file($cur_file, [ @stored_chunks ]);
137             }
138             else {
139 82 50       1199 print $metafh $cur_file->as_rfc822([ @stored_chunks ], $self) if $metafh;
140             }
141 110 50       19436 $self->add_saved_file($cur_file, [ @stored_chunks ]) if $self->{savefiles};
142 110         242 $n_files_done++;
143 110         471 $n_kb_done += $cur_file->size / 1024;
144 110         5908 $cur_file = undef;
145 8         83 };
146             my $show_status = sub {
147             # use either size of files in normal case, or if we pre-calculated
148             # the size-to-upload (by looking in inventory, then we'll show the
149             # more accurate percentage)
150 85 50   85   1054 my $percdone = 100 * ($n_kb_up_need ?
151             ($n_kb_up / $n_kb_up_need) :
152             ($n_kb_done / $n_kb));
153 85 50       426 my $mb_remain = ($n_kb_up_need ?
154             ($n_kb_up_need - $n_kb_up) :
155             ($n_kb - $n_kb_done)) / 1024;
156              
157 85         553 $self->debug(sprintf("* %-60s %d/%d (%0.02f%%; remain: %0.01f MB)",
158             $cur_file->path, $n_files_done, $n_files, $percdone,
159             $mb_remain));
160              
161 85         740 $self->report_progress($percdone);
162 8         109 };
163             my $start_file = sub {
164 110     110   490 $end_file->();
165 110         428 $cur_file = shift;
166 110         276 @stored_chunks = ();
167 110 100       2136 $show_status->() if $cur_file->is_dir;
168 110 100       1811 if ($gpg_iter) {
169             # catch our gpg iterator up. we want it to be ahead of us,
170             # nothing iteresting is behind us.
171 40         281 $gpg_iter->next while $gpg_iter->behind_by > 1;
172             }
173 110         266 $file_has_shown_status = 0;
174 8         45 };
175              
176             # records are either Brackup::File (for symlinks, directories, etc), or
177             # PositionedChunks, in which case the file can asked of the chunk
178 8         170 while (my $rec = $chunk_iterator->next) {
179 128 100       2741 if ($rec->isa("Brackup::File")) {
180 20         60 $start_file->($rec);
181 20         84 next;
182             }
183 108         373 my $pchunk = $rec;
184 108 100       571 if ($pchunk->file != $cur_file) {
185 90         315 $start_file->($pchunk->file);
186             }
187              
188             # have we already stored this chunk before? (iterative backup)
189 108         228 my $schunk;
190 108 100       1004 if ($schunk = $target->stored_chunk_from_inventory($pchunk)) {
191 23         147 $pchunk->forget_chunkref;
192 23         298 push @stored_chunks, $schunk;
193 23         597 next;
194             }
195              
196             # weird case... have we stored this same pchunk digest in the
197             # current comp_chunk we're building? these aren't caught by
198             # the above inventory check, because chunks in a composite
199             # chunk aren't added to the inventory until after the the composite
200             # chunk has fully grown (because it's not until it's fully grown
201             # that we know the handle for it, its digest)
202 85 100 100     1023 if ($comp_chunk && ($schunk = $comp_chunk->stored_chunk_from_dup_internal_raw($pchunk))) {
203 4         20 $pchunk->forget_chunkref;
204 4         14 push @stored_chunks, $schunk;
205 4         34 next;
206             }
207              
208 81 100       878 unless ($file_has_shown_status++) {
209 65         258 $show_status->();
210 65         134 $n_files_up++;
211             }
212 81         898 $self->debug(" * storing chunk: ", $pchunk->as_string, "\n");
213 81         528 $self->report_progress(undef, $pchunk->file->path . " (" . $pchunk->offset . "," . $pchunk->length . ")");
214              
215 81 50       369 unless ($self->{dryrun}) {
216 81         1449 $schunk = Brackup::StoredChunk->new($pchunk);
217              
218             # encrypt it
219 81 100       1309 if (@gpg_rcpts) {
220 33         317 $schunk->set_encrypted_chunkref($gpg_pm->enc_chunkref_of($pchunk));
221             }
222              
223             # see if we should pack it into a bigger blob
224 81         876 my $chunk_size = $schunk->backup_length;
225              
226             # see if we should merge this chunk (in this case, file) together with
227             # other small files we encountered earlier, into a "composite chunk",
228             # to be stored on the target in one go.
229              
230             # Note: no technical reason for only merging small files (is_entire_file),
231             # and not the tails of larger files. just don't like the idea of files being
232             # both split up (for big head) and also merged together (for little end).
233             # would rather just have 1 type of magic per file. (split it or join it)
234 81 100 100     980 if ($merge_under && $chunk_size < $merge_under && $pchunk->is_entire_file) {
      66        
235 11 50 66     210 if ($comp_chunk && ! $comp_chunk->can_fit($chunk_size)) {
236 0         0 $self->debug("Finalizing composite chunk $comp_chunk...");
237 0         0 $comp_chunk->finalize;
238 0         0 $comp_chunk = undef;
239 0         0 $self->flush_files($metafh);
240             }
241 11   66     1676 $comp_chunk ||= Brackup::CompositeChunk->new($root, $target);
242 11         102 $comp_chunk->append_little_chunk($schunk);
243             } else {
244             # store it regularly, as its own chunk on the target
245 70 50       7072 $target->store_chunk($schunk)
246             or die "Chunk storage failed.\n";
247 70         1478 $target->add_to_inventory($pchunk => $schunk);
248             }
249              
250             # if only this worked... (LWP protocol handler seems to
251             # get confused by its syscalls getting interrupted?)
252             #local $SIG{CHLD} = sub {
253             # print "some child finished!\n";
254             # $gpg_pm->start_some_processes;
255             #};
256              
257              
258 81         1097 $n_kb_up += $pchunk->length / 1024;
259 81         781 $schunk->forget_chunkref;
260 81         638 push @stored_chunks, $schunk;
261             }
262              
263             #$stats->note_stored_chunk($schunk);
264              
265             # DEBUG: verify it got written correctly
266 81 50       533 if ($ENV{BRACKUP_PARANOID}) {
267 0         0 die "FIX UP TO NEW API";
268             #my $saved_ref = $target->load_chunk($handle);
269             #my $saved_len = length $$saved_ref;
270             #unless ($saved_len == $chunk->backup_length) {
271             # warn "Saved length of $saved_len doesn't match our length of " . $chunk->backup_length . "\n";
272             # die;
273             #}
274             }
275              
276 81         2194 $stats->check_maxmem;
277 81         428 $pchunk->forget_chunkref;
278             }
279 8         55 $end_file->();
280 8 100       52 $comp_chunk->finalize if $comp_chunk;
281 8         57 $self->flush_files($metafh);
282 8         219 $stats->timestamp('Chunk Storage');
283 8         51 $stats->set('Number of Files Uploaded:', $n_files_up);
284 8         175 $stats->set('Total File Size Uploaded:', sprintf('%0.01f MB', $n_kb_up / 1024));
285              
286 8 50       43 unless ($self->{dryrun}) {
287 8 50       252 close $metafh or die "Close on metafile '$backup_file' failed: $!";
288 8 50       3281 rename $meta_filename, $backup_file
289             or die "Failed to rename temporary backup_file: $!\n";
290              
291 8         21 my ($store_fh, $store_filename);
292 8         19 my $is_encrypted = 0;
293              
294             # store the metafile, encrypted, on the target
295 8 100       41 if (@gpg_rcpts) {
296 3         19 my $encfile = $backup_file . ".enc";
297 3         18 my @recipients = map {("--recipient", $_)} @gpg_rcpts;
  4         41  
298 3 50       33 system($self->{root}->gpg_path, $self->{root}->gpg_args,
299             @recipients,
300             "--trust-model=always",
301             "--batch",
302             "--encrypt",
303             "--output=$encfile",
304             "--yes",
305             $backup_file)
306             and die "Failed to run gpg while encryping metafile: $!\n";
307 3 50       505 open ($store_fh, $encfile) or die "Failed to open encrypted metafile '$encfile': $!\n";
308 3         40 $store_filename = $encfile;
309 3         72 $is_encrypted = 1;
310             } else {
311             # Reopen $metafh to reset file pointer (no backward seek with IO::Compress::Gzip)
312 5 50       269 open($store_fh, $backup_file) or die "Failed to open metafile '$backup_file': $!\n";
313 5         16 $store_filename = $backup_file;
314             }
315              
316             # store it on the target
317 8         149 $self->debug("Storing metafile to " . ref($target));
318 8         109 my $name = $self->{root}->publicname . "-" . $self->backup_time;
319 8         169 $target->store_backup_meta($name, $store_fh, { filename => $store_filename, is_encrypted => $is_encrypted });
320 8         94 $stats->timestamp('Metafile Storage');
321              
322             # cleanup encrypted metafile
323 8 100       89 if ($is_encrypted) {
324 3 50       56 close $store_fh or die "Close on encrypted metafile failed: $!";
325 3         330 unlink $store_filename;
326             }
327             }
328 8         53 $self->report_progress(100, "Backup complete.");
329              
330 8         643 return $stats;
331             }
332              
333             sub default_file_mode {
334 98     98 0 253 my $self = shift;
335 98   66     1247 return $self->{_def_file_mode} ||= $self->_default_mode('f');
336             }
337              
338             sub default_directory_mode {
339 28     28 0 60 my $self = shift;
340 28   66     255 return $self->{_def_dir_mode} ||= $self->_default_mode('d');
341             }
342              
343             sub _default_mode {
344 16     16   35 my ($self, $type) = @_;
345 16   50     71 my $map = $self->{modecounts}{$type} || {};
346 16         289 return (sort { $map->{$b} <=> $map->{$a} } keys %$map)[0];
  8         79  
347             }
348              
349             sub default_uid {
350 118     118 0 268 my $self = shift;
351 118   66     1052 return $self->{_def_uid} ||= $self->_default_id('u');
352             }
353              
354             sub default_gid {
355 118     118 0 295 my $self = shift;
356 118   66     2330 return $self->{_def_gid} ||= $self->_default_id('g');
357             }
358              
359             sub _default_id {
360 16     16   32 my ($self, $type) = @_;
361 16   50     74 my $map = $self->{idcounts}{$type} || {};
362 16         128 return (sort { $map->{$b} <=> $map->{$a} } keys %$map)[0];
  0         0  
363             }
364              
365             # space-separated list of local uid:username mappings
366             sub uid_map {
367 8     8 0 17 my $self = shift;
368 8         15 my @map;
369 8         23 my $uidcounts = $self->{idcounts}{u};
370 8         44 for my $uid (sort { $a <=> $b } keys %$uidcounts) {
  0         0  
371 8 50       7943 if (my $name = getpwuid($uid)) {
372 0         0 push @map, "$uid:$name";
373             }
374             }
375 8         66 return join(' ', @map);
376             }
377              
378             # space-separated list of local gid:group mappings
379             sub gid_map {
380 8     8 0 22 my $self = shift;
381 8         18 my @map;
382 8         29 my $gidcounts = $self->{idcounts}{g};
383 8         55 for my $gid (sort { $a <=> $b } keys %$gidcounts) {
  0         0  
384 8 50       2243 if (my $name = getgrgid($gid)) {
385 0         0 push @map, "$gid:$name";
386             }
387             }
388 8         51 return join(' ', @map);
389             }
390              
391             sub backup_time {
392 16     16 0 49 my $self = shift;
393 16   66     171 return $self->{backup_time} ||= time();
394             }
395              
396             sub backup_header {
397 8     8 0 49 my $self = shift;
398 8         25 my $ret = "";
399 8         43 my $now = $self->backup_time;
400 8         1206 $ret .= "BackupTime: " . $now . " (" . localtime($now) . ")\n";
401 8         57 $ret .= "BackupDriver: " . ref($self->{target}) . "\n";
402 8 50       67 if (my $fields = $self->{target}->backup_header) {
403 8         82 foreach my $k (sort keys %$fields) {
404 16 50       113 die "Bogus header field from driver" unless $k =~ /^\w+$/;
405 16         34 my $val = $fields->{$k};
406 16 50 33     105 next if ! defined $val || $val eq ''; # skip keys with empty values
407 16 50       71 die "Bogus header value from driver" if $val =~ /[\r\n]/;
408 16         68 $ret .= "Driver-$k: $val\n";
409             }
410             }
411 8         119 $ret .= "RootName: " . $self->{root}->name . "\n";
412 8         49 $ret .= "RootPath: " . $self->{root}->path . "\n";
413 8         628 $ret .= "TargetName: " . $self->{target}->name . "\n";
414 8         46 $ret .= "DefaultFileMode: " . $self->default_file_mode . "\n";
415 8         45 $ret .= "DefaultDirMode: " . $self->default_directory_mode . "\n";
416 8         47 $ret .= "DefaultUID: " . $self->default_uid . "\n";
417 8         34 $ret .= "DefaultGID: " . $self->default_gid . "\n";
418 8         48 $ret .= "UIDMap: " . $self->uid_map . "\n";
419 8         80 $ret .= "GIDMap: " . $self->gid_map . "\n";
420 8         63 $ret .= "GPG-Recipient: $_\n" for $self->{root}->gpg_rcpts;
421 8         21 $ret .= "\n";
422 8         1242 return $ret;
423             }
424              
425             sub record_mode_ids {
426 110     110 0 561 my ($self, $file) = @_;
427 110         429 $self->{modecounts}{$file->type}{$file->mode}++;
428 110         2102 $self->{idcounts}{u}{$file->uid}++;
429 110         5017 $self->{idcounts}{g}{$file->gid}++;
430             }
431              
432             sub add_unflushed_file {
433 28     28 0 80 my ($self, $file, $handlelist) = @_;
434 28         35 push @{ $self->{unflushed_files} }, [ $file, $handlelist ];
  28         229  
435             }
436              
437             sub flush_files {
438 8     8 0 21 my ($self, $fh) = @_;
439 8         18 while (my $rec = shift @{ $self->{unflushed_files} }) {
  36         1178  
440 28 50       59 next unless $fh;
441 28         47 my ($file, $stored_chunks) = @$rec;
442 28         91 print $fh $file->as_rfc822($stored_chunks, $self);
443             }
444             }
445              
446             sub add_saved_file {
447 110     110 0 266 my ($self, $file, $handlelist) = @_;
448 110         192 push @{ $self->{saved_files} }, [ $file, $handlelist ];
  110         554  
449             }
450              
451             sub foreach_saved_file {
452 2     2 0 375 my ($self, $cb) = @_;
453 2         4 foreach my $rec (@{ $self->{saved_files} }) {
  2         15  
454 30         129 $cb->(@$rec); # Brackup::File, arrayref of Brackup::StoredChunk
455             }
456             }
457              
458             sub debug {
459 190     190 0 1391 my ($self, @m) = @_;
460 190 50       1192 return unless $self->{verbose};
461 0         0 my $line = join("", @m);
462 0         0 chomp $line;
463 0         0 print $line, "\n";
464             }
465              
466             sub report_progress {
467 182     182 0 601 my ($self, $percent, $message) = @_;
468              
469 182 50       802 if ($self->{zenityprogress}) {
470 0 0 0       if (defined($message) && length($message) > 100) {
471 0           $message = substr($message, 0, 100)."...";
472             }
473 0 0         print STDOUT "#", $message, "\n" if defined $message;
474 0 0         print STDOUT $percent, "\n" if defined $percent;
475             }
476             }
477              
478             1;
479