#!mkcmd
# $Id: kruft.m,v 1.28 2010/12/15 19:08:40 ksb Exp $
#
# A simpler version of mm's crut program.  No state in the file system	(ksb)
# and we qsort based on ctime/mtime/atime (-c,-m,-a).
# We honor the space percent (e.g.: -S 78) or the files percent (-I 60).
# We honor -n,-v, -h, -V of course.
#
from '<sys/types.h>'
from '<sys/param.h>'
from '<sys/stat.h>'
from '<sys/wait.h>'
from '<sysexits.h>'
from '<unistd.h>'
from '<stdio.h>'

require "util_errno.m"
require "std_help.m" "std_version.m" "std_control.m"
require "heap.m"

from '"machine.h"'

basename "kruft" ""

%i
static char rcsid[] =
	"$Id: kruft.m,v 1.28 2010/12/15 19:08:40 ksb Exp $";
%%
augment action 'V' {
	user "Version();"
}

boolean 'a' 'm' 'c' 'b' 'r' {
	named "cWhichTime"
	init "'c'"
	update "%n = %w;"
	help "change which inode time to honor (r for most recent)"
}
boolean 'T' {
	named "fTruncate"
	init "0"
	help "truncate files rather than unlinking them"
}

type "seconds" 'A' {
	named "tMaxAge" "pcOldest"
	track "fGaveAge"
	init '"23h"'
	exclude "SI"
	param "oldest"
	help "purge by an absolute time (seconds in the past)"
}
accum [":"] 'E' {
	named "pcPriv"
	after "if ((char *)0 == %n) {%n = \"index.html\";}"
	param "exclude"
	help "never unlink a file in this colon separated list"
}
integer 'S' {
	local named "iSpace"
	param "percent"
	init "-1"
	help "prune by space left on device"
}
integer 'I' {
	local named "iFiles"
	param "percent"
	init "-1"
	help "prune by files left on device"
}
type "bytes" 'd' {
	hidden named "lDirSize" "pcDirSize"
	exclude "SIA"
	init '"0"'
	help "specify a hard disk limit for the instance"
}
after "if (-1 == %rSn && -1 == %rIn) {%rSn = 90;}"
boolean 's' {
	named "fSync"
	help "sync the filesystem after every unlink to defeat softupdates"
}
boolean '0' {
	hidden named "fZeroFmt"
	help "output names to delete on stdout in find's -print0 format"
}

list {
	named "Mitosis"
	help ""
}

every type "dir" {
	param "dirs"
	named "pDEKruft" "pcTarget"
	user "Kruft(%n, %N, %rSn, %rIn);"
	help "directory to prune files from"
}

exit {
	update "if (-1 != %rSn) {DeKruft(%rSn);}"
	user "exit(EX_OK);"
}

%c
/* nada yet								(ksb)
 */
static void
Version()
{
#if HAVE_BIRTHTIME
	printf("%s: native brithtime support\n", progname);
#else
	printf("%s: brithtime aliases to modify time\n", progname);
#endif
}

/* never remove a file with the name (in any directory)			(ksb)
 * (see -E)
 */
static int
IsPriv(pcName)
char *pcName;
{
	register char *pcCur, *pc, *pcNext;
	register int iCmp;

	for (pcCur = pcPriv; (char *)0 != pcCur && '\000' != *pcCur; pcCur = pcNext) {
		if ((char *)0 == (pc = strchr(pcCur, ':'))) {
			iCmp = strlen(pcCur);
			pcNext = (char *)0;
		} else {
			iCmp = pc - pcCur;
			pcNext = pc+1;
		}
		if (0 == strncmp(pcCur, pcName, iCmp)) {
			return 1;
		}
	}
	return 0;
}

/* Decide how much we hate this file.  Higher numbers make it		(ksb)
 * much more likely we'll delete the file.
 *
 * If space is a factor larger older files get scorned more
 * if number of files is a factor older smaller files get scorned
 * dirs get excluded by link count.
 */
static int
Weight(pst, iSpace, iFiles)
struct stat *pst;
int iSpace, iFiles;
{
	register time_t *piCheck, tAge;
	static time_t tNow = 0;

	if (0 == tNow) {
		time(& tNow);
	}
	switch (cWhichTime) {
	case 'b':
#if HAVE_BIRTHTIME
#if __BSD_VISIBLE
		piCheck = (time_t *)& pst->st_birthtimespec;
#else
		piCheck = (time_t *)& pst->st_birthtimespec;
#endif
		break;
#endif
		/* no birth date for inode, replace with mtime */
		/*fallthrough*/
	case 'm':
		piCheck = & pst->st_mtime;
		break;
	case 'c':
		piCheck = & pst->st_ctime;
		break;
	case 'a':
		piCheck = & pst->st_atime;
		break;
	default:
	case 'r':	/* most recent time */
		piCheck = & pst->st_mtime;
		if (*piCheck < pst->st_ctime)
			piCheck = & pst->st_ctime;
		if (*piCheck < pst->st_atime)
			piCheck = & pst->st_atime;
		break;
	}
	/* space and files factors are assumed to be -1, or 0 to 100
	 */
	if (1 != pst->st_nlink) {
		return 0;
	}
	tAge = tNow - *piCheck;
	if (fGaveAge) {
		return (tAge > tMaxAge) ? tAge : 0;
	}
	tAge *= pst->st_blocks * (-1 == iSpace ? 0 : iSpace);
	return tAge + (-1 == iFiles ? 0 : iFiles);
}

