The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#
# Copyright (c) 2002 Paul Winkeler.  All Rights Reserved.
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself.
#
package NBU::Image;

use strict;
use Carp;

use NBU::Media;

my %imageList;

my $fileRecursionDepth = 1;
my $showEmptyFragments = 0;

BEGIN {
  use Exporter   ();
  use AutoLoader qw(AUTOLOAD);
  use vars       qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $AUTOLOAD);
  $VERSION =	 do { my @r=(q$Revision: 1.39 $=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };
  @ISA =         qw();
  @EXPORT =      qw();
  @EXPORT_OK =   qw();
  %EXPORT_TAGS = qw();
}

sub new {
  my $proto = shift;
  my $image;

  if (@_) {
    my $backupID = shift;
    if (exists($imageList{$backupID})) {
      $image = $imageList{$backupID};
    }
    else {
      $image = { };
      bless $image, $proto;
      $imageList{$image->id($backupID)} = $image;
    }
  }
  return $image;
}

sub populate {
  my $proto = shift;

  $proto->loadImages(NBU->cmd("bpimmedia -l |"));
}

sub byID {
  my $proto = shift;
  my $backupID = shift;

  if (exists($imageList{$backupID})) {
    return $imageList{$backupID};
  }

  return undef;
}

sub list {
  my $proto = shift;

  return (values %imageList);
}

sub id {
  my $self = shift;

  if (@_) {
    $self->{BACKUPID} = shift;
    # The backup ID of an image in part encodes the time the image was
    # created
    $self->{CTIME} = substr($self->{BACKUPID}, -10);
  }
  return $self->{BACKUPID};
}

sub ctime {
  my $self = shift;

  return $self->{CTIME};
}

sub client {
  my $self = shift;

  if (@_) {
    my $host = shift;
    if (defined($self->{CLIENT})) {
      if ((my $oldHost = $self->{CLIENT}) == $host) {
	return $host;
      }
      else {
#	$oldHost->removeImage($self);
      }
    }
    $host->addImage($self);
    $self->{CLIENT} = $host;
  }
  return $self->{CLIENT};
}

sub class {
  my $self = shift;

  return $self->policy(@_);
}

sub policy {
  my $self = shift;

  if (@_) {
    $self->{POLICY} = shift;
  }
  return $self->{POLICY};
}

sub schedule {
  my $self = shift;

  if (@_) {
    $self->{SCHEDULE} = shift;
  }
  return $self->{SCHEDULE};
}

sub expires {
  my $self = shift;

  if (@_) {
    $self->{EXPIRES} = shift;
  }
  return $self->{EXPIRES};
}

sub loadDetail {
  my $self = shift;

  #
  # Layout of this output divined from 
  # http://mailman.eng.auburn.edu/pipermail/veritas-bu/2001-May/003861.html
  my $pipe = NBU->cmd("bpimagelist -l -backupid ".$self->id." |");
  while (<$pipe>) {
    next if (/^HIST/);
    next if (/^FRAG/);
    if (/^IMAGE/) {
      my ($tag, $clientName,
	  $u3, $u4, $u5,
	  $id, $className,
	  $u8, $u9, $u10,
	  $scheduleName,
	  $u12, $u13, $u14,
	  $elapsed,
	  $u16, $u17, $u18,
	  $kbWritten,
	  $fileCount, $copyCount, $fragmentCount,
	  $compressed, $u24,
	  $softwareVersion, $u26, $u27,
	  $primary,
	  $imageType, $TIRInfo, $TIRExpiration,
	  $keywords
      ) = split;
      $self->{ELAPSED} = $elapsed;
    }
  }
  $self->{DETAILED} = 1;
  close($pipe);
}

sub elapsed {
  my $self = shift;

  $self->loadDetail if (!defined($self->{DETAILED}));

  return $self->{ELAPSED};
}

sub retention {
  my $self = shift;

  if (@_) {
    $self->{RETENTION} = shift;
  }
  return $self->{RETENTION};
}


