#!/usr/bin/perl #
# This document is in text, HTML, and perl script format.

sub __GNUC__ { 1 }
$|=1;  # prevent stderr/stdout conflicts

# /tmp cleaner script by zblaxell@myrus.com (Zygo Blaxell)
# Version 2.4, 96/05/29
# Now sets "$0" to be the filesystem being reaped.  This may impact 
# portability...

# Version 2.3, 96/05/27
# Fixed the silly 'INTERVAL < 0' feature, which last worked in v1.31.
# This allows you to run filereaper from crontab ("crontab" mode) or
# as a daemon running all the time ("daemon" mode).
#
# If INTERVAL>0 ("daemon" mode):
#   
#         - fork one child for each filesystem
#         - child reads filesystem filename <-> inode table to memory
#         - child waits for low disk space conditions
#         - child removes files whenever minimum space level is crossed
#         - child exits when filename <-> inode table exhausted
#         - parent forks new child if entire filesystem is reaped
#	  - parent never exits
#   
# If INTERVAL<0 ("crontab" mode):
# 
#         - ignore filesystems that are not already below minimum
#           free space requirements 
#         - fork one child for each such filesystem
#         - child reads filesystem filename <-> inode table to memory
#         - child removes files until minimum space level is crossed 
#         - child exits when minimum space requirement is statisfied
#         - parent process exits when last child has exited.
# 
# I strongly recommend using this program in 'daemon' mode rather
# than 'crontab' mode.  'daemon' mode is much less CPU intensive. 
# The drawback of 'daemon' mode is that it will occupy a lot of swap.
# The actual RSS footprint of filereaper drops down to a few K once
# it has read its filesystem databases.

# Version 2.2, 96/05/23
# Wrote some more docs and cleaned up a spelling error
# ('NOATIME' was spelled 'ATIME').

# Version 2.11, 96/05/08
# Added 'ADD_DATE' variable

# Version 2.1, 96/05/07
# Fixed long-standing stat_fs bug

# Version 2.0, 96/05/06
# Fairly major restructuring.  Now the children are forked on initial 
# startup, whether reaping is necessary or not.  The logging
# information is reorganized.
# The specification for threshold space levels is now one of:
# number	- percentage of disk
# number K	- kilobytes
# number M	- megabytes

# Version 1.31, 96/05/01
# Only print STDERR "Disk usage increased:" if QUIET is not set.

# Version 1.3, 96/04/26
# If we have a dangling symlink, ignore its atime.  We keep changing it
# with readlink().

# Version 1.24, 96/04/25
# Changes to constant strings in output and comments.  I was going to
# make a change, but then decided that the status quo is a feature.

# Version 1.23, 96/04/12
# Use 'localtime()' more often to print out dates.  No change in behavior.

# Version 1.22, 96/04/10
# Corrects a couple of reporting bugs (negative disk space and incorrect
# file size).  No change in behavior.

########################################################################
########################################################################
########################################################################

# No warranties express or implied; see the GNU GPL for copying
# restrictions.

# You may get a copy of the software licence from:

# ftp://ftp.gnu.ai.mit.edu/pub/gnu/COPYING

# This is filereaper, a stripped-down version of gfreaper.
# Actually, it's partly stripped-up too.

# This script is designed to maintain a particular amount of free
# disk space on a partition by deleting files in a directory structure.
# For example, if you wanted to always have 3% free space in /tmp, use:

# filereaper 3 /tmp

# Files are deleted in order approximated by "oldest files first".
# Actually, there are some anomalies where symbolic links and
# directories are concerned.  Directories are considered one second
# older than the oldest file in them.  Symbolic links are considered one
# second newer than the file that they point to, or their own age if they
# don't point to a file.  Directories can be maintained by typing 'ln -s
# . ...' in them--this will delete everything in the directory, of course,
# but never the directory itself (actually, filereaper never deletes the
# symlink, thus preserving the directory which never gets emptied).

# This program also maintains some state between reapings that allows it
# to run more efficiently.  It can begin deleting files within two
# seconds of low-disk-space conditions occurring.

# This program understands some security issues that many other programs
# (and some sysadmins) don't.  I don't know how many times I have seen:

# 0 * * * * find /tmp -mtime +1 -exec rm -f {} \;

# or, even worse:

