shithub: todofs

ref: a05459a8d6a5a190db2fe52a89ed22bf515834b4
dir: /todofs.c/

View raw version
#include <u.h>
#include <libc.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include <bio.h>
#include <ndb.h>

/* owner = assignee */
/* group = optional group */
/* filename = UID_short_description_based_on_title */
/* file contents = plain read from source file UID */
/* source directory contents:
	index - index file, ndb format
	UID1
	UID2
	UID3
*/

void
usage(void)
{
	fprint(2, "usage: %s [-s srvname] [-m mountpoint] directory\n", argv0);
	exits("usage");
}

#define HASHLEN 10

enum {
	Qdir,
	Qctl,
	Qstatus,
	Qtask,
	Qtitle,
	Qdata,
	Qassignee,
	Qgroup,
};

char *qnames[] = {
	[Qdir] "/",
	[Qctl] "ctl",
	[Qstatus] nil,
	[Qtask] nil,
	[Qtitle] "title",
	[Qdata] "data",
	[Qassignee] "assignee",
	[Qgroup] "group",
};

int
qidtype(ulong path)
{
	// -----0000
	return path & 15;
}

ulong
qidnum(ulong path)
{
	// 000000----
	return path >> 4;
}

ulong
mkqid(int type, int num)
{
	return (num << 4) | type & 15;
}

static char*
ultostr(ulong n)
{
	char buf[32];
	snprint(buf, sizeof(buf), "%0ulx", n);
	return buf;
}

static ulong
strtoid(char *s)
{
	return strtoul(s, nil, 16);
}

char *srcdir;
char *idxfile;
Ndb *index = nil;

typedef struct Task Task;
struct Task {
	Task* next;
	char *title;
	char *dataname;
	char *ext;
	ulong id;
	Dir *dir;
	int cassignee;
	int cgroup;
	int cfname;
};

typedef struct Status Status;
struct Status {
	Status *next;
	char *name;
	Task *tasks;
};

Status *statuses = nil;
ulong nextid;

Task* getgtask(ulong id, Status **status);

int
savedata(void)
{
	Biobuf *bout;
	Status *s;
	ulong tid;
	Task *t;
	
	ndbclose(index);
	index = nil;
	
	bout = Bopen(idxfile, OWRITE);
	if (!bout)
		goto Errout;
	
	Bprint(bout, "key=config "
		"nextid=\"%s\"\n"
		"\n",
		ultostr(nextid));
	
	for (s = statuses; s; s = s->next) {
		Bprint(bout, "key=status name=\"%s\"\n", s->name);
	}
	
	Bprint(bout, "\n");
	
	tid = 0;
	while (tid < nextid) {
		t = getgtask(tid, &s);
		if (t) {
			Bprint(bout, "key=task id=\"%s\" status=\"%s\"", ultostr(t->id), s->name);
			if (t->cassignee && strcmp(t->dir->uid, "na") != 0) {
				Bprint(bout, " assignee=\"%s\"", t->dir->uid);
			}
			if (t->cgroup && strcmp(t->dir->gid, "ng") != 0) {
				Bprint(bout, " group=\"%s\"", t->dir->gid);
			}
			if (t->title) {
				Bprint(bout, " title=\"%s\"", t->title);
			}
			Bprint(bout, "\n");
		}
		tid++;
	}
	
	Bterm(bout);
	index = ndbopen(idxfile);
	
	return 1;
	
Errout:
	index = ndbopen(idxfile);
	return 0;
}

Status*
getstatus(char *name, int *id)
{
	Status *s;
	int i;
	
	if (!statuses)
		return nil;
	
	i = 0;
	s = statuses;
	while (s && (strcmp(s->name, name) != 0)) {
		i++;
		s = s->next;
	}
	
	if (id)
		*id = i;
	return s; /* found or nil */
}

Status*
getnstatus(int id)
{
	Status *s;
	int i;
	
	i = 0;
	s = statuses;
	while (i != id && s->next) {
		i++;
		s = s->next;
	}
	return i == id ? s : nil;
}

static int
getsid(Status *s)
{
	int i = 0;
	for (Status *n = statuses; n; n = n->next) {
		if (s == n)
			return i;
		i++;
	}
	return -1;
}