sub size {
  my $self = shift;
  my $size;

  for my $f ($self->fragments) {
    next unless defined($f);
    $size += $f->size;
  }

  return $size;
}

#
# Add another fragment to the referenced copy of this image
sub insertFragment {
  my $self = shift;
  my $copy = shift;
  my $fragment = shift;
  
  my $fragmentListR = $self->{FRAGMENTLISTS};
  if (!defined(@$fragmentListR[$copy])) {
    @$fragmentListR[$copy] = [];
  }
  my $toc = @$fragmentListR[$copy];;

  return $$toc[$fragment->number - 1] = $fragment;
}

sub fragments {
  my $self = shift;

  my $fragmentListR = $self->{FRAGMENTLISTS};
  if (@_) {
    my $copy = shift;
    if (!defined(@$fragmentListR[$copy])) {
      return ();
    }
    my $toc = @$fragmentListR[$copy];;
    return @$toc;
  }

  my @toc;
  foreach my $copy (1..$self->copies) {
    if (defined(@$fragmentListR[$copy])) {
      my $copytoc = @$fragmentListR[$copy];
      @toc = (@toc, @$copytoc);
    }
  }
  return @toc;
}

sub loadImages {
  my $proto = shift;
  my $pipe = shift;

  my $image;
  while (<$pipe>) {
    if (/^IMAGE/) {
      my ($tag, $clientName, $clientVersion, $backupID, $className,
	$classType,
	$scheduleName,
	$scheduleType,
	$retentionLevel, $fileCount, $expires,
	$compressed, $encrypted
      ) = split;

      $image = undef;
      next if ($expires < time);

      $image = NBU::Image->new($backupID);
      $image->{ENCRYPTED} = $encrypted;
      $image->{COMPRESSED} = $compressed;
      $image->{FILECOUNT} = $fileCount;
      $image->{EXPIRES} = $expires;
      $image->{RETENTION} = NBU::Retention->byLevel($retentionLevel);
      $image->{COPYCOUNT} = 0;
      $image->{FRAGMENTLISTS} = [];

      my $class = $image->class(NBU::Class->new($className, $classType));
      $image->schedule(NBU::Schedule->new($class, $scheduleName, $scheduleType));

      my $host;
      $image->client($host = NBU::Host->new($clientName));
    }
    elsif (/^FRAG/) {
      next if (!defined($image));

      my ($tag, $copy, $number, $size, $removable,
	  $mediaType, $density, $fileNumber,
          $rest) = split(/[\s]+/, $_, 9);

      #
      # The fragment list contains failed fragments which are left-overs from
      # failed backups.  Normally we skip these, but they can be made visible
      # when running diagnostics
      # Such fragments appear to be identified by having a negative fragment number
      next if (!$showEmptyFragments && (($number < 1)));

      #
      # Is this the start of a new copy of this image?
      if ($copy > $image->{COPYCOUNT}) {
	$image->{COPYCOUNT} = $copy;
      }

      my ($mediaID, $volume);
      if (($removable == 0) && ($mediaType == 0)) {
        #
        # Non-removable media (aka disk files) occasionally have spaces in their
        # names and those will then be surrounded by double quotes...
        # Additionally we tag these "media" as removable when creating them.
        if ($rest =~ s/\"(.*)\"[\s]//) {
	  $mediaID = $1;
        }
        else {
	  $rest =~ s/([\S]+)//;
	  $mediaID = $1;
        }
        $volume = NBU::Media->new($mediaID, undef, 0);
      }
      else {
	$rest =~ s/([\S]+)//;
	$mediaID = $1;
        $volume = NBU::Media->new($mediaID, undef, 1);
        $volume->density($density);
      }
      $rest =~ s/^[\s]*//;
      my ($mmdbHostName,
	  $blockSize, $offset, $allocated, $dwo,
	  $u6, $u7,
	  $expires, $mpx
      ) = split(/[\s]+/, $rest);
      $volume->mmdbHost(NBU::Host->new($mmdbHostName));

      my $fragment = NBU::Fragment->new($number, $copy, $image, $volume, $offset, $size, $dwo, $fileNumber, $blockSize);

      $volume->insertFragment($fileNumber - 1, $fragment);
      $image->insertFragment($copy, $fragment);

      $image->{REMOVABLE} += $volume->removable;

      $image->volume($volume);
    }
  }
  close($pipe);
}

