
use File::Find;
use File::Stat ":FIELDS";             # all exports
use File::Basename;
use Data::Dumper;
use Time::HiRes qw(gettimeofday tv_interval);
use Pod::Usage;
use Getopt::Long;
# Test for Win32 operating system --------------------------------------------------------------------
my $windows=($^O=~/Win/)?1:0;# Are we running on windows?

if ($windows){
  use Win32::File;
  use Win32::FileOp qw(Mapped);
  use Win32::FileSecurity qw(Get EnumerateRights);
  use Win32::OLE qw(in with);
  use Win32::OLE::Variant;
  use Win32::OLE::Const 'Microsoft Excel';
}
#
use strict;
use constant True => 1;
my(@image_info,$usage,$i,$excel);
my ($name)     = Win32::LoginName;
my ($runtime)  = time;
my ($datetime) = &datetime($runtime);
my ($machine) = Win32::NodeName;    # Determine Machine (Computer) name
my ( $ID, $MAJOR, $MINOR, $YEAR, $MONTH, $DAY, $TIME, $AUTHOR ) =
q$Id: fsinv.pl 1.15 2016/03/11 08:36:32 Dave.Roberts Exp $ =~
/Id: (.+)\s+(\d+)\.(\d+)\s(\d+)\/(\d+)\/(\d+)\s(\d\d:\d\d:\d\d)\s(.+)\s(.+)/;
my (@CallArgs) = @ARGV;
# Process calling arguments -------------------------------------------------------------------
my $images =''; # True to report image files, default false
my $help =''; # True to display help message, default false
my $man =''; # True to display help message, default false
my $version = ""; # True to display version, default false
my $mp3 = ""; # True to report additional information from audio (mp3) files
my $report = "";
GetOptions ("images"  => \$images, "help" => \$help, "man" => \$man, "version" => \$version, "report=s" => \$report, "mp3" => \$mp3)   # flag
or &pod_help("Error in command line arguments\n");
# images switch is true ---------------------------------------------------------------------------
if ($images) {
  use Image::ExifTool qw(:Public);
  @image_info = qw(Model CreateDate CreationDate MediaCreateDate DateTimeOriginal ImageHeight ImageWidth XResolution YResolution ResolutionUnit GPSLatitude GPSLongitude );
}
# ---------------------------------------------------------------------------------------------------
if ($help){
&pod_help();
}
# ---------------------------------------------------------------------------------------------------
if ($version){
  my $message_text = "$ID version $MAJOR.$MINOR";
  print "$message_text\n";
  exit;
}
# ---------------------------------------------------------------------------------------------------
if ($man){
  pod2usage(-exitstatus => 0, -verbose => 2);
  exit;
}
# ---------------------------------------------------------------------------------------------------
if ($report){
  unless ($report =~ /(excel|dump)/ ){
    my $message_text = "valid values for report are excel or dump";
    print "$message_text\n";
  exit;        }
}
#-----------------------------------------------------------------------------------------------------
if ($mp3){
        use MP3::Info;
}
BEGIN { $| = 1; } # autoflush screen output...
my $hmessage = "
";
my $fs = shift or &pod_help("The filesystem name should be specified when calling fsinv\n");

my ($message) = <<"EOT";
*******************************************************************************
Executing script $ID
Script Details:
Version   $MAJOR.$MINOR
Date      $DAY-$MONTH-$YEAR
Time      $TIME
Author    $AUTHOR
Execution:
Computer  $machine
Account   $name
Date/Time $datetime
Calling Arguments: @CallArgs
*******************************************************************************
EOT
print $message;
die if ($usage);
my $start = gettimeofday;
my($inf);
my($loExcel);
# determine a Windows style drive mapping
if (($windows)&($fs =~ /^([a-zA-Z]:)/)){
  $inf->{drive} = $1;
  $inf->{$inf->{drive}} = Mapped $inf->{drive};
}

print " * Reading file system... ";
&File::Find::find(\&wanted,$fs);
my $end = gettimeofday;
my $time = $end - $start;

printf STDERR "\b   complete (took %s)\n",&best_int($time);