int
addstatus(char *name)
{
	Status *s;
	
	if (!statuses) {
		statuses = mallocz(sizeof(Status), 1);
		statuses->name = strdup(name);
		return 1;
	}
	
	if (getstatus(name, nil)) {
		werrstr("status '%s' already exists", name);
		return 0;
	}
	
	for (s = statuses; s->next; s = s->next)
		;
	
	s->next = mallocz(sizeof(Status), 1);
	s = s->next;
	s->name = strdup(name);
	return 1;
}

Task*
gettask(char *status, char *name, int *task)
{
	Status *s;
	Task *t;
	int i;
	ulong l;
	
	s = getstatus(status, nil);
	if (!s)
		return nil;
	
	l = strtoul(name, nil, 16);
	
	t = s->tasks;
	i = 0;
	while (t && l != t->id) {
		t = t->next;
		i++;
	}
	
	if (task)
		*task = i;
	
	return t; /* found or nil */
}

Task*
getstask(char *name, int *status, int *task)
{
	Status *s;
	Task *t;
	int i, j;
	
	i = 0;
	for (s = statuses; s; s = s->next, i++) {
		t = gettask(s->name, name, &j);
		if (t) {
			if (status)
				*status = i;
			if (task)
				*task = j;
			return t;
		}
	}
	return nil;
}

Task*
getftask(char *fname, int *status, int *task)
{
	Task *t;
	Status *s;
	int i, j;
	
	i = 0;
	for (s = statuses; s; s = s->next, i++) {
		t = s->tasks;
		j = 0;
		while (t) {
			if (strcmp(t->dir->name, fname) == 0) {
				if (status)
					*status = i;
				if (task)
					*task = t->id;
				return t;
			}
			t = t->next;
			j++;
		}
	}
	return nil;
}

Task*
getgtask(ulong id, Status **status)
{
	Task *t;
	Status *s;
	
	for (s = statuses; s; s = s->next) {
		t = s->tasks;
		while (t) {
			if (t->id == id) {
				if (status)
					*status = s;
				return t;
			}
			t = t->next;
		}
	}
	werrstr("task not found");
	return nil;
}

Task*
getntask(char *status, int id)
{
	Status *s;
	int i;
	Task *t;
	
	s = getstatus(status, nil);
	if (!s)
		return nil;
	
	i = 0;
	t = s->tasks;
	while (i != id && t->next) {
		i++;
		t = t->next;
	}
	return i == id ? t : nil;
}

void
freetask(Task *t)
{
	if (t->cassignee)
		free(t->dir->uid);
	if (t->cgroup)
		free(t->dir->gid);
	if (t->cfname)
		free(t->dir->name);
	if (t->title)
		free(t->title);
	if (t->dataname)
		free(t->dataname);
	if (t->ext)
		free(t->ext);
	free(t->dir);
	
	t->cassignee = 0;
	t->cgroup = 0;
	t->cfname = 0;
	t->title = nil;
	t->dir = nil;
	t->dataname = nil;
	t->ext = nil;
}

static void
settaskassignee(Task *t, char *assignee)
{
	assert(t);
	
	if (t->cassignee)
		free(t->dir->uid);
	
	t->dir->uid = strdup(assignee && assignee[0] ? assignee : "na");
	t->cassignee = 1;
}

static void
settaskgroup(Task *t, char *group)
{
	assert(t);
	
	if (t->cgroup)
		free(t->dir->uid);
	
	t->dir->gid = strdup(group && group[0] ? group : "ng");
	t->cgroup = 1;
}

static int
settasktitle(Task *t, char *title)
{
	char buf[64], *c;
	
	if (t->title)
		free(t->title);
	
	if (t->cfname && t->dir->name)
		free(t->dir->name);
	
	if (title && title[0]) {
		t->title = strdup(title);
		
		snprint(buf, sizeof(buf), "%s", title);
		for (c = buf; *c; c++) {
			if (*c == ' ' || *c == '\t')
				*c = '_';
		}
		t->dir->name = smprint("%s-%s", ultostr(t->id), buf);
	} else {
		t->title = nil;
		t->dir->name = smprint("%s", ultostr(t->id));
	}
	t->cfname = 1;
	return 1;
}

