Commit Diff


commit - 0c98da8bf8ea51d0288222f6c6ba3c125cf20f46
commit + 004aa293f360ea0f63ec50f5042f8c0fb2831e4f
blob - /dev/null
blob + 15e8df09c842ec3fe769970249f1549ee729228d (mode 644)
--- /dev/null
+++ man/man1/hist.1
@@ -0,0 +1,78 @@
+.TH HIST 1
+.SH NAME
+hist \- print file names from the dump
+.SH SYNOPSIS
+.B hist
+[
+.B -vdu
+] [
+.B -s
+.I yyyymmdd
+]
+.I files ...
+.SH DESCRIPTION
+.I Hist
+prints the names, dates, and sizes of all versions of the named
+.IR files ,
+looking backwards in time,
+stored in the dump file system.
+If the file exists in the main tree, the first line of output will be its current state.
+For example,
+.IP
+.EX
+hist ~rsc/.bash_history
+.EE
+.PP
+produces
+.IP
+.EX
+.nf
+May 19 16:11:37 EDT 2005 /home/am3/rsc/.bash_history 6175
+May 18 23:32:16 EDT 2005 /dump/am/2005/0519/home/am3/rsc/.bash_history 5156
+May 17 23:32:31 EDT 2005 /dump/am/2005/0518/home/am3/rsc/.bash_history 5075
+May 16 07:53:47 EDT 2005 /dump/am/2005/0517/home/am3/rsc/.bash_history 5065
+.fi
+.EE
+.PP
+The
+.B -v
+option enables verbose debugging printout.
+.PP
+The 
+.B -d
+option causes
+.IR diff (1)
+.B -c
+to be run for each adjacent pair of dump files, while
+.B -b
+runs
+.IR diff
+.BR -cb .
+.PP
+The
+.B -u
+option causes times to be printed in GMT (UT) rather than local time.
+.PP
+Finally, the
+.B -s
+option
+sets the starting (most recent) date for the output.
+.SH EXAMPLES
+.PP
+Examine changes in block.c:
+.IP
+.EX
+hist -d block.c
+.EE
+.SH FILES
+.B /dump
+.SH SOURCE
+.B /home/am3/rsc/src/backup/cmd/history.c
+.SH SEE ALSO
+.IR yesterday (1)
+.SH BUGS
+Should be called
+.IR history ,
+but
+that name is taken by
+.IR sh (1).
blob - /dev/null
blob + 72d93e73a9b8928f2e76b991006d4d9270dfb3fb (mode 644)
--- /dev/null
+++ man/man1/vbackup.1
@@ -0,0 +1,263 @@
+.TH VBACKUP 8
+.SH NAME
+vbackup, vcat, vftp, vmount, vmount0, vnfs \- 
+back up Unix file systems to Venti
+.SH SYNOPSIS
+.B vbackup
+[
+.B -DVnv
+]
+[
+.B -s
+.I secs
+]
+[
+.B -w
+.I n
+]
+.I disk
+[
+.I score
+]
+.PP
+.B vcat
+[
+.B -z
+]
+.I disk
+|
+.I score
+.B >
+.I disk
+.PP
+.B vftp
+.I disk
+|
+.I score
+.PP
+.B vmount
+[
+.B -v
+]
+.I addr
+.I mtpt
+.PP
+.B vmount0
+[
+.B -v
+]
+[
+.B -h
+.I handle
+]
+.I addr
+.I mtpt
+.PP
+.B vnfs
+[
+.B -LLMRVr
+]
+[
+.B -a
+.I addr
+]
+[
+.B -m
+.I mntaddr
+]
+[
+.B -b
+.I blocksize
+]
+[
+.B -c
+.I cachesize
+]
+.I config
+.SH DESCRIPTION
+These programs back up and restore standard
+Unix file system images stored in
+.IR venti (8).
+Images stored in
+.I venti
+are named by
+.IR scores ,
+which consist of a file system type followed
+by a colon and forty hexadecimal digits, as in:
+.IP
+.EX
+ffs:0123456789abcdef0123456789abcdef01234567
+.EE
+.PP
+(The hexadecimal data is the SHA1 hash of the Venti
+root block representing the file system image.)
+.PP
+These programs expect the environment variable
+.B $venti
+to be set to the network address of the Venti server to use
+(for example,
+.B yourhost
+or
+.BR tcp!yourhost!venti ).
+.PP
+.I Vbackup
+copies the file system stored on
+.I disk
+to the Venti server and prints the 
+score for the newly-stored image.
+The argument
+.I disk
+should be a disk or disk partition device
+that would be appropriate to pass to
+.IR mount (8).
+.PP
+The optional argument
+.I score
+is the score of a previous backup of the disk image.
+If
+.I score
+is given, 
+.I vbackup
+will not write to Venti any blocks that have not changed
+since the previous backup.
+This is only a speed optimization: since the blocks are already
+stored on Venti they need not be sent to the Venti server again.
+.PP
+The options to
+.I vbackup
+are:
+.TP
+.B -D
+.TP
+.B -V
+.TP
+.B -n
+.TP
+.B -v
+.TP
+.B -w \fIn
+.TP
+.B -s \fIsecs
+.PP
+.I Vcat
+writes the named disk image to standard output.
+Unused file system blocks are printed zeroed regardless
+of their actual content.
+.PP
+If the
+.B -z
+flag is given, 
+.I vcat
+will attempt to seek over unused blocks instead of writing to them.
+The
+.B -z
+flag should only be used when standard output is seekable
+.RI ( i.e. ,
+when it has been redirected to a file or disk).
+.PP
+.I Vftp
+presents the
+file system image named by
+.I disk
+or
+.I score
+in a shell-like
+interactive session.
+Type
+.B help
+at the
+.B vftp>
+prompt for details.
+.PP
+.I Vmount
+mounts the NFS service at the network connection
+.I address
+onto
+.IR mountpoint .
+On most operating systems,
+.I vmount
+must be run by the user
+.BR root .
+.PP
+.I Vmount0
+is a simple C program that 
+.I vmount
+uses if 
+.IR mount (8)
+does not suffice.
+.PP
+.I Vnfs
+serves, using the
+NFS version 3 protocol,
+one or more disk images in a synthetic tree defined
+by the configuration file
+.IR config .
+.I Vnfs
+announces NFS service at
+.IR addr 
+(default
+.BR udp!*!nfs )
+and NFS mount service at
+.IR mntaddr
+(default
+.BR udp!*!\fI999 ),
+registering both with the port mapper.
+If no port mapper is found running (on port 111),
+.I vnfs
+starts its own port mapper.
+The options are:
+.TP
+.B -r
+Reply to all NFS requests with RPC rejections.
+.TP
+.B -M
+Do not announce an NFS mount service.
+.TP
+.B -P
+Do not register service with the port mapper.
+.TP
+.B -a
+
+
+.SH EXAMPLES
+.PP
+Back up the file system stored on
+.BR /dev/da0s1a :
+.IP
+.EX
+% vbackup /dev/da0s1a
+ffs:0123456789abcdef0123456789abcdef01234567
+% 
+.EE
+.PP
+Serve that backup and a few others in a tree reminiscent
+of Plan 9's dump file system, but hide each day's contents of
+.B /tmp :
+.IP
+.EX
+% cat config
+mount /2005/0510 ffs:0123456789abcdef\fI...\fP
+mount /2005/0510/home ffs:0123456789abcdef\fI...\fP
+mount /2005/0510 ffs:0123456789abcdef\fI...\fP
+mount /2005/0510/home ffs:0123456789abcdef\fI...\fP
+hide /*/*/tmp
+% vnfs -m -b 16k -c 1k config
+% 
+.EE
+.PP
+Mount the backups on a client machine using
+.IR vmount :
+.IP
+.EX
+# vmount udp!yourserver!nfs /dump
+# ls /dump
+2005
+# 
+.EE
+.PP
+Mount the backups using the standard NFS mount program:
+.IP
+.EX
+# mount -t nfs -o soft,intr,ro,nfsv3,rsize=8192,timeo=100 \
+	-o nfsvers=3,nolock,noatime,nodev,nosuid \
+.EE
blob - /dev/null
blob + b8a198ba1739c9eb783b290be70277a9f95519fd (mode 644)
--- /dev/null
+++ man/man1/yesterday.1
@@ -0,0 +1,99 @@
+.TH YESTERDAY 1
+.SH NAME
+yesterday \- print file names from the dump
+.SH SYNOPSIS
+.B yesterday
+[
+.B -cCd
+] [
+.B -n
+.I daysago
+] [
+.I \-date
+]
+.I files ...
+.SH DESCRIPTION
+.I Yesterday
+prints the names of the
+.I files
+from the most recent dump.
+Since dumps are done early in the morning,
+yesterday's files are really in today's dump.
+For example, if today is February 11, 2003,
+.IP
+.EX
+yesterday /home/am3/rsc/.profile
+.EE
+.PP
+prints
+.IP
+.EX
+/dump/am/2003/0211/home/am3/rsc/.profile
+.EE
+.PP
+In fact, the implementation is to select the most recent dump in
+the current year, so the dump selected may not be from today.
+.PP
+By default, 
+.I yesterday
+prints the names of the dump files corresponding to the named files.
+The first set of options changes this behavior.
+.TP
+.B -c
+Copy the dump files over the named files.
+.TP
+.B -C
+Copy the dump files over the named files only when
+they differ.
+.TP
+.B -d
+Run 
+.B diff
+to compare the dump files with the named files.
+.PP
+The
+.I date
+option selects other day's dumps, with a format of
+1, 2, 4, 6, or 8 digits of the form
+.IR d,
+.IR dd ,
+.IR mmdd ,
+.IR yymmdd ,
+or
+.IR yyyymmdd .
+.PP
+The
+.B -n
+option selects the dump
+.I daysago
+prior to the current day.
+.PP
+.I Yesterday
+does not guarantee that the string it prints represents an existing file.
+.SH EXAMPLES
+.PP
+See what's changed in the last week in your profile:
+.IP
+.EX
+yesterday -d -n 7 ~/.profile
+.EE
+.PP
+Restore your profile from yesterday:
+.IP
+.EX
+yesterday -c ~/.profile
+.EE
+.SH FILES
+.B /dump
+.SH SOURCE
+.B /usr/local/bin/yesterday
+.SH SEE ALSO
+.IR diff (1),
+.IR hist (1)
+.SH BUGS
+Backups are only available on
+.B amsterdam
+and
+.BR toil .
+.PP
+It's hard to use this command without singing.
blob - /dev/null
blob + 2af95d83d9ac96aed1a71134c4b3e9ec6095b9b7 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/COPYRIGHT
@@ -0,0 +1,27 @@
+This software was developed as part of a project at MIT:
+	$PLAN9/src/libdiskfs/*
+	$PLAN9/include/diskfs.h
+	$PLAN9/src/cmd/vbackup/*
+
+Copyright (c) 2005 Russ Cox,
+                   Massachusetts Institute of Technology
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
blob - /dev/null
blob + 0efeeee92c8f47e6770d151f751717c6ddfef250 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/config.c
@@ -0,0 +1,515 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <sunrpc.h>
+#include <nfs3.h>
+#include <diskfs.h>
+#include <venti.h>
+#include <libsec.h>
+
+#undef stime
+#define stime configstime	/* sometimes in <time.h> */
+typedef struct Entry Entry;
+struct Entry
+{
+	Entry *parent;
+	Entry *nextdir;
+	Entry *nexthash;
+	Entry *kids;
+	int isfsys;
+	Fsys *fsys;
+	uchar score[VtScoreSize];	/* of fsys */
+	char *name;
+	uchar sha1[VtScoreSize];	/* of path to this entry */
+	ulong time;
+};
+
+typedef struct Config Config;
+struct Config
+{
+	VtCache *vcache;
+	Entry *root;
+	Entry *hash[1024];
+	Qid qid;
+};
+
+Config *config;
+static	ulong 	mtime;	/* mod time */
+static	ulong 	stime;	/* sync time */
+static	char*	configfile;
+
+static int addpath(Config*, char*, uchar[VtScoreSize], ulong);
+Fsys fsysconfig;
+
+static void
+freeconfig(Config *c)
+{
+	Entry *next, *e;
+	int i;
+
+	for(i=0; i<nelem(c->hash); i++){
+		for(e=c->hash[i]; e; e=next){
+			next = e->nexthash;
+			free(e);
+		}
+	}
+	free(c);
+}
+
+static int
+namehash(uchar *s)
+{
+	return (s[0]<<2)|(s[1]>>6);
+}
+
+static Entry*
+entrybyhandle(Nfs3Handle *h)
+{
+	int hh;
+	Entry *e;
+
+	hh = namehash(h->h);
+	for(e=config->hash[hh]; e; e=e->nexthash)
+		if(memcmp(e->sha1, h->h, VtScoreSize) == 0)
+			return e;
+	return nil;
+}
+
+static Config*
+readconfigfile(char *name, VtCache *vcache)
+{
+	char *p, *pref, *f[10];
+	int ok;
+	Config *c;
+	uchar score[VtScoreSize];
+	int h, nf, line;
+	Biobuf *b;
+	Dir *dir;
+
+	configfile = vtstrdup(name);
+
+	if((dir = dirstat(name)) == nil)
+		return nil;
+	
+	if((b = Bopen(name, OREAD)) == nil){
+		free(dir);
+		return nil;
+	}
+
+	line = 0;
+	ok = 1;
+	c = emalloc(sizeof(Config));
+	c->vcache = vcache;
+	c->qid = dir->qid;
+	free(dir);
+	c->root = emalloc(sizeof(Entry));
+	c->root->name = "/";
+	c->root->parent = c->root;
+	sha1((uchar*)"/", 1, c->root->sha1, nil);
+	h = namehash(c->root->sha1);
+	c->hash[h] = c->root;
+
+	for(; (p = Brdstr(b, '\n', 1)) != nil; free(p)){
+		line++;
+		if(p[0] == '#')
+			continue;
+		nf = tokenize(p, f, nelem(f));
+		if(nf != 3){
+			fprint(2, "%s:%d: syntax error\n", name, line);
+			// ok = 0;
+			continue;
+		}
+		if(vtparsescore(f[1], &pref, score) < 0){
+			fprint(2, "%s:%d: bad score '%s'\n", name, line, f[1]);
+			// ok = 0;
+			continue;
+		}
+		if(f[0][0] != '/'){
+			fprint(2, "%s:%d: unrooted path '%s'\n", name, line, f[0]);
+			// ok = 0;
+			continue;
+		}
+		if(addpath(c, f[0], score, strtoul(f[2], 0, 0)) < 0){
+			fprint(2, "%s:%d: %s: %r\n", name, line, f[0]);
+			// ok = 0;
+			continue;
+		}
+	}
+	Bterm(b);
+
+	if(!ok){
+		freeconfig(c);
+		return nil;
+	}
+	
+	return c;
+}
+
+static void
+refreshconfig(void)
+{
+	ulong now;
+	Config *c, *old;
+	Dir *d;
+
+	now = time(0);
+	if(now - stime < 60)
+		return;
+	if((d = dirstat(configfile)) == nil)
+		return;
+	if(d->mtime == mtime){
+		free(d);
+		stime = now;
+		return;
+	}
+
+	c = readconfigfile(configfile, config->vcache);
+	if(c == nil){
+		free(d);
+		return;
+	}
+
+	old = config;
+	config = c;
+	stime = now;
+	mtime = d->mtime;
+	free(d);
+	freeconfig(old);
+}
+
+static Entry*
+entrylookup(Entry *e, char *p, int np)
+{
+	for(e=e->kids; e; e=e->nextdir)
+		if(strlen(e->name) == np && memcmp(e->name, p, np) == 0)
+			return e;
+	return nil;
+}
+
+static Entry*
+walkpath(Config *c, char *name)
+{
+	Entry *e, *ee;
+	char *p, *nextp;
+	int h;
+
+	e = c->root;
+	p = name;
+	for(; *p; p=nextp){
+		assert(*p == '/');
+		p++;
+		nextp = strchr(p, '/');
+		if(nextp == nil)
+			nextp = p+strlen(p);
+		if(e->fsys){
+			werrstr("%.*s is already a mount point", utfnlen(name, nextp-name), name);
+			return nil;
+		}
+		if((ee = entrylookup(e, p, nextp-p)) == nil){
+			ee = emalloc(sizeof(Entry)+(nextp-p)+1);
+			ee->parent = e;
+			ee->nextdir = e->kids;
+			e->kids = ee;
+			ee->name = (char*)&ee[1];
+			memmove(ee->name, p, nextp-p);
+			ee->name[nextp-p] = 0;
+			sha1((uchar*)name, nextp-name, ee->sha1, nil);
+			h = namehash(ee->sha1);
+			ee->nexthash = c->hash[h];
+			c->hash[h] = ee;
+		}
+		e = ee;
+	}
+	if(e->kids){
+		werrstr("%s already has children; cannot be mount point", name);
+		return nil;
+	}
+	return e;
+}
+
+static int
+addpath(Config *c, char *name, uchar score[VtScoreSize], ulong time)
+{
+	Entry *e;
+
+	e = walkpath(c, name);
+	if(e == nil)
+		return -1;
+	e->isfsys = 1;
+	e->time = time;
+	memmove(e->score, score, VtScoreSize);
+	return 0;
+}
+
+static void
+mkhandle(Nfs3Handle *h, Entry *e)
+{
+	memmove(h->h, e->sha1, VtScoreSize);
+	h->len = VtScoreSize;
+}
+
+Nfs3Status
+handleparse(Nfs3Handle *h, Fsys **pfsys, Nfs3Handle *nh, int isgetattr)
+{
+	int hh;
+	Entry *e;
+	Disk *disk;
+	Fsys *fsys;
+
+	refreshconfig();
+
+	if(h->len < VtScoreSize)
+		return Nfs3ErrBadHandle;
+
+	hh = namehash(h->h);
+	for(e=config->hash[hh]; e; e=e->nexthash)
+		if(memcmp(e->sha1, h->h, VtScoreSize) == 0)
+			break;
+	if(e == nil)
+		return Nfs3ErrBadHandle;
+
+	if(e->isfsys == 1 && e->fsys == nil && (h->len != VtScoreSize || !isgetattr)){
+		if((disk = diskopenventi(config->vcache, e->score)) == nil){
+			fprint(2, "cannot open disk %V: %r\n", e->score);
+			return Nfs3ErrIo;
+		}
+		if((fsys = fsysopen(disk)) == nil){
+			fprint(2, "cannot open fsys on %V: %r\n", e->score);
+			diskclose(disk);
+			return Nfs3ErrIo;
+		}
+		e->fsys = fsys;
+	}
+
+	if(e->fsys == nil || (isgetattr && h->len == VtScoreSize)){
+		if(h->len != VtScoreSize)
+			return Nfs3ErrBadHandle;
+		*pfsys = &fsysconfig;
+		*nh = *h;
+		return Nfs3Ok;
+	}
+	*pfsys = e->fsys;
+	if(h->len == VtScoreSize)
+		return fsysroot(*pfsys, nh);
+	nh->len = h->len - VtScoreSize;
+	memmove(nh->h, h->h+VtScoreSize, nh->len);
+	return Nfs3Ok;
+}
+
+void
+handleunparse(Fsys *fsys, Nfs3Handle *h, Nfs3Handle *nh, int dotdot)
+{
+	Entry *e;
+	int hh;
+
+	refreshconfig();
+
+	if(fsys == &fsysconfig)
+		return;
+
+	if(dotdot && nh->len == h->len - VtScoreSize
+	&& memcmp(h->h+VtScoreSize, nh->h, nh->len) == 0){
+		/* walked .. but didn't go anywhere: must be at root */
+		hh = namehash(h->h);
+		for(e=config->hash[hh]; e; e=e->nexthash)
+			if(memcmp(e->sha1, h->h, VtScoreSize) == 0)
+				break;
+		if(e == nil)
+			return;	/* cannot happen */
+
+		/* walk .. */
+		e = e->parent;
+		nh->len = VtScoreSize;
+		memmove(nh->h, e->sha1, VtScoreSize);
+		return;
+	}
+
+	/* otherwise just insert the same prefix */
+	memmove(nh->h+VtScoreSize, nh->h, VtScoreSize);
+	nh->len += VtScoreSize;
+	memmove(nh->h, h->h, VtScoreSize);
+}
+
+Nfs3Status
+fsysconfigroot(Fsys *fsys, Nfs3Handle *h)
+{
+	USED(fsys);
+
+	mkhandle(h, config->root);
+	return Nfs3Ok;
+}
+
+Nfs3Status
+fsysconfiggetattr(Fsys *fsys, SunAuthUnix *au, Nfs3Handle *h, Nfs3Attr *attr)
+{
+	Entry *e;
+
+	USED(fsys);
+	USED(au);
+
+	if(h->len != VtScoreSize)
+		return Nfs3ErrBadHandle;
+
+	e = entrybyhandle(h);
+	if(e == nil)
+		return Nfs3ErrNoEnt;
+
+	memset(attr, 0, sizeof *attr);
+	attr->type = Nfs3FileDir;
+	attr->mode = 0555;
+	attr->nlink = 2;
+	attr->size = 1024;
+	attr->fileid = *(u64int*)h->h;
+	attr->atime.sec = e->time;
+	attr->mtime.sec = e->time;
+	attr->ctime.sec = e->time;
+	return Nfs3Ok;
+}
+
+Nfs3Status
+fsysconfigaccess(Fsys *fsys, SunAuthUnix *au, Nfs3Handle *h, u32int want, u32int *got, Nfs3Attr *attr)
+{
+	want &= Nfs3AccessRead|Nfs3AccessLookup|Nfs3AccessExecute;
+	*got = want;
+	return fsysconfiggetattr(fsys, au, h, attr);
+}
+
+Nfs3Status
+fsysconfiglookup(Fsys *fsys, SunAuthUnix *au, Nfs3Handle *h, char *name, Nfs3Handle *nh)
+{
+	Entry *e;
+
+	USED(fsys);
+	USED(au);
+
+	if(h->len != VtScoreSize)
+		return Nfs3ErrBadHandle;
+
+	e = entrybyhandle(h);
+	if(e == nil)
+		return Nfs3ErrNoEnt;
+
+	if(strcmp(name, "..") == 0)
+		e = e->parent;
+	else if(strcmp(name, ".") == 0){
+		/* nothing */
+	}else{
+		if((e = entrylookup(e, name, strlen(name))) == nil)
+			return Nfs3ErrNoEnt;
+	}
+
+	mkhandle(nh, e);
+	return Nfs3Ok;
+}
+
+Nfs3Status
+fsysconfigreadlink(Fsys *fsys, SunAuthUnix *au, Nfs3Handle *h, char **link)
+{
+	USED(h);
+	USED(fsys);
+	USED(au);
+
+	*link = 0;
+	return Nfs3ErrNotSupp;
+}
+
+Nfs3Status
+fsysconfigreadfile(Fsys *fsys, SunAuthUnix *au, Nfs3Handle *h, u32int count, u64int offset, uchar **pdata, u32int *pcount, u1int *peof)
+{
+	USED(fsys);
+	USED(h);
+	USED(count);
+	USED(offset);
+	USED(pdata);
+	USED(pcount);
+	USED(peof);
+	USED(au);
+
+	return Nfs3ErrNotSupp;
+}
+
+Nfs3Status
+fsysconfigreaddir(Fsys *fsys, SunAuthUnix *au, Nfs3Handle *h, u32int count, u64int cookie, uchar **pdata, u32int *pcount, u1int *peof)
+{
+	uchar *data, *p, *ep, *np;
+	u64int c;
+	Entry *e;
+	Nfs3Entry ne;
+
+	USED(fsys);
+	USED(au);
+
+	if(h->len != VtScoreSize)
+		return Nfs3ErrBadHandle;
+
+	e = entrybyhandle(h);
+	if(e == nil)
+		return Nfs3ErrNoEnt;
+
+	e = e->kids;
+	c = cookie;
+	for(; c && e; c--)
+		e = e->nextdir;
+	if(e == nil){
+		*pdata = 0;
+		*pcount = 0;
+		*peof = 1;
+		return Nfs3Ok;
+	}
+
+	data = emalloc(count);
+	p = data;
+	ep = data+count;
+	while(e && p < ep){
+		ne.name = e->name;
+		ne.cookie = ++cookie;
+		ne.fileid = *(u64int*)e->sha1;
+		if(nfs3entrypack(p, ep, &np, &ne) < 0)
+			break;
+		p = np;
+		e = e->nextdir;
+	}
+	*pdata = data;
+	*pcount = p - data;
+	*peof = 0;
+	return Nfs3Ok;
+}
+
+void
+fsysconfigclose(Fsys *fsys)
+{
+	USED(fsys);
+}
+
+int
+readconfig(char *name, VtCache *vcache, Nfs3Handle *h)
+{
+	Config *c;
+	Dir *d;
+
+	if((d = dirstat(name)) == nil)
+		return -1;
+
+	c = readconfigfile(name, vcache);
+	if(c == nil){
+		free(d);
+		return -1;
+	}
+
+	config = c;
+	mtime = d->mtime;
+	stime = time(0);
+	free(d);
+
+	mkhandle(h, c->root);
+	fsysconfig._lookup = fsysconfiglookup;
+	fsysconfig._access = fsysconfigaccess;
+	fsysconfig._getattr = fsysconfiggetattr;
+	fsysconfig._readdir = fsysconfigreaddir;
+	fsysconfig._readfile = fsysconfigreadfile;
+	fsysconfig._readlink = fsysconfigreadlink;
+	fsysconfig._root = fsysconfigroot;
+	fsysconfig._close = fsysconfigclose;
+	return 0;
+}
blob - /dev/null
blob + 9258689067918a0addc27770c22d2525876b8209 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/diskcat.c
@@ -0,0 +1,54 @@
+#include <u.h>
+#include <libc.h>
+#include <diskfs.h>
+
+void
+usage(void)
+{
+	fprint(2, "usage: fscat fspartition\n");
+	exits("usage");
+}
+
+int
+main(int argc, char **argv)
+{
+	extern int nfilereads;
+	u8int *zero;
+	u32int i;
+	u32int n;
+	Block *b;
+	Disk *disk;
+	Fsys *fsys;
+
+	ARGBEGIN{
+	default:
+		usage();
+	}ARGEND
+
+	if(argc != 1)
+		usage();
+
+	if((disk = diskopenfile(argv[0])) == nil)
+		sysfatal("diskopen: %r");
+	if((disk = diskcache(disk, 16384, 16)) == nil)
+		sysfatal("diskcache: %r");
+	if((fsys = fsysopen(disk)) == nil)
+		sysfatal("ffsopen: %r");
+
+	zero = emalloc(fsys->blocksize);
+	fprint(2, "%d blocks total\n", fsys->nblock);
+	n = 0;
+	for(i=0; i<fsys->nblock; i++){
+		if((b = fsysreadblock(fsys, i)) != nil){
+			write(1, b->data, fsys->blocksize);
+			n++;
+			blockput(b);
+		}else
+			write(1, zero, fsys->blocksize);
+		if(b == nil && i < 2)
+			sysfatal("block %d not in use", i);
+	}
+	fprint(2, "%d blocks in use, %d file reads\n", n, nfilereads);
+	exits(nil);
+	return 0;
+}
blob - /dev/null
blob + b8c26ed0f36866205a891690ed6037dff6d4980f (mode 644)
--- /dev/null
+++ src/cmd/vbackup/diskftp.c
@@ -0,0 +1,134 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <sunrpc.h>
+#include <nfs3.h>
+#include <diskfs.h>
+
+int debug;
+
+void
+usage(void)
+{
+	fprint(2, "usage: fsview fspartition cmd\n");
+	fprint(2, "cmd is:\n");
+	fprint(2, "\tcat file\n");
+	fprint(2, "\tls dir\n");
+	fprint(2, "\tstat file\n");
+	exits("usage");
+}
+
+void
+printattr(Nfs3Attr *attr)
+{
+	Fmt fmt;
+	char buf[256];
+
+	fmtfdinit(&fmt, 1, buf, sizeof buf);
+	nfs3attrprint(&fmt, attr);
+	fmtfdflush(&fmt);
+	print("\n");
+}
+
+char buf[8192];
+
+void
+x(int ok)
+{
+	if(ok != Nfs3Ok){
+		nfs3errstr(ok);
+		sysfatal("%r");
+	}
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	char *p, *q;
+	u32int n;
+	Disk *disk;
+	Fsys *fsys;
+	Nfs3Handle h;
+	SunAuthUnix au;
+	Nfs3Attr attr;
+	u64int offset;
+	u1int eof;
+	uchar *data;
+	char *link;
+
+	ARGBEGIN{
+	case 'd':
+		debug = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc != 3)
+		usage();
+
+	if((disk = diskopenfile(argv[0])) == nil)
+		sysfatal("diskopen: %r");
+	if((disk = diskcache(disk, 16384, 16)) == nil)
+		sysfatal("diskcache: %r");
+	if((fsys = fsysopen(disk)) == nil)
+		sysfatal("ffsopen: %r");
+
+	allowall = 1;
+	memset(&au, 0, sizeof au);
+
+	/* walk */
+	if(debug) fprint(2, "get root...");
+	x(fsysroot(fsys, &h));
+	p = argv[2];
+	while(*p){
+		while(*p == '/')
+			p++;
+		if(*p == 0)
+			break;
+		q = strchr(p, '/');
+		if(q){
+			*q = 0;
+			q++;
+		}else
+			q = "";
+		if(debug) fprint(2, "walk %s...", p);
+		x(fsyslookup(fsys, &au, &h, p, &h));
+		p = q;
+	}
+
+	if(debug) fprint(2, "getattr...");
+	x(fsysgetattr(fsys, &au, &h, &attr));
+	printattr(&attr);
+
+	/* do the op */
+	if(strcmp(argv[1], "cat") == 0){
+		switch(attr.type){
+		case Nfs3FileReg:
+			offset = 0;
+			for(;;){
+				x(fsysreadfile(fsys, &au, &h, sizeof buf, offset, &data, &n, &eof));
+				if(n){
+					write(1, data, n);
+					free(data);
+					offset += n;
+				}
+				if(eof)
+					break;
+			}
+			break;
+		case Nfs3FileSymlink:
+			x(fsysreadlink(fsys, &au, &h, &link));
+			print("%s\n", link);
+			break;
+		default:
+			print("cannot cat: not file, not link\n");
+			break;
+		}
+	}else if(strcmp(argv[1], "ls") == 0){
+		/* not implemented */
+	}else if(strcmp(argv[1], "stat") == 0){
+		/* already done */
+	}
+	threadexitsall(nil);
+}
blob - /dev/null
blob + edbcf3e421ddf7d7b483203e0f2f5b8e41a7a3e8 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/disknfs.c
@@ -0,0 +1,126 @@
+#include <u.h>
+#include <libc.h>
+#include <ip.h>
+#include <thread.h>
+#include <sunrpc.h>
+#include <nfs3.h>
+#include <diskfs.h>
+#include "nfs3srv.h"
+
+Disk *disk;
+Fsys *fsys;
+
+void
+usage(void)
+{
+	fprint(2, "usage: disknfs [-RTr] disk\n");
+	threadexitsall("usage");
+}
+
+extern int _threaddebuglevel;
+
+void
+threadmain(int argc, char **argv)
+{
+	char *addr;
+	SunSrv *srv;
+	Channel *nfs3chan;
+	Channel *mountchan;
+	Nfs3Handle h;
+
+	fmtinstall('B', sunrpcfmt);
+	fmtinstall('C', suncallfmt);
+	fmtinstall('H', encodefmt);
+	fmtinstall('I', eipfmt);
+	sunfmtinstall(&nfs3prog);
+	sunfmtinstall(&nfsmount3prog);
+
+	srv = sunsrv();
+	addr = "*";
+
+	ARGBEGIN{
+	case 'R':
+		srv->chatty++;
+		break;
+	case 'T':
+		_threaddebuglevel = 0xFFFFFFFF;
+		break;
+	case 'r':
+		srv->alwaysreject++;
+		break;
+	}ARGEND
+
+	if(argc != 1 && argc != 2)
+		usage();
+
+	if((disk = diskopenfile(argv[0])) == nil)
+		sysfatal("diskopen: %r");
+	if((disk = diskcache(disk, 16384, 256)) == nil)
+		sysfatal("diskcache: %r");
+
+	if((fsys = fsysopen(disk)) == nil)
+		sysfatal("ffsopen: %r");
+
+	nfs3chan = chancreate(sizeof(SunMsg*), 0);
+	mountchan = chancreate(sizeof(SunMsg*), 0);
+
+	if(argc > 1)
+		addr = argv[1];
+	addr = netmkaddr(addr, "udp", "2049");
+
+	if(sunsrvudp(srv, addr) < 0)
+		sysfatal("starting server: %r");
+	sunsrvprog(srv, &nfs3prog, nfs3chan);
+	sunsrvprog(srv, &nfsmount3prog, mountchan);
+
+	sunsrvthreadcreate(srv, nfs3proc, nfs3chan);
+	sunsrvthreadcreate(srv, mount3proc, mountchan);
+
+	fsgetroot(&h);
+	print("mountbackups -h %.*H %s /mountpoint\n", h.len, h.h, addr);
+
+	threadexits(nil);
+}
+
+void
+fsgetroot(Nfs3Handle *h)
+{
+	fsysroot(fsys, h);
+}
+
+Nfs3Status
+fsgetattr(SunAuthUnix *au, Nfs3Handle *h, Nfs3Attr *attr)
+{
+	return fsysgetattr(fsys, au, h, attr);
+}
+
+Nfs3Status
+fslookup(SunAuthUnix *au, Nfs3Handle *h, char *name, Nfs3Handle *nh)
+{
+	return fsyslookup(fsys, au, h, name, nh);
+}
+
+Nfs3Status
+fsaccess(SunAuthUnix *au, Nfs3Handle *h, u32int want, u32int *got, Nfs3Attr *attr)
+{
+	return fsysaccess(fsys, au, h, want, got, attr);
+}
+
+Nfs3Status
+fsreadlink(SunAuthUnix *au, Nfs3Handle *h, char **link)
+{
+	return fsysreadlink(fsys, au, h, link);
+}
+
+Nfs3Status
+fsreadfile(SunAuthUnix *au, Nfs3Handle *h, u32int count, u64int offset, uchar **data, u32int *pcount, u1int *peof)
+{
+	return fsysreadfile(fsys, au, h, count, offset, data, pcount, peof);
+}
+
+Nfs3Status
+fsreaddir(SunAuthUnix *au, Nfs3Handle *h, u32int count, u64int cookie, uchar **data, u32int *pcount, u1int *peof)
+{
+	return fsysreaddir(fsys, au, h, count, cookie, data, pcount, peof);
+}
+
blob - /dev/null
blob + 39836b1269a13e3ba34dd0d80d6a0330f79c07e5 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/hist.c
@@ -0,0 +1,282 @@
+#include	<u.h>
+#include	<libc.h>
+#include	<ctype.h>
+
+#define	MINUTE(x)	((long)(x)*60L)
+#define	HOUR(x)		(MINUTE(x)*60L)
+#define	YEAR(x)		(HOUR(x)*24L*360L)
+
+int	verb;
+int	uflag;
+int	force;
+int	diff;
+int	diffb;
+char*	sflag;
+char *sys;
+
+void	ysearch(char*);
+long	starttime(char*);
+void	lastbefore(ulong, char*, char*, char*);
+char*	prtime(ulong);
+
+int
+main(int argc, char *argv[])
+{
+	int i;
+	
+	sys = sysname();
+	if(strncmp(sys, "amsterdam", 9) == 0)
+		sys = "am";
+	else if(strncmp(sys, "toil", 4) == 0)
+		sys = "toil";
+
+	ARGBEGIN {
+	default:
+		goto usage;
+	case 'v':
+		verb = 1;
+		break;
+	case 'f':
+		force = 1;
+		break;
+	case 'd':
+		diff = 1;
+		break;
+	case 'b':
+		diffb = 1;
+		break;
+	case 's':
+		sflag = ARGF();
+		break;
+	case 'u':
+		uflag = 1;
+		break;
+	} ARGEND
+
+	if(argc == 0) {
+	usage:
+		fprint(2, "usage: hist [-bdfuv] [-s yyyymmdd] files\n");
+		exits(0);
+	}
+
+	for(i=0; i<argc; i++)
+		ysearch(argv[i]);
+	exits(0);
+	return 0;
+}
+
+int
+strprefix(char *a, char *aa)
+{
+	return memcmp(a, aa, strlen(a)) == 0;
+}
+
+void
+ysearch(char *file)
+{
+	char *ndump;
+	char fil[400], buf[500], nbuf[100], pair[2][500], *p;
+	Tm *tm;
+	Dir *dir, *d;
+	ulong otime, dt;
+	int toggle, started, missing;
+
+	started = 0;
+	dir = dirstat(file);
+	if(dir == nil)
+		fprint(2, "history: warning: %s does not exist\n", file);
+	else{
+		print("%s %s %lld\n", prtime(dir->mtime), file, dir->length);
+		started = 1;
+		strcpy(pair[1], file);
+	}
+	free(dir);
+	fil[0] = 0;
+	if(file[0] != '/') {
+		getwd(strchr(fil, 0), 100);
+		strcat(fil, "/");
+	}
+	strcat(fil, file);
+	cleanname(fil);
+
+	sprint(nbuf, "/dump/%s", sys);
+	ndump = nbuf;
+
+	tm = localtime(time(0));
+	sprint(buf, "%s/%.4d/", ndump, tm->year+1900);
+	if(access(buf, AREAD) < 0){
+		print("cannot access %s\n", buf);
+		return;
+	}
+
+	otime = starttime(sflag);
+	toggle = 0;
+	for(;;) {
+		lastbefore(otime, fil, buf, ndump);
+		dir = dirstat(buf);
+		if(dir == nil) {
+			if(!force)
+				return;
+			dir = malloc(sizeof(Dir));
+			nulldir(dir);
+			dir->mtime = otime + 1;
+		}
+		dt = HOUR(12);
+		missing = 0;
+		while(otime <= dir->mtime){
+			if(verb)
+				print("backup %ld, %ld\n", dir->mtime, otime-dt);
+			lastbefore(otime-dt, fil, buf, ndump);
+			d = dirstat(buf);
+			if(d == nil){
+				if(!force)
+					return;
+				if(!missing)
+					print("removed %s\n", buf);
+				missing = 1;
+			}else{
+				free(dir);
+				dir = d;
+			}
+			dt += HOUR(12);
+		}
+		strcpy(pair[toggle], buf);
+		if(diff && started){
+			if(verb)
+				print("diff %s %s\n", pair[toggle^1], pair[toggle]);
+			switch(rfork(RFFDG|RFPROC)){
+			case 0:
+				execlp("diff", "diff", diffb ? "-cb" : "-c", pair[toggle], pair[toggle ^ 1], 0);
+				fprint(2, "can't exec diff: %r\n");
+				exits(0);
+			case -1:
+				fprint(2, "can't fork diff: %r\n");
+				break;
+			default:
+				while(waitpid() != -1)
+					;
+				break;
+			}
+		}
+		print("%s %s %lld\n", prtime(dir->mtime), buf, dir->length);
+		toggle ^= 1;
+		started = 1;
+		otime = dir->mtime;
+		free(dir);
+	}
+}
+
+void
+lastbefore(ulong t, char *f, char *b, char *ndump)
+{
+	Tm *tm;
+	Dir *dir;
+	int vers, try;
+	ulong t0, mtime;
+
+	t0 = t;
+	if(verb)
+		print("%ld lastbefore %s\n", t0, f);
+	mtime = 0;
+	for(try=0; try<10; try++) {
+		tm = localtime(t);
+		sprint(b, "%s/%.4d/%.2d%.2d", ndump,
+			tm->year+1900, tm->mon+1, tm->mday);
+		dir = dirstat(b);
+		if(dir){
+			mtime = dir->mtime;
+			free(dir);
+		}
+		if(dir==nil || mtime > t0) {
+			if(verb)
+				print("%ld earlier %s\n", mtime, b);
+			t -= HOUR(24);
+			continue;
+		}
+		for(vers=0;; vers++) {
+			sprint(b, "%s/%.4d/%.2d%.2d%d", ndump,
+				tm->year+1900, tm->mon+1, tm->mday, vers+1);
+			dir = dirstat(b);
+			if(dir){
+				mtime = dir->mtime;
+				free(dir);
+			}
+			if(dir==nil || mtime > t0)
+				break;
+			if(verb)
+				print("%ld later %s\n", mtime, b);
+		}
+		sprint(b, "%s/%.4d/%.2d%.2d%s", ndump,
+			tm->year+1900, tm->mon+1, tm->mday, f);
+		if(vers)
+			sprint(b, "%s/%.4d/%.2d%.2d%d%s", ndump,
+				tm->year+1900, tm->mon+1, tm->mday, vers, f);
+		return;
+	}
+	strcpy(b, "XXX");	/* error */
+}
+
+char*
+prtime(ulong t)
+{
+	static char buf[100];
+	char *b;
+	Tm *tm;
+
+	if(uflag)
+		tm = gmtime(t);
+	else
+		tm = localtime(t);
+	b = asctime(tm);
+	memcpy(buf, b+4, 24);
+	buf[24] = 0;
+	return buf;
+}
+
+long
+starttime(char *s)
+{
+	Tm *tm;
+	long t, dt;
+	int i, yr, mo, da;
+
+	t = time(0);
+	if(s == 0)
+		return t;
+	for(i=0; s[i]; i++)
+		if(s[i] < '0' || s[i] > '9') {
+			fprint(2, "bad start time: %s\n", s);
+			return t;
+		}
+	if(strlen(s)==6){
+		yr = (s[0]-'0')*10 + s[1]-'0';
+		mo = (s[2]-'0')*10 + s[3]-'0' - 1;
+		da = (s[4]-'0')*10 + s[5]-'0';
+		if(yr < 70)
+			yr += 100;
+	}else if(strlen(s)==8){
+		yr = (((s[0]-'0')*10 + s[1]-'0')*10 + s[2]-'0')*10 + s[3]-'0';
+		yr -= 1900;
+		mo = (s[4]-'0')*10 + s[5]-'0' - 1;
+		da = (s[6]-'0')*10 + s[7]-'0';
+	}else{
+		fprint(2, "bad start time: %s\n", s);
+		return t;
+	}
+	t = 0;
+	dt = YEAR(10);
+	for(i=0; i<50; i++) {
+		tm = localtime(t+dt);
+		if(yr > tm->year ||
+		  (yr == tm->year && mo > tm->mon) ||
+		  (yr == tm->year && mo == tm->mon) && da > tm->mday) {
+			t += dt;
+			continue;
+		}
+		dt /= 2;
+		if(dt == 0)
+			break;
+	}
+	t += HOUR(12);	/* .5 day to get to noon of argument */
+	return t;
+}
blob - /dev/null
blob + 6f0aba7ee687363000007838a288066bc0b177ad (mode 644)
--- /dev/null
+++ src/cmd/vbackup/mkfile
@@ -0,0 +1,28 @@
+<$PLAN9/src/mkhdr
+CC=9c
+
+TARG=\
+	disknfs\
+	vbackup\
+	vcat\
+	vmount0\
+	vnfs\
+
+OFILES=util.$O
+HFILES=$PLAN9/include/diskfs.h
+
+<$PLAN9/src/mkmany
+
+disknfs.$O: nfs3srv.h
+mount-%.$O: mountnfs.h
+nfs3srv.$O: nfs3srv.h
+queue.$O: queue.h
+vbackup.$O: queue.h
+vmount0.$O: mountnfs.h
+vnfs.$O: nfs3srv.h
+
+$O.disknfs: nfs3srv.$O
+$O.vbackup: vbackup.$O queue.$O
+$O.vmount0: vmount0.$O mount-$SYSNAME.$O
+$O.vnfs: nfs3srv.$O
+
blob - /dev/null
blob + ac5cab2a9e03f6e1205b0935c280c490f50aa114 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/mount-FreeBSD.c
@@ -0,0 +1,52 @@
+#include <u.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netdb.h>
+#include <sys/stat.h>
+#include <sys/param.h>
+#include <sys/mount.h>
+#include <sys/syslog.h>
+#include <rpc/rpc.h>
+#include <rpc/pmap_clnt.h>
+#include <rpc/pmap_prot.h>
+#include <nfs/rpcv2.h>
+#include <nfs/nfsproto.h>
+#include <nfs/nfs.h>
+#include <libc.h>
+#include "mountnfs.h"
+
+void
+mountnfs(int proto, struct sockaddr_in *sa,
+	uchar *handle, int nhandle, char *mtpt)
+{
+	int mflag;
+	struct nfs_args na;
+
+	memset(&na, 0, sizeof na);
+	na.version = NFS_ARGSVERSION;
+	na.addr = (struct sockaddr*)sa;
+	na.addrlen = sizeof *sa;
+	na.sotype = proto;
+	na.proto = (proto == SOCK_STREAM) ? IPPROTO_TCP : IPPROTO_UDP;
+	na.fh = handle;
+	na.fhsize = nhandle;
+	na.flags = NFSMNT_RESVPORT|NFSMNT_NFSV3|NFSMNT_INT;
+	na.wsize = NFS_WSIZE;
+	na.rsize = NFS_RSIZE;
+	na.readdirsize = NFS_READDIRSIZE;
+	na.timeo = 2;
+	na.retrans = NFS_RETRANS;
+	na.maxgrouplist = NFS_MAXGRPS;
+	na.readahead = 0;
+	na.leaseterm = 0;
+	na.deadthresh = 0;
+	na.hostname = "backup";
+	na.acregmin = 60;
+	na.acregmax = 600;
+	na.acdirmin = 60;
+	na.acdirmax = 600;
+
+	mflag = MNT_RDONLY|MNT_NOSUID|MNT_NOATIME|MNT_NODEV;
+	if(mount("nfs", mtpt, mflag, &na) < 0)
+		sysfatal("mount: %r");
+}
blob - /dev/null
blob + 39b96a31f1df43c78d85d07333cbb39a54b14345 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/mount-Linux.c
@@ -0,0 +1,58 @@
+#include <u.h>
+#include <sys/socket.h>
+#include <sys/mount.h>
+#ifdef __Linux24__
+#	define __KERNEL__
+#	include <linux/nfs.h>
+#	undef __KERNEL__
+#else
+#	include <linux/nfs.h>
+#endif
+#include <linux/nfs2.h>
+#include <linux/nfs_mount.h>
+#include <libc.h>
+#include "mountnfs.h"
+
+void
+mountnfs(int proto, struct sockaddr_in *sa, uchar *handle, int nhandle, char *mtpt)
+{
+	int mflag, fd;
+	struct nfs_mount_data nfs;
+
+	fd = socket(AF_INET, proto, proto==SOCK_STREAM ? IPPROTO_TCP : IPPROTO_UDP);
+	if(fd < 0)
+		sysfatal("socket: %r");
+
+	memset(&nfs, 0, sizeof nfs);
+	nfs.version = NFS_MOUNT_VERSION;
+	nfs.fd = fd;
+	nfs.flags =
+		NFS_MOUNT_SOFT|
+		NFS_MOUNT_INTR|
+		NFS_MOUNT_NOAC|
+		NFS_MOUNT_NOCTO|
+		NFS_MOUNT_VER3|
+		NFS_MOUNT_NONLM;
+	if(proto==SOCK_STREAM)
+		nfs.flags |= NFS_MOUNT_TCP;
+	nfs.rsize = 8192;
+	nfs.wsize = 8192;
+	nfs.timeo = 120;
+	nfs.retrans = 2;
+	nfs.acregmin = 60;
+	nfs.acregmax = 600;
+	nfs.acdirmin = 60;
+	nfs.acdirmax = 600;
+	nfs.addr = *sa;
+	strcpy(nfs.hostname, "backup");
+	nfs.namlen = 1024;
+	nfs.bsize = 8192;
+	memcpy(nfs.root.data, handle, nhandle);
+	nfs.root.size = nhandle;
+	mflag = MS_NOATIME|MS_NODEV|MS_NODIRATIME|
+		MS_NOEXEC|MS_NOSUID|MS_RDONLY;
+
+	if(mount("backup:/", mtpt, "nfs", mflag, &nfs) < 0)
+		sysfatal("mount: %r");
+}
+
blob - /dev/null
blob + db8e1e5bc5e5e1d45a324be384081b52cfb0bd0a (mode 644)
--- /dev/null
+++ src/cmd/vbackup/mount-none.c
@@ -0,0 +1,12 @@
+#include <u.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <libc.h>
+#include "mountnfs.h"
+
+void
+mountnfs(int proto, struct sockaddr_in *addr, uchar *handle, int hlen, char *mtpt)
+{
+	sysfatal("mountnfs not implemented");
+}
+
blob - /dev/null
blob + b8aab4cb6f06c0b31bcc7d503e424f0e0be69542 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/mountnfs.h
@@ -0,0 +1 @@
+void mountnfs(int proto, struct sockaddr_in*, uchar*, int, char*);
blob - /dev/null
blob + b2cdd9cfe321c89541676289c8f4865c1f18173b (mode 644)
--- /dev/null
+++ src/cmd/vbackup/nfs3srv.c
@@ -0,0 +1,428 @@
+/*
+ * Simple read-only NFS v3 server.
+ * Runs every request in its own thread.
+ * Expects client to provide the fsxxx routines in nfs3srv.h.
+ */
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <sunrpc.h>
+#include <nfs3.h>
+#include "nfs3srv.h"
+
+static SunStatus
+authunixunpack(SunRpc *rpc, SunAuthUnix *au)
+{
+	uchar *p, *ep;
+	SunAuthInfo *ai;
+
+	ai = &rpc->cred;
+	if(ai->flavor != SunAuthSys)
+		return SunAuthTooWeak;
+	p = ai->data;
+	ep = p+ai->ndata;
+	if(sunauthunixunpack(p, ep, &p, au) < 0)
+		return SunGarbageArgs;
+	if(au->uid == 0)
+		au->uid = -1;
+	if(au->gid == 0)
+		au->gid = -1;
+
+	return SunSuccess;
+}
+
+static int
+rnull(SunMsg *m)
+{
+	NfsMount3RNull rx;
+
+	memset(&rx, 0, sizeof rx);
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+rmnt(SunMsg *m)
+{
+	Nfs3Handle nh;
+	NfsMount3RMnt rx;
+	SunAuthUnix au;
+	int ok;
+
+	if((ok = authunixunpack(&m->rpc, &au)) != SunSuccess)
+		return sunmsgreplyerror(m, ok);
+
+	/* ignore file system path and return the dump tree */
+
+	memset(&rx, 0, sizeof rx);
+	rx.nauth = 0;
+	rx.status = 0;
+	memset(&nh, 0, sizeof nh);
+	fsgetroot(&nh);
+	rx.handle = nh.h;
+	rx.len = nh.len;
+
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+rumnt(SunMsg *m)
+{
+	NfsMount3RUmnt rx;
+
+	/* ignore */
+	
+	memset(&rx, 0, sizeof rx);
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+rumntall(SunMsg *m)
+{
+	NfsMount3RUmntall rx;
+
+	/* ignore */
+
+	memset(&rx, 0, sizeof rx);
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+rexport(SunMsg *m)
+{
+	NfsMount3RExport rx;
+
+	/* ignore */
+
+	memset(&rx, 0, sizeof rx);
+	rx.count = 0;
+	return sunmsgreply(m, &rx.call);
+}
+
+static void
+rmount3(void *v)
+{
+	SunMsg *m;
+
+	m = v;
+	switch(m->call->type){
+	default:
+		sunmsgreplyerror(m, SunProcUnavail);
+	case NfsMount3CallTNull:
+		rnull(m);
+		break;
+	case NfsMount3CallTMnt:
+		rmnt(m);
+		break;
+	case NfsMount3CallTDump:
+		rmnt(m);
+		break;
+	case NfsMount3CallTUmnt:
+		rumnt(m);
+		break;
+	case NfsMount3CallTUmntall:
+		rumntall(m);
+		break;
+	case NfsMount3CallTExport:
+		rexport(m);
+		break;
+	}
+}
+
+void
+mount3proc(void *v)
+{
+	Channel *c;
+	SunMsg *m;
+
+	threadsetname("mount1");
+	c = v;
+	while((m=recvp(c)) != nil)
+		threadcreate(rmount3, m, SunStackSize);
+}
+
+static int
+senderror(SunMsg *m, SunCall *rc, Nfs3Status status)
+{
+	/* knows that status is first field in all replies */
+	((Nfs3RGetattr*)rc)->status = status;
+	return sunmsgreply(m, rc);
+}
+
+static int
+rnull0(SunMsg *m)
+{
+	Nfs3RNull rx;
+
+	memset(&rx, 0, sizeof rx);
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+rgetattr(SunMsg *m)
+{
+	Nfs3TGetattr *tx = (Nfs3TGetattr*)m->call;
+	Nfs3RGetattr rx;
+	SunAuthUnix au;
+	int ok;
+
+	if((ok = authunixunpack(&m->rpc, &au)) != SunSuccess)
+		return sunmsgreplyerror(m, ok);
+
+	memset(&rx, 0, sizeof rx);
+	rx.status = fsgetattr(&au, &tx->handle, &rx.attr);
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+rlookup(SunMsg *m)
+{
+	Nfs3TLookup *tx = (Nfs3TLookup*)m->call;
+	Nfs3RLookup rx;
+	SunAuthUnix au;
+	int ok;
+
+	if((ok = authunixunpack(&m->rpc, &au)) != SunSuccess)
+		return sunmsgreplyerror(m, ok);
+
+	memset(&rx, 0, sizeof rx);
+	rx.status = fsgetattr(&au, &tx->handle, &rx.dirAttr);
+	if(rx.status != Nfs3Ok)
+		return sunmsgreply(m, &rx.call);
+	rx.haveDirAttr = 1;
+	rx.status = fslookup(&au, &tx->handle, tx->name, &rx.handle);
+	if(rx.status != Nfs3Ok)
+		return sunmsgreply(m, &rx.call);
+	rx.status = fsgetattr(&au, &rx.handle, &rx.attr);
+	if(rx.status != Nfs3Ok)
+		return sunmsgreply(m, &rx.call);
+	rx.haveAttr = 1;
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+raccess(SunMsg *m)
+{
+	Nfs3TAccess *tx = (Nfs3TAccess*)m->call;
+	Nfs3RAccess rx;
+	SunAuthUnix au;
+	int ok;
+
+	if((ok = authunixunpack(&m->rpc, &au)) != SunSuccess)
+		return sunmsgreplyerror(m, ok);
+
+	memset(&rx, 0, sizeof rx);
+	rx.haveAttr = 1;
+	rx.status = fsaccess(&au, &tx->handle, tx->access, &rx.access, &rx.attr);
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+rreadlink(SunMsg *m)
+{
+	Nfs3RReadlink rx;
+	Nfs3TReadlink *tx = (Nfs3TReadlink*)m->call;
+	SunAuthUnix au;
+	int ok;
+
+	if((ok = authunixunpack(&m->rpc, &au)) != SunSuccess)
+		return sunmsgreplyerror(m, ok);
+
+	memset(&rx, 0, sizeof rx);
+	rx.haveAttr = 0;
+	rx.data = nil;
+	rx.status = fsreadlink(&au, &tx->handle, &rx.data);
+	sunmsgreply(m, &rx.call);
+	free(rx.data);
+	return 0;
+}
+
+static int
+rread(SunMsg *m)
+{
+	Nfs3TRead *tx = (Nfs3TRead*)m->call;
+	Nfs3RRead rx;
+	SunAuthUnix au;
+	int ok;
+
+	if((ok = authunixunpack(&m->rpc, &au)) != SunSuccess)
+		return sunmsgreplyerror(m, ok);
+
+	memset(&rx, 0, sizeof rx);
+	rx.haveAttr = 0;
+	rx.data = nil;
+	rx.status = fsreadfile(&au, &tx->handle, tx->count, tx->offset, &rx.data, &rx.count, &rx.eof);
+	if(rx.status == Nfs3Ok)
+		rx.ndata = rx.count;
+
+	sunmsgreply(m, &rx.call);
+	free(rx.data);
+	return 0;
+}
+
+static int
+rreaddir(SunMsg *m)
+{
+	Nfs3TReadDir *tx = (Nfs3TReadDir*)m->call;
+	Nfs3RReadDir rx;
+	SunAuthUnix au;
+	int ok;
+
+	if((ok = authunixunpack(&m->rpc, &au)) != SunSuccess)
+		return sunmsgreplyerror(m, ok);
+
+	memset(&rx, 0, sizeof rx);
+	rx.status = fsreaddir(&au, &tx->handle, tx->count, tx->cookie, &rx.data, &rx.count, &rx.eof);
+	sunmsgreply(m, &rx.call);
+	free(rx.data);
+	return 0;
+}
+
+static int
+rreaddirplus(SunMsg *m)
+{
+	Nfs3RReadDirPlus rx;
+
+	memset(&rx, 0, sizeof rx);
+	rx.status = Nfs3ErrNotSupp;
+	sunmsgreply(m, &rx.call);
+	return 0;
+}
+
+static int
+rfsstat(SunMsg *m)
+{
+	Nfs3RFsStat rx;
+
+	/* just make something up */
+	memset(&rx, 0, sizeof rx);
+	rx.status = Nfs3Ok;
+	rx.haveAttr = 0;
+	rx.totalBytes = 1000000000;
+	rx.freeBytes = 0;
+	rx.availBytes = 0;
+	rx.totalFiles = 100000;
+	rx.freeFiles = 0;
+	rx.availFiles = 0;
+	rx.invarSec = 0;
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+rfsinfo(SunMsg *m)
+{
+	Nfs3RFsInfo rx;
+
+	/* just make something up */
+	memset(&rx, 0, sizeof rx);
+	rx.status = Nfs3Ok;
+	rx.haveAttr = 0;
+	rx.readMax = MaxDataSize;
+	rx.readPref = MaxDataSize;
+	rx.readMult = MaxDataSize;
+	rx.writeMax = MaxDataSize;
+	rx.writePref = MaxDataSize;
+	rx.writeMult = MaxDataSize;
+	rx.readDirPref = MaxDataSize;
+	rx.maxFileSize = 1LL<<60;
+	rx.timePrec.sec = 1;
+	rx.timePrec.nsec = 0;
+	rx.flags = Nfs3FsHomogeneous|Nfs3FsCanSetTime;
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+rpathconf(SunMsg *m)
+{
+	Nfs3RPathconf rx;
+
+	memset(&rx, 0, sizeof rx);
+	rx.status = Nfs3Ok;
+	rx.haveAttr = 0;
+	rx.maxLink = 1;
+	rx.maxName = 1024;
+	rx.noTrunc = 1;
+	rx.chownRestricted = 0;
+	rx.caseInsensitive = 0;
+	rx.casePreserving = 1;
+	return sunmsgreply(m, &rx.call);
+}
+
+static int
+rrofs(SunMsg *m)
+{
+	uchar buf[512];	/* clumsy hack*/
+
+	memset(buf, 0, sizeof buf);
+	return senderror(m, (SunCall*)buf, Nfs3ErrRoFs);
+}
+	
+
+static void
+rnfs3(void *v)
+{
+	SunMsg *m;
+
+	m = v;
+	switch(m->call->type){
+	default:
+		abort();
+	case Nfs3CallTNull:
+		rnull0(m);
+		break;
+	case Nfs3CallTGetattr:
+		rgetattr(m);
+		break;
+	case Nfs3CallTLookup:
+		rlookup(m);
+		break;
+	case Nfs3CallTAccess:
+		raccess(m);
+		break;
+	case Nfs3CallTReadlink:
+		rreadlink(m);
+		break;
+	case Nfs3CallTRead:
+		rread(m);
+		break;
+	case Nfs3CallTReadDir:
+		rreaddir(m);
+		break;
+	case Nfs3CallTReadDirPlus:
+		rreaddirplus(m);
+		break;
+	case Nfs3CallTFsStat:
+		rfsstat(m);
+		break;
+	case Nfs3CallTFsInfo:
+		rfsinfo(m);
+		break;
+	case Nfs3CallTPathconf:
+		rpathconf(m);
+		break;
+	case Nfs3CallTSetattr:
+	case Nfs3CallTWrite:
+	case Nfs3CallTCreate:
+	case Nfs3CallTMkdir:
+	case Nfs3CallTSymlink:
+	case Nfs3CallTMknod:
+	case Nfs3CallTRemove:
+	case Nfs3CallTRmdir:
+	case Nfs3CallTLink:
+	case Nfs3CallTCommit:
+		rrofs(m);
+		break;
+	}
+}
+
+void
+nfs3proc(void *v)
+{
+	Channel *c;
+	SunMsg *m;
+
+	c = v;
+	threadsetname("nfs3");
+	while((m = recvp(c)) != nil)
+		threadcreate(rnfs3, m, SunStackSize);
+}
+
blob - /dev/null
blob + 329db782d5a14214153f012ae0dcc248f9ed5b60 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/nfs3srv.h
@@ -0,0 +1,16 @@
+void		fsgetroot(Nfs3Handle*);
+Nfs3Status	fsgetattr(SunAuthUnix*, Nfs3Handle*, Nfs3Attr*);
+Nfs3Status	fslookup(SunAuthUnix*, Nfs3Handle*, char*, Nfs3Handle*);
+Nfs3Status	fsaccess(SunAuthUnix*, Nfs3Handle*, u32int, u32int*, Nfs3Attr*);
+Nfs3Status	fsreadlink(SunAuthUnix*, Nfs3Handle*, char**);
+Nfs3Status	fsreadfile(SunAuthUnix*, Nfs3Handle*, u32int, u64int, uchar**, u32int*, u1int*);
+Nfs3Status	fsreaddir(SunAuthUnix*, Nfs3Handle*, u32int, u64int, uchar**, u32int*, u1int*);
+
+extern void nfs3proc(void*);
+extern void mount3proc(void*);
+
+enum
+{
+	MaxDataSize = 8192,
+};
+
blob - /dev/null
blob + 91fa221d399afd070b6e4499f1c27b769a32552a (mode 644)
--- /dev/null
+++ src/cmd/vbackup/queue.c
@@ -0,0 +1,64 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <venti.h>
+#include <diskfs.h>
+#include "queue.h"
+
+Queue*
+qalloc(void)
+{
+	Queue *q;
+
+	q = vtmallocz(sizeof(Queue));
+	q->r.l = &q->lk;
+	return q;
+}
+
+Block*
+qread(Queue *q, u32int *pbno)
+{
+	Block *db;
+	u32int bno;
+
+	qlock(&q->lk);
+	while(q->nel == 0 && !q->closed)
+		rsleep(&q->r);
+	if(q->nel == 0 && q->closed){
+		qunlock(&q->lk);
+		return nil;
+	}
+	db = q->el[q->ri].db;
+	bno = q->el[q->ri].bno;
+	if(++q->ri == MAXQ)
+		q->ri = 0;
+	if(q->nel-- == MAXQ/2)
+		rwakeup(&q->r);
+	qunlock(&q->lk);
+	*pbno = bno;
+	return db;
+}
+
+void
+qwrite(Queue *q, Block *db, u32int bno)
+{
+	qlock(&q->lk);
+	while(q->nel == MAXQ)
+		rsleep(&q->r);
+	q->el[q->wi].db = db;
+	q->el[q->wi].bno = bno;
+	if(++q->wi == MAXQ)
+		q->wi = 0;
+	if(q->nel++ == MAXQ/2)
+		rwakeup(&q->r);
+	qunlock(&q->lk);
+}
+
+void
+qclose(Queue *q)
+{
+	qlock(&q->lk);
+	q->closed = 1;
+	rwakeup(&q->r);
+	qunlock(&q->lk);
+}
blob - /dev/null
blob + 053346ad88319adc0689872a3f14582f436762de (mode 644)
--- /dev/null
+++ src/cmd/vbackup/queue.h
@@ -0,0 +1,22 @@
+enum
+{
+	MAXQ = 256,
+};
+
+typedef struct Queue Queue;
+struct Queue
+{
+	struct {
+		Block *db;
+		u32int bno;
+	} el[MAXQ];
+	int ri, wi, nel, closed;
+
+	QLock lk;
+	Rendez r;
+};
+
+Queue	*qalloc(void);
+void	qclose(Queue*);
+Block	*qread(Queue*, u32int*);
+void	qwrite(Queue*, Block*, u32int);
blob - /dev/null
blob + 0a46a7a006952958ad86d1fcb6e031bb59531fd0 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/util.c
@@ -0,0 +1,23 @@
+#include <u.h>
+#include <libc.h>
+#include <diskfs.h>
+
+void*
+emalloc(ulong n)
+{
+	void *v;
+
+	v = mallocz(n, 1);
+	if(v == nil)
+		abort();
+	return v;
+}
+
+void*
+erealloc(void *v, ulong n)
+{
+	v = realloc(v, n);
+	if(v == nil)
+		abort();
+	return v;
+}
blob - /dev/null
blob + 4fdaf3e4d2fea0806bd00f276f170af7c1ea2c3d (mode 644)
--- /dev/null
+++ src/cmd/vbackup/vbackup.c
@@ -0,0 +1,524 @@
+/*
+ * vbackup [-Dnv] fspartition [score]
+ *
+ * Copy a file system to a disk image stored on Venti.
+ * Prints a vnfs config line for the copied image.
+ *
+ *	-D	print debugging
+ *	-m	set mount name
+ *	-n	nop -- don't actually write blocks
+ *	-s	print status updates
+ *	-v	print debugging trace
+ *	-w	write parallelism
+ * 
+ * If score is given on the command line, it should be the
+ * score from a previous vbackup on this fspartition.
+ * In this mode, only the new blocks are stored to Venti.
+ * The result is still a complete image, but requires many
+ * fewer Venti writes in the common case.
+ *
+ * This program is structured as three processes connected
+ * by buffered queues:
+ *
+ * 	fsysproc | cmpproc | ventiproc
+ * 
+ * Fsysproc reads the disk and queues the blocks.
+ * Cmpproc compares the blocks against the SHA1 hashes
+ * in the old image, if any.  It discards the unchanged blocks
+ * and queues the changed ones.  Ventiproc writes blocks to Venti.
+ * 
+ * There is a fourth proc, statusproc, which prints status
+ * updates about how the various procs are progressing.
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <libsec.h>
+#include <venti.h>
+#include <diskfs.h>
+#include "queue.h"
+
+enum
+{
+	STACK = 8192,
+};
+
+typedef struct WriteReq WriteReq;
+struct WriteReq
+{
+	Packet *p;
+	uint type;
+};
+
+Biobuf	bscores;		/* biobuf filled with block scores */
+int		debug;		/* debugging flag (not used) */
+Disk*	disk;			/* disk being backed up */
+RWLock	endlk;		/* silly synchonization */
+int		errors;		/* are we exiting with an error status? */
+int		fsscanblock;	/* last block scanned */
+Fsys*	fsys;			/* file system being backed up */
+int		nchange;		/* number of changed blocks */
+int		nop;			/* don't actually send blocks to venti */
+int		nwrite;		/* number of write-behind threads */
+Queue*	qcmp;		/* queue fsys->cmp */
+Queue*	qventi;		/* queue cmp->venti */
+int		statustime;	/* print status every _ seconds */
+int		verbose;		/* print extra stuff */
+VtFile*	vfile;			/* venti file being written */
+Channel*	writechan;	/* chan(WriteReq) */
+VtConn*	z;			/* connection to venti */
+VtCache*	zcache;		/* cache of venti blocks */
+uchar*	zero;			/* blocksize zero bytes */
+
+extern	int	ncopy, nread, nwrite;	/* hidden in libventi */
+
+void		cmpproc(void*);
+void		fsysproc(void*);
+void		statusproc(void*);
+void		ventiproc(void*);
+int		timefmt(Fmt*);
+char*	mountplace(char *dev);
+
+void
+usage(void)
+{
+	fprint(2, "usage: vbackup [-DVnv] [-m mtpt] [-s secs] [-w n] disk [score]\n");
+	threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	char *pref, *mountname;
+	uchar score[VtScoreSize], prev[VtScoreSize];
+	int i, fd, csize;
+	vlong bsize;
+	Tm tm;
+	VtEntry e;
+	VtBlock *b;
+	VtCache *c;
+	VtRoot root;
+	char *tmp, *tmpnam;
+
+	fmtinstall('F', vtfcallfmt);
+	fmtinstall('H', encodefmt);
+	fmtinstall('T', timefmt);
+	fmtinstall('V', vtscorefmt);
+
+	mountname = sysname();
+	ARGBEGIN{
+	default:
+		usage();
+		break;
+	case 'D':
+		debug++;
+		break;
+	case 'V':
+		chattyventi = 1;
+		break;
+	case 'm':
+		mountname = EARGF(usage());
+		break;
+	case 'n':
+		nop = 1;
+		break;
+	case 's':
+		statustime = atoi(EARGF(usage()));
+		break;
+	case 'v':
+		verbose = 1;
+		break;
+	case 'w':
+		nwrite = atoi(EARGF(usage()));
+		break;
+	}ARGEND
+
+	if(argc != 1 && argc != 2)
+		usage();
+
+	if(statustime)
+		print("# %T vbackup %s %s\n", argv[0], argc>=2 ? argv[1] : "");
+	/*
+	 * open fs
+	 */
+	if((disk = diskopenfile(argv[0])) == nil)
+		sysfatal("diskopen: %r");
+	if((disk = diskcache(disk, 16384, 2*MAXQ+16)) == nil)
+		sysfatal("diskcache: %r");
+	if((fsys = fsysopen(disk)) == nil)
+		sysfatal("ffsopen: %r");
+
+	/*
+	 * connect to venti
+	 */
+	if((z = vtdial(nil)) == nil)
+		sysfatal("vtdial: %r");
+	if(vtconnect(z) < 0)
+		sysfatal("vtconnect: %r");
+
+	/*
+	 * set up venti block cache
+	 */
+	zero = vtmallocz(fsys->blocksize);
+	bsize = fsys->blocksize;
+	csize = 50;	/* plenty; could probably do with 5 */
+
+	if(verbose)
+		fprint(2, "cache %d blocks\n", csize);
+	c = vtcachealloc(z, bsize, csize, VtORDWR);
+	zcache = c;
+
+	/*
+	 * parse starting score
+	 */
+	memset(prev, 0, sizeof prev);
+	if(argc == 1){
+		vfile = vtfilecreateroot(c, (fsys->blocksize/VtScoreSize)*VtScoreSize,
+			fsys->blocksize, VtDataType);
+		if(vfile == nil)
+			sysfatal("vtfilecreateroot: %r");
+		vtfilelock(vfile, VtORDWR);
+		if(vtfilewrite(vfile, zero, 1, bsize*fsys->nblock-1) != 1)
+			sysfatal("vtfilewrite: %r");
+		if(vtfileflush(vfile) < 0)
+			sysfatal("vtfileflush: %r");
+	}else{
+		if(vtparsescore(argv[1], &pref, score) < 0)
+			sysfatal("bad score: %r");
+		if(pref!=nil && strcmp(pref, fsys->type) != 0)
+			sysfatal("score is %s but fsys is %s", pref, fsys->type);
+		b = vtcacheglobal(c, score, VtRootType);
+		if(b){
+			if(vtrootunpack(&root, b->data) < 0)
+				sysfatal("bad root: %r");
+			if(strcmp(root.type, fsys->type) != 0)
+				sysfatal("root is %s but fsys is %s", root.type, fsys->type);
+			memmove(prev, score, VtScoreSize);
+			memmove(score, root.score, VtScoreSize);
+			vtblockput(b);
+		}
+		b = vtcacheglobal(c, score, VtDirType);
+		if(b == nil)
+			sysfatal("vtcacheglobal %V: %r", score);
+		if(vtentryunpack(&e, b->data, 0) < 0)
+			sysfatal("%V: vtentryunpack failed", score);
+		if(verbose)
+			fprint(2, "entry: size %llud psize %d dsize %d\n",
+				e.size, e.psize, e.dsize);
+		vtblockput(b);
+		if((vfile = vtfileopenroot(c, &e)) == nil)
+			sysfatal("vtfileopenroot: %r");
+		vtfilelock(vfile, VtORDWR);
+		if(e.dsize != bsize)
+			sysfatal("file system block sizes don't match %d %lld", e.dsize, bsize);
+		if(e.size != fsys->nblock*bsize)
+			sysfatal("file system block counts don't match %lld %lld", e.size, fsys->nblock*bsize);
+	}
+
+	/*
+	 * write scores of blocks into temporary file
+	 */
+	if((tmp = getenv("TMP")) != nil){
+		/* okay, good */
+	}else if(access("/var/tmp", 0) >= 0)
+		tmp = "/var/tmp";
+	else
+		tmp = "/tmp";
+	tmpnam = smprint("%s/vbackup.XXXXXX", tmp);
+	if(tmpnam == nil)
+		sysfatal("smprint: %r");
+
+	if((fd = opentemp(tmpnam)) < 0)
+		sysfatal("opentemp %s: %r", tmpnam);
+	if(statustime)
+		print("# %T reading scores into %s\n", tmpnam);
+	if(verbose)
+		fprint(2, "read scores into %s...\n", tmpnam);
+
+	Binit(&bscores, fd, OWRITE);
+	for(i=0; i<fsys->nblock; i++){
+		if(vtfileblockscore(vfile, i, score) < 0)
+			sysfatal("vtfileblockhash %d: %r", i);
+		if(Bwrite(&bscores, score, VtScoreSize) != VtScoreSize)
+			sysfatal("Bwrite: %r");
+	}
+	Bterm(&bscores);
+	vtfileunlock(vfile);
+
+	/*
+	 * prep scores for rereading
+	 */
+	seek(fd, 0, 0);
+	Binit(&bscores, fd, OREAD);
+
+	/*
+	 * start the main processes 
+	 */
+	if(statustime)
+		print("# %T starting procs\n");
+	qcmp = qalloc();
+	qventi = qalloc();
+
+	rlock(&endlk);
+	proccreate(fsysproc, nil, STACK);
+	rlock(&endlk);
+	proccreate(ventiproc, nil, STACK);
+	rlock(&endlk);
+	proccreate(cmpproc, nil, STACK);
+	if(statustime){
+		rlock(&endlk);
+		proccreate(statusproc, nil, STACK);
+	}
+
+	/*
+	 * wait for processes to finish
+	 */
+	wlock(&endlk);
+
+	if(statustime)
+		print("# %T procs exited: %d blocks changed, %d read, %d written, %d copied\n",
+			nchange, nread, nwrite, ncopy);
+
+	/*
+	 * prepare root block
+	 */
+	vtfilelock(vfile, -1);
+	if(vtfileflush(vfile) < 0)
+		sysfatal("vtfileflush: %r");
+	if(vtfilegetentry(vfile, &e) < 0)
+		sysfatal("vtfilegetentry: %r");
+
+	b = vtcacheallocblock(c, VtDirType);
+	if(b == nil)
+		sysfatal("vtcacheallocblock: %r");
+	vtentrypack(&e, b->data, 0);
+	if(vtblockwrite(b) < 0)
+		sysfatal("vtblockwrite: %r");
+
+	memset(&root, 0, sizeof root);
+	strecpy(root.name, root.name+sizeof root.name, argv[0]);
+	strecpy(root.type, root.type+sizeof root.type, fsys->type);
+	memmove(root.score, b->score, VtScoreSize);
+	root.blocksize = fsys->blocksize;
+	memmove(root.prev, prev, VtScoreSize);
+	vtblockput(b);
+
+	b = vtcacheallocblock(c, VtRootType);
+	if(b == nil)
+		sysfatal("vtcacheallocblock: %r");
+	vtrootpack(&root, b->data);
+	if(vtblockwrite(b) < 0)
+		sysfatal("vtblockwrite: %r");
+
+	tm = *localtime(time(0));
+	tm.year += 1900;
+	tm.mon++;
+	print("mount /%s/%d/%02d%02d%s %s:%V %d/%02d%02d/%02d%02d\n",
+		mountname, tm.year, tm.mon, tm.mday, 
+		mountplace(argv[0]),
+		root.type, b->score,
+		tm.year, tm.mon, tm.mday, tm.hour, tm.min);
+	print("# %T %s %s:%V\n", argv[0], root.type, b->score);
+	if(statustime)
+		print("# %T venti sync\n");
+	vtblockput(b);
+	if(vtsync(z) < 0)
+		sysfatal("vtsync: %r");
+	if(statustime)
+		print("# %T synced\n");
+	threadexitsall(nil);
+}
+
+void
+fsysproc(void *dummy)
+{
+	u32int i;
+	Block *db;
+
+	USED(dummy);
+
+	for(i=0; i<fsys->nblock; i++){
+		fsscanblock = i;
+		if((db = fsysreadblock(fsys, i)) != nil)
+			qwrite(qcmp, db, i);
+	}
+	fsscanblock = i;
+	qclose(qcmp);
+
+	print("# %T fsys proc exiting\n");
+	runlock(&endlk);
+}
+
+void
+cmpproc(void *dummy)
+{
+	uchar *data;
+	Block *db;
+	u32int bno, bsize;
+	uchar score[VtScoreSize];
+	uchar score1[VtScoreSize];
+
+	USED(dummy);
+
+	bsize = fsys->blocksize;
+	while((db = qread(qcmp, &bno)) != nil){
+		data = db->data;
+		sha1(data, vtzerotruncate(VtDataType, data, bsize), score, nil);
+		if(Bseek(&bscores, (vlong)bno*VtScoreSize, 0) < 0)
+			sysfatal("cmpproc Bseek: %r");
+		if(Bread(&bscores, score1, VtScoreSize) != VtScoreSize)
+			sysfatal("cmpproc Bread: %r");
+		if(memcmp(score, score1, VtScoreSize) != 0){
+			nchange++;
+			if(verbose)
+				print("# block %ud: old %V new %V\n", bno, score1, score);
+			qwrite(qventi, db, bno);
+		}else
+			blockput(db);
+	}
+	qclose(qventi);
+	runlock(&endlk);
+}
+
+void
+writethread(void *v)
+{
+	WriteReq wr;
+	uchar score[VtScoreSize];
+
+	USED(v);
+
+	while(recv(writechan, &wr) == 1){
+		if(wr.p == nil)
+			break;
+		if(vtwritepacket(z, score, wr.type, wr.p) < 0)
+			sysfatal("vtwritepacket: %r");
+	}
+}
+
+int
+myvtwrite(VtConn *z, uchar score[VtScoreSize], uint type, uchar *buf, int n)
+{
+	WriteReq wr;
+
+	if(nwrite == 0)
+		return vtwrite(z, score, type, buf, n);
+
+	wr.p = packetalloc();
+	packetappend(wr.p, buf, n);
+	packetsha1(wr.p, score);
+	wr.type = type;
+	send(writechan, &wr);
+	return 0;
+}
+
+void
+ventiproc(void *dummy)
+{
+	int i;
+	Block *db;
+	u32int bno;
+	u64int bsize;
+
+	USED(dummy);
+
+	proccreate(vtsendproc, z, STACK);
+	proccreate(vtrecvproc, z, STACK);
+
+	writechan = chancreate(sizeof(WriteReq), 0);
+	for(i=0; i<nwrite; i++)
+		threadcreate(writethread, nil, STACK);
+	vtcachesetwrite(zcache, myvtwrite);
+
+	bsize = fsys->blocksize;
+	vtfilelock(vfile, -1);
+	while((db = qread(qventi, &bno)) != nil){
+		if(nop){
+			blockput(db);
+			continue;
+		}
+		if(vtfilewrite(vfile, db->data, bsize, bno*bsize) != bsize)
+			sysfatal("ventiproc vtfilewrite: %r");
+		if(vtfileflushbefore(vfile, (bno+1)*bsize) < 0)
+			sysfatal("ventiproc vtfileflushbefore: %r");
+		blockput(db);
+	}
+	vtfileunlock(vfile);
+	vtcachesetwrite(zcache, nil);
+	for(i=0; i<nwrite; i++)
+		send(writechan, nil);
+	runlock(&endlk);
+}
+
+static int
+percent(u32int a, u32int b)
+{
+	return (vlong)a*100/b;
+}
+
+void
+statusproc(void *dummy)
+{
+	int n;
+	USED(dummy);
+
+	for(n=0;;n++){
+		sleep(1000);
+		if(qcmp->closed && qcmp->nel==0 && qventi->closed && qventi->nel==0)
+			break;
+		if(n < statustime)
+			continue;
+		n = 0;
+		print("# %T fsscan=%d%% cmpq=%d%% ventiq=%d%%\n",
+			percent(fsscanblock, fsys->nblock),
+			percent(qcmp->nel, MAXQ),
+			percent(qventi->nel, MAXQ));
+	}
+	runlock(&endlk);
+}
+
+int
+timefmt(Fmt *fmt)
+{
+	vlong ns;
+	Tm tm;
+	ns = nsec();
+	tm = *localtime(time(0));
+	return fmtprint(fmt, "%04d/%02d%02d %02d:%02d:%02d.%03d", 
+		tm.year+1900, tm.mon+1, tm.mday, tm.hour, tm.min, tm.sec,
+		(int)(ns%1000000000)/1000000);
+}
+
+char*
+mountplace(char *dev)
+{
+	char *cmd, *q;
+	int p[2], fd[3], n;
+	char buf[100];
+	
+	if(pipe(p) < 0)
+		sysfatal("pipe: %r");
+
+	fd[0] = -1;
+	fd[1] = p[1];
+	fd[2] = -1;
+	cmd = smprint("mount | awk '$1==\"%s\" && $2 == \"on\" {print $3}'", dev);
+	if(threadspawnl(fd, "sh", "sh", "-c", cmd, nil) < 0)
+		sysfatal("exec mount|awk (to find mtpt of %s): %r", dev);
+	/* threadspawnl closed p[1] */
+	n = readn(p[0], buf, sizeof buf-1);
+	close(p[0]);
+	if(n <= 0)
+		return dev;
+	buf[n] = 0;
+	if((q = strchr(buf, '\n')) == nil)
+		return dev;
+	*q = 0;
+	q = buf+strlen(buf);
+	if(q>buf && *(q-1) == '/')
+		*--q = 0;
+	return strdup(buf);
+}
+
blob - /dev/null
blob + 581adcb880ca78819ceac7371aafec22ae5d59ab (mode 644)
--- /dev/null
+++ src/cmd/vbackup/vcat.c
@@ -0,0 +1,76 @@
+#include <u.h>
+#include <libc.h>
+#include <venti.h>
+#include <diskfs.h>
+#include <thread.h>
+
+void
+usage(void)
+{
+	fprint(2, "usage: vcat [-z] score >diskfile\n");
+	threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	extern int nfilereads;
+	char *pref;
+	int zerotoo;
+	uchar score[VtScoreSize];
+	u8int *zero;
+	u32int i;
+	u32int n;
+	Block *b;
+	Disk *disk;
+	Fsys *fsys;
+	VtCache *c;
+	VtConn *z;
+
+	zerotoo = 0;
+	ARGBEGIN{
+	case 'z':
+		zerotoo = 1;
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc != 1)
+		usage();
+
+	fmtinstall('V', vtscorefmt);
+
+	if(vtparsescore(argv[0], &pref, score) < 0)
+		sysfatal("bad score '%s'", argv[0]);
+	if((z = vtdial(nil)) == nil)
+		sysfatal("vtdial: %r");
+	if(vtconnect(z) < 0)
+		sysfatal("vtconnect: %r");
+	if((c = vtcachealloc(z, 16384, 32, VtOREAD)) == nil)
+		sysfatal("vtcache: %r");
+	if((disk = diskopenventi(c, score)) == nil)
+		sysfatal("diskopenventi: %r");
+	if((fsys = fsysopen(disk)) == nil)
+		sysfatal("ffsopen: %r");
+
+	zero = emalloc(fsys->blocksize);
+	fprint(2, "%d blocks total\n", fsys->nblock);
+	n = 0;
+	for(i=0; i<fsys->nblock; i++){
+		if((b = fsysreadblock(fsys, i)) != nil){
+			if(pwrite(1, b->data, fsys->blocksize,
+			    (u64int)fsys->blocksize*i) != fsys->blocksize)
+				fprint(2, "error writing block %lud: %r\n", i);
+			n++;
+			blockput(b);
+		}else if(zerotoo)
+			if(pwrite(1, zero, fsys->blocksize,
+			    (u64int)fsys->blocksize*i) != fsys->blocksize)
+				fprint(2, "error writing block %lud: %r\n", i);
+		if(b == nil && i < 2)
+			sysfatal("block %d not in use", i);
+	}
+	fprint(2, "%d blocks in use, %d file reads\n", n, nfilereads);
+	threadexitsall(nil);
+}
blob - /dev/null
blob + e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (mode 644)
blob - /dev/null
blob + 8d6505866d5b1c323f392464d915dac9dd045384 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/vmount.c
@@ -0,0 +1,77 @@
+#include <u.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <libc.h>
+#include "mountnfs.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: vmount [-v] [-h handle] address mountpoint\n");
+	exits("usage");
+}
+
+int handlelen = 20;
+uchar handle[64] = {
+	/* SHA1("/") */
+	0x42, 0x09, 0x9B, 0x4A, 0xF0, 0x21, 0xE5, 0x3F, 0xD8, 0xFD,
+	0x4E, 0x05, 0x6C, 0x25, 0x68, 0xD7, 0xC2, 0xE3, 0xFF, 0xA8,
+};
+
+void
+main(int argc, char **argv)
+{
+	char *p, *net, *unx;
+	u32int host;
+	int n, port, proto, verbose;
+	struct sockaddr_in sa;
+
+	verbose = 0;
+	ARGBEGIN{
+	case 'h':
+		p = EARGF(usage());
+		n = strlen(p);
+		if(n%2)
+			sysfatal("bad handle '%s'", p);
+		if(n > 2*sizeof handle)
+			sysfatal("handle too long '%s'", p);
+		handlelen = n/2;
+		if(dec16(handle, n/2, p, n) != n/2)
+			sysfatal("bad hex in handle '%s'", p);
+		break;
+
+	case 'v':
+		verbose = 1;
+		break;
+
+	default:
+		usage();
+	}ARGEND
+
+	if(argc != 2)
+		usage();
+
+	p = p9netmkaddr(argv[0], "udp", "nfs");
+	if(p9dialparse(strdup(p), &net, &unx, &host, &port) < 0)
+		sysfatal("bad address '%s'", p);
+
+	if(verbose)
+		print("nfs server is net=%s addr=%d.%d.%d.%d port=%d\n",
+			net, host&0xFF, (host>>8)&0xFF, (host>>16)&0xFF, host>>24, port);
+
+	proto = 0;
+	if(strcmp(net, "tcp") == 0)
+		proto = SOCK_STREAM;
+	else if(strcmp(net, "udp") == 0)
+		proto = SOCK_DGRAM;
+	else
+		sysfatal("bad proto %s: can only handle tcp and udp", net);
+
+	memset(&sa, 0, sizeof sa);
+	memmove(&sa.sin_addr, &host, 4);
+	sa.sin_family = AF_INET;
+	sa.sin_port = htons(port);
+
+	mountnfs(proto, &sa, handle, handlelen, argv[1]);
+	exits(0);
+}
blob - /dev/null
blob + 6d717f0b7fa23ebfcdb39af66f6d6ebb21a7a8fc (mode 755)
--- /dev/null
+++ src/cmd/vbackup/vmount.rc
@@ -0,0 +1,19 @@
+#!/usr/local/plan9/bin/rc
+
+if(! ~ $#* 2){
+	echo 'usage: vmount server mtpt' >[1=2]
+	exit usage
+}
+
+server=$1
+mtpt=$2
+
+switch(`{uname}){
+case Linux
+	exec mount -o 'ro,timeo=100,rsize=8192,retrans=5,port=12049,mountport=12049,mountvers=3,nfsvers=3,nolock,soft,intr,udp' \
+		$server:/dump $mtpt
+case *
+	echo 'cannot mount on' `{uname} >[1=2]
+	exit usage
+}
+
blob - /dev/null
blob + 8d6505866d5b1c323f392464d915dac9dd045384 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/vmount0.c
@@ -0,0 +1,77 @@
+#include <u.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <libc.h>
+#include "mountnfs.h"
+
+void
+usage(void)
+{
+	fprint(2, "usage: vmount [-v] [-h handle] address mountpoint\n");
+	exits("usage");
+}
+
+int handlelen = 20;
+uchar handle[64] = {
+	/* SHA1("/") */
+	0x42, 0x09, 0x9B, 0x4A, 0xF0, 0x21, 0xE5, 0x3F, 0xD8, 0xFD,
+	0x4E, 0x05, 0x6C, 0x25, 0x68, 0xD7, 0xC2, 0xE3, 0xFF, 0xA8,
+};
+
+void
+main(int argc, char **argv)
+{
+	char *p, *net, *unx;
+	u32int host;
+	int n, port, proto, verbose;
+	struct sockaddr_in sa;
+
+	verbose = 0;
+	ARGBEGIN{
+	case 'h':
+		p = EARGF(usage());
+		n = strlen(p);
+		if(n%2)
+			sysfatal("bad handle '%s'", p);
+		if(n > 2*sizeof handle)
+			sysfatal("handle too long '%s'", p);
+		handlelen = n/2;
+		if(dec16(handle, n/2, p, n) != n/2)
+			sysfatal("bad hex in handle '%s'", p);
+		break;
+
+	case 'v':
+		verbose = 1;
+		break;
+
+	default:
+		usage();
+	}ARGEND
+
+	if(argc != 2)
+		usage();
+
+	p = p9netmkaddr(argv[0], "udp", "nfs");
+	if(p9dialparse(strdup(p), &net, &unx, &host, &port) < 0)
+		sysfatal("bad address '%s'", p);
+
+	if(verbose)
+		print("nfs server is net=%s addr=%d.%d.%d.%d port=%d\n",
+			net, host&0xFF, (host>>8)&0xFF, (host>>16)&0xFF, host>>24, port);
+
+	proto = 0;
+	if(strcmp(net, "tcp") == 0)
+		proto = SOCK_STREAM;
+	else if(strcmp(net, "udp") == 0)
+		proto = SOCK_DGRAM;
+	else
+		sysfatal("bad proto %s: can only handle tcp and udp", net);
+
+	memset(&sa, 0, sizeof sa);
+	memmove(&sa.sin_addr, &host, 4);
+	sa.sin_family = AF_INET;
+	sa.sin_port = htons(port);
+
+	mountnfs(proto, &sa, handle, handlelen, argv[1]);
+	exits(0);
+}
blob - /dev/null
blob + 6c6421c538130f592af63a2ae8a317b1e2d76056 (mode 644)
--- /dev/null
+++ src/cmd/vbackup/vnfs.c
@@ -0,0 +1,1273 @@
+/*
+ * TO DO:
+ *	- gc of file systems (not going to do just yet?)
+ *	- statistics file
+ *	- configure on amsterdam
+ */
+
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ip.h>
+#include <thread.h>
+#include <libsec.h>
+#include <sunrpc.h>
+#include <nfs3.h>
+#include <diskfs.h>
+#include <venti.h>
+#include "nfs3srv.h"
+
+#define trace if(!tracecalls){}else print
+
+typedef struct Ipokay Ipokay;
+typedef struct Config Config;
+typedef struct Ctree Ctree;
+typedef struct Cnode Cnode;
+
+struct Ipokay
+{
+	int okay;
+	uchar ip[IPaddrlen];
+	uchar mask[IPaddrlen];
+};
+
+struct Config
+{
+	Ipokay *ok;
+	uint nok;
+	ulong mtime;
+	Ctree *ctree;
+};
+
+char 		*addr;
+int			blocksize;
+int			cachesize;
+Config		config;
+char			*configfile;
+int			encryptedhandles = 1;
+Channel		*nfschan;
+Channel		*mountchan;
+Channel		*timerchan;
+Nfs3Handle	root;
+SunSrv		*srv;
+int			tracecalls;
+VtCache		*vcache;
+VtConn		*z;
+
+void			cryptinit(void);
+void			timerthread(void*);
+void			timerproc(void*);
+
+extern	void			handleunparse(Fsys*, Nfs3Handle*, Nfs3Handle*, int);
+extern	Nfs3Status	handleparse(Nfs3Handle*, Fsys**, Nfs3Handle*, int);
+
+Nfs3Status	logread(Cnode*, u32int, u64int, uchar**, u32int*, u1int*);
+Nfs3Status	refreshdiskread(Cnode*, u32int, u64int, uchar**, u32int*, u1int*);
+Nfs3Status	refreshconfigread(Cnode*, u32int, u64int, uchar**, u32int*, u1int*);
+
+int readconfigfile(Config *cp);
+void setrootfid(void);
+int ipokay(uchar *ip, ushort port);
+
+void
+usage(void)
+{
+	fprint(2, "usage: vnfs [-LLRVr] [-a addr] [-b blocksize] [-c cachesize] configfile\n");
+	threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	fmtinstall('B', sunrpcfmt);
+	fmtinstall('C', suncallfmt);
+	fmtinstall('F', vtfcallfmt);
+	fmtinstall('H', encodefmt);
+	fmtinstall('I', eipfmt);
+	fmtinstall('V', vtscorefmt);
+	sunfmtinstall(&nfs3prog);
+	sunfmtinstall(&nfsmount3prog);
+
+	addr = "udp!*!2049";
+	blocksize = 8192;
+	cachesize = 400;
+	srv = sunsrv();
+	srv->ipokay = ipokay;
+	cryptinit();
+
+	ARGBEGIN{
+	default:
+		usage();
+	case 'E':
+		encryptedhandles = 0;
+		break;
+	case 'L':
+		if(srv->localonly == 0)
+			srv->localonly = 1;
+		else
+			srv->localparanoia = 1;
+		break;
+	case 'R':
+		srv->chatty++;
+		break;
+	case 'T':
+		tracecalls = 1;
+		break;
+	case 'V':
+		chattyventi = 1;
+		break;
+	case 'a':
+		addr = EARGF(usage());
+		break;
+	case 'b':
+		blocksize = atoi(EARGF(usage()));
+		break;
+	case 'c':
+		cachesize = atoi(EARGF(usage()));
+		break;
+	case 'r':
+		srv->alwaysreject++;
+		break;
+	}ARGEND
+
+	if(argc != 1)
+		usage();
+
+	if((z = vtdial(nil)) == nil)
+		sysfatal("vtdial: %r");
+	if(vtconnect(z) < 0)
+		sysfatal("vtconnect: %r");
+	if((vcache = vtcachealloc(z, blocksize, cachesize, OREAD)) == nil)
+		sysfatal("vtcache: %r");
+
+	configfile = argv[0];
+	if(readconfigfile(&config) < 0)
+		sysfatal("readConfig: %r");
+	setrootfid();
+
+	nfschan = chancreate(sizeof(SunMsg*), 0);
+	mountchan = chancreate(sizeof(SunMsg*), 0);
+	timerchan = chancreate(sizeof(void*), 0);
+	
+	if(sunsrvudp(srv, addr) < 0)
+		sysfatal("starting server: %r");
+
+	sunsrvthreadcreate(srv, nfs3proc, nfschan);
+	sunsrvthreadcreate(srv, mount3proc, mountchan);
+	sunsrvthreadcreate(srv, timerthread, nil);
+	proccreate(timerproc, nil, 32768);
+	
+	sunsrvprog(srv, &nfs3prog, nfschan);
+	sunsrvprog(srv, &nfsmount3prog, mountchan);
+
+	threadexits(nil);
+}
+
+/*
+ * Handles.
+ * 
+ * We store all the state about which file a client is accessing in
+ * the handle, so that we don't have to maintain any per-client state
+ * ourselves.  In order to avoid leaking handles or letting clients 
+ * create arbitrary handles, we sign and encrypt each handle with
+ * AES using a key selected randomly when the server starts.
+ * Thus, handles cannot be used across sessions.  
+ *
+ * The decrypted handles begin with the following header:
+ *
+ *	rand[12]		random bytes used to make encryption non-deterministic
+ *	len[4]		length of handle that follows
+ *	sessid[8]		random session id chosen at start time
+ *
+ * If we're pressed for space in the rest of the handle, we could
+ * probably reduce the amount of randomness.
+ *
+ * Security woes aside, the fact that we have to shove everything
+ * into the handles is quite annoying.  We have to encode, in 40 bytes:
+ *
+ *	- position in the synthesized config tree
+ *	- enough of the path to do glob matching
+ *	- position in an archived file system image
+ *
+ * and the handles need to be stable across changes in the config file
+ * (though not across server restarts since encryption screws
+ * that up nicely).
+ * 
+ * We encode each of the first two as a 10-byte hash that is 
+ * the first half of a SHA1 hash.  
+ */
+
+enum
+{
+	RandSize = 16,
+	SessidSize = 8,
+	HeaderSize = RandSize+SessidSize,
+	MaxHandleSize = Nfs3MaxHandleSize - HeaderSize,
+};
+
+AESstate		aesstate;
+uchar		sessid[SessidSize];
+
+static void
+hencrypt(Nfs3Handle *h)
+{
+	uchar *p;
+	AESstate aes;
+
+	/*
+	 * root handle has special encryption - a single 0 byte - so that it
+	 * never goes stale.
+	 */
+	if(h->len == root.len && memcmp(h->h, root.h, root.len) == 0){
+		h->h[0] = 0;
+		h->len = 1;
+		return;
+	}
+
+	if(!encryptedhandles)
+		return;
+
+	if(h->len > MaxHandleSize){
+		/* oops */
+		fprint(2, "handle too long: %.*lH\n", h->len, h->h);
+		memset(h->h, 'X', Nfs3MaxHandleSize);
+		h->len = Nfs3MaxHandleSize;
+		return;
+	}
+
+	p = h->h;
+	memmove(p+HeaderSize, p, h->len);
+	*(u32int*)p = fastrand();
+	*(u32int*)(p+4) = fastrand();
+	*(u32int*)(p+8) = fastrand();
+	*(u32int*)(p+12) = h->len;
+	memmove(p+16, sessid, SessidSize);
+	h->len += HeaderSize;
+
+	if(encryptedhandles){
+		while(h->len < MaxHandleSize)
+			h->h[h->len++] = fastrand();
+		aes = aesstate;
+		aesCBCencrypt(h->h, MaxHandleSize, &aes);
+	}
+}
+
+static Nfs3Status
+hdecrypt(Nfs3Handle *h)
+{
+	AESstate aes;
+	
+	if(h->len == 1 && h->h[0] == 0){	/* single 0 byte is root */
+		*h = root;
+		return Nfs3Ok;
+	}
+
+	if(!encryptedhandles)
+		return Nfs3Ok;
+
+	if(h->len <= HeaderSize)
+		return Nfs3ErrBadHandle;
+	if(encryptedhandles){
+		if(h->len != MaxHandleSize)
+			return Nfs3ErrBadHandle;
+		aes = aesstate;
+		aesCBCdecrypt(h->h, h->len, &aes);
+	}
+	if(memcmp(h->h+RandSize, sessid, sizeof sessid) != 0)
+		return Nfs3ErrStale;	/* give benefit of doubt */
+	h->len = *(u32int*)(h->h+12); /* XXX byte order */
+	memmove(h->h, h->h+HeaderSize, h->len);
+	return Nfs3Ok;
+}
+
+void
+cryptinit(void)
+{
+	uchar key[32], ivec[AESbsize];
+	int i;
+	
+	*(u32int*)sessid = truerand();
+	for(i=0; i<nelem(key); i+=4)
+		*(u32int*)&key[i] = truerand();
+	for(i=0; i<nelem(ivec); i++)
+		ivec[i] = fastrand();
+	setupAESstate(&aesstate, key, sizeof key, ivec);
+}
+
+/*
+ * Config file.
+ *
+ * The main purpose of the configuration file is to define a tree
+ * in which the archived file system images are mounted.
+ * The tree is stored as Entry structures, defined below.
+ *
+ * The configuration file also allows one to define shell-like
+ * glob expressions matching paths that are not to be displayed.
+ * The matched files or directories are shown in directory listings
+ * (could suppress these if we cared) but they cannot be opened,
+ * read, or written, and getattr returns zeroed data.
+ */
+enum
+{
+	/* sizes used in handles; see nfs server below */
+	CnodeHandleSize = 8,
+	FsysHandleOffset = CnodeHandleSize,
+};
+
+/*
+ * Config file tree.
+ */
+struct Ctree
+{
+	Cnode *root;
+	Cnode *hash[1024];
+};
+
+struct Cnode
+{
+	char *name;	/* path element */
+	Cnode *parent;	/* in tree */
+	Cnode *nextsib;	/* in tree */
+	Cnode *kidlist;	/* in tree */
+	Cnode *nexthash;	/* in hash list */
+	
+	Nfs3Status (*read)(Cnode*, u32int, u64int, uchar**, u32int*, u1int*);	/* synthesized read fn */
+
+	uchar handle[VtScoreSize];	/* sha1(path to here) */
+	ulong mtime;	/* mtime for this directory entry */
+	
+	/* fsys overlay on this node */
+	Fsys *fsys;	/* cache of memory structure */
+	Nfs3Handle fsyshandle;
+	int isblackhole;	/* walking down keeps you here */
+
+	/*
+	 * mount point info.
+	 * if a mount point is inside another file system,
+	 * the fsys and fsyshandle above have the old fs info,
+	 * the mfsys and mfsyshandle below have the new one.
+	 * getattrs must use the old info for consistency.
+	 */
+	int ismtpt;	/* whether there is an fsys mounted here */
+	uchar fsysscore[VtScoreSize];	/* score of fsys image on venti */
+	char *fsysimage;	/* raw disk image */
+	Fsys *mfsys;	/* mounted file system (nil until walked) */
+	Nfs3Handle mfsyshandle;	/* handle to root of mounted fsys */
+	
+	int mark;	/* gc */
+};
+
+static uint
+dumbhash(uchar *s)
+{
+	return (s[0]<<2)|(s[1]>>6);	/* first 10 bits */
+}
+
+static Cnode*
+mkcnode(Ctree *t, Cnode *parent, char *elem, uint elen, char *path, uint plen)
+{
+	uint h;
+	Cnode *n;
+	
+	n = emalloc(sizeof *n + elen+1);
+	n->name = (char*)(n+1);
+	memmove(n->name, elem, elen);
+	n->name[elen] = 0;
+	n->parent = parent;
+	if(parent){
+		n->nextsib = parent->kidlist;
+		parent->kidlist = n;
+	}
+	n->kidlist = nil;
+	sha1((uchar*)path, plen, n->handle, nil);
+	h = dumbhash(n->handle);
+	n->nexthash = t->hash[h];
+	t->hash[h] = n;
+	
+	return n;
+}
+
+void
+markctree(Ctree *t)
+{
+	int i;
+	Cnode *n;
+
+	for(i=0; i<nelem(t->hash); i++)
+		for(n=t->hash[i]; n; n=n->nexthash)
+			if(n->name[0] != '+')
+				n->mark = 1;
+}
+
+int
+refreshdisk(void)
+{
+	int i;
+	Cnode *n;
+	Ctree *t;
+	
+	t = config.ctree;
+	for(i=0; i<nelem(t->hash); i++)
+		for(n=t->hash[i]; n; n=n->nexthash){
+			if(n->mfsys)
+				disksync(n->mfsys->disk);
+			if(n->fsys)
+				disksync(n->fsys->disk);
+		}
+	return 0;
+}
+
+void
+sweepctree(Ctree *t)
+{
+	int i;
+	Cnode *n;
+
+	/* just zero all the garbage and leave it linked into the tree */
+	for(i=0; i<nelem(t->hash); i++){
+		for(n=t->hash[i]; n; n=n->nexthash){
+			if(!n->mark)
+				continue;
+			n->fsys = nil;
+			free(n->fsysimage);
+			n->fsysimage = nil;
+			memset(n->fsysscore, 0, sizeof n->fsysscore);
+			n->mfsys = nil;
+			n->ismtpt = 0;
+			memset(&n->fsyshandle, 0, sizeof n->fsyshandle);
+			memset(&n->mfsyshandle, 0, sizeof n->mfsyshandle);
+		}
+	}
+}
+
+static Cnode*
+cnodewalk(Cnode *n, char *name, uint len, int markokay)
+{
+	Cnode *nn;
+	
+	for(nn=n->kidlist; nn; nn=nn->nextsib)
+		if(strncmp(nn->name, name, len) == 0 && nn->name[len] == 0)
+		if(!nn->mark || markokay)
+			return nn;
+	return nil;
+}
+
+Cnode*
+ctreewalkpath(Ctree *t, char *name, ulong createmtime)
+{
+	Cnode *n, *nn;
+	char *p, *nextp;
+	
+	n = t->root;
+	p = name;
+	for(; *p; p=nextp){
+		n->mark = 0;
+		assert(*p == '/');
+		p++;
+		nextp = strchr(p, '/');
+		if(nextp == nil)
+			nextp = p+strlen(p);
+		if((nn = cnodewalk(n, p, nextp-p, 1)) == nil){
+			if(createmtime == 0)
+				return nil;
+			nn = mkcnode(t, n, p, nextp-p, name, nextp-name);
+			nn->mtime = createmtime;
+		}
+		if(nn->mark)
+			nn->mark = 0;
+		n = nn;
+	}
+	n->mark = 0;
+	return n;
+}
+
+Ctree*
+mkctree(void)
+{
+	Ctree *t;
+	
+	t = emalloc(sizeof *t);
+	t->root = mkcnode(t, nil, "", 0, "", 0);
+	
+	ctreewalkpath(t, "/+log", time(0))->read = logread;
+	ctreewalkpath(t, "/+refreshdisk", time(0))->read = refreshdiskread;
+	ctreewalkpath(t, "/+refreshconfig", time(0))->read = refreshconfigread;
+
+	return t;
+}
+
+Cnode*
+ctreemountfsys(Ctree *t, char *path, ulong time, uchar *score, char *file)
+{
+	Cnode *n;
+	
+	if(time == 0)
+		time = 1;
+	n = ctreewalkpath(t, path, time);
+	if(score){
+		if(n->ismtpt && (n->fsysimage || memcmp(n->fsysscore, score, VtScoreSize) != 0)){
+			free(n->fsysimage);
+			n->fsysimage = nil;
+			n->fsys = nil;	/* leak (might be other refs) */
+		}
+		memmove(n->fsysscore, score, VtScoreSize);
+	}else{
+		if(n->ismtpt && (n->fsysimage==nil || strcmp(n->fsysimage, file) != 0)){
+			free(n->fsysimage);
+			n->fsysimage = nil;
+			n->fsys = nil;	/* leak (might be other refs) */
+		}
+		n->fsysimage = emalloc(strlen(file)+1);
+		strcpy(n->fsysimage, file);
+	}
+	n->ismtpt = 1;
+	return n;
+}
+
+Cnode*
+cnodebyhandle(Ctree *t, uchar *p)
+{
+	int h;
+	Cnode *n;
+	
+	h = dumbhash(p);
+	for(n=t->hash[h]; n; n=n->nexthash)
+		if(memcmp(n->handle, p, CnodeHandleSize) == 0)
+			return n;
+	return nil;
+}
+
+static int
+parseipandmask(char *s, uchar *ip, uchar *mask)
+{
+	char *p, *q;
+	
+	p = strchr(s, '/');
+	if(p)
+		*p++ = 0;
+	if(parseip(ip, s) == ~0UL)
+		return -1;
+	if(p == nil)
+		memset(mask, 0xFF, IPaddrlen);
+	else{
+		if(isdigit(*p) && strtol(p, &q, 10)>=0 && *q==0)
+			*--p = '/';
+		if(parseipmask(mask, p) == ~0UL)
+			return -1;
+		if(*p != '/')
+			*--p = '/';
+	}
+//fprint(2, "parseipandmask %s => %I %I\n", s, ip, mask);
+	return 0;
+}
+
+static int
+parsetime(char *s, ulong *time)
+{
+	ulong x;
+	char *p;
+	int i;
+	Tm tm;
+	
+	/* decimal integer is seconds since 1970 */
+	x = strtoul(s, &p, 10);
+	if(x > 0 && *p == 0){
+		*time = x;
+		return 0;
+	}
+	
+	/* otherwise expect yyyy/mmdd/hhmm */
+	if(strlen(s) != 14 || s[4] != '/' || s[9] != '/')
+		return -1;
+	for(i=0; i<4; i++)
+		if(!isdigit(s[i]) || !isdigit(s[i+5]) || !isdigit(s[i+10]))
+			return -1;
+	memset(&tm, 0, sizeof tm);
+	tm.year = atoi(s)-1900;
+	if(tm.year < 0 || tm.year > 200)
+		return -1;
+	tm.mon = (s[5]-'0')*10+s[6]-'0' - 1;
+	if(tm.mon < 0 || tm.mon > 11)
+		return -1; 
+	tm.mday = (s[7]-'0')*10+s[8]-'0';
+	if(tm.mday < 0 || tm.mday > 31)
+		return -1;
+	tm.hour = (s[10]-'0')*10+s[11]-'0';
+	if(tm.hour < 0 || tm.hour > 23)
+		return -1;
+	tm.min = (s[12]-'0')*10+s[13]-'0';
+	if(tm.min < 0 || tm.min > 59)
+		return -1;
+	strcpy(tm.zone, "XXX");	/* anything but GMT */
+if(0){
+print("tm2sec %d/%d/%d/%d/%d\n",
+	tm.year, tm.mon, tm.mday, tm.hour, tm.min);
+}
+	*time = tm2sec(&tm);
+if(0) print("time %lud\n", *time);
+	return 0;
+}
+
+
+int
+readconfigfile(Config *cp)
+{
+	char *f[10], *image, *p, *pref, *q, *name;
+	int nf, line;
+	uchar scorebuf[VtScoreSize], *score;
+	ulong time;
+	Biobuf *b;
+	Config c;
+	Dir *dir;
+
+	name = configfile;
+	c = *cp;
+	if((dir = dirstat(name)) == nil)
+		return -1;
+	if(c.mtime == dir->mtime){
+		free(dir);
+		return 0;
+	}
+	c.mtime = dir->mtime;
+	free(dir);
+	if((b = Bopen(name, OREAD)) == nil){
+		free(dir);
+		return -1;
+	}
+	
+	/*
+	 * Reuse old tree, garbage collecting entries that
+	 * are not mentioned in the new config file.
+	 */
+	if(c.ctree == nil)
+		c.ctree = mkctree();
+
+	markctree(c.ctree);
+	c.ok = nil;
+	c.nok = 0;
+	
+	line = 0;
+	for(; (p=Brdstr(b, '\n', 1)) != nil; free(p)){
+		line++;
+		if((q = strchr(p, '#')) != nil)
+			*q = 0;
+		nf = tokenize(p, f, nelem(f));
+		if(nf == 0)
+			continue;
+		if(strcmp(f[0], "mount") == 0){
+			if(nf != 4){
+				werrstr("syntax error: mount /path /dev|score mtime");
+				goto badline;
+			}
+			if(f[1][0] != '/'){
+				werrstr("unrooted path %s", f[1]);
+				goto badline;
+			}
+			score = nil;
+			image = nil;
+			if(f[2][0] == '/'){
+				if(access(f[2], AEXIST) < 0){
+					werrstr("image %s does not exist", f[2]);
+					goto badline;
+				}
+				image = f[2];
+			}else{
+				if(vtparsescore(f[2], &pref, scorebuf) < 0){
+					werrstr("bad score %s", f[2]);
+					goto badline;
+				}
+				score = scorebuf;
+			}
+			if(parsetime(f[3], &time) < 0){
+				fprint(2, "%s:%d: bad time %s\n", name, line, f[3]);
+				time = 1;
+			}
+			ctreemountfsys(c.ctree, f[1], time, score, image);
+			continue;
+		}
+		if(strcmp(f[0], "allow") == 0 || strcmp(f[0], "deny") == 0){
+			if(nf != 2){
+				werrstr("syntax error: allow|deny ip[/mask]"); 
+				goto badline;
+			}
+			c.ok = erealloc(c.ok, (c.nok+1)*sizeof(c.ok[0]));
+			if(parseipandmask(f[1], c.ok[c.nok].ip, c.ok[c.nok].mask) < 0){
+				werrstr("bad ip[/mask]: %s", f[1]);
+				goto badline;
+			}
+			c.ok[c.nok].okay = (strcmp(f[0], "allow") == 0);
+			c.nok++;
+			continue;
+		}
+		werrstr("unknown verb '%s'", f[0]);
+	badline:
+		fprint(2, "%s:%d: %r\n", name, line);
+	}
+	Bterm(b);
+
+	sweepctree(c.ctree);
+	free(cp->ok);
+	*cp = c;
+	return 0;
+}
+
+int
+ipokay(uchar *ip, ushort port)
+{
+	int i;
+	uchar ipx[IPaddrlen];
+	Ipokay *ok; 
+	
+	for(i=0; i<config.nok; i++){
+		ok = &config.ok[i];
+		maskip(ip, ok->mask, ipx);
+if(0) fprint(2, "%I & %I = %I (== %I?)\n",
+	ip, ok->mask, ipx, ok->ip);
+		if(memcmp(ipx, ok->ip, IPaddrlen) == 0)
+			return ok->okay;
+	}
+	if(config.nok == 0)	/* all is permitted */
+		return 1;
+	/* otherwise default is none allowed */
+	return 0;
+}
+
+Nfs3Status
+cnodelookup(Ctree *t, Cnode **np, char *name)
+{
+	Cnode *n, *nn;
+	
+	n = *np;
+	if(n->isblackhole)
+		return Nfs3Ok;
+	if((nn = cnodewalk(n, name, strlen(name), 0)) == nil){
+		if(n->ismtpt || n->fsys){
+			if((nn = cnodewalk(n, "", 0, 1)) == nil){
+				nn = mkcnode(t, n, "", 0, (char*)n->handle, SHA1dlen);
+				nn->isblackhole = 1;
+			}
+			nn->mark = 0;
+		}
+	}
+	if(nn == nil)
+		return Nfs3ErrNoEnt;
+	*np = nn;
+	return Nfs3Ok;
+}
+
+Nfs3Status
+cnodegetattr(Cnode *n, Nfs3Attr *attr)
+{
+	memset(attr, 0, sizeof *attr);
+	if(n->read){
+		attr->type = Nfs3FileReg;
+		attr->mode = 0444;
+		attr->size = 512;
+		attr->nlink = 1;
+	}else{
+		attr->type = Nfs3FileDir;
+		attr->mode = 0555;
+		attr->size = 1024;
+		attr->nlink = 10;
+	}
+	attr->fileid = *(u64int*)n->handle;
+	attr->atime.sec = n->mtime;
+	attr->mtime.sec = n->mtime;
+	attr->ctime.sec = n->mtime;
+	return Nfs3Ok;
+}
+
+Nfs3Status
+cnodereaddir(Cnode *n, u32int count, u64int cookie, uchar **pdata, u32int *pcount, u1int *peof)
+{
+	uchar *data, *p, *ep, *np;
+	u64int c;
+	Nfs3Entry ne;
+	
+	n = n->kidlist;
+	c = cookie;
+	for(; c && n; c--)
+		n = n->nextsib;
+	if(n == nil){
+		*pdata = 0;
+		*pcount = 0;
+		*peof = 1;
+		return Nfs3Ok;
+	}
+	
+	data = emalloc(count);
+	p = data;
+	ep = data+count;
+	while(n && p < ep){
+		if(n->mark || n->name[0] == '+'){
+			n = n->nextsib;
+			++cookie;
+			continue;
+		}
+		ne.name = n->name;
+		ne.cookie = ++cookie;
+		ne.fileid = *(u64int*)n->handle;
+		if(nfs3entrypack(p, ep, &np, &ne) < 0)
+			break;
+		p = np;
+		n = n->nextsib;
+	}
+	*pdata = data;
+	*pcount = p - data;
+	*peof = n==nil;
+	return Nfs3Ok;
+}
+
+void
+timerproc(void *v)
+{
+	for(;;){
+		sleep(60*1000);
+		sendp(timerchan, 0);
+	}
+}
+
+void
+timerthread(void *v)
+{
+	for(;;){
+		recvp(timerchan);
+	//	refreshconfig();
+	}
+}
+
+/*
+ * Actually serve the NFS requests.  Called from nfs3srv.c.  
+ * Each request runs in its own thread (coroutine).
+ *
+ * Decrypted handles have the form:
+ *
+ *	config[20] - SHA1 hash identifying a config tree node
+ *	glob[10] - SHA1 hash prefix identifying a glob state
+ *	fsyshandle[<=10] - disk file system handle (usually 4 bytes)
+ */
+ 
+/*
+ * A fid represents a point in the file tree.
+ * There are three components, all derived from the handle:
+ *
+ *	- config tree position (also used to find fsys)
+ *	- glob state for exclusions
+ *	- file system position
+ */
+enum
+{
+	HAccess,
+	HAttr,
+	HWalk,
+	HDotdot,
+	HRead
+};
+typedef struct Fid Fid;
+struct Fid
+{
+	Cnode *cnode;
+	Fsys *fsys;
+	Nfs3Handle fsyshandle;
+};
+
+int
+handlecmp(Nfs3Handle *h, Nfs3Handle *h1)
+{
+	if(h->len != h1->len)
+		return h->len - h1->len;
+	return memcmp(h->h, h1->h, h->len);
+}
+
+Nfs3Status
+handletofid(Nfs3Handle *eh, Fid *fid, int mode)
+{
+	int domount;
+	Cnode *n;
+	Disk *disk, *cdisk;
+	Fsys *fsys;
+	Nfs3Status ok;
+	Nfs3Handle h2, *h, *fh;
+	
+	memset(fid, 0, sizeof *fid);
+
+	domount = 1;
+	if(mode == HDotdot)
+		domount = 0;
+	/*
+	 * Not necessary, but speeds up ls -l /dump/2005
+	 * HAttr and HAccess must be handled the same way
+	 * because both can be used to fetch attributes.
+	 * Acting differently yields inconsistencies at mount points,
+	 * and causes FreeBSD ls -l to fail.
+	 */
+	if(mode == HAttr || mode == HAccess)
+		domount = 0;
+
+	/*
+	 * Decrypt handle.
+	 */
+	h2 = *eh;
+	h = &h2;
+	if((ok = hdecrypt(h)) != Nfs3Ok)
+		return ok;
+	trace("handletofid: decrypted %.*lH\n", h->len, h->h);
+	if(h->len < FsysHandleOffset)
+		return Nfs3ErrBadHandle;
+
+	/*
+	 * Find place in config tree.
+	 */
+	if((n = cnodebyhandle(config.ctree, h->h)) == nil)
+		return Nfs3ErrStale;
+	fid->cnode = n;
+
+	if(n->ismtpt && domount){
+		/*
+		 * Open fsys for mount point if needed.
+		 */
+		if(n->mfsys == nil){
+			trace("handletofid: mounting %V/%s\n", n->fsysscore, n->fsysimage);
+			if(n->fsysimage){
+				if(strcmp(n->fsysimage, "/dev/null") == 0)
+					return Nfs3ErrAcces;
+				if((disk = diskopenfile(n->fsysimage)) == nil){
+					fprint(2, "cannot open disk %s: %r\n", n->fsysimage);
+					return Nfs3ErrIo;
+				}
+				if((cdisk = diskcache(disk, blocksize, 64)) == nil){
+					fprint(2, "cannot cache disk %s: %r\n", n->fsysimage);
+					diskclose(disk);
+				}
+				disk = cdisk;
+			}else{
+				if((disk = diskopenventi(vcache, n->fsysscore)) == nil){
+					fprint(2, "cannot open venti disk %V: %r\n", n->fsysscore);
+					return Nfs3ErrIo;
+				}
+			}
+			if((fsys = fsysopen(disk)) == nil){
+				fprint(2, "cannot open fsys on %V: %r\n", n->fsysscore);
+				diskclose(disk);
+				return Nfs3ErrIo;
+			}
+			n->mfsys = fsys;
+			fsysroot(fsys, &n->mfsyshandle);
+		}
+		
+		/*
+		 * Use inner handle.
+		 */
+		fid->fsys = n->mfsys;
+		fid->fsyshandle = n->mfsyshandle;
+	}else{
+		/*
+		 * Use fsys handle from tree or from handle.
+		 * This assumes that fsyshandle was set by fidtohandle
+		 * earlier, so it's not okay to reuse handles (except the root)
+		 * across sessions.  The encryption above makes and 
+		 * enforces the same restriction, so this is okay.
+		 */
+		fid->fsys = n->fsys;
+		fh = &fid->fsyshandle;
+		if(n->isblackhole){
+			fh->len = h->len-FsysHandleOffset;
+			memmove(fh->h, h->h+FsysHandleOffset, fh->len);
+		}else
+			*fh = n->fsyshandle;
+		trace("handletofid: fsyshandle %.*lH\n", fh->len, fh->h);
+	}
+
+	/*
+	 * TO DO (maybe): some sort of path restriction here.
+	 */
+	trace("handletofid: cnode %s fsys %p fsyshandle %.*lH\n",
+		n->name, fid->fsys, fid->fsyshandle.len, fid->fsyshandle.h);
+	return Nfs3Ok;
+}
+
+void
+_fidtohandle(Fid *fid, Nfs3Handle *h)
+{
+	Cnode *n;
+	
+	n = fid->cnode;
+	/*
+	 * Record fsys handle in n, don't bother sending it to client
+	 * for black holes.
+	 */
+	n->fsys = fid->fsys;
+	if(!n->isblackhole){
+		n->fsyshandle = fid->fsyshandle;
+		fid->fsyshandle.len = 0;
+	}
+	memmove(h->h, n->handle, CnodeHandleSize);
+	memmove(h->h+FsysHandleOffset, fid->fsyshandle.h, fid->fsyshandle.len);
+	h->len = FsysHandleOffset+fid->fsyshandle.len;
+}
+
+void
+fidtohandle(Fid *fid, Nfs3Handle *h)
+{
+	_fidtohandle(fid, h);
+	hencrypt(h);
+}
+
+void
+setrootfid(void)
+{
+	Fid fid;
+	
+	memset(&fid, 0, sizeof fid);
+	fid.cnode = config.ctree->root;
+	_fidtohandle(&fid, &root);
+fprint(2, "handle %.*lH\n", root.len, root.h);
+}
+
+void
+fsgetroot(Nfs3Handle *h)
+{
+	*h = root;
+	hencrypt(h);
+}
+
+Nfs3Status
+fsgetattr(SunAuthUnix *au, Nfs3Handle *h, Nfs3Attr *attr)
+{
+	Fid fid;
+	Nfs3Status ok;
+
+	trace("getattr %.*lH\n", h->len, h->h);
+	if((ok = handletofid(h, &fid, HAttr)) != Nfs3Ok)
+		return ok;
+	if(fid.fsys)
+		return fsysgetattr(fid.fsys, au, &fid.fsyshandle, attr);
+	else
+		return cnodegetattr(fid.cnode, attr);
+}
+
+/*
+ * Lookup is always the hard part.
+ */
+Nfs3Status
+fslookup(SunAuthUnix *au, Nfs3Handle *h, char *name, Nfs3Handle *nh)
+{
+	Fid fid;
+	Cnode *n;
+	Nfs3Status ok;
+	Nfs3Handle xh;
+	int mode;
+	
+	trace("lookup %.*lH %s\n", h->len, h->h, name);
+
+	mode = HWalk;
+	if(strcmp(name, "..") == 0 || strcmp(name, ".") == 0)
+		mode = HDotdot;
+	if((ok = handletofid(h, &fid, mode)) != Nfs3Ok){
+		nfs3errstr(ok);
+		trace("lookup: handletofid %r\n");
+		return ok;
+	}
+	
+	if(strcmp(name, ".") == 0){
+		fidtohandle(&fid, nh);
+		return Nfs3Ok;
+	}
+
+	/*
+	 * Walk down file system and cnode simultaneously.
+	 * If dotdot and file system doesn't move, need to walk
+	 * up cnode.  Save the corresponding fsys handles in
+	 * the cnode as we walk down so that we'll have them 
+	 * for dotdotting back up.
+	 */
+	n = fid.cnode;
+	if(mode == HWalk){
+		/*
+		 * Walk down config tree and file system simultaneously.
+		 */
+		if((ok = cnodelookup(config.ctree, &n, name)) != Nfs3Ok){
+			nfs3errstr(ok);
+			trace("lookup: cnodelookup: %r\n");
+			return ok;
+		}
+		fid.cnode = n;
+		if(fid.fsys){
+			if((ok = fsyslookup(fid.fsys, au, &fid.fsyshandle, name, &xh)) != Nfs3Ok){
+				nfs3errstr(ok);
+				trace("lookup: fsyslookup: %r\n");
+				return ok;
+			}
+			fid.fsyshandle = xh;
+		}		
+	}else{
+		/*
+		 * Walking dotdot.  Ick.
+		 */
+		trace("lookup dotdot fsys=%p\n", fid.fsys);
+		if(fid.fsys){
+			/*
+			 * Walk up file system, then try up config tree.
+			 */
+			if((ok = fsyslookup(fid.fsys, au, &fid.fsyshandle, "..", &xh)) != Nfs3Ok){
+				nfs3errstr(ok);
+				trace("lookup fsyslookup: %r\n");
+				return ok;
+			}
+			fid.fsyshandle = xh;
+
+			/*
+			 * Usually just go to n->parent.
+			 * 
+			 * If we're in a subtree of the mounted file system that
+			 * isn't represented explicitly by the config tree (instead
+			 * the black hole node represents the entire file tree),
+			 * then we only go to n->parent when we've dotdotted back
+			 * to the right handle.
+			 */
+			if(n->parent == nil)
+				trace("lookup dotdot: no parent\n");
+			else{
+				trace("lookup dotdot: parent %.*lH, have %.*lH\n",
+					n->parent->fsyshandle.len, n->parent->fsyshandle.h,
+					xh.len, xh.h);
+			}
+			
+			if(n->isblackhole){
+				if(handlecmp(&n->parent->mfsyshandle, &xh) == 0)
+					n = n->parent;
+			}else{
+				if(n->parent)
+					n = n->parent;
+			}
+		}else{
+			/*
+			 * No file system, just walk up.
+			 */
+			if(n->parent)
+				n = n->parent;
+		}
+		fid.fsys = n->fsys;
+		fid.fsyshandle = n->fsyshandle;
+		fid.cnode = n;
+	}
+	fidtohandle(&fid, nh);
+	return Nfs3Ok;
+}
+
+Nfs3Status
+fsaccess(SunAuthUnix *au, Nfs3Handle *h, u32int want, u32int *got, Nfs3Attr *attr)
+{
+	Fid fid;
+	Nfs3Status ok;
+	
+	trace("access %.*lH 0x%ux\n", h->len, h->h, want);
+	if((ok = handletofid(h, &fid, HAccess)) != Nfs3Ok)
+		return ok;
+	if(fid.fsys)
+		return fsysaccess(fid.fsys, au, &fid.fsyshandle, want, got, attr);
+	*got = want & (Nfs3AccessRead|Nfs3AccessLookup|Nfs3AccessExecute);
+	return cnodegetattr(fid.cnode, attr);
+}
+
+Nfs3Status
+fsreadlink(SunAuthUnix *au, Nfs3Handle *h, char **link)
+{
+	Fid fid;
+	Nfs3Status ok;
+	
+	trace("readlink %.*lH\n", h->len, h->h);
+	if((ok = handletofid(h, &fid, HRead)) != Nfs3Ok)
+		return ok;
+	if(fid.fsys)
+		return fsysreadlink(fid.fsys, au, &fid.fsyshandle, link);
+	*link = 0;
+	return Nfs3ErrNotSupp;
+}
+
+Nfs3Status
+fsreadfile(SunAuthUnix *au, Nfs3Handle *h, u32int count, u64int offset, uchar **data, u32int *pcount, u1int *peof)
+{
+	Fid fid;
+	Nfs3Status ok;
+	
+	trace("readfile %.*lH\n", h->len, h->h);
+	if((ok = handletofid(h, &fid, HRead)) != Nfs3Ok)
+		return ok;
+	if(fid.cnode->read)
+		return fid.cnode->read(fid.cnode, count, offset, data, pcount, peof);
+	if(fid.fsys)
+		return fsysreadfile(fid.fsys, au, &fid.fsyshandle, count, offset, data, pcount, peof);
+	return Nfs3ErrNotSupp;
+}
+
+Nfs3Status
+fsreaddir(SunAuthUnix *au, Nfs3Handle *h, u32int len, u64int cookie, uchar **pdata, u32int *pcount, u1int *peof)
+{
+	Fid fid;
+	Nfs3Status ok;
+
+	trace("readdir %.*lH\n", h->len, h->h);	
+	if((ok = handletofid(h, &fid, HRead)) != Nfs3Ok)
+		return ok;
+	if(fid.fsys)
+		return fsysreaddir(fid.fsys, au, &fid.fsyshandle, len, cookie, pdata, pcount, peof);
+	return cnodereaddir(fid.cnode, len, cookie, pdata, pcount, peof);
+}
+
+Nfs3Status
+logread(Cnode *n, u32int count, u64int offset, uchar **data, u32int *pcount, u1int *peof)
+{
+	*pcount = 0;
+	*peof = 1;
+	return Nfs3Ok;
+}
+
+Nfs3Status
+refreshdiskread(Cnode *n, u32int count, u64int offset, uchar **data, u32int *pcount, u1int *peof)
+{
+	char buf[128];
+	
+	if(offset != 0){
+		*pcount = 0;
+		*peof = 1;
+		return Nfs3Ok;
+	}
+	if(refreshdisk() < 0)
+		snprint(buf, sizeof buf, "refreshdisk: %r\n");
+	else
+		strcpy(buf, "ok\n");
+	*data = emalloc(strlen(buf));
+	strcpy((char*)*data, buf);
+	*pcount = strlen(buf);
+	*peof = 1;
+	return Nfs3Ok;
+}
+
+Nfs3Status
+refreshconfigread(Cnode *n, u32int count, u64int offset, uchar **data, u32int *pcount, u1int *peof)
+{
+	char buf[128];
+	
+	if(offset != 0){
+		*pcount = 0;
+		*peof = 1;
+		return Nfs3Ok;
+	}
+	if(readconfigfile(&config) < 0)
+		snprint(buf, sizeof buf, "readconfig: %r\n");
+	else
+		strcpy(buf, "ok\n");
+	*data = emalloc(strlen(buf));
+	strcpy((char*)*data, buf);
+	*pcount = strlen(buf);
+	*peof = 1;
+	return Nfs3Ok;
+}
+
+void
+abort(void)
+{
+	for(;;)
+		*(int*)0=0;
+}
blob - /dev/null
blob + bf31dac62b026789d56b90f6c9c60925015fdad1 (mode 755)
--- /dev/null
+++ src/cmd/vbackup/yesterday.rc
@@ -0,0 +1,109 @@
+#!/usr/local/plan9/bin/rc
+
+path=($path $PLAN9/bin)
+
+fn usage {
+	echo 'usage: yesterday [-cd] [-[[yy]yy]mm]dd] [-n daysago] file ...' >[1=2]
+	exit 1
+}
+
+fn Xcp {
+	echo cp $1 $2
+	cp $1 $2
+}
+
+fn Xcarefulcp {
+	if(! cmp -s $1 $2) Xcp $1 $2
+}
+
+fn Xdiff {
+	echo diff -c $1 $2
+	diff -c $1 $2
+}
+
+fn Xecho {
+	echo $1
+}
+
+year=`{date|sed 's/.* //'}
+copy=Xecho
+last=()
+while(! ~ $#* 0 && ~ $1 -* && ! ~ $1 --){
+	switch($1){
+	case -c
+		copy=Xcp
+		shift
+	case -d
+		copy=Xdiff
+		shift
+	case -C
+		copy=Xcarefulcp
+		shift
+	case -n*
+		if(~ $1 -n){
+			if(~ $#* 1)
+				usage
+			shift
+			days=$1
+		}
+		if not
+			days=`{echo $1 | sed 's/^-.//'}
+		last=`{date -r `{perl -e 'print time() - '$days'*60*60*24'} | 
+				9 sed -e 's%... (...) (..) ..:..:.. ... (....)%\3/\1\2%' -e 'y/ /0/' -e $smon}
+		shift
+	case -[0-9]
+		mon=`{date|9 sed 's/^....(...).*/\1/' -e $smon}
+		last=$year/$mon ^`{echo $1|sed 's/^-/0/'}
+		shift
+	case -[0-9][0-9]
+		mon=`{date|9 sed 's/^....(...).*/\1/' -e $smon}
+		last=$year/$mon ^`{echo $1|9 sed 's/^-//'}
+		shift
+	case -[0-9][0-9][0-9][0-9]
+		last=$year/ ^ `{echo $1|9 sed 's/^-//'}
+		shift
+	case -[0-9][0-9][0-9][0-9][0-9][0-9]
+		last=`{echo $year|9 sed 's/..$//'} ^ `{echo $1|9 sed 's/^-(..)/\1\//'}
+		shift
+	case -[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]
+		last=`{echo $1|9 sed 's/^-(....)/\1\//'}
+		shift
+	case *
+		usage
+	}
+}
+if(! ~ $#* 0 && ~ $1 --)
+	shift
+
+if(~ $#* 0)
+	usage
+
+dir=`{pwd}
+if(! ~ $status ''){
+	echo 'yesterday: can''t find directory' >[1=2]
+	exit 'pwd failed'
+}
+
+h=`{hostname}
+switch($h){
+case amsterdam
+	xdump=/dump/am
+case *
+	if(! test -d /dump/$h){
+		echo 'no dumps on '^`{hostname} >[1=2]
+		exit 1
+	}
+	xdump=/dump/$h
+}
+
+for(i){
+	xpath=$i
+	if(! ~ $xpath /*)
+		xpath=`{9 cleanname -d `{pwd} $i}
+	dumppath=$xpath
+	if(~ $#last 0)
+		xlast=`{9 ls -t $xdump/$year|sed 1q}
+	if not
+		xlast=$xdump/$last
+	$copy $xlast^$dumppath $xpath
+}