The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# @(#)Ident: TapeBackup.pm 2014-01-12 01:03 pjf ;

package CatalystX::Usul::TapeBackup;

use strict;
use version; our $VERSION = qv( sprintf '0.17.%d', q$Rev: 1 $ =~ /\d+/gmx );

use CatalystX::Usul::Moose;
use CatalystX::Usul::Constants;
use CatalystX::Usul::Constraints qw( Directory Lock Path );
use CatalystX::Usul::Functions   qw( io throw );
use Class::Usul::File;
use Class::Usul::IPC;
use Class::Usul::Time            qw( str2time time2str );
use English                      qw( -no_match_vars );
use File::Spec::Functions        qw( catdir catfile rootdir );
use TryCatch;

has 'dev_dir'      => is => 'lazy', isa => Directory, coerce => TRUE,
   default         => sub { [ NUL, q(dev) ] };

has 'default_tape' => is => 'ro',   isa => NonEmptySimpleStr, default => 'st0';

has 'dump_cmd'     => is => 'ro',   isa => NonEmptySimpleStr,
   default         => sub { catfile( NUL, qw(sbin dump) ).q( -aqu -b 128) };

has 'dump_dates'   => is => 'lazy', isa => Path, coerce => TRUE,
   default         => sub { [ NUL, qw(etc dumpdates) ] };

has 'form'         => is => 'ro',   isa => NonEmptySimpleStr,
   default         => 'backup';

has 'level_map'    => is => 'ro',   isa => HashRef, init_arg => undef,
   default         => sub { { 0 => 1, 1 => 3, 2 => 5, 3 => 2, 4 => 7,
                              5 => 4, 6 => 9, 7 => 6, 8 => 9, 9 => 8 } };

has 'locale'       => is => 'ro',   isa => NonEmptySimpleStr,
   default         => sub { $_[ 0 ]->config->locale };

has 'max_wait'     => is => 'ro',   isa => PositiveInt, default => 43_200;

has 'mt_cmd'       => is => 'ro',   isa => NonEmptySimpleStr,
   default         => 'mt -f';

has 'no_rew_pref'  => is => 'ro',   isa => SimpleStr, default => 'n';

has 'no_rew_suff'  => is => 'ro',   isa => SimpleStr, default => NUL;

has 'pattern'      => is => 'ro',   isa => NonEmptySimpleStr,
   default         => 'st[0-9]+';

has 'static_data'  => is => 'lazy', isa => HashRef, init_arg => undef;

has 'tar_cmd'      => is => 'ro',   isa => NonEmptySimpleStr,
   default         => 'tar -c -b 256';


has '_file' => is => 'lazy', isa => FileClass,
   default  => sub { Class::Usul::File->new( builder => $_[ 0 ]->usul ) },
   init_arg => undef, reader => 'file';

has '_ipc'  => is => 'lazy', isa => IPCClass,
   default  => sub { Class::Usul::IPC->new( builder => $_[ 0 ]->usul ) },
   handles  => [ qw( run_cmd ) ], init_arg => undef, reader => 'ipc';

has '_usul' => is => 'ro',   isa => BaseClass,
   handles  => [ qw( config debug lock log ) ], init_arg => 'builder',
   reader   => 'usul', required => TRUE, weak_ref => TRUE;

sub eject {
   my ($self, $args) = @_;

   my $path = $self->_dev_path( $args->{device} );
   my $cmd  = $self->mt_cmd.SPC.$path.q( eject);

   $self->run_cmd( $cmd, { async => 1, debug => $self->debug } );
   return $args->{device};
}

sub get_status {
   my ($self, $args) = @_; my $stat = $self->static_data;

   $stat->{device    } = $args->{device    } || $self->default_tape;
   $stat->{dump_type } = $args->{type      } || q(daily);
   $stat->{format    } = $args->{format    } || q(dump);
   $stat->{operation } = $args->{operation } || 1;
   $stat->{next_level} = $args->{next_level} || 0;

   my $volume = $args->{volume};
   my $form   = $self->form;
   my $pat    = $self->pattern;
   my $io     = io( $self->dev_dir )->filter( sub {
      return (-c $_->pathname) && ($_->filename =~ m{ \A $pat \z }mx) } );

   for my $device (map { $_->filename } $io->all_files) {
      push @{ $stat->{devices} }, $device;

      $device eq $stat->{device} or next; $stat->{working} = FALSE;

      for my $lock (@{ $self->lock->list }) {
         if ($lock->{key} =~ m{ $device }mx) { $stat->{working} = TRUE; last }
      }

      if ($stat->{working}) {
         $stat->{position} = "${form}.tapeInProgress"; next;
      }

      $self->_read_device_position( $stat, $device );
   }

   ($stat->{format} eq q(dump) and $volume) or return $stat;

   ($stat->{last_dump}, $stat->{last_level}) = $self->_get_last( $volume );

   my $type  = $stat->{dump_type};
   my $level = { complete => 0,
                 weekly   => 1,
                 daily    => $self->level_map->{ $stat->{last_level} } || 0,
                 specific => $stat->{next_level} }->{ $type };

   $stat->{next_level} = $level;
   $level == 0 and $type ne q(specific) and $type = q(complete);
   $level == 1 and $type ne q(specific) and $type = q(weekly);
   $stat->{dump_type } = $type;
   $stat->{dump_msg  } = $stat->{last_dump} ? "${form}.dumpedBefore"
                                            : "${form}.neverDumped";
   return $stat;
}