static int
updatetask(Task *t, char *name, char *assignee, char *group, char *title)
{
	char buf[32];
	char *ext;
	Dir *dir;
	int fd;
	int n, i;
	freetask(t);
	
	fd = open(".", OREAD);
	if (fd < 0)
		sysfatal("unable to open dir: %r");
	
	ext = nil;
	i = 0;
	while (n = dirread(fd, &dir)) {
		for (i = 0; i < n; i++) {
			if (strcmp(dir[i].name, "..") == 0)
				continue;
			ext = strchr(dir[i].name, '.');
			if (ext) {
				snprint(buf, sizeof(buf), "%s.", name);
				if (strncmp(buf, dir[i].name, strlen(buf)) == 0) {
					goto Break;
				}
			} else {
				if (strcmp(name, dir[i].name) == 0)
					goto Break;
			}
		}
		free(dir);
	}
Break:
	close(fd);
	
	if (!dir) {
		werrstr("task not found: %s", name);
		return 0;
	}
	
	t->dir = dirstat(dir[i].name);
	free(dir);
	
	if (!ext) {
		t->dataname = strdup(qnames[Qdata]);
	} else {
		t->dataname = smprint("%s%s", qnames[Qdata], ext);
	}
	
	t->ext = ext ? strdup(ext) : nil;
	
	settaskassignee(t, assignee);
	settaskgroup(t, group);
	
	return settasktitle(t, title);
}

int
addtask(char *status, char *name, char *assignee, char *group, char *title, ulong id)
{
	Status *s;
	Task *t;
	
	s = getstatus(status, nil);
	if (!s) {
		werrstr("status %s not found", status);
		return 0;
	}
	
	t = getstask(name, nil, nil);
	if (t) {
		t->id = id;
		return updatetask(t, name, assignee, group, title);
	}
	
	if (!s->tasks) {
		s->tasks = mallocz(sizeof(Task), 1);
		t = s->tasks;
	} else {
		for (t = s->tasks; t->next; t = t->next)
			;
		t->next = mallocz(sizeof(Task), 1);
		t = t->next;
	}
	
	t->id = id;
	return updatetask(t, name, assignee, group, title);
}

void
readstatuses(void)
{
	Ndbtuple *tuple, *val;
	Ndbs s;
	
	if (ndbchanged(index))
		ndbreopen(index);
	
	for (tuple = ndbsearch(index, &s, "key", "status"); tuple != nil; tuple = ndbsnext(&s, "key", "status")) {
		if (val = ndbfindattr(tuple, s.t, "name")) {
			addstatus(val->val);
		} else
			sysfatal("invalid index file");
	}
}

static void
readtasks(void)
{
	Ndbtuple *tuple, *val, *sval;
	Ndbtuple *assignee, *group, *tval;
	Ndbs ns;
	ulong id;
	
	if (ndbchanged(index))
		ndbreopen(index);
	
	tuple = ndbsearch(index, &ns, "key", "config");
	if (!tuple)
		sysfatal("bad index: missing config");
	val = ndbfindattr(tuple, ns.t, "nextid");
	if (!val)
		sysfatal("bad config: missing nextid");
	nextid = strtoul(val->val, nil, 16);
	
	for (tuple = ndbsearch(index, &ns, "key", "task"); tuple != nil; tuple = ndbsnext(&ns, "key", "task")) {
		if ((val = ndbfindattr(tuple, ns.t, "id")) &&
			(sval = ndbfindattr(tuple, ns.t, "status"))) {
			assignee = ndbfindattr(tuple, ns.t, "assignee");
			group = ndbfindattr(tuple, ns.t, "group");
			tval = ndbfindattr(tuple, ns.t, "title");
			id = strtoul(val->val, nil, 16);
			addtask(sval->val, val->val, assignee ? assignee->val : nil, group ? group->val : nil, tval ? tval->val : nil, id);
		} else
			sysfatal("invalid index");
	}
}

static ulong
newtask(char *name, char *status)
{
	int fd;
	char *id;
	ulong newid;
	
	readstatuses();
	readtasks();
	
	id = ultostr(nextid);
	fd = create(id, OREAD, 0666);
	if (fd < 0) {
		werrstr("unable to open task file '%s'", name);
		return 0;
	}
	close(fd);
	
	if (!addtask(status, id, nil, nil, name, nextid)) {
		werrstr("cannot add task: %r");
		return 0;
	}
	
	newid = nextid++;
	return savedata() ? newid : 0;
}

void
fsopen(Req *r)
{
	respond(r, nil);
}

