#!/usr/bin/perl
#
# findDuplicates.pl Copyright Michael Banack 2010
#
# findDuplicates.pl is free software: you can redistribute it 
# and/or modify it under the terms of the GNU Lesser General 
# Public License as published by the Free Software Foundation, 
# either version 3 of the License, or (at your option) any later 
# version.
#
# findDuplicates.pl is distributed in the hope that it will be 
# useful, but WITHOUT ANY WARRANTY; without even the implied 
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
# PURPOSE.  See the GNU General Public License for more 
# details.  You should have received a copy of the GNU General 
# Public License along with this program.  If not, see 
# <http://www.gnu.org/licenses/gpl.html>.


use strict;
use File::Find;
use File::Compare;
use Getopt::Std;

my $HAS_FORMAT = eval { require Format::Human::Bytes; 1; };

my %fileList; #hashes size => files
my $duplicates; #reference to array of arrays
my $wastedSpace = 0; #wasted bytes in duplicates

my %options; #holds the global options

$Getopt::Std::STANDARD_HELP_VERSION = 1;
our $VERSION = "1.0.1";

getopts( 'dpvzlfh', \%options);

my $pretend = $options{"p"};
my $verbose = $options{"v"};
my $delete = $options{"d"};
my $checkZero = $options{"z"};
my $useLines = $options{"l"};
my $followSymlinks = $options{"f"};

if($options{"h"}) {
	VERSION_MESSAGE(\*STDOUT);
	HELP_MESSAGE(\*STDOUT);
	exit(1);
}

sub HELP_MESSAGE($;$$$)
{
	my ($fh, $optName, $optVer, $switches) = @_;
	
	#Getopt::Std::help_mess($switches);
	print $fh "\n";
	print $fh "Usage: findDuplicates.pl [OPTIONS] DIR1 DIR2 ...\n";
	print $fh "\n";
	print $fh "Finds duplicate files in the specified directories.\n";
	print $fh "\n";
	print $fh "Options:\n";
	print $fh "        -p:       pretend mode (don't actually do anything)\n";
	print $fh "        -d:       delete duplicates (leaving only one copy)\n";
	print $fh "        -v:       verbose mode\n";
	print $fh "        -z:       include zero-length files\n";
	print $fh "        -l:       format output in lines\n";
	print $fh "        -f:       follow symlinks\n";
	print $fh "    --help:       display this message\n";
	print $fh " --version:       display version information\n";
	print $fh "\n";
}

sub VERSION_MESSAGE($;$$$)
{
	my ($fh, $optName, $optVer, $switches) = @_;
	
	#Getopt::Std::version_mess($switches);
	print $fh "findDuplicates.pl version $VERSION\n";
}

###BEGIN MAIN###
if(@ARGV == 0 ) {
	print STDERR "findDuplicates.pl: No directories specified!\n";
	print STDERR "See --help for usage instructions\n";
	exit 1;
}

my %findOpt = ( "wanted" => \&processFile,
	            "follow" => $followSymlinks,
		      );

find(\%findOpt, (@ARGV));

foreach my $s (keys(%fileList)) {
	my @curFiles = @{$fileList{$s}};

	my @reps = ();
	my $dupList = ();
	my $x;

	if (@curFiles > 1) {
		foreach my $f (@curFiles) {
			$x = 0;
			my $foundMatch = 0;
			while(!$foundMatch && $x < @reps) {
				verbosePrint("Comparing: $f $reps[$x]\n");
				if(compare($f, $reps[$x]) == 0) {
					push(@{$dupList->[$x]}, $f);
					$foundMatch = 1;
					verbosePrint("    Found Match!\n");
					$wastedSpace += $s;
				}
				$x++;
			}
			if (!$foundMatch) {
				push(@reps, $f);
				my @newArr = ($f);
				push(@{$dupList}, \@newArr);
			}
		}
	}

	$x = 0;
	while($x < @reps) {
		if (@{$dupList->[$x]} > 1) {
			push(@{$duplicates}, \@{$dupList->[$x]});
		}
		$x++;
	}
}

verbosePrint("\n");

foreach my $list (@{$duplicates}) {
	print "Matching: ";
	foreach my $file (@{$list}) {
		print " $file ";
		if($useLines) {
			print "\n          ";
		}
	}
	print "\n";
}

if($delete) {
	print "\n";
	
	foreach my $list (@{$duplicates}) {
		my $x = 1;
		while($x < @{$list}) {
			my $file = $list->[$x];
			if(!$pretend) {
				print "Deleting $file\n";
				unlink($file);
				
			} else {
				print "Would delete $file\n";
			}
			verbosePrint("    in favor of $list->[0]\n");
			$x++;
		}
	}
}

my $prettySize;
if($HAS_FORMAT) {
	$prettySize = Format::Human::Bytes::base2($wastedSpace);
} else {
	$prettySize = "$wastedSpace bytes";
}

print "\nWasted Space: $prettySize";
if($delete && !$pretend) {
	print " (reclaimed)";
}
print "\n";

exit 0;
###END MAIN###

sub verbosePrint($)
{
	if(!$verbose) {
		return;
	}
	
	my $msg = shift;
	
	print $msg;
}

sub processFile
{
	my $file = $_;
	my $amRegular = (-f $file) && ! (-l $file);
	my $size = (-s $file);
	
	if ( ! $amRegular ) {
		return;
	}
	
	if($size == 0 && !$checkZero) {
		return;
	}
	
	if(!defined($fileList{$size})) {
		@{$fileList{$size}} = ();
	}
	
	push (@{$fileList{$size}}, $File::Find::name);	
}
