#!/usr/local/bin/perl
#
#
# filelogger
#
# Program for monitoring files
#
# Copyright (c) 2001 by Ardan Patwardhan. All rights reserved.
#
# DISCLAIMER
# This software is provided on an ``as is'' and any express or implied
# warranties, including, but not limited to, the implied warranties of
# merchantability and fitness for a particular purpose are disclaimed. 
# In no event shall the copyright owner be liable for any direct, 
# indirect, incidental, special, exemplary, or consequential damages 
# (including, but not limited to, procurement of substitute goods or 
# services; loss of use, data, or profits; or business interruption) 
# however caused and on any theory of liability, whether in contract, 
# strict liability, or tort (including negligence or otherwise) arising 
# in any way out of the use of this software, even if advised of the 
# possibility of such damage. 
#
# $Header: /usr/users/ardan/cvsroot/perl/filelogger,v 1.4 2002/01/04 08:49:58 ardan Exp $
#

# Program version
my $VERSION = '$Revision: 1.4 $';

########################################################################
#                                                                      #
# COMMAND LINE PROCESSING                                              #
#                                                                      #
########################################################################

use Getopt::Std;
getopt('c',\%opts);    # Configuration file
die "No config file specified, stopped" unless ($configMod = $opts{c});
die "Unable to read config file, stopped" unless (do $configMod);



# The following lines up to the main block should be cut and paste into
# a separate resource file. The file is specified for inclusion using the
# -c option. The lines with "##" need to be uncommented and modified.

########################################################################
#                                                                      #
# CONFIGURATION                                                        #
#                                                                      #
########################################################################

# Used to identify configuration in log file
##$reportId = "Ardan Patwardhan";

# Log file to contain state of monitored directories
# Meant only to be used internally by the program
##$lastLog = "lastLog.log";

# Base log file - All info about changes to monitored files will be
# appended to this file. If it does not exist, it will be created.
##$baseLog = "baseLog.log";

# Directories to monitor
##@dirList = ("/f03/image/tbsv",
##            "/f07/image/tbsv",
##            "/f08/ardan",
##            "/f10/ardan",
##            "/f11/Image/ardan",
##            "/f11/Image/marc",
##            "/f13/ardan",
##            "/f14/ardan",
##            "/f15/ardan",
##            "/f15/mount",
##            "/f15/patches",
##            "/wolf1/ardan",
##            "/wolf9/ardan",
##            "/disk1/ardan",
##            "/disk1/Image/ardan",
##            "/usr/users/ardan");

# List of patterns specifying files and directories that should be
# excluded. Use Perl regular expression syntax
##@excludeList = ('~$',                 # Emacs old files
##                '.saves-',            # Emacs .saves files
##                '#\w+(\.\w+)*#$',     # Emacs backup files
##                '/core$',             # Core dumps
##                'junk',               # Junk files and directories
##                 );



########################################################################
#                                                                      #
# MAIN BODY                                                            #
#                                                                      #
########################################################################

# USE statements
use File::Find;



# Variables and Data structures
%hashOfFiles     = ();       # keys = all files and directories to be monitored; values = (mode, user, group, size, mtime)
%hashOfLastFiles = ();       # keys = all files and direcories in lastLog file; values = (mode, user, group, size, mtime)
@failDir         = ();       # Directories that could not be accessed
@modeChngFiles   = ();       # List of files/dir for which the mode has changed
@userChngFiles   = ();       # List of files/dir for which the user has changed
@groupChngFiles  = ();       # List of files/dir for which the group has changed
@modifiedFiles   = ();       # List of files that have been modified
@deletedFiles    = ();       # List of files/dir that have been deleted
@createdFiles    = ();       # List of files/dir that have been created
$lastLogExists   = 0;        # Assume false to start with
$lastLogRead     = 0;        # Assume false to start with
$sourceSize      = 0;        # Total size of all the files monitored



# Create list of all files in monitored directories
foreach my $dir (@dirList) {
   if(! -d $dir) {
      push @failDir, ($dir);
      next;
   }       
   find { wanted => sub {
      my @fileStatus = ();
      if(-l $File::Find::name) {
         @fileStatus = lstat($File::Find::name);
      } else {
         @fileStatus = stat($File::Find::name);
      }
      my $mode  = $fileStatus[2];
      my $user  = getpwuid($fileStatus[4]);
      my $group = getgrgid($fileStatus[5]);
      my $size  = $fileStatus[7];
      my $mtime = $fileStatus[9];
      my @fileInfo = ($mode, $user, $group, $size, $mtime);
      $sourceSize += $fileInfo[3];
      $hashOfFiles{$File::Find::name} = [ @fileInfo ];
   }, follow => 0}, "$dir";
}

            