# Test if Excel is available for output --------------------------------------------
if ($windows){
# Use existing instance if Excel is already running or start Excel
        eval {$loExcel = Win32::OLE->GetActiveObject('Excel.Application')};
        print "Excel not installed\n" if $@;
        unless (defined $loExcel) {
                $loExcel = Win32::OLE->new('Excel.Application','Quit')
                or print "Unable to start Excel\n";
         }
         print "Excel is installed\n" if ($loExcel);
}
# Logic for determining report to produce -------------------------------------
if ($report eq ""){
        if ($loExcel){
                $report = "excel" ;
        }else{
                $report = "dump" ;
        }
}
if ($report eq "excel"){
      print "Generating Excel report\n";
      &excelreport($loExcel,$inf);
}elsif($report eq "dump"){
      print "Generating dump report\n";
      print Dumper($inf);
}else{
      print "No report defined\n";
}
exit;
#-------------------------------------------------------------------------------
sub wanted {
use Integer;
if (-d $_){
  my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
  $atime,$mtime,$ctime,$blksize,$blocks) = stat($_);
  $inf->{d}->{$File::Find::name . "/"}->{atime} = &datetime($atime);
  $inf->{d}->{$File::Find::name . "/"}->{mtime} = &datetime($mtime);
  $inf->{d}->{$File::Find::name . "/"}->{ctime} = &datetime($ctime);
  $inf->{stats}->{totalfolders} +=1;
  }elsif(-f $_){
# we're checking files
  my($name,$path,$suffix) = fileparse($File::Find::name, qr/\.[^.]*/);
  my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
  $atime,$mtime,$ctime,$blksize,$blocks) = stat($_);
  $inf->{f}->{$path . $name . $suffix}->{path}=$path;
  $inf->{f}->{$path . $name . $suffix}->{suffix}=$suffix;
  $inf->{f}->{$path . $name . $suffix}->{filename}=$name . $suffix;
  $inf->{f}->{$path . $name . $suffix}->{size} = $size;
  $inf->{f}->{$path . $name . $suffix}->{atime} = &datetime($atime);
  $inf->{f}->{$path . $name . $suffix}->{mtime} = &datetime($mtime);
  $inf->{f}->{$path . $name . $suffix}->{ctime} = &datetime($ctime);
  $inf->{d}->{$path}->{size} +=$size;
  $inf->{d}->{$path}->{sizeinc} +=$size;
  $inf->{d}->{$path}->{totalfiles} +=1;
  $inf->{stats}->{totalfiles} +=1;
  if (($images) & ($suffix =~ /jpg$/i)){  # get image info - for image files
    my $tool = Image::ExifTool->new();
    $tool->Options(Unknown => 1, DateFormat => '%d/%M/%Y %H:%M:%S');
    if ($tool->ExtractInfo($_)){
      my($z);
      foreach $z (@image_info){
        if ($tool->GetValue($z)){
          $inf->{f}->{$path . $name . $suffix}->{image}->{$z} = $tool->GetValue($z);
        }
      }
      }else{
      print "Image::ExifTool Error: $!\n";
    }
  }elsif(($mp3) & ($suffix =~ /mp3$/i)){  # get image info - for image files
  print "getting mp3 info for $path $name $suffix\n";
      my $info = get_mp3info($_);
      my $tags = get_mp3tag($_) or print "No TAG info\n";
      $inf->{f}->{$path . $name . $suffix}->{MP3INFO}=$info;
      $inf->{f}->{$path . $name . $suffix}->{MP3TAGS}=$tags;
      }
  my($parent) = $path;
  while ($parent =~ s/[^\/]+\/$//){
    last unless ($inf->{d}->{$parent});
    $inf->{d}->{$parent}->{sizeinc} +=$size;
  }
}
&spin();      # prints next character
}
#-------------------------------------------------------------------------------
sub datetime {
my($t) = @_;
my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($t);
my($month) = ('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec')[$mon];
#my($datetime) = sprintf "%2d %s %02d",
my($datetime) = sprintf "%2d %s %4d %2d:%02d:%02d",
$mday,$month,($year+1900), $hour, $min, $sec;
return $datetime;
}
#-------------------------------------------------------------------------------
sub excelreport {
my($excel) = shift;
my($inf) = shift;
my($vnt);

$excel->{Visible} = 1;
$excel->{SheetsInNewWorkbook} = 3;
my ($book) = $excel->Workbooks->Add;
# Create Files Worksheet .................................................................................................
my ($w) = $book->Worksheets(2);
my($r) = 1;
my ($col) = 7;
$w->Cells($r,1)->Font() -> {'Bold'}= True;
$w->Cells($r,1)->{Value} = "Name";
$w->Cells($r,2)->Font() -> {'Bold'}= True;
$w->Cells($r,2)->{Value} = "File";
$w->Cells($r,3)->Font() -> {'Bold'}= True;
$w->Cells($r,3)->{Value} = "Suffix";
$w->Cells($r,4)->Font() -> {'Bold'}= True;
$w->Cells($r,4)->{Value} = "Size";
$w->Cells($r,5)->Font() -> {'Bold'}= True;
$w->Cells($r,5)->{Value} = "atime (accessed)";
$w->Cells($r,6)->Font() -> {'Bold'}= True;
$w->Cells($r,6)->{Value} = "mtime (modified)";
$w->Cells($r,7)->Font() -> {'Bold'}= True;
$w->Cells($r,7)->{Value} = "ctime (created)";
with($w->Columns(1),  ColumnWidth => 40, HorizontalAlignment => xlLeft);
with($w->Columns(2),  ColumnWidth => 25, HorizontalAlignment => xlLeft);
with($w->Columns(4),  NumberFormat        => '#,##0', ColumnWidth => 18, HorizontalAlignment => xlRight);
  with($w->Columns(5),  NumberFormat        => 'dd/mm/yyyy hh:mm:ss', ColumnWidth => 18, HorizontalAlignment => xlRight);
  with($w->Columns(6),  NumberFormat        => 'dd/mm/yyyy hh:mm:ss', ColumnWidth => 18, HorizontalAlignment => xlRight);
  with($w->Columns(7),  NumberFormat        => 'dd/mm/yyyy hh:mm:ss', ColumnWidth => 18, HorizontalAlignment => xlRight);
  my %cols;
  if ($images) {      # Create titles/formats for columns showing image info
    $col = 8;
    foreach (@image_info) {
      $cols{$_} = $col;
      $w->Cells($r,$col)->Font() -> {'Bold'}= True;
      $w->Cells($r,$col)->{Value} = $_;
      with($w->Columns($col),  NumberFormat        => 'dd/mm/yyyy hh:mm:ss', ColumnWidth => 18, HorizontalAlignment => xlRight) if ($_ =~ /Date/);
      $col++;
    }
  }
  $w->{Name} = "Files";
  foreach (sort keys %{$inf->{f}}){
    $r++;
    $w->Cells($r,1)->{Value} = &backslash($_);
    $w->Cells($r,2)->{Value} = $inf->{f}->{$_}->{filename};
    $w->Cells($r,3)->{Value} = $inf->{f}->{$_}->{suffix};
    $w->Cells($r,4)->{Value} = Win32::OLE::Variant->new(VT_R8,  $inf->{f}->{$_}->{size});
    $w->Cells($r,5)->{Value} = Win32::OLE::Variant->new(VT_DATE, $inf->{f}->{$_}->{atime});
    $w->Cells($r,6)->{Value} = Win32::OLE::Variant->new(VT_DATE, $inf->{f}->{$_}->{mtime});
    $w->Cells($r,7)->{Value} = Win32::OLE::Variant->new(VT_DATE, $inf->{f}->{$_}->{ctime});
    if ($images) {
      $col = 8;
      foreach my $z (@image_info) {
        if ($inf->{f}->{$_}->{image}) {
          if ($z =~ /Date/){
            $w->Cells($r,$col)->{Value} = Win32::OLE::Variant->new(VT_DATE, $inf->{f}->{$_}->{image}->{$z});
            }else{
            $w->Cells($r,$col)->{Value} = $inf->{f}->{$_}->{image}->{$z};
          }
        }
        $col++;
      }
    }
  }
  $w->Range("A:" . &col($col))->Columns->AutoFilter("1","<>"); # set AutoFilter on
  $w->Range("A:" . &col($col))->Columns->AutoFit;              # Set AutoFit Column Widths
  for (1..$col) {
#foreach( qw (A B C D E F G)){                    # Set max column width to 50 characters
    if (($w->Columns(&col($_))->{'ColumnWidth'}) > 50 ) {
      $w->Columns(&col($_))->{'ColumnWidth'} = 50;
    }
  }
# Create Folders Worksheet ...............................................................................................
  $w = $book->Worksheets(3);
  $r = 1;
  $w->Cells($r,1)->Font() -> {'Bold'}= True;
  $w->Cells($r,1)->{Value} = "Directory";
  $w->Cells($r,2)->Font() -> {'Bold'}= True;
  $w->Cells($r,2)->{Value} = "Size (excl subfolders)";
  $w->Cells($r,3)->Font() -> {'Bold'}= True;
  $w->Cells($r,3)->{Value} = "Size (incl subfolders)";
  $w->Cells($r,4)->Font() -> {'Bold'}= True;
  $w->Cells($r,4)->{Value} = "Total Files";
  $w->Cells($r,5)->Font() -> {'Bold'}= True;
  $w->Cells($r,5)->{Value} = "atime (accessed)";
  $w->Cells($r,6)->Font() -> {'Bold'}= True;
  $w->Cells($r,6)->{Value} = "mtime (modified)";
  $w->Cells($r,7)->Font() -> {'Bold'}= True;
  $w->Cells($r,7)->{Value} = "ctime (created)";
  with($w->Columns(1),  ColumnWidth => 40, HorizontalAlignment => xlLeft);
  with($w->Columns(2),  ColumnWidth => 18, HorizontalAlignment => xlRight);
  with($w->Columns(3),  ColumnWidth => 18, HorizontalAlignment => xlRight);
  with($w->Columns(4),  ColumnWidth => 18, HorizontalAlignment => xlRight);
  with($w->Columns(2),  NumberFormat        => '#,##0', ColumnWidth => 18, HorizontalAlignment => xlRight);
    with($w->Columns(3),  NumberFormat        => '#,##0', ColumnWidth => 18, HorizontalAlignment => xlRight);
      with($w->Columns(5),  NumberFormat        => 'dd/mm/yyyy hh:mm:ss', ColumnWidth => 18, HorizontalAlignment => xlRight);
      with($w->Columns(6),  NumberFormat        => 'dd/mm/yyyy hh:mm:ss', ColumnWidth => 18, HorizontalAlignment => xlRight);
      with($w->Columns(7),  NumberFormat        => 'dd/mm/yyyy hh:mm:ss', ColumnWidth => 18, HorizontalAlignment => xlRight);
      $w->{Name} = "Folders";
      foreach (sort keys %{$inf->{d}}){
        $r++;
        $w->Cells($r,1)->{Value} = &backslash($_);
        $w->Cells($r,2)->{Value} = Win32::OLE::Variant->new(VT_R8,  $inf->{d}->{$_}->{size});
        $w->Cells($r,3)->{Value} = Win32::OLE::Variant->new(VT_R8,  $inf->{d}->{$_}->{sizeinc});
        $w->Cells($r,4)->{Value} = $inf->{d}->{$_}->{totalfiles};
        $w->Cells($r,5)->{Value} = Win32::OLE::Variant->new(VT_DATE, $inf->{d}->{$_}->{atime});
        $w->Cells($r,6)->{Value} = Win32::OLE::Variant->new(VT_DATE, $inf->{d}->{$_}->{mtime});
        $w->Cells($r,7)->{Value} = Win32::OLE::Variant->new(VT_DATE, $inf->{d}->{$_}->{ctime});
      }
      $w->Range("A:F")->Columns->AutoFilter("1","<>"); # set AutoFilter on
      $w->Range("A:F")->Columns->AutoFit;              # Set AutoFit Column Widths
      foreach( qw (A B C D E F)){                    # Set max column width to 50 characters
        if (($w->Columns($_)->{'ColumnWidth'}) > 50 ) {
          $w->Columns($_)->{'ColumnWidth'} = 50;
        }
      }
# Create Report Summary Worksheet .................................................................................................
      $w = $book->Worksheets(1);
      my($c) = 2;
      $w->{Name} = "Report Summary";
      $w->Cells(2,$c)->Font() -> {'Bold'}= True;
      $w->Cells(2,$c)->{Value} = "Account Name";
      $w->Cells(3,$c)->Font() -> {'Bold'}= True;
      $w->Cells(3,$c)->{Value} = "Computer Name";
      $w->Cells(4,$c)->Font() -> {'Bold'}= True;
      $w->Cells(4,$c)->{Value} = "Date/Time - Report Start";
      $w->Cells(4,$c)->Font() -> {'Bold'}= True;
      $w->Cells(5,$c)->{Value} = "Runtime";
      $w->Cells(5,$c)->Font() -> {'Bold'}= True;
      $w->Cells(6,$c)->{Value} = "Name";
      $w->Cells(6,$c)->Font() -> {'Bold'}= True;
      $w->Cells(7,$c)->{Value} = "Version";
      $w->Cells(7,$c)->Font() -> {'Bold'}= True;
      $w->Cells(8,$c)->{Value} = "Date";
      $w->Cells(8,$c)->Font() -> {'Bold'}= True;
      $w->Cells(9,$c)->{Value} = "Argument";
      $w->Cells(9,$c)->Font() -> {'Bold'}= True;
      $w->Cells(10,$c)->{Value} = "Total Files";
      $w->Cells(10,$c)->Font() -> {'Bold'}= True;
      $w->Cells(11,$c)->{Value} = "Total File Size";
      $w->Cells(11,$c)->Font() -> {'Bold'}= True;
      $w->Cells(12,$c)->{Value} = "Total Folders";
      $w->Cells(12,$c)->Font() -> {'Bold'}= True;
      if ($inf->{drive} & $inf->{$inf->{drive}}){
        $w->Cells(13,$c)->{Value} = "Drive Mapping";
        $w->Cells(13,$c)->Font() -> {'Bold'}= True;
      }
      with($w->Columns(1),  ColumnWidth => 2, HorizontalAlignment => xlLeft);
      with($w->Columns(2),  ColumnWidth => 25, HorizontalAlignment => xlLeft);
      with($w->Columns(3),  ColumnWidth => 20, HorizontalAlignment => xlLeft);
      $c = 3;
      with($w->Cells(4,$c),  NumberFormat        => 'dd/mm/yyyy hh:mm:ss');
      with($w->Cells(5,$c),  NumberFormat        => 'dd/mm/yyyy');
      with($w->Cells(10,$c),  NumberFormat        => '#,##0', HorizontalAlignment => xlRight);
        with($w->Cells(11,$c),  NumberFormat        => '[>999999999999]0.000,,,," TB";[>999999999]0.000,,," GB";0.000,," MB"', HorizontalAlignment => xlRight);
        with($w->Cells(12,$c),  NumberFormat        => '#,##0', HorizontalAlignment => xlRight);
          $w->Cells(2,$c)->{Value} = $name;
          $w->Cells(3,$c)->{Value} = $machine;
          $w->Cells(4,$c)->{Value} = Win32::OLE::Variant->new(VT_DATE, $datetime);
          $w->Cells(5,$c)->{Value} = &best_int($time);
          $w->Cells(6,$c)->{Value} = $ID;
          $w->Cells(7,$c)->{Value} = sprintf "%d.%d",$MAJOR,$MINOR;
          $w->Cells(8,$c)->{Value} = sprintf "%d-%s-%d",$DAY,$MONTH,$YEAR;
          my $arg;
          foreach (@CallArgs){
            $arg .= $_ . " ";
          }
          $w->Cells(9,$c)->{Value} = $arg;
          $w->Cells(10,$c)->{Value} = $inf->{stats}->{totalfiles};
          $w->Cells(11,$c)->{Value} = "=SUM(FILES!D:D)";
          $w->Cells(12,$c)->{Value} = $inf->{stats}->{totalfolders};
          if ($inf->{drive} & $inf->{$inf->{drive}}){
            $w->Cells(13,$c)->{Value} = sprintf "%s  =>  %s",$inf->{drive},$inf->{$inf->{drive}};
          }
        }