static char*
statusname(int id)
{
	Status *s = getnstatus(id);
	return s ? strdup(s->name) : nil;
}

static void
filltaskstat(Dir *d, Task *t)
{
	d->uid = estrdup9p(t->dir->uid);
	d->gid = estrdup9p(t->dir->gid);
	d->atime = t->dir->atime;
	d->mtime = t->dir->mtime;
}

static void
fillstat(Dir *d, uvlong path)
{
	int type = 0;
	Task *t;
	
	// memset(d, 0, sizeof(Dir));
	d->uid = estrdup9p("todo");
	d->gid = estrdup9p("todo");
	
	switch (qidtype(path)) {
	case Qdir:
	case Qstatus:
	case Qtask:
		type = QTDIR;
		break;
	case Qctl:
		type = 0;
		break;
	}
	d->qid = (Qid){path, 0, type};
	
	d->atime = d->mtime = 0;
	d->length = 0;
	
	if (qidtype(path) == Qtask) {
		d->length = 999;
	}
	
	t = getgtask(qidnum(path), nil);
	
	switch (qidtype(path)) {
	case Qdir:
		d->name = estrdup9p(qnames[Qdir]);
		d->mode = DMDIR|0555;
		break;
	case Qstatus:
		d->name = statusname(qidnum(path));
		d->mode = DMDIR|0555;
		break;
	case Qtask:
		d->name = estrdup9p(t->dir->name);
		d->mode = DMDIR|0555;
	case Qtitle:
		d->name = estrdup9p(qnames[Qtitle]);
		d->mode = 0666;
		goto Commontask;
	case Qdata:
		d->name = estrdup9p(t->dataname);
		d->mode = 0666;
		goto Commontask;
	case Qassignee:
		d->name = estrdup9p(qnames[Qassignee]);
		d->mode = 0666;
		goto Commontask;
	case Qgroup:
		d->name = estrdup9p(qnames[Qgroup]);
		d->mode = 0666;
		/* falls through */
	Commontask:
		filltaskstat(d, t);
		break;
	case Qctl:
		d->name = estrdup9p(qnames[Qctl]);
		d->mode = 0666;
		break;
	}
}

static int
rootgen(int i, Dir *d, void *aux)
{
	Status *s;
	USED(aux);
	
	switch (i) {
	case 0: /* ctl */
		fillstat(d, mkqid(Qctl, 0));
		return 0;
	default: /* statuses */
		i -= 1;
		s = getnstatus(i);
		if (!s)
			return -1;
		fillstat(d, mkqid(Qstatus, i));
		return 0;
	}
}

static int
statusgen(int i, Dir *d, void *aux)
{
	Status *s = (Status*)aux;
	Task *t;
	
	t = getntask(s->name, i);
	if (!t)
		return -1;
	
	d->name = strdup(t->dir->name);
	d->qid = (Qid){mkqid(Qtask, t->id), 0, QTDIR};
	d->mode = DMDIR|0555;
	d->atime = t->dir->atime;
	d->mtime = t->dir->mtime;
	d->length = t->dir->length;
	d->uid = strdup(t->dir->uid);
	d->gid = strdup(t->dir->gid);
	return 0;
}

static int
taskgen(int i, Dir *d, void *aux)
{
	Task *t = (Task*)aux;
	
	switch (i + Qtitle) {
	case Qtitle:
		d->name = strdup(qnames[Qtitle]);
		d->qid = (Qid){mkqid(Qtitle, t->id), 0, 0};
		d->length = t->title ? strlen(t->title) : 0;
		break;
	case Qdata:
		d->name = strdup(t->dataname);
		d->qid = (Qid){mkqid(Qdata, t->id), 0, 0};
		d->length = t->dir->length;
		break;
	case Qassignee:
		d->name = strdup(qnames[Qassignee]);
		d->qid = (Qid){mkqid(Qassignee, t->id), 0, 0};
		d->length = t->dir->uid ? strlen(t->dir->uid) : 0;
		break;
	case Qgroup:
		d->name = strdup(qnames[Qgroup]);
		d->qid = (Qid){mkqid(Qgroup, t->id), 0, 0};
		d->length = t->dir->gid ? strlen(t->dir->gid) : 0;
		break;
	default:
		return -1;
	}
	d->mode = 0666;
	filltaskstat(d, t);
	return 0;
}

