File Coverage

blib/lib/Brackup/Restore.pm
Criterion Covered Total %
statement 197 227 86.7
branch 72 132 54.5
condition 30 62 48.3
subroutine 27 29 93.1
pod 0 3 0.0
total 326 453 71.9


line stmt bran cond sub pod time code
1             package Brackup::Restore;
2 13     13   81 use strict;
  13         25  
  13         902  
3 13     13   75 use warnings;
  13         33  
  13         419  
4 13     13   78 use Carp qw(croak);
  13         33  
  13         771  
5 13     13   85 use Digest::SHA1;
  13         29  
  13         530  
6 13     13   69 use POSIX qw(mkfifo);
  13         39  
  13         116  
7 13     13   990 use Fcntl qw(O_RDONLY O_CREAT O_WRONLY O_TRUNC);
  13         27  
  13         741  
8 13     13   85 use String::Escape qw(unprintable);
  13         32  
  13         678  
9 13     13   658 use Brackup::DecryptedFile;
  13         29  
  13         330  
10 13     13   70 use Brackup::Decrypt;
  13         26  
  13         41807  
11              
12             sub new {
13 11     11 0 123 my ($class, %opts) = @_;
14 11         47 my $self = bless {}, $class;
15              
16 11         80 $self->{to} = delete $opts{to}; # directory we're restoring to
17 11         184 $self->{prefix} = delete $opts{prefix}; # directory/file filename prefix, or "" for all
18 11         54 $self->{filename}= delete $opts{file}; # filename we're restoring from
19 11         56 $self->{config} = delete $opts{config}; # brackup config (if available)
20 11         34 $self->{verbose} = delete $opts{verbose};
21              
22 11         76 $self->{_local_uid_map} = {}; # remote/metafile uid -> local uid
23 11         46 $self->{_local_gid_map} = {}; # remote/metafile gid -> local gid
24              
25 11 100       59 $self->{prefix} =~ s/\/$// if $self->{prefix};
26              
27 11         55 $self->{_stats_to_run} = []; # stack (push/pop) of subrefs to reset stat info on
28              
29 11 50 33     956 die "Destination directory doesn't exist" unless $self->{to} && -d $self->{to};
30 11 50       80 croak("Unknown options: " . join(', ', keys %opts)) if %opts;
31              
32 11         186 $self->{metafile} = Brackup::DecryptedFile->new(filename => $self->{filename});
33              
34 11         62 return $self;
35             }
36              
37             # returns a hashref of { "foo" => "bar" } from { ..., "Driver-foo" => "bar" }
38             sub _driver_meta {
39 11     11   24 my $src = shift;
40 11         27 my $ret = {};
41 11         79 foreach my $k (keys %$src) {
42 147 100       577 next unless $k =~ /^Driver-(.+)/;
43 22         111 $ret->{$1} = $src->{$k};
44             }
45 11         45 return $ret;
46             }
47              
48             sub restore {
49 11     11 0 28 my ($self) = @_;
50 11         74 my $parser = $self->parser;
51 11         75 my $meta = $parser->readline;
52 11         33 my $driver_class = $meta->{BackupDriver};
53 11 50       32 die "No driver specified" unless $driver_class;
54              
55 11         43 my $driver_meta = _driver_meta($meta);
56              
57 11         29 my $confsec;
58 11 50 33     57 if ($self->{config} && $meta->{TargetName}) {
59 0         0 $confsec = eval { $self->{config}->get_section('TARGET:' . $meta->{TargetName}) };
  0         0  
60             }
61             # If no config section, use an empty one up with no keys to simplify Target handling
62 11   33     271 $confsec ||= Brackup::ConfigSection->new('fake');
63              
64 11 50   4   2196 eval "use $driver_class; 1;" or die
  4     4   33  
  4     1   8  
  4     1   91  
  4     1   53  
  4         14  
  4         299  
  1         10  
  1         2  
  1         23  
  1         10  
  1         3  
  1         25  
  1         8  
  1         2  
  1         21  
65             "Failed to load driver ($driver_class) to restore from: $@\n";
66 11         27 my $target = eval {"$driver_class"->new_from_backup_header($driver_meta, $confsec); };
  11         115  
67 11 50       43 if ($@) {
68 0         0 die "Failed to instantiate target ($driver_class) for restore. Perhaps it doesn't support restoring yet?\n\nThe error was: $@";
69             }
70 11         123 $self->{_target} = $target;
71 11         47 $self->{_meta} = $meta;
72              
73             # handle absolute prefixes by stripping off RootPath to relativise
74 11 50 66     158 if ($self->{prefix} && $self->{prefix} =~ m/^\//) {
75 0         0 $self->{prefix} =~ s/^\Q$meta->{RootPath}\E\/?//;
76             }
77              
78             # we first process directories, then files sorted by their first chunk,
79             # then the rest. The file sorting allows us to avoid loading composite
80             # chunks and identical single chunk files multiple times from the target
81             # (see _restore_file)
82 11         23 my (@dirs, @files, @rest);
83 11         48 while (my $it = $parser->readline) {
84 155   100     638 my $type = $it->{Type} || 'f';
85 155 100       458 if($type eq 'f') {
    50          
86             # find dig of first chunk
87 126   50     545 ($it->{Chunks} || '') =~ /^(\S+)/;
88 126   50     790 my ($offset, $len, $enc_len, $dig) = split(/;/, $1 || '');
89 126   50     419 $it->{fst_dig} = $dig || '';
90 126         520 push @files, $it;
91             } elsif($type eq 'd') {
92 29         157 push @dirs, $it;
93             } else {
94 0         0 push @rest, $it;
95             }
96             }
97 11         118 @files = sort { $a->{fst_dig} cmp $b->{fst_dig} } @files;
  289         473  
98              
99 11         31 my $restore_count = 0;
100 11         47 for my $it (@dirs, @files, @rest) {
101 155   100     1195 my $type = $it->{Type} || "f";
102 155         1395 my $path = unprintable($it->{Path});
103 155         1649 my $path_escaped = $it->{Path};
104 155         333 my $path_escaped_stripped = $it->{Path};
105 155 50       892 die "Unknown filetype: type=$type, file: $path_escaped" unless $type =~ /^[ldfp]$/;
106              
107 155 100       467 if ($self->{prefix}) {
108 60 100       477 next unless $path =~ m/^\Q$self->{prefix}\E(?:\/|$)/;
109             # if non-dir and $path eq $self->{prefix}, strip all but last component
110 6 100 100     91 if ($type ne 'd' && $path =~ m/^\Q$self->{prefix}\E\/?$/) {
111 2 100       17 if (my ($leading_prefix) = ($self->{prefix} =~ m/^(.*\/)[^\/]+\/?$/)) {
112 1         13 $path =~ s/^\Q$leading_prefix\E//;
113 1         10 $path_escaped_stripped =~ s/^\Q$leading_prefix\E//;
114             }
115             }
116             else {
117 4         36 $path =~ s/^\Q$self->{prefix}\E\/?//;
118 4         43 $path_escaped_stripped =~ s/^\Q$self->{prefix}\E\/?//;
119             }
120             }
121              
122 101         463 $restore_count++;
123 101         353 my $full = $self->{to} . "/" . $path;
124 101         278 my $full_escaped = $self->{to} . "/" . $path_escaped_stripped;
125              
126             # restore default modes/user/group from header
127 101 100 66     1593 $it->{Mode} ||= ($type eq 'd' ? $meta->{DefaultDirMode} : $meta->{DefaultFileMode});
128 101   33     672 $it->{UID} ||= $meta->{DefaultUID};
129 101   33     574 $it->{GID} ||= $meta->{DefaultGID};
130              
131 101 50       312 warn " * restoring $path_escaped to $full_escaped\n" if $self->{verbose};
132 101 50       397 $self->_restore_link ($full, $it) if $type eq "l";
133 101 100       417 $self->_restore_directory($full, $it) if $type eq "d";
134 101 50       570 $self->_restore_fifo ($full, $it) if $type eq "p";
135 101 100       1031 $self->_restore_file ($full, $it) if $type eq "f";
136              
137 101 50 33     10935 $self->_chown($full, $it, $type, $meta) if $it->{UID} || $it->{GID};
138             }
139              
140             # clear chunk cached by _restore_file
141 11         103 delete $self->{_cached_dig};
142 11         58 delete $self->{_cached_dataref};
143              
144 11 100       64 if ($restore_count) {
145 10 50       56 warn " * fixing stat info\n" if $self->{verbose};
146 10         67 $self->_exec_statinfo_updates;
147 10 50       52 warn " * done\n" if $self->{verbose};
148 10         871 return 1;
149             } else {
150 1 50       48 die "nothing found matching '$self->{prefix}'.\n" if $self->{prefix};
151 0         0 die "nothing found to restore.\n";
152             }
153             }
154              
155             sub _lookup_remote_uid {
156 101     101   380 my ($self, $remote_uid, $meta) = @_;
157              
158 101 100       802 return $self->{_local_uid_map}->{$remote_uid}
159             if defined $self->{_local_uid_map}->{$remote_uid};
160              
161             # meta remote user map - remote_uid => remote username
162 10   50     129 $self->{_remote_user_map} ||= { map { split /:/, $_, 2 } split /\s+/, $meta->{UIDMap} };
  0         0  
163              
164             # try and lookup local uid using remote username
165 10 50       55 if (my $remote_user = $self->{_remote_user_map}->{$remote_uid}) {
166 0         0 my $local_uid = getpwnam($remote_user);
167 0 0       0 return $self->{_local_uid_map}->{$remote_uid} = $local_uid
168             if defined $local_uid;
169             }
170              
171             # if remote username missing locally, fallback to $remote_uid
172 10         58 return $self->{_local_uid_map}->{$remote_uid} = $remote_uid;
173             }
174              
175             sub _lookup_remote_gid {
176 101     101   311 my ($self, $remote_gid, $meta) = @_;
177              
178 101 100       586 return $self->{_local_gid_map}->{$remote_gid}
179             if defined $self->{_local_gid_map}->{$remote_gid};
180              
181             # meta remote group map - remote_gid => remote group
182 10   50     121 $self->{_remote_group_map} ||= { map { split /:/, $_, 2 } split /\s+/, $meta->{GIDMap} };
  0         0  
183              
184             # try and lookup local gid using remote group
185 10 50       46 if (my $remote_group = $self->{_remote_group_map}->{$remote_gid}) {
186 0         0 my $local_gid = getgrnam($remote_group);
187 0 0       0 return $self->{_local_gid_map}->{$remote_gid} = $local_gid
188             if defined $local_gid;
189             }
190              
191             # if remote group missing locally, fallback to $remote_gid
192 10         91 return $self->{_local_gid_map}->{$remote_gid} = $remote_gid;
193             }
194              
195             sub _chown {
196 101     101   347 my ($self, $full, $it, $type, $meta) = @_;
197              
198 101 50       834 my $uid = $self->_lookup_remote_uid($it->{UID}, $meta) if $it->{UID};
199 101 50       702 my $gid = $self->_lookup_remote_gid($it->{GID}, $meta) if $it->{GID};
200              
201 101 50       348 if ($type eq 'l') {
202 0 0       0 if (! defined $self->{_lchown}) {
203 13     13   106 no strict 'subs';
  13         27  
  13         23485  
204 0   0     0 $self->{_lchown} = eval { require Lchown } && Lchown::LCHOWN_AVAILABLE;
205             }
206 0 0       0 if ($self->{_lchown}) {
207 0 0       0 Lchown::lchown($uid, -1, $full) if defined $uid;
208 0 0       0 Lchown::lchown(-1, $gid, $full) if defined $gid;
209             }
210             } else {
211             # ignore errors, but change uid and gid separately to sidestep unprivileged failures
212 101 50       5118 chown $uid, -1, $full if defined $uid;
213 101 50       4017 chown -1, $gid, $full if defined $gid;
214             }
215             }
216              
217             sub _update_statinfo {
218 101     101   680 my ($self, $full, $it) = @_;
219              
220 101         3114 push @{ $self->{_stats_to_run} }, sub {
221 101 50   101   268 if (defined $it->{Mode}) {
222 101 50       3705 chmod(oct $it->{Mode}, $full) or
223             die "Failed to change mode of $full: $!";
224             }
225              
226 101 50 33     369 if ($it->{Mtime} || $it->{Atime}) {
227 101 50 33     4069 utime($it->{Atime} || $it->{Mtime},
      33        
228             $it->{Mtime} || $it->{Atime},
229             $full) or
230             die "Failed to change utime of $full: $!";
231             }
232 101         155 };
233             }
234              
235             sub _exec_statinfo_updates {
236 10     10   173 my $self = shift;
237              
238             # change the modes/times in backwards order, going from deep
239             # files/directories to shallow ones. (so we can reliably change
240             # all the directory mtimes without kernel doing it for us when we
241             # modify files deeper)
242 10         28 while (my $sb = pop @{ $self->{_stats_to_run} }) {
  111         383  
243 101         241 $sb->();
244             }
245             }
246              
247             sub _restore_directory {
248 19     19   40 my ($self, $full, $it) = @_;
249              
250 19 100       838 unless (-d $full) {
251 11 50       952 mkdir $full or # FIXME: permissions on directory
252             die "Failed to make directory: $full ($it->{Path})";
253             }
254              
255 19         70 $self->_update_statinfo($full, $it);
256             }
257              
258             sub _restore_link {
259 0     0   0 my ($self, $full, $it) = @_;
260              
261 0 0       0 if (-e $full) {
262             # TODO: add --conflict={skip,overwrite} option, defaulting to nothing: which dies
263 0         0 die "Link $full ($it->{Path}) already exists. Aborting.";
264             }
265 0 0       0 symlink $it->{Link}, $full
266             or die "Failed to link";
267             }
268              
269             sub _restore_fifo {
270 0     0   0 my ($self, $full, $it) = @_;
271              
272 0 0       0 if (-e $full) {
273 0         0 die "Named pipe/fifo $full ($it->{Path}) already exists. Aborting.";
274             }
275              
276 0 0       0 mkfifo($full, $it->{Mode}) or die "mkfifo failed: $!";
277              
278 0         0 $self->_update_statinfo($full, $it);
279             }
280              
281             sub _restore_file {
282 82     82   169 my ($self, $full, $it) = @_;
283              
284 82 50 33     3283 if (-e $full && -s $full) {
285             # TODO: add --conflict={skip,overwrite} option, defaulting to nothing: which dies
286 0         0 die "File $full ($it->{Path}) already exists. Aborting.";
287             }
288              
289 82 50       15200 sysopen(my $fh, $full, O_CREAT|O_WRONLY|O_TRUNC) or die "Failed to open '$full' for writing: $!";
290 82         280 binmode($fh);
291 82   50     891 my @chunks = grep { $_ } split(/\s+/, $it->{Chunks} || "");
  100         577  
292 82         360 foreach my $ch (@chunks) {
293 100         1122 my ($offset, $len, $enc_len, $dig) = split(/;/, $ch);
294              
295             # we process files sorted by the dig of their first chunk, caching
296             # the last seen chunk to avoid loading composite chunks multiple
297             # times (all files included in composite chunks are single-chunk
298             # files, by definition). Even for non-composite chunks there is a
299             # speedup if we have single-chunk identical files.
300 100         181 my $dataref;
301 100 100 100     669 if($dig eq ($self->{_cached_dig} || '')) {
302 23 50       132 warn " ** using cached chunk $dig\n" if $self->{verbose};
303 23         60 $dataref = $self->{_cached_dataref};
304             } else {
305 77 50       259 warn " ** loading chunk $dig from target\n" if $self->{verbose};
306 77 50       1068 $dataref = $self->{_target}->load_chunk($dig) or
307             die "Error loading chunk $dig from the restore target\n";
308 77         242 $self->{_cached_dig} = $dig;
309 77         183 $self->{_cached_dataref} = $dataref;
310             }
311              
312 100         359 my $len_chunk = length $$dataref;
313              
314             # using just a range of the file
315 100 100       468 if ($enc_len =~ /^(\d+)-(\d+)$/) {
316 15         85 my ($from, $to) = ($1, $2);
317             # file range. gotta be at least as big as bigger number
318 15 50       57 unless ($len_chunk >= $to) {
319 0         0 die "Backup chunk $dig isn't at least as big as range: got $len_chunk, needing $to\n";
320             }
321 15         90 my $region = substr($$dataref, $from, $to-$from);
322 15         35 $dataref = \$region;
323             } else {
324             # using the whole chunk, so make sure fetched size matches
325             # expected size
326 85 50       347 unless ($len_chunk == $enc_len) {
327 0         0 die "Backup chunk $dig isn't of expected length: got $len_chunk, expecting $enc_len\n";
328             }
329             }
330              
331 100         578 my $decrypted_ref = Brackup::Decrypt::decrypt_data($dataref, meta => $self->{_meta});
332 100         1909 print $fh $$decrypted_ref;
333             }
334 82 50       4842 close($fh) or die "Close failed";
335              
336 82 50       473 if (my $good_dig = $it->{Digest}) {
337 82 50       1533 die "not capable of verifying digests of from anything but sha1"
338             unless $good_dig =~ /^sha1:(.+)/;
339 82         582 $good_dig = $1;
340              
341 82 50       4776 sysopen(my $readfh, $full, O_RDONLY) or die "Failed to reopen '$full' for verification: $!";
342 82         278 binmode($readfh);
343 82         1775 my $sha1 = Digest::SHA1->new;
344 82         2411 $sha1->addfile($readfh);
345 82         991 my $actual_dig = $sha1->hexdigest;
346              
347             # TODO: support --onerror={continue,prompt}, etc, but for now we just die
348 82 50 33     1811 unless ($actual_dig eq $good_dig || $full =~ m!\.brackup-digest\.db\b!) {
349 0         0 die "Digest of restored file ($full) doesn't match";
350             }
351             }
352              
353 82         1093 $self->_update_statinfo($full, $it);
354             }
355              
356             # returns iterator subref which returns hashrefs or undef on EOF
357             sub parser {
358 11     11 0 29 my $self = shift;
359 11         69 return Brackup::Metafile->open($self->{metafile}->name);
360             }
361              
362             1;
363