#-------------------------------------------------------------------------------
sub backslash {
          my ($a) = @_;
          $a =~ s/\//\\/g;
          return $a;
        }
#-------------------------------------------------------------------------------
sub best_int
        {
          use integer;
          my $s = shift;
          return sprintf ":%02d sec", $s if $s < 60;
          my $m = $s / 60; $s = $s % 60;
          return sprintf "%02d:%02d min:sec", $m, $s if $m < 60;
          my $h = $m /  60; $m %= 60;
          return sprintf "%02d:%02d:%02d hr:min:sec", $h, $m, $s if $h < 24;
          my $d = $h / 24; $h %= 24;
          return sprintf "%d:%02d:%02d:%02d day:hr:min:sec", $d, $h, $m, $s;
        }
#-------------------------------------------------------------------------------        
sub spin {
          my %spinner = (
            '|' => '/',
            '/' => '-',
            '-' => '\\',
            '\\' => '|'
          );
          $i = (! defined $i) ? '|' : $spinner{$i};
          print STDERR "\b$i";
        }
        sub col{
          my ($num) = @_;
          my($base)=26;
          my($mem) = "";
          my($h,$c);
          while ($num >= $base){
            $h = int($num/$base);
            $c = $num -($h*$base);
            if ($c eq 0){
              $c = $base;
              $h--;
            }
            $mem = chr($c+64) . $mem;
            $num = $h;
          }
          $mem = chr($num+64) . $mem unless ($num eq 0);
          return  $mem;
        }