int
taskread(Req *r)
{
	Task *t;
	char *f;
	Biobuf *bin;
	long n;
	
	t = getgtask(qidnum(r->fid->qid.path), nil);
	if (!t) {
		werrstr("task not found");
		return 0;
	}
	
	f = t->ext ? smprint("%s%s", ultostr(t->id), t->ext) : strdup(ultostr(t->id));
	
	bin = Bopen(f, OREAD);
	free(f);
	if (!bin)
		return 0;
	
	Bseek(bin, r->ifcall.offset, 0);
	n = Bread(bin, r->ofcall.data, r->ifcall.count);
	if (n < 0) {
		Bterm(bin);
		return 0;
	}
	r->ofcall.count = n;
	Bterm(bin);
	return 1;
}

void
fsread(Req *r)
{
	Status *s;
	Task *t;
	
	ulong qnum = qidnum(r->fid->qid.path);
	
	switch (qidtype(r->fid->qid.path)) {
	case Qdir:
		readstatuses();
		dirread9p(r, rootgen, nil);
		respond(r, nil);
		break;
	case Qstatus:
		readstatuses();
		s = getnstatus(qnum);
		readtasks();
		dirread9p(r, statusgen, s);
		respond(r, nil);
		break;
	case Qtask:
		readtasks();
		t = getgtask(qnum, nil);
		if (!t) {
			respond(r, "task not found");
			return;
		}
		dirread9p(r, taskgen, t);
		respond(r, nil);
		break;
	case Qtitle:
		readtasks();
		t = getgtask(qnum, nil);
		if (!t) {
			respond(r, "task not found");
			return;
		}
		if (t->title)
			readstr(r, t->title);
		respond(r, nil);
		break;
	case Qdata:
		if (!taskread(r))
			responderror(r);
		else
			respond(r, nil);
		break;
	case Qassignee:
		readtasks();
		t = getgtask(qnum, nil);
		if (!t) {
			respond(r, "task not found");
			return;
		}
		if (t->cassignee)
			readstr(r, t->dir->uid);
		respond(r, nil);
		break;
	case Qgroup:
		readtasks();
		t = getgtask(qnum, nil);
		if (!t) {
			respond(r, "task not found");
			return;
		}
		if (t->cgroup)
			readstr(r, t->dir->gid);
		respond(r, nil);
		break;
	case Qctl:
		respond(r, nil);
		break;
	default:
		respond(r, "error");
	}
}

int
taskwrite(Req *r)
{
	Task *t;
	Biobuf *bout;
	long n;
	
	t = getgtask(qidnum(r->fid->qid.path), nil);
	if (!t) {
		werrstr("task not found");
		return 0;
	}
	
	bout = Bopen(ultostr(t->id), OWRITE);
	if (!bout)
		return 0;
	
	Bseek(bout, r->ifcall.offset, 0);
	n = Bwrite(bout, r->ifcall.data, r->ifcall.count);
	if (n < 0) {
		Bterm(bout);
		return 0;
	}
	r->ofcall.count = n;
	Bterm(bout);
	return 1;
}

int
ctlwrite(Req *r)
{
	char str[256];
	char *args[5];
	int n;
	
	memset(str, 0, sizeof(str));
	memcpy(str, r->ifcall.data, r->ifcall.count);
	n = tokenize(str, args, 5);
	
	if (n < 1)
		return 1;
	
	if (strcmp(args[0], "addstatus") == 0) {
		if (n != 2)
			goto Addstatuserr;
		if (args[1] && *args[1]) {
			if (addstatus(args[1]))
				return savedata();
			return 1;
		}
Addstatuserr:
		werrstr("usage: addstatus status");
		return 0;
	}
	
	if (strcmp(args[0], "dump") == 0) {
		if (n != 1) {
			werrstr("usage: dump");
			return 0;
		}
		readtasks();
		return savedata();
	}
	
	werrstr("invalid command: '%s'", args[0]);
	return 0;
}