/* emulate df, as best we can with statfs(2)				(ksb)
 */
void
KruftDF(char *pcDir, double *pdCurSpace, double *pdCurFiles)
{
	register double dUsed;
#if USE_STATVFS
	auto struct statvfs sfDir;
#else
	auto struct statfs sfDir;
#endif

#if USE_STATVFS
	if (-1 == statvfs(pcDir, & sfDir)) {
		fprintf(stderr, "%s: statvfs: %s: %s\n", progname, pcDir, strerror(errno));
		exit(EX_OSERR);
	}
#else
	if (-1 == statfs(pcDir, & sfDir)) {
		fprintf(stderr, "%s: statfs: %s: %s\n", progname, pcDir, strerror(errno));
		exit(EX_OSERR);
	}
#endif
	dUsed = (double)(sfDir.f_blocks - sfDir.f_bfree);
	if (0.0 == sfDir.f_bavail + dUsed)
		*pdCurSpace = 100.00;
	else
		*pdCurSpace = dUsed*100.0/(double)(sfDir.f_bavail + dUsed);
	if (0 == sfDir.f_ffree)
		*pdCurFiles = 100.00;
	else
		*pdCurFiles = (double)(sfDir.f_files - sfDir.f_ffree)*100.0/(double)sfDir.f_ffree;
}

/* Queue cruft files to remove, by some metric				(ksb)
 */
static void
Kruft(pDIR, pcDir, iSpace, iFiles)
DIR *pDIR;
char *pcDir;
int iSpace, iFiles;
{
	register struct dirent *pDE;
	register int i;
	auto double dCurSpace, dCurFiles;
	auto struct stat stCand;
	auto char acName[MAXPATHLEN+4];

	if ((char *)0 == pcDir || (DIR *)0 == pDIR) {
		return;
	}
	KruftDF(pcDir, & dCurSpace, & dCurFiles);
	if (fVerbose) {
		printf("%s: %s: by %ctime" , progname, pcDir, cWhichTime);
		if (fGaveAge) {
			printf(" (oldest %ld second%s)", (long)tMaxAge, 1 == tMaxAge ? "" : "s");
		} else {
			if (-1 != iSpace) {
				if (-1 != iFiles) {
					printf(" (space %d%%, files %d%%)", iSpace, iFiles);
				} else {
					printf(" (space %d%%)", iSpace);
				}
			} else if (-1 != iFiles) {
				printf(" (files %d%%)", iFiles);
			}
		}
		printf("\n%s: presently at %5.2f space and %5.2f files\n", progname, dCurSpace, dCurFiles);
	}
	if (-1 != iSpace && dCurSpace < (double)iSpace) {
		iSpace = -1;
	}
	if (-1 != iFiles && dCurFiles < (double)iFiles) {
		iFiles = -1;
	}
	if (!fGaveAge && -1 == iSpace && -1 == iFiles) {
		if (fVerbose) {
			printf("%s: %s: nothing to clean\n", progname, pcDir);
		}
		return;
	}

	/* OK we have some work to do.  We'll build a heap of the files
	 * we might unlink, then pull off the heap to remove the correct
	 * ones.  If we run out of RAM while building the heap we fail. (ksb)
	 */
	while ((struct dirent *)0 != (pDE = readdir(pDIR))) {
		if (IsPriv(pDE->d_name)) {
			continue;
		}
		sprintf(acName, "%s/%s", pcDir, pDE->d_name);
		if (-1 == lstat(acName, & stCand)) {
			fprintf(stderr, "%s: lstat: %s: %s\n", progname, acName, strerror(errno));
			continue;
		}
		if (S_IFREG != (S_IFMT & stCand.st_mode)) {
			continue;
		}
		if (0 >= (i = Weight(& stCand, iSpace, iFiles))) {
			continue;
		}
		HeapInsert(pcDir, pDE->d_name, i);
	}
}

/* Now we have a list of crufty files, pick some to purge.		(ksb)
 */
static void
DeKruft(iSpace)
int iSpace;
{
	auto char *pcDir, *pcFile;
	auto int iWeight;
	auto double dCurSpace, dCurFiles;
	auto char acRm[MAXPATHLEN+4];

	while (HeapDequeue(&pcDir, &pcFile, &iWeight)) {
		if (!fGaveAge) {
			KruftDF(pcDir, & dCurSpace, & dCurFiles);
			if (dCurSpace < (double)iSpace) {
				return;
			}
		}
		sprintf(acRm, "%s/%s", pcDir, pcFile);
		if (fVerbose) {
			printf("%s %s\n", fTruncate ? ">" : "rm", acRm);
		}
		if (fExec  && -1 == (fTruncate ? truncate(acRm, 0L) : unlink(acRm))) {
			fprintf(stderr, "%s: %s: %s: %s\n", progname, fTruncate ? "truncate" : "unlink", acRm, strerror(errno));
		}
		if (fExec && fSync) {
			sync();
		}
	}
	if (fGaveAge) {
		exit(EX_OK);
	}
	if (fVerbose) {
		printf("%s: heap empty\n", progname);
	}
	/* we failed
	 */
	exit(EX_TEMPFAIL);
}

