use strict; require 'timelocal.pl'; our($debug) = 0; our($error) = ''; our($odir) = ' Organized Trash'; our($ddir) = ' Delete Immediately'; our($cycles) = 9; # Debugging can be enabled via the variable above, or by giving # a command line flag "-d". $debug++ if ( $#ARGV >= 0 && $ARGV[0]=~m/-d/ ); &dbgo("debugging enabled\n"); # Check that our runtime environment looks sane. We have no idea where # we'll be invoked from. We'd like to assume the Trash is in the # directory named in the invoking user's 'HOME' environment variable. # Make sure that env. var. exists. While we're at it, make sure there's # a 'USER' env. var. and that the named user appears in 'HOME'. if ( !exists($ENV{'HOME'}) || !exists($ENV{'USER'}) || $ENV{'HOME'}!~m|/$ENV{'USER'}$| ) { $error = 'cannot figure out where your trash can directory is'; &my_die($error, __LINE__); } &dbgo("\%ENV has keys 'HOME' and 'USER'; and they look sane\n"); # Determine the path to the invoking user's Trash our($trash_dir) = $ENV{'HOME'}.'/.Trash'; &dbgo("trash directory is '".$trash_dir."'\n"); # Carefully chdir() into the Trash if ( ! chdir($trash_dir) ) { $error = "could not chdir() into '" .$trash_dir ."', " .lc($!); &my_die($error, __LINE__); } &dbgo("chdir()'d into Trash\n"); # Make sure the directories that we expect exist. &ensure_dir($odir); &ensure_dir($ddir); # We presume anything in the Trash which we haven't organized was thrown # into the trash yesterday. What directory in the organized trash # folder will we use to store these items? our($dated_dir) = &odir_yesterday(); # Does the dated trash directory already exist? if ( -d $odir.'/'.$dated_dir ) { # We've already been run today! There's nothing for us to do. &dbgo("'".$odir.'/'.$dated_dir."' already exists; we do not need to herd fresh trash\n"); } else { if ( ! mkdir($odir.'/'.$dated_dir, 0700) ) { $error = "could not create directory '" .$dated_dir ."' in '" .$odir ."', " .lc($!); &my_die($error, __LINE__); } &dbgo("created directory '".$dated_dir."' in '".$odir."'\n"); &dbgo("will herd fresh trash\n"); &herd_trash_into($dated_dir); } # Carefully chdir() into the organized Trash directory and look for # dated directories which are old enough to be permanently deleted. if ( ! chdir($odir) ) { $error = "could not chdir() into '" .$odir ."', " .lc($!); &my_die($error, __LINE__); } &dbgo("chdir()'d into '".$odir."'\n"); &dbgo("looking for old, organized trash to delete...\n"); &delete_old_trash($dated_dir); chdir('..'); # out of $odir... # chdir() into the 'delete immediately' directory and remove anything # we find there. if ( ! chdir($ddir) ) { $error = "could not chdir() into '" .$ddir ."', " .lc($!); &my_die($error,__LINE__); } &dbgo("chdir()'d into '".$ddir."'\n"); &dbgo("looking for trash to be deleted immediately...\n"); &delete_immediately_trash(); exit(0); # ____________________________________________________________________ # | # | subroutines # | sub dbgo { print($_[0]) if $debug; } sub delete_immediately_trash { # Open the current directory so we can scan all of its contents. if ( ! opendir(DDIR, '.') ) { $error = "cannot open '".$ddir."' to read contents of dir, " .lc($!); &my_die($error, __LINE__); } # Get a list of all interesting nodes in the current directory my($node, @nodes); while ( $node = readdir(DDIR) ) { # Ignore the self and parent nodes that all directories possess next if ( $node eq '.' || $node eq '..' ); # Ignore the node that the Mac OS gui manipulates next if $node eq '.DS_Store'; push(@nodes, $node); } closedir(DDIR); foreach my $i ( 0..$#nodes ) { &recursive_remove('./'.$nodes[$i]); } } sub delete_old_trash { my($todays_dir) = $_[0]; # Open the Trash directory so we can scan all of its contents. if ( ! opendir(ODIR, '.') ) { $error = "cannot open '".$odir."' to read contents of dir, " .lc($!); &my_die($error, __LINE__); } # Get a list of all interesting nodes in the current directory my($node, @nodes); while ( $node = readdir(ODIR) ) { # Ignore the self and parent nodes that all directories possess next if ( $node eq '.' || $node eq '..' ); # Ignore the node that the Mac OS gui manipulates next if $node eq '.DS_Store'; push(@nodes, $node); } closedir(ODIR); # @nodes should be a list of directories like "YYYY.MM.DD.trash". # So a reversed sort means the newer dates will be first. Any weird # nodes we encounter are handled as a special case in the loop below. @nodes = reverse(sort(@nodes)); my($count) = 0; foreach my $i ( 0..$#nodes ) { &dbgo($nodes[$i]."...\n"); if ( $nodes[$i]!~m/^\d{4}\.\d\d\.\d\d.trash$/ ) { # What is this node doing in here? Move it up to the main # trash. If this fails, it's probably because there is # something in the topmost Trash directory with the same # name. Next time we run, we'll move that item down into # a dated directory, and then when we come back in here, # we'll move this item up to the topmost Trash directory. rename($nodes[$i], '../'.$nodes[$i]); &dbgo(" ?! moved stray thing '".$nodes[$i]."' up to topmost Trash\n"); next; } # Look for organized trash directories (dated) that have # nothing in them. We want to remove them so they don't # count toward the cycles we'll keep laying around. But we # don't delete today's directory even if it is empty. The # means you have cycles number of days of trash on hand, not # just cycles calendar days of organized directories. if ( $nodes[$i] ne $todays_dir && &dir_effectively_empty($nodes[$i]) ) { &dbgo(" is effectively empty; recursively removing it\n"); &recursive_remove('./'.$nodes[$i]); next; } # We've seen another dated directory! $count++; # We want to preserve $cycles (global setting at top) number # of dated, organized trash sub-diretories. Once we've seen # enough, we remove any more that we've seen. if ( $count > $cycles ) { &dbgo(" recursively removing '".$nodes[$i]."'...\n"); &recursive_remove('./'.$nodes[$i]); } else { &dbgo(" preserving '".$nodes[$i]."'\n"); } } } sub dir_effectively_empty { my($node) = $_[0]; # Open the directory so we can scan its contents. if ( ! opendir(TEMPDIR, $node) ) { $error = "cannot open '".$node."' to read contents of dir, " .lc($!); &my_die($error, __LINE__); } # Are there any interesting nodes in the directory? my($node); my($empty) = 1; while ( $node = readdir(TEMPDIR) ) { # Ignore the self and parent nodes that all directories possess next if ( $node eq '.' || $node eq '..' ); # Ignore the node that the Mac OS gui manipulates next if $node eq '.DS_Store'; $empty = 0; last; } closedir(TEMPDIR); return($empty); } sub ensure_dir { my($dir) = $_[0]; if ( -d $dir ) { &dbgo("directory '".$dir."' exists in the Trash\n"); } else { if ( ! mkdir($dir, 0700) ) { $error = "could not create directory '" .$dir ."' in the Trash, " .lc($!); &my_die($error, __LINE__); } &dbgo("created directory '".$dir."' in the Trash\n"); } } sub herd_trash_into { # Anything we find that we want to move, we'll move into the dated # dir (passed as our argument) inside the orangized Trash directory. my($destination) = $odir.'/'.$_[0]; # Open the Trash directory so we can scan all of its contents. if ( ! opendir(TRASH, '.') ) { $error = "cannot open Trash to read contents of dir, " .lc($!); &my_die($error, __LINE__); } # Consider each item we find in the Trash my($node); while ( $node = readdir(TRASH) ) { # Ignore the self and parent nodes that all directories possess next if ( $node eq '.' || $node eq '..' ); # Ignore the node that the Mac OS gui manipulates next if $node eq '.DS_Store'; # Ignore the directories that we manage next if $node eq $odir; next if $node eq $ddir; # Move this node into the dated directory inside the organized # Trash directory. if ( ! rename($node, $destination.'/'.$node) ) { $error = "cannot move '" .$node ."' into '" .$destination ."', " .lc($!); &my_die($error, __LINE__); } &dbgo("moved '".$node."' into '".$destination."'\n"); } closedir(TRASH); } sub my_die { my($msg, $line) = @_; print(STDERR "error: ".$msg."\n"); die('error at line '.$line.', died'); } sub odir_yesterday { # Just return the cached value if we've already figured this out. if ( defined($MAIN::odir_yesterday_cache) ) { return($MAIN::odir_yesterday_cache); } # today's date my($mday,$mon,$year) = (localtime())[3,4,5]; # time_t for first second of today my($time) = timelocal(0,0,0,$mday,$mon,$year); $time -= 10; # back into the previous day # date for yesterday ($mday,$mon,$year) = (localtime($time))[3,4,5]; # Directory name like "YYYY.MM.DD.trash". Do not change this # unless you also change the logic in the delete_old_trash() # which relies on this exact naming scheme. my($dir) = sprintf('%4d.%02d.%02d.trash', (1900+$year), (1+$mon), $mday); # remember for any subsequent calls $MAIN::odir_yesterday_cache = $dir; return($dir); } sub recursive_remove { # - Removes whatever you indicate. Returns the empty string on # success. Otherwise, returns an error message. # - You may pass it a filename or a directory. # my($target) = $_[0]; # $target should not be sporting a trailing directory separator $target=~s|/+$||; # Targets that aren't directories are easy to remove. if ( ! -d $target ) { return("cannot remove non-directory item '".$target."', ".lc($!)) unless unlink($target); &dbgo(" unlink()'d ''".$target."''\n"); } # Note that because the filehandle 'RECREM_DIR' is a static string, # open/read/close it. At we dive in recursion, we end up with instances # of @nodes on each stack layer, not multiple open directory handles. return("cannot open directory '".$target."' to read, $!") unless opendir(RECREM_DIR, $target); my(@nodes) = readdir(RECREM_DIR); closedir(RECREM_DIR); foreach my $i (0..$#nodes) { # Don't remove the 'this dir' or 'parent dir' nodes next if ( $nodes[$i] eq '.' || $nodes[$i] eq '..' ); # Remove everything in the directory. &recursive_remove($target.'/'.$nodes[$i]); } # Remove the directory itself return("cannot remove directory '".$target."', $!") unless rmdir($target); &dbgo(" rmdir()'d ''".$target."''\n"); return(''); # success }