# Exclude files and directories that match exclude patterns
foreach my $pat (@excludeList) {
   foreach my $file (keys %hashOfFiles) {
      if($file =~ m/$pat/) {
         $sourceSize -= @{$hashOfFiles{$file}}[3];
         delete $hashOfFiles{$file};
      }
   }
}



# Read data from lastLog file
if (-f $lastLog && -r $lastLog) {
   $lastLogExists = 1;
   open(LASTLOG, "< $lastLog") || die "can't open $lastLog: $!";
   while (my $line = <LASTLOG>) {
      chomp($line);
      my $mode  = oct(cleanWhitespace(substr($line,0,7)));
      my $mtime = cleanWhitespace(substr($line,8,15));
      my $size  = cleanWhitespace(substr($line,24,19));
      my $user  = cleanWhitespace(substr($line,44,10));
      my $group = cleanWhitespace(substr($line,55,10));
      my $file  = cleanWhitespace(substr($line,66));
      $hashOfLastFiles{$file} = [ $mode, $user, $group, $size, $mtime ];
   }
   $lastLogRead = 1;
   close LASTLOG;
}



# Check which files have been deleted or modified
if($lastLogRead) {
   for my $file (sort keys %hashOfLastFiles) {
      if ($hashOfFiles{$file}=="") {
         push @deletedFiles, ($file);
      } else { 
         my ($mode1, $user1, $group1, $size1, $mtime1) = @{$hashOfLastFiles{$file}};
         my ($mode2, $user2, $group2, $size2, $mtime2) = @{$hashOfFiles{$file}};
         if ($mode1 != $mode2) {
            push @modeChngFiles, ($file);
	 }
         if ($user1 ne $user2) {
            push @userChngFiles, ($file);
	 }
         if ($group1 ne $group2) {
            push @groupChngFiles, ($file);
	 }
         if ($mtime1 != $mtime2) {
            # This case is not interesting if this is a directory
            if (! -d $file) {
               push @modifiedFiles, ($file);
            }
         }
      }
   }
}



# Check which files have been created
if($lastLogRead) {
   foreach my $file (sort keys %hashOfFiles) {
      if ($hashOfLastFiles{$file}=="") {
         push @createdFiles, ($file);
      } 
   }
}



# If nothing has happened since last time just exit
if($lastLogRead && !(@createdFiles||@modeChngFiles||@userChngFiles||@groupChngFiles||@modifiedFiles||@deletedFiles||@failDir)) {
   exit;
}



# Append activity to base log file
open(BASELOG, ">> $baseLog") || die "can't open $baseLog: $!";
printf(BASELOG "\n");
printf(BASELOG "FILELOGGER REPORT BEGIN\n");
@timeList = niceTime(time);
printf(BASELOG "Prepared for %s on %d\/%d\/%d at %d:%02d:%02d \n", $reportId,$timeList[5],$timeList[4],$timeList[3],$timeList[2],$timeList[1],$timeList[0]);
printf(BASELOG "Source size(bytes)        : %d\n\n", $sourceSize);

if(!$lastLogRead) {

   # First time
   $txt = "Dump of all monitored files";
   printFileList([keys %hashOfFiles],\%hashOfFiles,\*BASELOG,\$txt);

} else {

   $txt = "New files";
   printFileList(\@createdFiles,\%hashOfFiles,\*BASELOG,\$txt);

   $txt = "Modified files";
   printFileList(\@modifiedFiles,\%hashOfFiles,\*BASELOG,\$txt);

   $txt = "Deleted files";
   printFileList(\@deletedFiles,\%hashOfLastFiles,\*BASELOG,\$txt );

   # Output list of files that have had mode changes
   if(@modeChngFiles) {
      printf(BASELOG "File-mode change: \n\n");
      foreach my $file (sort @modeChngFiles) {
         my $mode1 = @{$hashOfLastFiles{$file}}[0];
         my $mode2 = @{$hashOfFiles{$file}}[0];
         my $modeText1 = niceMode($mode1);
         my $modeText2 = niceMode($mode2);
         printf(BASELOG "%s -> %s : %s\n",$modeText1,$modeText2,$file);
      }
      printf(BASELOG "\n\n\n");
   }

   # Output list of files for which ownership has changed
   if(@userChngFiles) {
      printf(BASELOG "File-owner change: \n\n");
      foreach my $file (sort @userChngFiles) {
         my $user1 = @{$hashOfLastFiles{$file}}[1];
         my $user2 = @{$hashOfFiles{$file}}[1];
         printf(BASELOG "(%s) -> (%s) : %s\n",$user1,$user2,$file);
      }
      printf(BASELOG "\n\n\n");
   }

   # Output list of files for which group has changed
   if(@groupChngFiles) {
      printf(BASELOG "File-group change: \n\n");
      foreach my $file (sort @groupChngFiles) {
         my $group1 = @{$hashOfLastFiles{$file}}[2];
         my $group2 = @{$hashOfFiles{$file}}[2];
         printf(BASELOG "(%s) -> (%s) : %s\n",$group1,$group2,$file);
      }
      printf(BASELOG "\n\n\n");
   }

}