#-------------------------------------------------------------------------------
sub pod_help{
          my $message_text  = shift;
          my $exit_status   = 2;          ## The exit status to use
          my $verbose_level = 1;          ## The verbose level to use
          my $filehandle    = \*STDERR;   ## The filehandle to write to
 pod2usage( { -message => $message_text ,
              -exitval => $exit_status  ,
             -verbose => $verbose_level,
            -output  => $filehandle } );
   exit;
   }
#-------------------------------------------------------------------------------
   
__END__
=pod
        
=head1 NAME
        
   fsinv.pl - A simple program that documents a file system
        
=head1 SYNOPSIS
        
  fsinv.pl filesystem [options]
        
=head1 OPTIONS
        
=over 8
        
=item B<--help>

Show brief usage help information.        
=item B<--manual>

Read the manual, with examples.
        
=item B<--images>

Include information from image files in the report
        
=item B<--version>

Show the version number and exit.
=item B<--images>

Read and report the following attributes of .jpg suffix files (see MP3::Info for more information). These are only reported if they are identified.
         
=over 16         
=item Model 

=item CreateDate

=item CreationDate

=item MediaCreateDate

=item DateTimeOriginal

=item ImageHeight

=item ImageWidth 

=item XResolution 

=item YResolution

=item ResolutionUnit

=item GPSLatitude

=item GPSLongitude
=back
=item B<--mp3>

Read MP3TAGS and MP3INFO for MP3 files and include these in the report where files have the .mp3 suffix (see MP3::Info for more information)
        
=item B<--report=name>

Where report is set to either
        "excel" - the default for a windows system with Microsoft Excel installed
        "dump" - the default where Excel is not available

=back
        
=head1 EXAMPLES
        
        fsinv.pl //server/share/folder
        
        fsinv.pl \\server\share\folder -images
        
        fsinv.pl D:\temp
        
        fsinv.pl /myfolder

=head1 DESCRIPTION
        
This is a simple program to generate an inventory report for a file system.
The report is generated as an Excel file (on Win32 systems with Excel installed)
, or otherwise is dumped as STDOUT.
 
 =head1 COPYRIGHT AND LICENSE

Copyright 2015 Dave Roberts

This is released under the Artistic 
License. See L<perlartistic>.
       
=head1 AUTHOR
        
Dave Roberts
--
david.roberts@cpan.org
        
=head1  VERSION
        
$Id: fsinv.pl 1.15 2016/03/11 08:36:32 Dave.Roberts Exp $
        
=cut
