add bug closure to changelog
[debian/amanda] / server-src / amrestore.pl
1 #! @PERL@
2 # Copyright (c) 2009-2012 Zmanda, Inc.  All Rights Reserved.
3 #
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License version 2 as published
6 # by the Free Software Foundation.
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
19
20 use lib '@amperldir@';
21 use strict;
22 use warnings;
23
24 use Getopt::Long;
25
26 use Amanda::Device qw( :constants );
27 use Amanda::Debug qw( :logging );
28 use Amanda::Config qw( :init :getconf config_dir_relative );
29 use Amanda::Util qw( :constants );
30 use Amanda::Changer;
31 use Amanda::Constants;
32 use Amanda::MainLoop;
33 use Amanda::MainLoop qw( :GIOCondition );
34 use Amanda::Header;
35 use Amanda::Holding;
36 use Amanda::Cmdline;
37 use Amanda::Tapelist;
38 use Amanda::Xfer qw( :constants );
39
40 sub usage {
41     my ($msg) = @_;
42     print STDERR "$msg\n" if $msg;
43     print STDERR <<EOF;
44 Usage: amrestore [--config config] [-b blocksize] [-r|-c|-C] [-p] [-h]
45     [-f filenum] [-l label] [-o configoption]*
46     {device | [--holding] holdingfile}
47     [hostname [diskname [datestamp [hostname [diskname [datestamp ... ]]]]]]"));
48 EOF
49     exit(1);
50 }
51
52 ##
53 # main
54
55 Amanda::Util::setup_application("amrestore", "server", $CONTEXT_CMDLINE);
56
57 my $config_overrides = new_config_overrides($#ARGV+1);
58
59 my ($opt_config, $opt_blocksize, $opt_raw, $opt_compress, $opt_compress_best,
60     $opt_pipe, $opt_header, $opt_filenum, $opt_label, $opt_holding, $opt_restore_src);
61
62 debug("Arguments: " . join(' ', @ARGV));
63 Getopt::Long::Configure(qw(bundling));
64 GetOptions(
65     'version' => \&Amanda::Util::version_opt,
66     'help|usage|?' => \&usage,
67     'config=s' => \$opt_config,
68     'holding' => \$opt_holding,
69     'b=i' => \$opt_blocksize,
70     'r' => \$opt_raw,
71     'c' => \$opt_compress,
72     'C' => \$opt_compress_best,
73     'p' => \$opt_pipe,
74     'h' => \$opt_header,
75     'f=i' => \$opt_filenum,
76     'l=s' => \$opt_label,
77     'o=s' => sub { add_config_override_opt($config_overrides, $_[1]); },
78 ) or usage();
79
80 $opt_compress = 1 if $opt_compress_best;
81
82 # see if we have a holding file or a device
83 usage("Must specify a device or holding-disk file") unless (@ARGV);
84 $opt_restore_src = shift @ARGV;
85 if (!$opt_holding) {
86     $opt_holding = 1
87         if (Amanda::Holding::get_header($opt_restore_src));
88 }
89
90 my @opt_dumpspecs = Amanda::Cmdline::parse_dumpspecs([@ARGV],
91     $Amanda::Cmdline::CMDLINE_PARSE_DATESTAMP);
92
93 usage("Cannot check a label on a holding-disk file")
94     if ($opt_holding and $opt_label);
95 usage("Cannot use both -r (raw) and -c/-C (compression) -- use -h instead")
96     if ($opt_raw and $opt_compress);
97
98 # -r implies -h, plus appending ".RAW" to filenames
99 $opt_header = 1 if $opt_raw;
100
101 set_config_overrides($config_overrides);
102 if ($opt_config) {
103     config_init($CONFIG_INIT_EXPLICIT_NAME, $opt_config);
104 } else {
105     config_init(0, undef);
106 }
107 my ($cfgerr_level, @cfgerr_errors) = config_errors();
108 if ($cfgerr_level >= $CFGERR_WARNINGS) {
109     config_print_errors();
110     if ($cfgerr_level >= $CFGERR_ERRORS) {
111         die("errors processing config file");
112     }
113 }
114
115 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
116
117 my $exit_status = 0;
118 my $res;
119
120 sub failure {
121     my ($msg, $finished_cb) = @_;
122     print STDERR "ERROR: $msg\n";
123     $exit_status = 1;
124     if ($res) {
125         $res->release(finished_cb => sub {
126             # ignore error
127             $finished_cb->();
128         });
129     } else {
130         $finished_cb->();
131     }
132 }
133
134 sub main {
135     my ($finished_cb) = @_;
136
137     my $dev;
138     my $hdr;
139     my $chg;
140     my $filenum = $opt_filenum;
141     $filenum = 1 if (!$filenum);
142     $filenum = 0 + "$filenum"; # convert to integer
143     my %all_filter;
144     my $restore_done;
145
146     my $steps = define_steps
147         cb_ref => \$finished_cb,
148         finalize => sub { $chg->quit() if defined $chg };
149
150     step start => sub {
151         # first, return to the original working directory we were started in
152         if (!chdir Amanda::Util::get_original_cwd()) {
153             return failure("Cannot chdir to original working directory", $finished_cb);
154         }
155
156         if ($opt_holding) {
157             $steps->{'read_header'}->();
158         } else {
159             my $tlf = Amanda::Config::config_dir_relative(getconf($CNF_TAPELIST));
160             my $tl = Amanda::Tapelist->new($tlf);
161             $chg = Amanda::Changer->new($opt_restore_src, tapelist => $tl);
162             if ($chg->isa("Amanda::Changer::Error")) {
163                 return failure($chg, $finished_cb);
164             }
165
166             $chg->load(relative_slot => "current", mode => "read",
167                 res_cb => $steps->{'slot_loaded'});
168         }
169     };
170
171     step slot_loaded => sub {
172         (my $err, $res) = @_;
173         return failure($err, $finished_cb) if $err;
174
175         $dev = $res->{'device'};
176
177         if ($opt_blocksize) {
178             if ( !$dev->property_set("BLOCK_SIZE", $opt_blocksize)) {
179                 return failure($dev->error_or_status, $finished_cb);
180             }
181
182             # re-read the label with the correct blocksize
183             $dev->read_label();
184         }
185
186         if ($dev->status != $DEVICE_STATUS_SUCCESS) {
187             return failure($dev->error_or_status, $finished_cb);
188         }
189
190         $steps->{'check_label'}->();
191     };
192
193     step check_label => sub {
194         if ($dev->status != $DEVICE_STATUS_SUCCESS) {
195             return failure($dev->error_or_status, $finished_cb);
196         }
197
198         if ($opt_label) {
199             if ($dev->volume_label ne $opt_label) {
200                 my $got = $dev->volume_label;
201                 return failure("Found unexpected label '$got'", $finished_cb);
202             }
203         }
204
205         my $lbl = $dev->volume_label;
206         print STDERR "Restoring from tape $lbl starting with file $filenum.\n";
207
208         $steps->{'start_device'}->();
209     };
210
211     step start_device => sub {
212         if (!$dev->start($ACCESS_READ, undef, undef)) {
213             return failure($dev->error_or_status(), $finished_cb);
214         }
215
216         $steps->{'read_header'}->();
217     };
218
219     step read_header => sub {
220         if ($opt_holding) {
221             print STDERR "Reading from '$opt_restore_src'\n";
222             $hdr = Amanda::Holding::get_header($opt_restore_src);
223         } else {
224             $hdr = $dev->seek_file($filenum);
225             if (!$hdr) {
226                 return failure("while reading next header: " . $dev->error_or_status(),
227                             $finished_cb);
228             } elsif ($hdr->{'type'} == $Amanda::Header::F_TAPEEND) {
229                 return $steps->{'finished'}->();
230             }
231
232             # seek_file may have skipped ahead; plan accordingly
233             $filenum = $dev->file + 1;
234         }
235
236         $steps->{'filter_dumpspecs'}->();
237     };
238
239     step filter_dumpspecs => sub {
240         if (@opt_dumpspecs and not $hdr->matches_dumpspecs([@opt_dumpspecs])) {
241             if (!$opt_holding) {
242                 my $dev_filenum = $dev->file;
243                 print STDERR "amrestore: $dev_filenum: skipping ",
244                         $hdr->summary(), "\n";
245             }
246
247             # skip to the next file without restoring this one
248             return $steps->{'next_file'}->();
249         }
250
251         if (!$opt_holding) {
252             my $dev_filenum = $dev->file;
253             print STDERR "amrestore: $dev_filenum: restoring ";
254         }
255         print STDERR $hdr->summary(), "\n";
256
257         $steps->{'xfer_dumpfile'}->();
258     };
259
260     step xfer_dumpfile => sub {
261         my ($src, $dest);
262
263         # set up the source..
264         if ($opt_holding) {
265             $src = Amanda::Xfer::Source::Holding->new($opt_restore_src);
266         } else {
267             $src = Amanda::Xfer::Source::Device->new($dev);
268         }
269
270         # and set up the destination..
271         my $dest_fh;
272         if ($opt_pipe) {
273             $dest_fh = \*STDOUT;
274         } else {
275             my $filename = sprintf("%s.%s.%s.%d",
276                     $hdr->{'name'},
277                     Amanda::Util::sanitise_filename("".$hdr->{'disk'}), # workaround SWIG bug
278                     $hdr->{'datestamp'},
279                     $hdr->{'dumplevel'});
280             if ($hdr->{'partnum'} > 0) {
281                 $filename .= sprintf(".%07d", $hdr->{'partnum'});
282             }
283
284             # add an appropriate suffix
285             if ($opt_raw) {
286                 $filename .= ".RAW";
287             } elsif ($opt_compress) {
288                 $filename .= ($hdr->{'compressed'} && $hdr->{'comp_suffix'})?
289                     $hdr->{'comp_suffix'} : $Amanda::Constants::COMPRESS_SUFFIX;
290             }
291
292             if (!open($dest_fh, ">", $filename)) {
293                 return failure("Could not open '$filename' for writing: $!", $finished_cb);
294             }
295         }
296         $dest = Amanda::Xfer::Dest::Fd->new($dest_fh);
297
298         # set up any filters that need to be applied, decryption first
299         my @filters;
300         if ($hdr->{'encrypted'} and not $opt_raw) {
301             if ($hdr->{'srv_encrypt'}) {
302                 push @filters,
303                     Amanda::Xfer::Filter::Process->new(
304                         [ $hdr->{'srv_encrypt'}, $hdr->{'srv_decrypt_opt'} ], 0);
305             } elsif ($hdr->{'clnt_encrypt'}) {
306                 push @filters,
307                     Amanda::Xfer::Filter::Process->new(
308                         [ $hdr->{'clnt_encrypt'}, $hdr->{'clnt_decrypt_opt'} ], 0);
309             } else {
310                 return failure("could not decrypt encrypted dump: no program specified",
311                             $finished_cb);
312             }
313
314             $hdr->{'encrypted'} = 0;
315             $hdr->{'srv_encrypt'} = '';
316             $hdr->{'srv_decrypt_opt'} = '';
317             $hdr->{'clnt_encrypt'} = '';
318             $hdr->{'clnt_decrypt_opt'} = '';
319             $hdr->{'encrypt_suffix'} = 'N';
320         }
321         if (!$opt_raw and $hdr->{'compressed'} and not $opt_compress) {
322             # need to uncompress this file
323
324             if ($hdr->{'srvcompprog'}) {
325                 # TODO: this assumes that srvcompprog takes "-d" to decompress
326                 push @filters,
327                     Amanda::Xfer::Filter::Process->new(
328                         [ $hdr->{'srvcompprog'}, "-d" ], 0);
329             } elsif ($hdr->{'clntcompprog'}) {
330                 # TODO: this assumes that clntcompprog takes "-d" to decompress
331                 push @filters,
332                     Amanda::Xfer::Filter::Process->new(
333                         [ $hdr->{'clntcompprog'}, "-d" ], 0);
334             } else {
335                 push @filters,
336                     Amanda::Xfer::Filter::Process->new(
337                         [ $Amanda::Constants::UNCOMPRESS_PATH,
338                           $Amanda::Constants::UNCOMPRESS_OPT ], 0);
339             }
340             
341             # adjust the header
342             $hdr->{'compressed'} = 0;
343             $hdr->{'uncompress_cmd'} = '';
344         } elsif (!$opt_raw and !$hdr->{'compressed'} and $opt_compress) {
345             # need to compress this file
346
347             my $compress_opt = $opt_compress_best?
348                 $Amanda::Constants::COMPRESS_BEST_OPT :
349                 $Amanda::Constants::COMPRESS_FAST_OPT;
350             @filters = (
351                 Amanda::Xfer::Filter::Process->new(
352                     [ $Amanda::Constants::COMPRESS_PATH,
353                       $compress_opt ], 0),
354             );
355             
356             # adjust the header
357             $hdr->{'compressed'} = 1;
358             $hdr->{'uncompress_cmd'} = " $Amanda::Constants::UNCOMPRESS_PATH " .
359                 "$Amanda::Constants::UNCOMPRESS_OPT |";
360             $hdr->{'comp_suffix'} = $Amanda::Constants::COMPRESS_SUFFIX;
361         }
362
363         # write the header to the destination if requested
364         if ($opt_header) {
365             $hdr->{'blocksize'} = Amanda::Holding::DISK_BLOCK_BYTES;
366             $dest_fh->syswrite($hdr->to_string(32768, 32768));
367         }
368
369         # start reading all filter stderr
370         foreach my $filter (@filters) {
371             my $fd = $filter->get_stderr_fd();
372             $fd.="";
373             $fd = int($fd);
374             my $src = Amanda::MainLoop::fd_source($fd,
375                                                   $G_IO_IN|$G_IO_HUP|$G_IO_ERR);
376             my $buffer = "";
377             $all_filter{$src} = 1;
378             $src->set_callback( sub {
379                 my $b;
380                 my $n_read = POSIX::read($fd, $b, 1);
381                 if (!defined $n_read) {
382                     return;
383                 } elsif ($n_read == 0) {
384                     delete $all_filter{$src};
385                     $src->remove();
386                     POSIX::close($fd);
387                     if (!%all_filter and $restore_done) {
388                         $finished_cb->();
389                     }
390                 } else {
391                     $buffer .= $b;
392                     if ($b eq "\n") {
393                         my $line = $buffer;
394                         print STDERR "filter stderr: $line";
395                         chomp $line;
396                         debug("filter stderr: $line");
397                         $buffer = "";
398                     }
399                 }
400             });
401         }
402         
403         my $xfer = Amanda::Xfer->new([ $src, @filters, $dest ]);
404         my $got_err = undef;
405         $xfer->get_source()->set_callback(sub {
406             my ($src, $msg, $xfer) = @_;
407
408             if ($msg->{'type'} == $XMSG_INFO) {
409                 Amanda::Debug::info($msg->{'message'});
410             } elsif ($msg->{'type'} == $XMSG_ERROR) {
411                 $got_err = $msg->{'message'};
412             } elsif ($msg->{'type'} == $XMSG_DONE) {
413                 $src->remove();
414                 $steps->{'xfer_done'}->($got_err);
415             }
416         });
417         $xfer->start();
418     };
419
420     step xfer_done => sub {
421         my ($err) = @_;
422         return failure($err, $finished_cb) if $err;
423
424         $steps->{'next_file'}->('extracted');
425     };
426
427     step next_file => sub {
428         my ($extracted) = @_;
429         # amrestore does not loop over multiple files when reading from holding
430         # when outputting to a pipe amrestore extracts only the first file
431         if ($opt_holding or ($opt_pipe and $extracted)) {
432             return $steps->{'finished'}->();
433         }
434
435         # otherwise, try to read the next header from the device
436         $steps->{'read_header'}->();
437     };
438
439     step finished => sub {
440         if ($res) {
441             $res->release(finished_cb => $steps->{'quit'});
442         } else {
443             $steps->{'quit'}->();
444         }
445     };
446
447     step quit => sub {
448         my ($err) = @_;
449         $res = undef;
450         $restore_done = 1;
451         return failure($err, $finished_cb) if $err;
452
453         if (!%all_filter) {
454             $finished_cb->();
455         }
456     };
457 }
458 main(\&Amanda::MainLoop::quit);
459 Amanda::MainLoop::run();
460 Amanda::Util::finish_application();
461 exit $exit_status;