# Output list of directories not accessed
if(@failDir) {
   printf(BASELOG "Unable to access following directories: \n\n");
   foreach my $dir (sort @failDir) {
      printf(BASELOG "%s\n",$dir);
   }
   printf(BASELOG "\n\n\n");
}
printf(BASELOG "FILELOGGER REPORT END\n\n\n");
close BASELOG;



# Output monitored files to lastLog file
open(LASTLOG, "> $lastLog") || die "can't open $lastLog: $!";
foreach my $file (sort keys %hashOfFiles) {
   my @fileInfo = @{$hashOfFiles{$file}};
   my ($mode, $user, $group, $size, $mtime) = @fileInfo;
   printf(LASTLOG "%07o %15d %19d %10s %10s %s\n",$mode,$mtime,$size,$user,$group,$file);
}
close LASTLOG;



########################################################################
#                                                                      #
# SUBROUTINES                                                          #
#                                                                      #
########################################################################

# Subroutine for deleting leading and trailing whitespace
sub cleanWhitespace {
   my ($inStr) = @_;
   for ($outStr = $inStr) {
      s/^\s+//;
      s/\s+$//;
   }
   return $outStr;
}

# Get current time into nice format
sub niceTime {
   my ($time) = @_;
   my @timeList = localtime($time);
   $timeList[4] += 1;      # Month 1->12
   $timeList[5] += 1900;   # Year
   return @timeList;
}



# Get mode into nice format
sub niceMode {
use Fcntl ':mode';
   my ($mode) = @_;
   my @modeList = ();
   $modeList[0] = ($mode & S_IXUSR)?'x':'-';
   $modeList[1] = ($mode & S_IWUSR)?'w':'-';
   $modeList[2] = ($mode & S_IRUSR)?'r':'-';
   $modeList[3] = ($mode & S_IXGRP)?'x':'-';
   $modeList[4] = ($mode & S_IWGRP)?'w':'-';
   $modeList[5] = ($mode & S_IRGRP)?'r':'-';
   $modeList[6] = ($mode & S_IXOTH)?'x':'-';
   $modeList[7] = ($mode & S_IWOTH)?'w':'-';
   $modeList[8] = ($mode & S_IROTH)?'r':'-';
   if (S_ISDIR($mode)) {
      $modeList[9] = 'd';
   } elsif (S_ISLNK($mode)) {
      $modeList[9] = 'l';
   } elsif (S_ISBLK($mode)) {
      $modeList[9] = 'b';
   } elsif (S_ISCHR($mode)) {
      $modeList[9] = 'c';
   } elsif (S_ISFIFO($mode)) {
      $modeList[9] = 'p';
   } elsif (S_ISSOCK($mode)) {
      $modeList[9] = 's';
   } elsif (S_ISREG($mode)) { 
      $modeList[9] = '-';
   } else {
      $modeList[9] = '?';
   }
   my $modeText = sprintf("%s%s%s%s%s%s%s%s%s%s",$modeList[9],$modeList[2],$modeList[1],$modeList[0],$modeList[5],$modeList[4],$modeList[3],$modeList[8],$modeList[7],$modeList[6]);
   return $modeText;
}



# Output list of files
sub printFileList {
   my ($fileList, $hashOfFiles,$FD, $txt) = @_;
   if(@$fileList) {
      my $nTotal = 0;
      my $totSize = 0;
      foreach my $file (@$fileList) {
         my @fileInfo = @{$$hashOfFiles{$file}};
         my ($mode, $user, $group, $size, $mtime) = @fileInfo;         
         $nTotal++;
         $totSize += $size;
      }
      printf($FD "%s:\n\n", $$txt);
      printf($FD "Number      : %d\n", $nTotal);
      printf($FD "Size (bytes): %d\n\n",$totSize);
      printf($FD "      Mode     Date     Time                Size      Owner      Group File-name\n");
      printf($FD "==========================================================================================\n");
#                      drwxr-xr-x 20011210 16:17:05                 512      ardan     system /f14/ardan
      foreach $file (sort @$fileList) {
         my @fileInfo = @{$$hashOfFiles{$file}};
         my ($mode, $user, $group, $size, $mtime) = @fileInfo;
         my @timeList = niceTime($mtime);
         my $modeText = niceMode($mode);
         printf($FD "%s %d%02d%02d %02d:%02d:%02d %19d %10s %10s %s\n",$modeText,$timeList[5],$timeList[4],$timeList[3],$timeList[2],$timeList[1],$timeList[0],$size,$user,$group,$file);
      }
      printf($FD "\n\n\n");
   }
}