# 0 * * * * find /tmp -mtime +1 -print | xargs rm -f

# in crontab files running as root.

# The problem with these is that they can be used by any hostile user
# with write access to /tmp to delete any file on the filesystem.  In the
# second case, the exploit is trivially easy.  To delete /etc/passwd and
# /etc/group:

# $ touch '/tmp/this filename contains whitespace and newlines
# /etc/passwd
# /etc/group'

# In the first case, you need to exploit a race condition between find
# and rm (which is exacerbated by xargs, I might add).  Consider a file
# named '/tmp/foo/bar/baz/a/b/c/d/e/etc/passwd'.  The race condition is
# exploited thus:

# [find feeds '/tmp/foo/bar/baz/a/b/c/d/e/etc/passwd' to 'rm']
# $ mv /tmp/foo/bar/baz/a/b/c/d/e/etc /tmp/e
# $ ln -s /etc /tmp/foo/bar/baz/a/b/c/d/e/etc
# [rm now deletes '/tmp/foo/bar/baz/a/b/c/d/e/etc/passwd', but the
# directory '/tmp/.../etc' is now a symlink to the real '/etc', so in fact
# rm will actually delete '/etc/passwd'.]

# With some creativity it is possible to make stat() calls take several
# minutes, so this race condition is not difficult to exploit.  Consider
# what happens when you call stat() on a path with 500 directory
# components, each of which is actually a symlink 8 levels deep through
# 500 other directory components.  Each.  We're talking about reading
# almost the entire inode table into memory here just to do ONE stat()
# call, and that can't be very fast.  The attacker just moves the first
# directory component aside and puts a symlink to '/etc/' (or wherever) in
# its place, so the actual unlink() call to the wrong file will be nice
# and fast.

# Incidentally, since these examples use '-mtime', the file modification
# time is used to schedule file deletion.  Since the file modification
# time is entirely under the control of the user, the user can delete any
# file they want deleted, when they want it deleted, and they can
# prevent their own files from being deleted.  Use '-ctime' instead.  If
# your 'find' doesn't have this feature, 'rm -f /usr/bin/find' (in case
# someone else tries to use it) and get one of the freely-available
# replacements, or write your own.

# ONCE AGAIN:  DO NOT USE:
# 0 * * * * find /tmp -mtime +1 -exec rm -f {} \;
# IN ROOT'S CRONTAB.  YOU WILL ALLOW ANY USER TO DELETE ANY FILE ON THE
# SYSTEM.  THIS IS A SECURITY HOLE THAT IS EASY TO EXPLOIT.  There, that
# should place this document in a few full-text search engines.  :-)

# There are alternatives to this that work.  One is:
# 0 * * * * find /tmp -mtime +1 -exec safe_rm -f {} \;

# where 'safe_rm' implements an algorithm similar to 'ch_dir' in this
# program.  The algorithm is:

# If any system call below fails, exit.
# chdir("/");
# split name into a list of path components and a filename.  The
# filename must be non-empty and does not contain "/".
# for each member of list:
#	old1=lstat(member) (save the device and inode numbers in old1)
#	old2=lstat(".")    (save the device and inode numbers in old2)
#	chdir(member)
#	new1=lstat(".")
#       new2=lstat("..")
#       if (old1 != new1 || old2 != new2) exit
# next member
# unlink or rmdir (filename)

# This avoids the race condition by not following symlinks in the
# directory components of the path name.  Attempts to exploit the
# find/unlink race condition will fail because the ch_dir routine will
# fail if it encounters a symbolic link in the directory components of
# the path name.

# This is not perfect; however, it is necessary for the user to be able
# to write to parent directories in a path name in order to subvert files
# later in a path name.  For instance, consider:

# /tmp/user1/
# /tmp/foo/bar/root/user2

# where 'user1' is owned by user1, 'root' is owned by root and not
# writable, and 'user2' is a file owned by user2.  User1 cannot delete
# '/tmp/foo/bar/root/user2', because 'root' is not writable by user1.

# However, User1 can exploit the security vulnerabiltiies left in the
# algorithm above to cause the daemon to delete /tmp/foo/bar/root/user2 IF
# AND ONLY IF /tmp/foo and /tmp/foo/bar are writable by user2, as follows:

