#!/bin/bash

# finds all files with the same name and size, compares and hard-links them

# Copyright (c) 1999, 2000, 2002 Rafał Maszkowski <rzm@icm.edu.pl>
# Licence: GNU GPL v. 2

# 19990514
# 19990516 more statistics
# 19991217 cmp
# 19991220 options: -h (help), -n (dry run), arguments
# 20000820 saving the directory time
# 20000822 -ls -> -printf "..." - GNU findutils
# 20001019 date!; names in ""
# 20001025 -N, -D, -S options
# 20001101 sorting acc. to size, file name, date
#	   account files #/size only if _not_ already linked to some other group
# 20020416 finishing message
# 20020926 -l/-u - lower und upper limit for hardlinking
# 20021008 about -l in help
# 200211222 gawk \+ -> +
# 20050713 better protection from line noise file names type
# 20070705 %TX -> %TT to get rid of AM/PM on Linux
# 20071119 sort -T . cos 2 GB /tmp overflows
# 20090406 -T DIR option; -v option

# TODO
# - real size (+=size/#links
# - fix counter overflow after 2 GB (?) - workarounded by  /1024
# - survive (*%^&%$&%^ in names - partially
# - lower and upper size (and time?) limit
# - include/exclude name patterns

command="bash"
touch="/bin/touch"
awk="/bin/gawk"
ref=~/.$$
printf="-ls"
form=""
printf="-printf"
form="%-i %5k %10m %3n %-8u %-8g %8s %TY %Tm%Td %TX %p %f %l\n"
form="%-i %5k %10m %3n %-8u %-8g %8s %TY %Tm%Td %TX %p\n"
form="%-i %5k %10m %3n %-8u %-8g %8s %TY %Tm%Td %TT %p\n"
tmp=.
#tmp=/home/rzm
ver=20090406

noname=0
nodate=0
nosize=0
lsize=0
usize=1000000000000c

# sed 's/^\(.\).*/\1/'		?

while [ "$1" = "-h" -o "$1" = "-n" -o "$1" = "-N" -o "$1" = "-D" -o "$1" = "-S" -o "$1" = "-l" -o "$1" = "-u" -o "$1" = "-T" -o "$1" = "-v" ]; do
	case "$1" in
		"-n") shift; command="cat";;
		"-N") shift; noname=1;;
		"-D") shift; nodate=1;;
		"-S") shift; nosize=1;;
		"-l") shift; lsize=$1; shift;;
		"-u") shift; usize=$1; shift;;
		"-T") shift; tmp=$1; shift;;
		"-v") shift; echo $ver;;
		"-h") shift; cat <<EOT
Usage:
relink [-n] [-h] [-N] [-D] [-S] [dir]...

Finds all files with the same name, date and size, compares the contents and
hard-links them.

dir		compare files in following directory/ies, default .
-h		help
-n		dry run, only a shell script is generated
-N		compare even if names differ
-D		compare even if dates differ - do not use with mirror
-S		compare even if sizes differ - do not use at all
-l SIZE		do not check files below SIZE blocks/bytes/kilobytes/words if
		b/c/k/w specified, default: 512-bytes blocks
		This option can substantially speed up looking for files
-u SIZE		do not check files above SIZE blocks/bytes/kilobytes/words if
		b/c/k/w specified, default: 512-bytes blocks
-v		version
-T DIR		temporary directory (default: $tmp)

Examples:
relink -N		# checks all files in current directory
relink -N PDB rcsb	# checks files in specified directories
relink -N -l 50000k	# checks files bigger than 50000 KB

relink is able to link together groups of already link files, e.g. 4 links with
7 other links will give 11 links - if the files fulfil the criteria.
EOT
		exit;;
	esac
done

dirs="."

if [ "$1" != "" ]; then
	dirs="$@"
fi

for dir in $dirs; do
	find $dir -type f -size -$usize -size +$lsize $printf "$form"
done | sort -T $tmp -k 7nr -k 12d -k 8,10 \
| $awk --re-interval --assign ref="$ref" --assign noname=$noname --assign nodate=$nodate --assign nosize=$nosize 'BEGIN {
	print "nfiles=0"
	print "	touch \"" ref "\""
} 

function a(name		, acc) {
	acc=sprintf("%c", 39)
	gsub(acc, acc"\\"acc acc, name)
	return sprintf("%c%s%c", 39, name, 39)
}

{
	checked++; csize+=$7
	path = $0
	gsub("^([^	 ]+[	 ]+){10}", "", path)
#print "echo path: \""path"\""
	elem=split(path, name, "/")

	# equal name, date and size unless turned off by an option, different inode number
	if (  (noname || (name[elem]==pfile) ) &&  (nodate || (pdate==$8 " " $9 " " $10)) &&  (nosize || ($7==psize)) &&  ($1!=pnode)  ) {
		dir = path; gsub("/[^/]+$","",dir)
#		print "#", $0
		printf "\n"
		printf "if cmp -s %s %s; then\n", a(ppath), a(path)
		printf "	touch -r %s %s\n", a(dir), a(ref)
		printf "	echo %s %s; rm %s\n", a(path), a(ppath), a(path)
		printf "	ln %s %s\n", a(ppath), a(path)
		printf "	touch -r %s %s\n", a(ref), a(dir)
		if ($4 == 1) {					# links
			print "	tsize=$[ $tsize +", $7" / 1024 ]"
			print "	nfiles=$[$nfiles + 1]"
		}
		print "fi"
		$1="changed"
		tsize+=$7
		nfiles++
	}
	ppath=path; pfile=name[elem]; pnode=$1; psize=$7; pdate=$8 " " $9 " " $10
} END {
	print "echo Checked", checked, "files,", csize/1024/1024, "MB"
	print "echo Raw:"
	if (nfiles>0) {
		print "echo Total size saved:", tsize/1024/1024, "MB,", nfiles, "files,", tsize/nfiles, "B/file"
	} else {
		print "echo No savings."
	}
	print "echo Verified:"
	print "if [ $nfiles -gt 0 ]; then"
	print "	echo Total size saved: $[ $tsize / 1024 ] MB, $nfiles files, $[ $tsize/$nfiles ] KB/file"
	print "else"
	print "	echo No savings."
	print "fi"
	print "rm", ref
}' | $command
echo Directories in `pwd`: $dirs finished.