static int
taskwritefield(Task *t, int field, Req *r)
{
	char *s;
	
	if (r->ifcall.offset != 0) {
		werrstr("write offset not 0");
		return 0;
	}
	
	if (r->ifcall.count == 0) {
		switch(field) {
		case Qtitle:
			settasktitle(t, nil);
			break;
		case Qassignee:
			settaskassignee(t, nil);
			break;
		case Qgroup:
			settaskgroup(t, nil);
			break;
		}
		return 1;
	}
	
	s = r->ifcall.data;
	r->ifcall.data[r->ifcall.count-1] = 0; // last byte 0
	for (int i = 0; i < r->ifcall.count; i++) {
		if (s[i] == '\n') {
			s[i] = 0;
			break;
		}
	}
	
	switch (field) {
	case Qtitle:
		settasktitle(t, s);
		break;
	case Qassignee:
		settaskassignee(t, s);
		break;
	case Qgroup:
		settaskgroup(t, s);
		break;
	default:
		sysfatal("cannot happen");
	}
	
	return savedata();
}

void
fswrite(Req *r)
{
	Task *t;
	ulong qnum;
	int qtype;
	
	qnum = qidnum(r->fid->qid.path);
	qtype = qidtype(r->fid->qid.path);
	
	switch (qtype) {
	case Qctl:
		if (!ctlwrite(r))
			responderror(r);
		else
			respond(r, nil);
		break;
	case Qtitle:
	case Qassignee:
	case Qgroup:
		t = getgtask(qnum, nil);
		if (!t) {
			responderror(r);
			break;
		}
		if (!taskwritefield(t, qtype, r)) {
			responderror(r);
			break;
		}
		r->ofcall.count = r->ifcall.count;
		respond(r, nil);
		break;
	case Qdata:
		if (!taskwrite(r))
			responderror(r);
		else
			respond(r, nil);
		break;
	case Qtask:
	case Qdir:
	case Qstatus:
		respond(r, nil);
		break;
	default:
		respond(r, "error");
	}
}

int
taskmv(Task *t, Status *from, Status *to)
{
	Task *prev;
	
	if (from->tasks == t) {
		from->tasks = t->next;
		goto Chain;
	}
	
	for (prev = from->tasks; prev->next && prev->next != t; prev = prev->next)
		;
	
	prev->next = t->next;
	
Chain:
	t->next = nil;
	if (!to->tasks) {
		to->tasks = t;
		return 1;
	}
	for (prev = to->tasks; prev->next; prev = prev->next)
		;
	prev->next = t;
	return 1;
}

void
fscreate(Req *r)
{
	Status *s, *sfrom;
	Task *t;
	int sid;
	ulong id, qnum;
	
	qnum = qidnum(r->fid->qid.path);
	
	switch (qidtype(r->fid->qid.path)) {
	case Qstatus:
		s = getnstatus(qnum);
		if (!s) {
			respond(r, "status not found");
			return;
		}
		id = strtoid(r->ifcall.name);
		t = getgtask(id, &sfrom);
		sid = getsid(sfrom);
		if (t) {
			/* move existing task */
			if (sid == qnum) {
				respond(r, "task already has status");
				return;
			}
			if (!taskmv(t, sfrom, s)) {
				responderror(r);
				return;
			}
			r->fid->qid = (Qid){mkqid(Qtask, t->id), 0, QTDIR};
			r->ofcall.qid = r->fid->qid;
		} else {
			/* create new task */
			id = newtask(r->ifcall.name, s->name);
			if (!id) {
				responderror(r);
				return;
			}
			t = getgtask(id, nil);
			if (!t) {
				respond(r, "error creating new task");
				return;
			}
			r->fid->qid = (Qid){mkqid(Qtask, t->id), 0, QTDIR};
			r->ofcall.qid = r->fid->qid;
		}
		savedata();
		break;
	default:
		respond(r, "not allowed");
		return;
	}
	respond(r, nil);
}

static void
fsattach(Req *r)
{
	r->ofcall.qid = (Qid){Qdir, 0, QTDIR};
	r->fid->qid = r->ofcall.qid;
	r->fid->aux = 0;
	respond(r, nil);
}