/* like a funky long jump						(ksb)
 */
static int
ImAKid(piRet)
int *piRet;
{
	register int iPid, iWait;
	auto int wExit;

	switch (iPid = fork()) {
	case -1:
		fprintf(stderr, "%s: fork: %s\n", progname, strerror(errno));
		exit(EX_OSERR);
	case 0:		/* I am the kid */
		return 1;
	default:
		break;
	}
	while (iPid != (iWait = wait(& wExit))) {
		if (-1 == iWait) {
			*piRet = EX_OSERR;
			return 0;
		}
	}
	if (EX_OK == *piRet && 0 != wExit) {
		*piRet = EX_NOPERM;	/* a guess */
	}
	return 0;
}

/* given 2 directories, sort by device (fsid would be better)		(ksb)
 */
static int fDiffDevs = 0;
static int
DevCmp(ppcLeft, ppcRight)
const void *ppcLeft, *ppcRight;
{
	auto struct stat stLeft, stRight;

	if (-1 == stat(*(const char **)ppcLeft, & stLeft)) {
		fprintf(stderr, "%s: stat: %s: %s\n", progname, *(char **)ppcLeft, strerror(errno));
		exit(EX_OSERR);
	}
	if (-1 == stat(*(const char **)ppcRight, & stRight)) {
		fprintf(stderr, "%s: stat: %s: %s\n", progname, *(char **)ppcRight, strerror(errno));
		exit(EX_OSERR);
	}
	fDiffDevs |= stLeft.st_dev != stRight.st_dev;
	return stLeft.st_dev - stRight.st_dev;
}

/* I could put a list in here to fork a kruft for each unique fs	(ksb)
 * the forked kids would do the work, exit for me and I'd return
 * (or exit) w/o doing any of the work, else if there is only
 * one fs I'd just do it.  This is cool, but hard to follow code.
 */
static void
Mitosis(argc, argv)
int argc;
char **argv;
{
	register int i, iFlip, iStart;
	auto struct stat astCursor[2];
	auto int iRet = EX_OK;
	register char **ppcCopy;

	if (0 == argc) {
		return;
	}
	/* Copy-out args and sort them:
	 * if all the dirs are on the same filesystem just return to do them.
	 */
	ppcCopy = calloc(argc+1, sizeof(char *));
	if ((char **)0 == ppcCopy) {
		fprintf(stderr, "%s: calloc: out of memory\n", progname);
		exit(EX_UNAVAILABLE);
	}
	for (i = 0; i <= argc; ++i) {
		ppcCopy[i] = argv[i];
	}
	(void)qsort(ppcCopy, argc, sizeof(char *), DevCmp);
	if (! fDiffDevs) {
		return;
	}

	/* They are not all on the same filesystem, we'll fork a process
	 * for each unique filesystem.  We zero out argv and copy back
	 * only the directories we want for each child.  The mkcmd parser
	 * doesn't see a changed argc -- but the code skips (char *)0
	 * slots in the every list (for just this kinda thing).
	 */
	iFlip = 1;
	if (-1 == stat(ppcCopy[0], & astCursor[0])) {
		fprintf(stderr, "%s: stat: %s: %s\n", progname, ppcCopy[0], strerror(errno));
		exit(EX_OSERR);
	}
	argv[0] = ppcCopy[0];
	for (i = 1; i <= argc; ++i) {
		argv[i] = (char *)0;
	}
	for (iStart = i = 1; i < argc; ++i) {
		if (-1 != stat(ppcCopy[i], & astCursor[iFlip])) {
			/* nada */;
		} else if (ENOENT == errno) {
			continue;
		} else {
			fprintf(stderr, "%s: stat: %s: %s\n", progname, ppcCopy[i], strerror(errno));
			exit(EX_OSERR);
		}
		/* When we are at a file not on the present device then
		 * we fork-return the list we have (argv[0] .. argv[iStart-1])
		 * to the called in another process and build a new list.
		 */
		if (astCursor[0].st_dev != astCursor[1].st_dev) {
			if (ImAKid(& iRet)) {
				return;
			}
			while (iStart > 0) {
				argv[--iStart] = (char *)0;
			}
			iFlip ^= 1;
		}
		argv[iStart++] = ppcCopy[i];
	}
	/* send the last list back to the caller
	 */
	if (ImAKid(& iRet)) {
		return;
	}
	/* tell the Customer all the return codes we saw (or'd together)
	 */
	exit(iRet);
}