sub process {
   my ($self, $args, @paths) = @_; my $msg;

   my $dev = $args->{position} == 2
           ? $self->_dev_path( $args->{device} )
           : $self->_dev_path( $self->_no_rewind( $args->{device} ) );

   -c $dev or throw error => 'Path [_1] not a character device',
                    args  => [ $dev ];

   defined $paths[ 0 ] or $paths[ 0 ] = rootdir;

   $self->lock->set( k => $dev, t => $self->max_wait );

   try        { $msg = $self->_process( $dev, $args, \@paths ) }
   catch ($e) { $self->lock->reset( k => $dev ); throw $e }

   $self->lock->reset( k => $dev );
   return $msg;
}

sub start {
   my ($self, $args, $paths) = @_; my $cmd;

   $paths or throw 'No file path specified';
   $cmd  = $self->config->suid.q( -c tape_backup ).$self->debug_flag;
   $cmd .= q( -L ).$self->locale;

   while (my ($k, $v) = each %{ $args }) {
      $cmd .= q( -o ).$k.'="'.$v.'"';
   }

   $cmd .= q( -- ).$paths;

   return $self->run_cmd( $cmd, { async => 1,
                                  debug => $self->debug,
                                  err   => q(out),
                                  out   => $self->file->tempname } );
}

# Private methods

sub _build_static_data {
   my $sd = {};

   $sd->{devices   } = [];
   $sd->{dump_msg  } = NUL;
   $sd->{dump_types} = [ qw(complete weekly daily specific) ];
   $sd->{f_labels  } = { dump => 'Filesystem Dump', tar => 'Tape Archive' };
   $sd->{file_no   } = 0;
   $sd->{formats   } = [ qw(dump tar) ];
   $sd->{last_dump } = NUL;
   $sd->{last_level} = 0;
   $sd->{o_labels  } = { 1 => 'Status', 2 => 'Rewind' };
   $sd->{online    } = FALSE;
   $sd->{p_labels  } = { 1 => 'EOD (norewind)', 2 => 'BOT (rewind)' };
   $sd->{position  } = NUL;
   $sd->{working   } = FALSE;
   return $sd;
}

sub _dev_path {
   my ($self, $device) = @_; return catfile( $self->dev_dir, $device );
}

sub _get_last {
   my ($self, $volume) = @_; my ($dstr, $level); my $lastd = 0;

   $volume or throw 'No disk volume specified';

   -f $self->dump_dates or return (NUL, 0);

   for my $line (io( $self->dump_dates )->chomp->getlines) {
      $line !~ m{ \A $volume \s+ (\d+) \s+ (.*) }mx and next;

      my $date = str2time( $2 );

      if ($date > $lastd) { $level = $1; $dstr = $2; $lastd = $date }
   }

   return defined $level ? ($dstr, $level) : (NUL, 0);
}

sub _no_rewind {
   return $_[ 0 ]->no_rew_pref . $_[ 1 ] . $_[ 0 ]->no_rew_suff;
}

sub _process {
   my ($self, $dev, $args, $paths);

   my $cmd = $self->mt_cmd.SPC.$dev; my $text;

   if ($args->{operation} == 2) {
      $text = "Rewinding ${dev}\n"; $cmd .= q( rewind);
   }
   else { $text = "Appending to ${dev}\n"; $cmd .= q( status) }

   $self->run_cmd( $cmd, { err => q(out) } );

   for my $path (@{ $paths }) {
      $text .= "Dumping ${path} ".time2str()."\n";

      if ($args->{format} eq q(dump)) {
         $cmd  = $self->dump_cmd.($self->debug ? q( -v) : NUL);
         $cmd .= q( -).$args->{level};

         $args->{except_inodes} and $cmd .= q( -e )
            .(join q(,), split SPC, $args->{except_inodes});
      }
      else { $cmd = $self->tar_cmd }

      $cmd  .= q( -f ).$dev.SPC.$path;
      $text .= $self->run_cmd( $cmd, { err => q(out) } )->stdout;
   }

   return $text;
}