# 1.  Create files and directories such that '/tmp/user1/bar/root/user2'
#     exists.
# 2.  Wait for 'find' to output the name '/tmp/user1/root/user2'.
# 3.  'mv /tmp/user1/bar /tmp/user1/BAR'
# 4.  'mv /tmp/foo/bar /tmp/user1'.  This moves
#     '/tmp/foo/bar/root/user2' to '/tmp/user1/bar/root/user2'.
# 5.  The safe_rm will now delete '/tmp/user1/bar/root/user2'.  Because
#     there are no symlinks, no anomalies will be detected.

# This program has additional checks to try to prevent attacks like this.
# It can still be exploited if /tmp/user1/bar/root/user2 is a hard link
# to /tmp/foo/bar/root/user2.

# The moral of the story is:  if someone can write to your parent
# directory, all bets are off.

# Note that in real life, /tmp should be mode 1777, i.e. it has the
# sticky bit set.  In this case /tmp/foo has to be owned by user1 for
# the exploit to work.  RTFM chmod(1,2) if you want to know why.

# This program also does chroot() to the base of filesystems to be reaper,
# if run as root, for extra security.

########################################################################
########################################################################
########################################################################

# BUGS AND PORTABILITY ISSUES

# If your filesystem isn't a true blue Unix system, there may be problems.

# All inodes are assumed to be unique and constant.  If your filesystem
# generates inode numbers to be compatible with POSIX, and this
# generation is not repeatable, then this program will scream about
# files constantly changing before they can get deleted.  Some 
# my-toy-filesystem-to-NFS gateways create and cache inode numbers
# on the fly; if not accessed frequently, inode numbers of files go stale.

# Filesystems that don't have a notion of 'ctime' or 'atime' or user IDs
# (such as the MSDOS filesystem) can cause problems.  The implementation
# of MSDOS filesystem that I use has an 'mtime' stamp, and uses it for all
# three timestamp values.   This means that simply reading a file
# doesn't 'touch' it, because there is no 'atime' field to update.  Also,
# since writing the 'mtime' field sets the 'ctime' field to the same
# value, we have no idea whether a file is old or new, or just dated
# that way, and the order in which files are deleted becomes very
# arbitrary.  Finally, if the default UID of the MSDOS filesystem is
# root, you'll have to set ROOTAGE to a large negative value for it to
# work, because people can set the "ctime" of files to far into the future.  
# I set the UID and GID for MSDOS filesystems to a non-root user
# named 'msdos' to work around this problem.

########################################################################
########################################################################
########################################################################

# Here's a sample script to run from /etc/rc* at boot time.  Remove '### '
# from the beginning of every line.

### #!/bin/sh
### # Filereaper sample script by Zygo Blaxell (C) 1996
### # License: ftp://ftp.gnu.ai.mit.edu/pub/gnu/COPYING
### 
### # Prevent core dumps.  Those would be bad, even when chroot.
### ulimit -c 0
### # Set PATH so we can find filereaper
### PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin
### 
### {
###     # Delete old files in spool and temp directories if <1% free space.
###     # The /*/windows/temp tries to get /[CD]/windows/temp on our systems.
###	# Yes, we've been forced to use Windows '95, hence /*/recycled.
###	# When we have multiple partitions, there is a /tmp on each.
###	# You probably want this at elevated priority to prevent disks
###	# from getting filled faster than they can be emptied, hence 'nice'.
### 
###     ADD_DATE=true QUIET=true INTERVAL=60 nice -n -20 filereaper 1 /*/tmp /tmp /*/recycled /*/windows/temp
### 
### } >>/var/log/filereaper 2>&1 &

########################################################################
########################################################################
########################################################################

# Variables:

# inode_info{inode} = age\0(parent_inode_number,name\0)+
#                    Information about an inode.

# oldest_age = oldest timestamp of files we let stand.

# root_inode = inode of highest-level directory to tmpclean (used to
#              stop searching backwards)

# root_dev = device number of root of filesystem (used to stop searching
#            other filesystems)

# root_path = string to prepend to all pathnames ("" if chroot,
#             "/foo/bar/baz" if not).

# min_spec = amount of space to keep free on filesystem (%, K, or M)

# ROOTAGE = minimum age of a file owned by root before we delete it.
# Use this if you have daemons writing stuff to /tmp that would be
# annoyed, destructive, or vulnerable if their /tmp files go away.