static char*
fswalk1(Fid *fid, char *name, Qid *qid)
{
	int isdotdot;
	Status *s;
	Task *t;
	int sid, tid;
	ulong qnum;
	
	isdotdot = strcmp(name, "..") == 0;
	qnum = qidnum(fid->qid.path);
	
	switch (qidtype(fid->qid.path)) {
	case Qdir:
		if (isdotdot) {
			*qid = fid->qid;
			return nil;
		}
		if (strcmp(name, qnames[Qctl]) == 0) {
			*qid = (Qid){mkqid(Qctl, 0), 0, 0};
			return nil;
		}
		s = getstatus(name, &sid);
		if (!s)
			return "file not found";
		*qid = (Qid){mkqid(Qstatus, sid), 0, QTDIR};
		return nil;
	case Qstatus:
		if (isdotdot) {
			*qid = (Qid){mkqid(Qdir, 0), 0, QTDIR};
			return nil;
		}
		if (t = getftask(name, &sid, &tid)) {
			if (qnum != sid) {
				return "task has a different status";
			}
			*qid = (Qid){mkqid(Qtask, t->id), 0, QTDIR};
			return nil;
		}
		return "task not found";
	case Qtask:
		t = getgtask(qnum, &s);
		if (!t) {
			return "task not found";
		}
		sid = getsid(s);
		if (isdotdot) {
			*qid = (Qid){mkqid(Qstatus, sid), 0, QTDIR};
			return nil;
		}
		if (strcmp(name, qnames[Qtitle]) == 0) {
			*qid = (Qid){mkqid(Qtitle, qnum), 0, 0};
			return nil;
		}
		if (strcmp(name, qnames[Qassignee]) == 0) {
			*qid = (Qid){mkqid(Qassignee, qnum), 0, 0};
			return nil;
		}
		if (strcmp(name, qnames[Qgroup]) == 0) {
			*qid = (Qid){mkqid(Qgroup, qnum), 0, 0};
			return nil;
		}
		if (strcmp(name, t->dataname) == 0) {
			*qid = (Qid){mkqid(Qdata, qnum), 0, 0};
			return nil;
		}
		if (strcmp(name, qnames[Qdata]) == 0) {
			*qid = (Qid){mkqid(Qdata, qnum), 0, 0};
			return nil;
		}
	}
	return "error";
}

static void
fsstat(Req *r)
{
	fillstat(&r->d, r->fid->qid.path);
	respond(r, nil);
}

static void
fswstat(Req *r)
{
	Task *t;
	int save = 0;
	
	switch (qidtype(r->fid->qid.path)) {
	case Qtask:
		t = getgtask(qidnum(r->fid->qid.path), nil);
		if (!t) {
			respond(r, "invalid task");
			return;
		}
		
		if (r->d.uid && r->d.uid[0]) {
			if (t->cassignee && t->dir->uid)
				free(t->dir->uid);
			t->dir->uid = strdup(r->d.uid);
			t->cassignee = 1;
			save++;
		}
		if (r->d.gid && r->d.gid[0]) {
			if (t->cgroup && t->dir->gid)
				free(t->dir->gid);
			t->dir->gid = strdup(r->d.gid);
			t->cgroup = 1;
			save++;
		}
		if (r->d.name && r->d.name[0]) {
			settasktitle(t, r->d.name);
			save++;
		}
		
		if (save)
			savedata();
		
		break;
	default:
		respond(r, "invalid operation");
		return;
	}
	respond(r, nil);
}

Srv fs = {
	.attach = fsattach,
	.open = fsopen,
	.read = fsread,
	.write = fswrite,
	.create = fscreate,
	.walk1 = fswalk1,
	.stat = fsstat,
	.wstat = fswstat,
};

void
main(int argc, char **argv)
{
	char *srvname = nil;
	char *mtpt = "/mnt/todo";
	int gen = 0;
	int fd;
	
	ARGBEGIN{
	case 's':
		srvname = EARGF(usage());
		break;
	case 'm':
		mtpt = EARGF(usage());
		break;
	case 'c':
		gen++;
		break;
	case '9':
		chatty9p++;
		break;
	default:
		usage();
	}ARGEND;
	
	if (argc != 1)
		usage();
	
	quotefmtinstall();
	
	srcdir = *argv;
	
	if (chdir(srcdir))
		sysfatal("unable to chdir: %r");
	
	idxfile = "index";
	if (gen) {
		fd = create(idxfile, OWRITE, 0666);
		if (fd < 0)
			sysfatal("unable to create index file: %r");
		fprint(fd, "key=config nextid=\"1\"\n");
		close(fd);
		exits(0);
	}
	
	assert(idxfile);
	index = ndbopen(idxfile);
	if (!index)
		sysfatal("unable to open index file: %r");
	
	postmountsrv(&fs, srvname, mtpt, MREPL|MCREATE);
	exits(0);
}