########################################################################
#                                                                      #
# POD                                                                  #
#                                                                      #
########################################################################

=head1 NAME

filelogger

=head1 SYNOPSIS

filelogger -c config.rc >& error.log

=head1 DESCRIPTION

This program analyses changes in files and directories in monitored directory trees (@dirList) between consecutive runs of the program and appends a report to a file specified by $baseLog. It does so by storing the state of the files and directories in @dirList in a file specified by $lastLog, each time the program runs. Whereas the file specified by $baseLog is meant to be examined by the user, the file specified by $lastLog is for internal consumption only. The program reports which files and directories have been created, deleted or modified. Changes to the following attributes of files are monitored: mode, user, group, size, and mtime.

=head2 Configuration

Configuration information is provided in a separate file that must be specified using the -c command-line option, e.g. filelogger -c filelogger.rc

The following is a sample configuration file:

 ########################################################################
 #                                                                      #
 # CONFIGURATION                                                        #
 #                                                                      #
 ########################################################################

 # Used to identify configuration in log file
 $reportId = "Ardan Patwardhan";

 # Log file to contain state of monitored directories
 # Meant only to be used internally by the program
 $lastLog = "lastLog.log";

 # Base log file - All info about changes to monitored files will be
 # appended to this file. If it does not exist, it will be created.
 $baseLog = "baseLog.log";

 # Directories to monitor
 @dirList = ("/f03/image/tbsv",
             "/f07/image/tbsv",
             "/f08/ardan",
             "/f10/ardan",
             "/f11/Image/ardan",
             "/f11/Image/marc",
             "/f13/ardan",
             "/f14/ardan",
             "/f15/ardan",
             "/f15/mount",
             "/f15/patches",
             "/wolf1/ardan",
             "/wolf9/ardan",
             "/disk1/ardan",
             "/disk1/Image/ardan",
             "/usr/users/ardan");

 # List of patterns specifying files and directories that should be
 # excluded. Use Perl regular expression syntax
 @excludeList = ('~$',                 # Emacs old files
                 '.saves-',            # Emacs .saves files
                 '#\w+(\.\w+)*#$',     # Emacs backup files
                 '/core$',             # Core dumps
                 'junk',               # Junk files and directories
                  );

=head2 crontab

The best way to run this script is to have it run periodically by adding a crontab entry. The following is an example crontab entry that runs every night at 1:10:

 10 1 * * * /usr/users/ardan/perl/filelogger -c/usr/users/ardan/filelogger.rc >& /usr/users/ardan/error.log

=head2 Sample output

The following is a sample from a base log file:

 FILELOGGER REPORT BEGIN
 Prepared for Ardan Patwardhan on 2002/1/3 at 15:09:53 
 Source size(bytes)        : 23907240

 Modified files:

 Number      : 2
 Size (bytes): 35215

       Mode     Date     Time                Size      Owner      Group File-name
 ==========================================================================================
 -rw-r--r-- 20020103 15:09:45               21017      ardan      users /usr/users/ardan/history.log
 -rwxr-xr-x 20020103 15:09:25               14198      ardan      users /usr/users/ardan/perl/filelogger



 Deleted files:

 Number      : 1
 Size (bytes): 512

       Mode     Date     Time                Size      Owner      Group File-name
 ==========================================================================================
 drwxr-xr-x 20020103 12:25:11                 512      ardan      users /usr/users/ardan/tmp1



 FILELOGGER REPORT END

=head1 BUGS


=head1 SEE ALSO


=head1 COPYRIGHT

Copyright 2001-2002 by Ardan Patwardhan. All Rights Reserved.

=head1 DISCLAIMER

This software is provided on an 'as is' basis and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. 

=head1 README

Script for monitoring specified directory trees for changes in files and directories.

=head1 PREREQUISITES

 File::Find

=head1 OSNAMES

 dec_osf (tested)
 Any Unix or Linux OS (not tested)

=head1 SCRIPT CATEGORIES

 Unix/System_administration

=cut
