shithub: todofs

ref: 6dc0bc759e48e1fc98cd5faf7237c128dd8eb914
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,
};

char *qnames[] = {
	[Qdir] "/",
	[Qctl] "ctl",
	[Qstatus] nil,
	[Qtask] nil,
};

int
qidtype(ulong path)
{
	// -----00
	return path & 3;
}

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

ulong
mkqid(int type, int num)
{
	return (num << 2) | type & 3;
}

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;
	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) {
				Bprint(bout, " assignee=\"%s\"", t->dir->uid);
			}
			if (t->cgroup) {
				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;
}

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 = j;
				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;
		}
	}
	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);
	free(t->dir);
	
	t->cassignee = 0;
	t->cgroup = 0;
	t->cfname = 0;
	t->title = nil;
	t->dir = nil;
}

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->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)
{
	freetask(t);
	
	t->dir = dirstat(name);
	
	t->dir->uid = strdup(assignee ? assignee : "na");
	t->cassignee = 1;
	
	t->dir->gid = strdup(group ? group : "ng");
	t->cgroup = 1;
	
	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
fillstat(Dir *d, uvlong path)
{
	int type = 0;
	
	// memset(d, 0, sizeof(Dir));
	d->uid = estrdup9p("todo");
	d->gid = estrdup9p("todo");
	
	switch (qidtype(path)) {
	case Qdir:
	case Qstatus:
		type = QTDIR;
		break;
	case Qctl:
	case Qtask:
		type = 0;
		break;
	}
	d->qid = (Qid){path, 0, type};
	
	d->atime = d->mtime = 0;
	d->length = 0;
	
	if (qidtype(path) == Qtask) {
		d->length = 999;
	}
	
	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 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;
	int snum;
	Task *t;
	
	getstatus(s->name, &snum);
	
	t = getntask(s->name, i);
	if (!t)
		return -1;
	
	d->name = strdup(t->dir->name);
	d->qid = (Qid){mkqid(Qtask, t->id), 0, 0};
	d->mode = 0666;
	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;
}

int
taskread(Req *r)
{
	Task *t;
	Biobuf *bin;
	long n;
	
	t = getgtask(qidnum(r->fid->qid.path), nil);
	if (!t) {
		werrstr("task not found");
		return 0;
	}
	
	bin = Bopen(ultostr(t->id), OREAD);
	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;
	
	switch (qidtype(r->fid->qid.path)) {
	case Qdir:
		readstatuses();
		dirread9p(r, rootgen, nil);
		respond(r, nil);
		break;
	case Qstatus:
		readstatuses();
		s = getnstatus(qidnum(r->fid->qid.path));
		readtasks();
		dirread9p(r, statusgen, s);
		respond(r, nil);
		break;
	case Qtask:
		readtasks();
		if (!taskread(r))
			responderror(r);
		else
			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]) {
			addstatus(args[1]);
			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;
}

void
fswrite(Req *r)
{
	switch (qidtype(r->fid->qid.path)) {
	case Qctl:
		if (!ctlwrite(r))
			responderror(r);
		else
			respond(r, nil);
		break;
	case Qtask:
		if (!taskwrite(r))
			responderror(r);
		else
			respond(r, nil);
		break;
	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, *s2;
	Task *t;
	int tid, sid;
	ulong id;
	
	switch (qidtype(r->fid->qid.path)) {
	case Qstatus:
		s = getnstatus(qidnum(r->fid->qid.path));
		if (!s) {
			respond(r, "status not found");
			return;
		}
		id = strtoid(r->ifcall.name);
		fprint(2, "id of task: %ulx\n", id);
		t = getgtask(id, &s2);
		if (t) {
			/* move existing task */
			if (sid == qidnum(r->fid->qid.path)) {
				respond(r, "task already has status");
				return;
			}
			if (!taskmv(t, s2, s)) {
				responderror(r);
				return;
			}
			r->fid->qid = (Qid){mkqid(Qtask, t->id), 0, 0};
			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, 0};
			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;
	
	isdotdot = strcmp(name, "..") == 0;
	
	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)) {
			*qid = (Qid){mkqid(Qtask, t->id), 0, 0};
			return nil;
		}
		return "file not found";
	}
	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";
	
	ARGBEGIN{
	case 's':
		srvname = EARGF(usage());
		break;
	case 'm':
		mtpt = EARGF(usage());
		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";
	assert(idxfile);
	index = ndbopen(idxfile);
	if (!index)
		sysfatal("unable to open index file: %r");
	
	postmountsrv(&fs, srvname, mtpt, MREPL|MCREATE);
	exits(0);
}