sub _read_device_position {
   my ($self, $stat, $device) = @_; my $posn;

   my $form = $self->form;
   my $path = $self->_dev_path( $self->_no_rewind( $device ) );
   my $cmd  = $self->mt_cmd.SPC.$path.q( status);
   my $out  = eval { $self->run_cmd( $cmd, { err => q(out) } )->out } || NUL;

   for my $line (split m{ \n }mx, $out) {
      $stat->{online } = TRUE if ($line =~ m{ ONLINE }mx ||
                                  $line =~ m{ resource \s+ busy }mx);
      $stat->{file_no} = $1   if ($line =~ m{ \A File \s+ number= (\d+) }mx);
      $posn = 1               if ($line =~ m{ BOT }mx);
      $posn = 2               if ($line =~ m{ EOF }mx);
      $posn = 3               if ($line =~ m{ resource \s+ busy }mx);
   }

   if ($stat->{online}) {
      if    ($posn == 3) { $stat->{position} = "${form}.tapeBusy" }
      elsif ($posn == 2) { $stat->{position} = "${form}.tapeEOF" }
      elsif ($posn == 1) { $stat->{position} = "${form}.tapeBOT" }
      else               { $stat->{position} = "${form}.tapeUnknown" }
   }
   else { $stat->{position} = "${form}.tapeNotOnline" }

   return;
}

__PACKAGE__->meta->make_immutable;

1;

__END__

=pod

=head1 Name

CatalystX::Usul::TapeBackup - Provides tape device methods

=head1 Version

Describes v0.17.$Rev: 1 $

=head1 Synopsis

   use CatalystX::Usul::TapeBackup;
   use Class::Usul;

   my $attr     = { builder  => Class::Usul->new, };

   my $tape_obj = CatalystX::Usul::TapeBackup->new( $attr );

   my $status_hash_ref = $tape_obj->get_status( {} );

   my $ipc_response_obj = $tape_obj->start( $args, $paths );

   my $tape_device = $tape_obj->eject( { device => $tape_device } );

=head1 Description

Provides methods to perform tape backups using either C<dump> or C<tar>

=head1 Configuration and Environment

Defines the following attributes

=over 3

=item dev_dir

Directory path which defaults to F</dev>

=item default_tape

String which defaults to C<st0>

=item dump_cmd

String which defaults to C</sbin/dump -aqu -b 128>

=item dump_dates

Path which defaults to F</etc/dumpdates>

=item form

String which defaults to C<backup>

=item locale

String which defaults to C<en_GB>

=item max_wait

Integer which defaults to C<43_200>

=item mt_cmd

String which defaults to C<mt -f>

=item no_rew_pref

String which defaults to C<n>

=item no_rew_suff

String which defaults to null

=item pattern

String which defaults to C<st[0-9]+>

=item tar_cmd

String which defaults to C<tar -c -b 256>

=back

=head1 Subroutines/Methods

=head2 eject

   $tape_device = $self->eject( { device => $tape_device } );

Ejects the tape in the selected drive

=head2 get_status

   $status_hash_ref = $self->get_status( $args );

Returns a hash ref of information about the selected tape device

=head2 process

   $display_message = $self->process( $options, $paths );

Called from a command line wrapper this method executes the actual C<dump>
or C<tar> command

=head2 start

   $ipc_response_obj = $self->start( $args, $paths );

Calls the external command line wrapper which performs the
backup. Runs the command asynchronously so that it can return
immediately to the action that called it

=head1 Diagnostics

None

=head1 Dependencies

=over 3

=item L<Class::Usul::File>

=item L<Class::Usul::IPC>

=item L<CatalystX::Usul::Moose>

=item L<Class::Usul::Time>

=item L<CatalystX::Usul::Constraints>

=item L<TryCatch>

=back

=head1 Incompatibilities

There are no known incompatibilities in this module

=head1 Bugs and Limitations

There are no known bugs in this module.
Please report problems to the address below.
Patches are welcome

=head1 Author

Peter Flanigan, C<< <Support at RoxSoft.co.uk> >>

=head1 License and Copyright

Copyright (c) 2014 Peter Flanigan. All rights reserved

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself. See L<perlartistic>

This program is distributed in the hope that it will be useful,
but WITHOUT WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE

=cut

# Local Variables:
# mode: perl
# tab-width: 3
# End: