#!/usr/bin/perl -w use strict; use Getopt::Long; use Cwd qw(abs_path cwd); use Digest::MD4; use Digest::MD5; our $SUMS_FILE = '.rsyncsums'; &Getopt::Long::Configure('bundling'); &usage if !&GetOptions( 'recurse|r' => \( my $recurse_opt ), 'mode|m=s' => \( my $cmp_mode = 'strict' ), 'check|c' => \( my $check_opt ), 'verbose|v+' => \( my $verbosity = 0 ), 'help|h' => \( my $help_opt ), ); &usage if $help_opt || $cmp_mode !~ /^(lax|strict)$/; my $ignore_ctime_and_inode = $cmp_mode eq 'lax' ? 0 : 1; my $start_dir = cwd(); my @dirs = @ARGV; @dirs = '.' unless @dirs; foreach (@dirs) { $_ = abs_path($_); } $| = 1; my $exit_code = 0; my $md4 = Digest::MD4->new; my $md5 = Digest::MD5->new; while (@dirs) { my $dir = shift @dirs; if (!chdir($dir)) { warn "Unable to chdir to $dir: $!\n"; next; } if (!opendir(DP, '.')) { warn "Unable to opendir $dir: $!\n"; next; } my $reldir = $dir; $reldir =~ s#^$start_dir(/|$)# $1 ? '' : '.' #eo; if ($verbosity) { print "$reldir ... "; print "\n" if $check_opt; } my %cache; my $f_cnt = 0; if (open(FP, '<', $SUMS_FILE)) { while () { chomp; my($sum4, $sum5, $size, $mtime, $ctime, $inode, $fn) = split(' ', $_, 7); $cache{$fn} = [ 0, $sum4, $sum5, $size, $mtime, $ctime & 0xFFFFFFFF, $inode & 0xFFFFFFFF ]; $f_cnt++; } close FP; } my @subdirs; my $d_cnt = 0; my $update_cnt = 0; while (defined(my $fn = readdir(DP))) { next if $fn =~ /^\.\.?$/ || $fn =~ /^\Q$SUMS_FILE\E$/o || -l $fn; if (-d _) { push(@subdirs, "$dir/$fn") unless $fn =~ /^(CVS|\.svn|\.git|\.bzr)$/; next; } next unless -f _; my($size,$mtime,$ctime,$inode) = (stat(_))[7,9,10,1]; $ctime &= 0xFFFFFFFF; $inode &= 0xFFFFFFFF; my $ref = $cache{$fn}; $d_cnt++; if (!$check_opt) { if (defined $ref) { $$ref[0] = 1; if ($$ref[3] == $size && $$ref[4] == $mtime && ($ignore_ctime_and_inode || ($$ref[5] == $ctime && $$ref[6] == $inode)) && $$ref[1] !~ /=/ && $$ref[2] !~ /=/) { next; } } if (!$update_cnt++) { print "UPDATING\n" if $verbosity; } } if (!open(IN, $fn)) { print STDERR "Unable to read $fn: $!\n"; if (defined $ref) { delete $cache{$fn}; $f_cnt--; } next; } my($sum4, $sum5); while (1) { while (sysread(IN, $_, 64*1024)) { $md4->add($_); $md5->add($_); } $sum4 = $md4->hexdigest; $sum5 = $md5->hexdigest; print " $sum4 $sum5" if $verbosity > 2; print " $fn" if $verbosity > 1; my($size2,$mtime2,$ctime2,$inode2) = (stat(IN))[7,9,10,1]; $ctime2 &= 0xFFFFFFFF; $inode2 &= 0xFFFFFFFF; last if $size == $size2 && $mtime == $mtime2 && ($ignore_ctime_and_inode || ($ctime == $ctime2 && $inode == $inode2)); $size = $size2; $mtime = $mtime2; $ctime = $ctime2; $inode = $inode2; sysseek(IN, 0, 0); print " REREADING\n" if $verbosity > 1; } close IN; if ($check_opt) { my $dif; if (!defined $ref) { $dif = 'MISSING'; } elsif ($sum4 ne $$ref[1] || $sum5 ne $$ref[2]) { $dif = 'FAILED'; } else { print " OK\n" if $verbosity > 1; next; } if ($verbosity < 2) { print $verbosity ? ' ' : "$reldir/"; print $fn; } print " $dif\n"; $exit_code = 1; } else { print "\n" if $verbosity > 1; $cache{$fn} = [ 1, $sum4, $sum5, $size, $mtime, $ctime, $inode ]; } } closedir DP; unshift(@dirs, sort @subdirs) if $recurse_opt; if ($check_opt) { ; } elsif ($d_cnt == 0) { if ($f_cnt) { print "(removed $SUMS_FILE) " if $verbosity; unlink($SUMS_FILE); } print "empty\n" if $verbosity; } elsif ($update_cnt || $d_cnt != $f_cnt) { print "UPDATING\n" if $verbosity && !$update_cnt; open(FP, '>', $SUMS_FILE) or die "Unable to write $dir/$SUMS_FILE: $!\n"; foreach my $fn (sort keys %cache) { my $ref = $cache{$fn}; my($found, $sum4, $sum5, $size, $mtime, $ctime, $inode) = @$ref; next unless $found; printf FP '%s %s %10d %10d %10d %10d %s' . "\n", $sum4, $sum5, $size, $mtime, $ctime, $inode, $fn; } close FP; } else { print "ok\n" if $verbosity; } } exit $exit_code; sub usage { die <