# Return values of ch_dir():
$CH_DIR_SUCCESS=0;
$CH_DIR_CROSS=1;
$CH_DIR_ERROR=2;

###########################################################################
###########################################################################
###########################################################################

# Initialization
($min_spec,@given_filesystems)=@ARGV;
die <  [ [ [...]]]
Deletes old files whenever the amount of free space on the filesystem drops
below .  The amount of space can be specified as one of
the following:

		- percentage of total disk space available to users
	%	- same as 
	K	- number of kilobytes
	M	- number of megabytes

Environment Variables (set them if you want them):
TEST    - test only, don't actually delete any files
          (default of course is to really delete files)
DEBUG   - print debugging information
          (default is to print only warnings and fatal and non-fatal errors,
          as well as the name of every file we try to delete)
NOATIME - ignore the atime of files, use ctime only.
          (default is to use the later of atime and ctime for non-directories.
          mtime can be set by the file owner to any value, so it's ignored.
          With NOATIME, simply reading a file doesn't help preserve it)
ROOTAGE - minimum age of files owned by root in seconds.
          (by default, any file is eligible for deletion unless that
          file is owned by root and it is less than $ROOTAGE seconds old)
MINAGE  - minimum age applies to all users, not just root (boolean).
	  (by default, files not owned by root are always eligible for
          deletion)
INTERVAL- interval, in seconds, between FS checks.  Default 60.
          Negative values mean run only once.
QUIET	- don't print messages that explain why we aren't doing something.
NODIRS	- don't preserve directories using 'ln -s . ...'.  By default,
          if a directory contains the symlink '...' with target '.', 
          it will never be deleted by this program.
ADD_DATE- prefix each output line with date and time.
USAGE

srand();

$min_spec =~ s/\d$/$&\%/o;

$TEST=$ENV{'TEST'};
$DEBUG=$ENV{'DEBUG'};
$NOATIME=$ENV{'NOATIME'};  
$ROOTAGE=$ENV{'ROOTAGE'} || 24*60*60;
$MINAGE=$ENV{'MINAGE'};
$INTERVAL=$ENV{'INTERVAL'} || 60;
$QUIET=$ENV{'QUIET'};
$NODIRS=$ENV{'NODIRS'};
$ADD_DATE=$ENV{'ADD_DATE'};

print STDERR "$0 - parameter dump at ".localtime(time()).":\n";
print STDERR $> ? "Running as uid $>--can't chroot, will try to manage without it\n" : "Running as root, can and will do chroot\n";
print STDERR $TEST ? "Running in test mode only\n" : "This is not a test.\n";
print STDERR "Debugging messages enabled\n" if $DEBUG;
print STDERR "Will maintain $min_spec free space\n";
print STDERR $NOATIME ? "Will ignore atime of files\n" : "Will honour atime of files\n";
print STDERR "Minimum age of files owned by ", $MINAGE ? "all users" : "root", " is $ROOTAGE seconds.\n";
print STDERR "Will check filesystems every $INTERVAL seconds.\n";
print STDERR $QUIET ? "Will suppress spurious filesystem status messages\n" : "Will print STDERR spurious filesystem status messages\n";
print STDERR $NOATIME ? "Will ignore symlinks to '.' named '...'\n" : "Will not delete directories containing symlinks to '.' named '...'\n";

print STDERR "Reading passwd and group files...\n";
setpwent();
while (
    ($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell) = getpwent
    ) {
        $username_of{$uid}=$name unless defined($username_of{$uid});
}
endpwent();
setgrent();
while (
    ($name,$passwd,$gid,$members) = getgrent
    ) {
        $groupname_of{$gid}=$name unless defined($groupname_of{$gid});
}
endgrent();
print STDERR "Done reading passwd and group files.\n";

$last_ch_dir="\0";

# Really go to work now...

print STDERR "Entering main loop\n";
$SIG{'CHLD'}="IGNORE";

while (1) {
	#print STDERR "Reaping filesystems at ".localtime()."\n" unless $QUIET;

	undef @filesystems;
	foreach (@given_filesystems) {
		local($chdir_result)=&ch_dir($_);
		if ($chdir_result) {
			# Arguably, the status of paths on the command line may change...
			print STDERR "$_ could not be found or crossed symlinks--will not try again.\n";
			next;
		}
		push(@filesystems,$_) if $INTERVAL>0;
		if ($INTERVAL<0) {
			$root_path=".";
			$disk_demand=&stat_fs();
			if ($disk_demand<0) {
				print STDERR "$_: insufficient demand for disk space: would reap $disk_demand\n";
				next;
			}
		}
		# Grim File Reaper...
		if ($fs_to_pid{$_}) {
			print STDERR "Still running: pid $fs_to_pid{$_}, fs $_\n" unless $QUIET;
			next;
		}
		$parent_pid=$$;
		$child=fork();
		unless (defined($child)) {
			warn "Didn't fork?  $!";
			next;
		}
		if (!($child)) {
			# Child of fork()
			$other_parent_id=$$;
			unless (open(STDERR ,"|-")) {
				# Child of open()
				die "Didn't fork?  $!" if ($$ == $other_parent_id);
				$0="filereaper $_ logger";
				while ($in=) {
					if ($ADD_DATE) {
						print STDERR localtime()." $_: $in";
					} else {
						print STDERR "$_: $in";
					}
				}
				print STDERR "$_: Done\n" unless $QUIET;
				exit(0);
			} else {
				# parent of open()
				open(STDOUT,">&STDERR ") || die "Dup stderr: $!";
				$0="filereaper $_ reaper";
			}
			&reap($_);
			exit(0);
		} else {
			# Parent of fork()
			#print STDERR "Reaping $_ in pid $child\n";	# Hardly spurious...
			$pid_to_fs{$child}=$_;
			$fs_to_pid{$_}=$child;
		}
	}
	@given_filesystems=@filesystems;

	#print STDERR "Idle.\n" unless $QUIET;
	if ($INTERVAL>=1) {
		sleep($INTERVAL);
	} else {
		sleep(1);
	}

	$children=0;
	for $child (keys(%pid_to_fs)) {
		$children++;
		next if kill(0,$child); # test for existence
		#print STDERR "Child $child no longer exists, free to clean $pid_to_fs{$child} again.\n";
		delete $fs_to_pid{$pid_to_fs{$child}};
		delete $pid_to_fs{$child};
	}
	last if $children==0 && $INTERVAL<0;
}
exit(0);

# reap(directory);
# Does the GFR bit in a given directory.  Sets up inodes, sets up state 
# for directory trees.

sub reap {
        ($root_path)=@_;
	local($begin_reaping)=time();                    # reference point to "now" - saves time() calls
        &ch_dir($root_path) && die "chdir '$root_path' failed.  Aborting.\n";
        if ($>) {
                print STDERR "Not root, so can't chroot.  Too bad.\n";
        } else {
                print STDERR "Running as root.  Doing chroot($root_path)..." unless $QUIET;
                local(@stat1)=(lstat("."))[0,1];
                die "Couldn't lstat '.' in '$root_path': $!\n" unless defined($stat1[0]);
                chroot(".") || die "chroot failed ($!).  Aborting.\n";
                chdir("/") || die "chdir '/' failed in chroot ($!).  Aborting.\n";
                local(@stat2)=(lstat("."))[0,1];
                die "Couldn't lstat '.' in '$root_path' after chroot: $!\n" unless defined($stat2[0]);
                local(@stat3)=(lstat(".."))[0,1];
                die "Couldn't lstat '..' in '$root_path' after chroot: $!\n" unless defined($stat3[0]);
                local(@stat4)=(lstat("/"))[0,1];
                die "Couldn't lstat '/' in '$root_path' after chroot: $!\n" unless defined($stat4[0]);
                # After chroot, '/' should be the same as '.' and '..',
                # which should be the same as '.' before chroot.
                die "I don't believe I have chrooted successfully.  Aborting.\n" if
                        (join(" ",@stat1) ne join(" ",@stat2) ||
                        join(" ",@stat2) ne join(" ",@stat3) ||
                        join(" ",@stat3) ne join(" ",@stat4));
                print STDERR "Successfully chroot($root_path)\n" unless $QUIET;
                $root_path='';
		$last_ch_dir="\0";
        }
        ($root_dev,$root_inode)=(lstat("."))[0,1];
	die "Couldn't lstat '.' in '$root_path': $!\n" unless defined($root_dev);

        print STDERR "Gathering filesystem information\n" unless $QUIET;
	$0="filereaper $_ reaper gathering";
        &get_dir($root_path);

	local($want_before_reaping)=&stat_fs();

        $oldest_age=time();

	print STDERR "Waiting for go-ahead\n" unless $QUIET;
	$0="filereaper $_ reaper waiting";
	local($want)=&stat_fs();
	local($lastwant)=$want;
	until ($want>0 || $INTERVAL<0) {
		sleep(2);
		$want=&stat_fs();
		print STDERR (-$want)." bytes left\n" if ($want>$lastwant) && !$QUIET;
		$lastwant=$want if $lastwant<$want;
	}

	print STDERR "Now reaping files\n" unless $QUIET;
	$0="filereaper $_ reaper reaping";

inode:
	for $inode (sort { $inode_info{$a} <=> $inode_info{$b} } keys(%inode_info)) {
		local($inode_age,@other_stuff)=split(/\0/,$inode_info{$inode});
		$oldest_age=$inode_age;
		
		local($want)=&stat_fs();
                if ($want<=0) {
			# We hang around until we run out of inodes,
			# Then our parent starts us again.
                        print STDERR "Want $want bytes...pausing...Oldest file is stamped ".localtime($oldest_age)."\n";
			$0="filereaper $_ reaper waiting";
			local($lastwant)=$want;
			last inode if ($want<0 && $INTERVAL<0);
			until ($want>0) {
				sleep(2);
				$want=&stat_fs();
				print STDERR (-$want)." bytes left\n" if ($want>$lastwant) && !$QUIET;
				$lastwant=$want if $lastwant<$want;
			}
			$0="filereaper $_ reaper reaping";
                }

		$count=0;
name:
                foreach $file_name (&get_names($inode)) {
                        local($dir,$file)= $file_name =~ m!^(.*)/(.*)$!;

			($was_link,$link_target,$was_dir)=&get_inode_info($dir,$file);
			next name unless defined($was_link);

			unless ($count++) {
				print STDERR "Want $want bytes, reaping inode $inode, age ".localtime($inode_age).", owner " .
					($username_of{$ino_uid} || ":$ino_uid:") . ":" . 
					($groupname_of{$ino_gid} || ":$ino_gid:") . ", size " . ($link_blocks*512) . "\n";
				if ( ( ! $ino_uid || $MINAGE ) && $inode_age > time()) {
					print STDERR "skipping: Inode $inode name '$dir/$file' is owned by " .
						($username_of{$ino_uid} || ":$ino_uid:") . ":" . 
						($groupname_of{$ino_gid} || ":$ino_gid:") . " and not old enough (",time()-$inode_age,").\n";
					next inode;
				}
				# Timestamps of directories are changed by gfreaper.
				unless ($was_dir) {
					local($new_age);
					if ($ino_atime>$ino_ctime && !$NOATIME && $ino_atime < time()+1) {
						$new_age=$ino_atime;
					} else {
						$new_age=$ino_ctime;
					}
					$new_age+=$ROOTAGE if ( ( ! $link_uid || $MINAGE ));
					$new_age-- if $was_link;
					print STDERR "old age $inode_age, new age $new_age\n" if $DEBUG;
					if ($new_age > $inode_age) {
						print STDERR "skipping: Inode $inode name '$dir/$file' age is newer (".localtime($new_age)." > ".localtime($inode_age).")\n";
						next inode;
					}
				}
			}

			if ($link_dev != $root_dev) {
				warn "skipping: Inode $inode name '$dir/$file' is not on the correct filesystem ('$link_dev' should be '$root_dev').\n";
				next name;
			}
			if ($link_ino != $inode) {
				warn "skipping: Name '$dir/$file' is associated with new inode ('$link_ino' should be '$inode')\n";
				next name;
			}

			print STDERR "Reaping: $file_name\n";
                        if ($TEST) {
                                print STDERR "Pretending to unlink $dir/$file\n";
                        } else {
                                if ( $was_dir ) {
                                        unless (rmdir($file)) {
						warn "rmdir('$file') in '$dir' failed: $!\n";
						next name;
					}
                                } else {  # if -d _ ...
                                        unless (unlink($file)) {
						warn "unlink('$file') in '$dir' failed: $!\n";
						next name;
					}
                                }
                        } # if $TEST...
                } # foreach (&get_names...
        } # for $inode (...

	# Done reaping, print STDERR reapage stats
        printf STDERR ("All files deleted.  Run time (seconds):  User %d, Sys %d, Total %d, Real %d\n",
                (times)[0],(times)[1],(times)[0]+(times)[1],time()-$begin_reaping) unless $QUIET;
}

# get_names(inode); 
# Finds the full pathnames of all links to the given inode, and appends
# all of the names in basename.  

sub get_names {
        local($inode)=@_;

        # The inode has a list of parents, and a separate list of names
        # under each parent.  We must find each parent, and prepend its
        # name to all the names of the inode under that parent.

        # There are no parents of the root inode.

	# This is a little bogus in perl.  Infinite loops are possible
	# here if you can rewrite the directory structure a bit, 
	# particularly if you can change the parent/child relationships
	# on directories.

        print STDERR "get_names called on $inode (root=$root_inode)\n" if $DEBUG;

        return ($root_path) if ($inode==$root_inode);

	if (!($inode_info{$inode})) {
		die "WARNING:  get_names called on unknown inode $inode";
	}
	
	local($age,@names)=split(/\0/,$inode_info{$inode});
	local(%my_names);
	foreach (@names) {
		local($parent_inode,$name)=m/^(\d+),([\000-\377]+)$/;
		warn "WARNING:  parent_inode is null?" unless $parent_inode;
		foreach (&get_names($parent_inode)) {
			$my_names{"$_/$name"}++;
		}
	}

        warn "WARNING:  No names found for '$inode'" unless keys(%my_names);
	print STDERR "get_names($inode)=".join(" ",keys(%my_names))."\n" if $DEBUG;
	return sort(keys(%my_names));
}
        
# get_dir(directory,parent_inode);
# Incorporates information about all files in the directory into the
# global database.  Returns the age of the newest file in the directory.

sub get_dir {
        local($directory)=@_;
	local($newest_child_age)=0;
        print STDERR "get_dir($directory)\n" if $DEBUG;
	($was_link,$link_target,$was_dir)=&get_inode_info($directory,".");
	unless (defined($was_link)) {
                warn "Could not lstat '.' in '$directory': $!\n";
                return;
        }
        local($parent_inode)=$ino_ino;
        unless (opendir(DIR,".")) {
                warn "Couldn't opendir '.' in '$directory': $!\n";
                return;
        }
        local(@dirfiles)=readdir(DIR);
        closedir(DIR);
        foreach (@dirfiles) {
                next if /^\.\.?$/;  # Ignore '.' and '..'
		($was_link,$link_target,$was_dir)=&get_inode_info($directory,$_);
                next unless (defined($was_link));
                if ($link_dev != $root_dev) {
                        warn "Not crossing device in '$directory/$_' (root is $root_dev, $_ is $link_dev)\n";
                        # Not likely to be able to delete it, either...
                        # I guess we could explicitly umount it... ;-)
                        next;
                }
		local($inode)=$link_ino;
		local($inode_age,@parents)=split(/\0/,$inode_info{$inode});
		# file, fifo, block device, char device, socket...
		if ($ino_atime>$ino_ctime && !$NOATIME && $ino_atime < time()+1) {
			$inode_age=$ino_atime;
		} else {
			$inode_age=$ino_ctime;
		}
                if ( $was_link ) { # symlink to extant file
			$inode_age--;	# guarantee symlink goes away before file does
		}
                $inode_age+=$ROOTAGE if ( ( ! $link_uid || $MINAGE ));
		if ($was_link && !$NODIRS && $link_target eq '.' && $_ eq '...') {
			# Omit this from inode list
			$inode_age=time();
		} else {
			if ( $was_dir ) { # directory
				$inode_age=$ino_ctime;
				local($subdir_age)=&get_dir("$directory/$_");
				$inode_age=$subdir_age if ($subdir_age>$inode_age);
			} 
			$inode_info{$inode}=join("\0",$inode_age,@parents,"$parent_inode,$_");
		}
		$newest_child_age=$inode_age if $inode_age>$newest_child_age;
		#print STDERR "$inode_info{$inode}\n";
        }
        return $newest_child_age+1;
}

$foo=<