[net.sources] Recoverable delete

kg@hplabs.UUCP (Ken Greer) (11/04/84)

I've gotten several requests for this.  The source
attached implements a recoverable file delete.


: Run this file as a shell script to extract contents.
: This file contains files Makefile README del.1 del.c expunge skulker

echo x - Makefile
cat << '//E*O*F Makefile' > Makefile
SHELL=/bin/sh
CC = /bin/cc
CFLAGS = -O
B = /usr/local/bin

del: del.o; $(CC) $(CFLAGS) -o $@ del.o

install inst-del: del
	-/bin/rm $B/del $B/undel
	mv del $B
	ln $B/del $B/undel
	@echo '*** remember to install the nightly skulker ***'

FILES = Makefile README del.1 del.c expunge skulker
sar: $(FILES)
	sar $(FILES) > /tmp/del.sar

del.c: del.c,v; co -q del.c
//E*O*F Makefile
echo x - README
cat << '//E*O*F README' > README
The shell script "skulker" should be run from cron in the wee hours.
The shell script "expunge" is run from skulker.

Ken Greer
...!ucbvax!hplabs!kg  (uucp)
kg@HP-Labs            (CSNET)
kg.HP-Labs@Rand-Relay (ARPANET)
//E*O*F README
echo x - del.1
cat << '//E*O*F del.1' > del.1
.TH DEL 1 local
.SH NAME
del, undel \- recoverable file delete, undelete
.SH SYNOPSIS
\fBdel\fR [\fB-i -f -k\fR# \fB] \fIfiles...
.br
\fBundel \fIfiles...
.SH DESCRIPTION
.I Del
implements a recoverable file delete command.
The files, which are arguments to 
.I del
are moved, rather than expunged, into an invisible directory.
A subsequent
.I undel
of a
\fIdel\fR-ed
file will recover it.
.PP
.I Del
works by relinking the file into a directory named
.I .gone
in the same directory as the original file.
The .gone directory is created, if necessary.
Files are marked with deletion time and date which defaults to now plus
24 hours.
(Only files which have a single link will have their times changed.)
Your
\fIdel\fR-ed
files will be expunged from the disk on that time or later by a nightly skulker.
Thus, files may be recovered at any time up to their expunge time,
independent of intervening logouts.
.PP
If the a file is not writable by you, \fIdel\fR prompts with the message:
.PP
	override protection \fInnn\fR for file \fIfilename\fR?
.PP
A response of 'y' or 'Y' causes the file to be deleted.
Anything else leaves the file alone.
The switch -f, for force, turns off this query.
It is also turned off is the standard input is not a terminal.
.PP
The optional switches are:
.TP
-i
Interactive.  \fIDel\fR will ask before deleting any file.
.TP
-f
Force.  \fIDel\fR won't bother to ask even if the file is not writable by you.
.TP
-k
Keep time.
The number immediately following the -k is the number of days to keep
the file.  Thus, -k3 keeps the file for (at least) 3 days, instead of the
default 1.
.PP
Most people find it useful to place the following aliases in their .cshrc file:
.PP
	alias  rm   del
.br
	alias  rm!  /bin/rm
.PP
.SH FILES
Deleted files are moved to the directory ".gone".
.SH SEE ALSO
rm (1)
.SH BUGS
You cannot 
.I del
a directory.
Undel-ed files get their accessed and updated times set to the time
of undel-ing.
Files which have more than one link to them intentionally do not have their
times modified.
Therefore, these links are expunged at arbitrary times.
But, of course,
your del-ed file (link) takes no extra disk space while it stays around.
.SH AUTHOR
Ken Greer
//E*O*F del.1
echo x - del.c
cat << '//E*O*F del.c' > del.c
static char *RCSid =
"$Header: /usr/local/src/cmd/del/del.c,v 1.9 83/08/25 11:01:49 kgsu Exp $";

#include <stdio.h>
#include <ctype.h>
#include <sys/types.h>
#include <time.h>
#include <sys/stat.h>

/*
 * DEL and UNDEL virtual delete and undelete
 * Ken Greer
 */

#define equal(a, b)	(strcmp(a, b) == 0)
#define DAY		(60L * 60L * 24L)	/* Seconds in a day */
#define FILESIZE	512

extern time_t time ();

typedef enum {FALSE = 0, TRUE = 1} BOOL;

typedef struct
{
    time_t keep;	/* -k: time to keep file around */
    BOOL   ask;		/* -i: Always ask before deleting */
    BOOL   force;	/* -f: don't ask even if no write permissions */
} OPTIONS;

/*
 * Return pointer to the final file name in a path.
 * I.e. sname ("/foo/baz/mumble")
 * returns a pointer to "mumble".
 */
char *
sname (s)
register char *s;
{
    register char *p;

    for (p = s; *p;)
       if (*p++ == '/')
	   s = p;
    return (s);
}

mkdir (dir)
char *dir;
{
    switch (vfork ())
    {
	case -1:
	    perror ("cannot create .gone directory");
	    exit (-1);
	case 0:			/* child side */
	    execl ("/bin/mkdir", "mkdir", dir, 0);
	    fprintf (stderr, "Cannot find /bin/mkdir\n");
	    _exit (-1);
	default:		/* parent side */
	    wait (0);
    }
}

gone_names (file, gone_dir, gone_file)
char *file, *gone_dir, *gone_file;
{
    extern char *rindex ();
    register char *g = gone_dir, *start_of_name = file, *last_slash;
    if (last_slash = rindex (file, '/'))
    {
	register char *p;
	for (p = file; p <= last_slash; *g++ = *p++);
	start_of_name = &last_slash[1];
    }
    strcpy (g, ".gone");
    sprintf (gone_file, "%s/%s", gone_dir, start_of_name);
}

undelete (file)
char *file;
{
    char gone_dir[FILESIZE], gone_file[FILESIZE];
    time_t timep[2];
    struct stat statb;

    gone_names (file, gone_dir, gone_file);
    if (link (gone_file, file) < 0)
    {
	fprintf (stderr, "%s: not found\n", gone_file);
	return (-1);
    }

    if (unlink (gone_file) < 0)
    {
	fprintf (stderr, "%s: not removed\n", gone_file);
	return (-1);
    }

    /*
     * Reset accessed and updated time to now
     * since we have already changed it to some time in the
     * future when we "del"-ed it.
     * But only if file has no other links to it.
     */
    if(stat(file, &statb) == 0 && statb.st_nlink == 1)
    {
	timep[0] = timep[1] = time (0);
	utime (file, timep);
    }

    return (0);
}

yes ()
{
    int a, b;

    while ((a = getchar ()) <= ' ' && a != EOF && a != '\n');
    if (a != EOF && a != '\n')
	while ((b = getchar ()) != EOF && b != '\n');

    return (a == 'y' || a == 'Y');
}

delete (file, options)
char *file;
OPTIONS options;
{
    char gone_dir[FILESIZE], gone_file[FILESIZE];
    time_t timep[2];
    struct stat statb;

    gone_names (file, gone_dir, gone_file);
    if(stat(file, &statb) < 0)
    {
	fprintf (stderr, "del: %s nonexistent\n", file);
	return (-1);
    }

    if ((statb.st_mode&S_IFMT) == S_IFDIR)
    {
	fprintf (stderr, "del: %s is a directory; not removed\n", file);
	return(-1);
    }

    if (options.ask)				/* interactive */
    {
	printf ("del: remove %s? ", file);
	if (!yes ())
	    return;
    }
    if(!options.force && access(file, 02) < 0)	/* no write permission */
    {
	printf("del: override protection %o for %s? ",
	    statb.st_mode & 0777, file);
	if (!yes ())
	    return;
    }
    if (access (gone_dir, 0) < 0)
	mkdir (gone_dir);
    unlink (gone_file);
    if (link (file, gone_file) < 0)
    {
	fprintf (stderr, "del: %s not removed (link failed)\n", file);
	return (-1);
    }
    if (unlink (file) < 0)
    {
	fprintf (stderr, "%s: not removed (unlink failed)\n", file);
	return (-1);
    }
    /*
     * Set the gone file's accessed time and updated time
     * to the current time plus number of days to keep.
     * Only do it if file has no other links to it.
     */
    if (statb.st_nlink == 1)
    {
	timep[0] = timep[1] = time (0) + options.keep;
	utime (gone_file, timep);
    }

    return (0);
}

main (argc, argv)
char **argv;
{
    extern char *sname ();
    register char *myname = sname (argv[0]);
    register int i, failed = 0, del;
    OPTIONS options;

    options.keep = 1 * DAY;
    options.ask = FALSE;
    options.force = FALSE;

    if (isatty(0) == 0)
	options.force = TRUE;

    for (; argc > 1 && argv[1][0] == '-'; argc--, argv++)
    {
	switch (argv[1][1])
	{
	    case 'k':			/* Keep this many days */
		if (!isdigit (argv[1][2]))
		{
		    fprintf (stderr, "Numeric parameter after -k required.\n");
		    exit (1);
		}
		options.keep = atoi (&argv[1][2]) * DAY;
		break;
	    case 'f':			/* force (as in rm and mv) */
		options.force = TRUE;
		break;
	    case 'i':			/* interactive */
		options.ask = TRUE;
		break;
	    default:
		fprintf(stderr,"%s: unknown switch \"%s\".\n", myname, argv[1]);
		exit (1);
	}
    }
    if (argc < 2)
	fprintf (stderr, "Usage: %s files...\n", myname), exit (1);

    del = equal (myname, "del");
    for (i = 1; i < argc; i++)
	failed |= del ? delete (argv[i], options) : undelete (argv[i]);
    return (failed);
}
//E*O*F del.c
echo x - expunge
cat << '//E*O*F expunge' > expunge
#! /bin/csh -fe
# Run from "skulker" for the del command.
# Remove files older than current time/date.
if ($#argv < 1) exit 1
cd $1
cp /dev/null /tmp/now$$
find . -type f \! -newer /tmp/now$$ -exec /bin/rm -f {} \;
rm -f /tmp/now$$
cd .. && rmdir $1
//E*O*F expunge
echo x - skulker
cat << '//E*O*F skulker' > skulker
#! /bin/sh
# 'For the del command...'
find / -type d -name .gone -exec /usr/local/etc/expunge {} \;
//E*O*F skulker

-- 
Ken Greer
kg.hplabs@CSNET-Relay (ARPA)
kg@HP-Labs (CSNET)
hplabs!kg (UUCP)