#!/usr/local/bin/perl -- # -*- perl -*-
#
# $Id: scan_fs,v 1.3 1993/07/15 16:20:04 ejb Exp $
# $Source: /home/ejb/scripts/RCS/scan_fs,v $
# $Author: ejb $
#
# Scan a filesystem starting from a root position specified.  Print
# a list of users and their usages sorted by username.  See usage
# subroutine for usage.
#

require 'stat.pl';
$IFMT  = 0170000;
$IFREG = 0100000;
$IFDIR = 0040000;
$IFLNK = 0120000;
$blocksize = 512;		# How do you get this?  $ST_BLKSIZE isn't it.

{
    local(@whoami) = split('/', $0);
    $whoami = pop(@whoami);
}

#
# Process commandline arguments.  Put filenames into @dirs, and set
# other flags as appropriate.
#

$local_flag = $verbose_flag = $idev_flag = $xdev_flag =
    $usort_flag = $fsort_flag = 0;
while (@ARGV) {
    local($arg) = shift(@ARGV);
    if (substr($arg, 0, 1) eq "-") {
	($arg eq "-v")     && do { $verbose_flag = 1; next; };
	($arg eq "-xdev")  && do { $xdev_flag    = 1; next; };
	($arg eq "-idev")  && do { $idev_flag    = 1; next; };
	($arg eq "-usort") && do { $usort_flag   = 1; next; };
	($arg eq "-fsort") && do { $fsort_flag   = 1; next; };
	($arg eq "-local") && do { $local_flag   = 1; next; };
	($arg eq "--")     && last;
	warn sprintf("%s: invalid argument %s\n", $whoami, $arg);
	&usage;
    }
    else {
	push(@dirs, $arg);
    };
};
# Put all remaining arguments in dir list
push(@dirs, @ARGV);

# If fsort and usort, fsort wins; fsort default
$usort_flag = 0 if ($usort_flag && $fsort_flag);
$fsort_flag = 1 if (! ($usort_flag || $fsort_flag));

# If xdev and idev, xdev wins; xdev default
$idev_flag = 0 if ($idev_flag && $xdev_flag);
$xdev_flag = 1 if (! ($idev_flag || $xdev_flag));

if ($local_flag) {
    if (-d "/usr/sbin")
    {
	open(DF, "df -F ufs |");
	while (<DF>) {
	    chop;
	    local(@df_line) = split(/ /);
	    push(@dirs, shift(@df_line));
	};
    }
    else
    {
	open(DF, "df -t 4.2 |");
	<DF>;			# throw away first line
	while (<DF>) {
	    chop;
	    local(@df_line) = split(/ /);
	    push(@dirs, pop(@df_line));
	};
    }
}    

($#dirs == -1) && &usage;

$ntotals = 0;
for (@dirs) {
    $ntotals++;
    %totals = &scan_fs($_);
    for (keys %totals) {
	$grandtotals{$_} += $totals{$_};
    }
};

&print_totals("Grand totals", %grandtotals) if ($ntotals > 1);



sub scan_fs {
    local($top) = $_[0];
    local($topdev, @files, $file, @stat, %bytesused, %entries, %totals);
    # %multilinks is not local; we want that accross all directories.

    (@stat = lstat($top)) || do {
	warn sprintf("%s: stat %s failed: %s\n", $whoami, $top, $!);
	return ();
    };
    $topdev = $stat[ST_DEV];

    push(@files, $top);
    while (@files) {
	$file = pop(@files);
	
	@stat = lstat($file);
	next if ($xdev_flag && ($stat[ST_DEV] != $topdev));
	
	# If regular file, handle specially if there is more than one link
	if ((($stat[$ST_MODE] & $IFMT) == $IFREG) && ($stat[$ST_NLINK] > 1)) {
	    local($handle) = $stat[$ST_DEV] . ":" . $stat[$ST_INO];
	    # If we've seen this inode before, skip it; otherwise, mark that
	    # we've seen it.
	    $multilinks{$handle} ? next : ($multilinks{$handle} = 1);
	};
	
	# Add the size to the count for this user
	$bytesused{$stat[$ST_UID]} || ($bytesused{$stat[$ST_UID]} = 0);
	$bytesused{$stat[$ST_UID]} += $stat[$ST_BLOCKS] * $blocksize;
	
	# If directory, push entries onto the queue.
	if (($stat[$ST_MODE] & $IFMT) == $IFDIR) {
	    printf("Scanning %s\n", $file) if $verbose_flag;
	    opendir(DIR, $file) || do {
		warn sprintf("%s: opendir %s failed: %s\n", $whoami, $file, $!);
		next;
	    };
	    @entries = readdir(DIR);
	    closedir(DIR);
	    # Prevent // from starting files when scanning /
	    if ($file eq "/") {
		$file = "";
	    };
	    # Kill . and ..
	    shift(@entries);
	    shift(@entries);
	    while (@entries) {
		push(@files, $file . "/" . shift(@entries));
	    };
	};
    };
    
    print "\n" if $verbose_flag;
    
    for (keys %bytesused) {
	$username = ((getpwuid($_))[0] || sprintf("uid %05d", $_));
	$totals{$username} = $bytesused{$_};
    };
    
    &print_totals($top, %totals);
    %totals;
}


sub usage {
    print STDERR <<EOF;

Usage: $whoami [ options ] { -local | [ -- ] dir } [ dir ... ]
   Options include:
      -v      display directories as traversed
      -xdev   do not traverse over mountpoints (default)
      -idev   traverse over mountpoints
      -usort  sort results by username
      -fsort  sort results by filesystem use (default)
   Arguments are treated as options if they start with -.  After --,
   all arguments are treated as filenames.

   If -local is specified, all filesystems mounted local (as
   return by df) are included.  One of -local or directories must be
   specified.

   If -fsort and -usort are both specified, -fsort is used.
   If -xdev and -idev are both specified, -xdev is  used.

EOF
}


sub print_totals {
    local($fs, %totals) = @_;
    local($total, $nkeys);
    local(@sortkeys);

    printf("== %s =>\n", $fs);
    $total = 0;
    $nkeys = 0;
    if ($usort_flag) {
	@sortkeys = sort keys %totals;
    }
    else {
	@sortkeys = sort {$totals{$b} <=> $totals{$a}} keys %totals;
    }
    for (@sortkeys) {
	&print_row($_, $totals{$_});
	$total += $totals{$_};
	$nkeys++;
    }
    
    if ($nkeys > 1) {
	&print_row("TOTAL", $total);
    }
    print "\n";
}


sub print_row {
    local($key, $value) = @_;
    local($valstring, @vals, $i);

    @vals = split('', $value);
    $i = 0;
    while (@vals) {
	$valstring = (pop(@vals) . $valstring);
	if ((++$i % 3 == 0) && ($#vals != -1)) {
	    $valstring = "," . $valstring;
	};
    };

    printf("%9s: %s\n", $key, $valstring);
}