sub loadFileList {
  my $self = shift;
  my $func = shift;
  my $depth = shift;
  my @fl;
 
  my ($s, $m, $h, $dd, $mon, $year, $wday, $yday, $isdst) = localtime($self->ctime);
  my $mm = $mon + 1;
  my $yy = sprintf("%02d", ($year + 1900) % 100);

  $depth = $fileRecursionDepth if (!defined($depth));
  my $pipe = NBU->cmd("bpflist -t ANY"
    ." -option GET_ALL_FILES"
    ." -client ".$self->client->name
    ." -backupid ".$self->id
    ." -d ${mm}/${dd}/${yy}"
    ." -rl $depth"
    ." |");

  while (<$pipe>) {
    next if (/^FILES/);
    chop;
    #
    # Since file names can contain spaces and some bright soul decided to place the file
    # name in the middle of this line, we need to pick it apart in three pieces:
    #  before, filename, after
    # More details at https://forums.symantec.com/syment/board/message?board.id=21&thread.id=5475
    my ($i, $compressedFileSize, $pathLength, $u3, $offset,
            $imageMode, $rawPartitionSize,
            $largeFileSize, # only set if file over 2GB; units are in GB
            $physicalDeviceNumber,
        $rest) = split(/[\s]+/, $_, 10);
    if (!($rest =~ /^(.*)[\s]([\S]+)[\s]([\S]+)[\s]([\S]+)[\s]([\S]+)[\s]([\S]+)[\s]([\S]+)[\s]([\S]+)/)) {
      print STDERR "IMAGE filename match failed on $_\n";
      exit;
    }
    my ($name, $mode, $user, $group, $size,
               $lastAccessed, $lastModified, $lastInodeModified
       ) = ($1, $2, $3, $4, $5, $6. $7, $8);
#    next if ($name =~ /(\/|\\)$/);
    if (defined($func)) {
      &$func($name);
    }
    else {
      push @fl, $name;
    }
  }
  close($pipe);

  if (!defined($func)) {
    $self->{FLIST} = \@fl;
  }
}

sub fileList {
  my $self = shift;

  if (!$self->{FLIST}) {
    $self->loadFileList(undef, undef);
  }

  if (my $flR = $self->{FLIST}) {
    return @$flR;
  }
  return undef;
}

sub copies {
  my $self = shift;

  return $self->{COPYCOUNT};
}

#
# An image is considered removable if any of its fragments is
# is stored on removable media.  This attribute is computed during
# the loading if the image
sub removable {
  my $self = shift;

  return $self->{REMOVABLE};
}

sub density {
  my $self = shift;

  return $self->volume->density
    if ($self->volume);
  return undef;
}

sub showEmptyFragments {
  my $proto = shift;

  if (@_) {
    $showEmptyFragments = shift;
  }

  return $showEmptyFragments;
}

sub fileRecursionDepth {
  my $proto = shift;

  if (@_) {
    $fileRecursionDepth = shift;
  }

  return $fileRecursionDepth;
}

sub volume {
  my $self = shift;

  if (@_) {
    my $volume = shift;

    $self->{VOLUME} = $volume;
  }

  return $self->{VOLUME};
}

1;

__END__

=head1 NAME

NBU::Image - Support for NetBackup Images, i.e. discrete backup sets

=head1 SUPPORTED PLATFORMS

=over 4

=item * 

Solaris

=item * 

Windows/NT

=back

=head1 SYNOPSIS

    To come...

=head1 DESCRIPTION

This module provides support for ...

=head1 SEE ALSO

=over 4

=item L<NBU::Media|NBU::Media>

=back

=head1 AUTHOR

Winkeler, Paul pwinkeler@pbnj-solutions.com

=head1 COPYRIGHT

Copyright (C) 2002-2007 Paul Winkeler

=cut