ref: a05459a8d6a5a190db2fe52a89ed22bf515834b4
parent: 6dc0bc759e48e1fc98cd5faf7237c128dd8eb914
author: sirjofri <[email protected]>
date: Thu Jun 27 12:14:19 EDT 2024
makes each task a directory, with files
--- a/README
+++ b/README
@@ -12,6 +12,8 @@
key=config nextid=1
+todofs -c path/to/dir initializes the directory with this index file.
+
It will be filled with metadata when creating tasks and statuses.
@@ -25,16 +27,18 @@
Imagine the following scenario:
- /mnt/todo/StatusA/1-First_Task
- /mnt/todo/StatusB/2-Second_Task
+ /mnt/todo/StatusA/1-First_Task/
+ /mnt/todo/StatusB/2-Second_Task/
; cd /mnt/todo
-To move task 1 to StatusB, use create:
+To move task 1 to StatusB, use create with the task id:
; touch StatusB/1
-To edit a task, just open/read/write the task file. This will be forwarded to files-on-disk automatically.
+To edit a task, just open/read/write the task/data file. This will be forwarded to files-on-disk automatically.
+The data file can have a file extension, depending on the file extension. This should be used for plumbing. Adding a file extension using the filesystem is a TODO.
+
To rename a task:
; mv StatusB/2-Second_Task 'StatusB/My other task'
@@ -53,3 +57,10 @@
; chgrp engineering StatusA/3-New_Task
; ls -l StatusA
... alice engineering ... 3-New_Task
+
+It is also possible to change the metadata by file. The filesystem looks like this:
+
+ /mnt/todo/StatusA/1-First_Task/title
+ /mnt/todo/StatusA/1-First_Task/assignee
+ /mnt/todo/StatusA/1-First_Task/group
+ /mnt/todo/StatusA/1-First_Task/data (or data.extension)
--- a/todofs.c
+++ b/todofs.c
@@ -31,6 +31,10 @@
Qctl,
Qstatus,
Qtask,
+ Qtitle,
+ Qdata,
+ Qassignee,
+ Qgroup,
};
char *qnames[] = {
@@ -38,26 +42,30 @@
[Qctl] "ctl",
[Qstatus] nil,
[Qtask] nil,
+ [Qtitle] "title",
+ [Qdata] "data",
+ [Qassignee] "assignee",
+ [Qgroup] "group",
};
int
qidtype(ulong path)
{
- // -----00
- return path & 3;
+ // -----0000
+ return path & 15;
}
ulong
qidnum(ulong path)
{
- // 000000--
- return path >> 2;
+ // 000000----
+ return path >> 4;
}
ulong
mkqid(int type, int num)
{
- return (num << 2) | type & 3;
+ return (num << 4) | type & 15;
}
static char*
@@ -82,6 +90,8 @@
struct Task {
Task* next;
char *title;
+ char *dataname;
+ char *ext;
ulong id;
Dir *dir;
int cassignee;
@@ -132,10 +142,10 @@
t = getgtask(tid, &s);
if (t) {
Bprint(bout, "key=task id=\"%s\" status=\"%s\"", ultostr(t->id), s->name);
- if (t->cassignee) {
+ if (t->cassignee && strcmp(t->dir->uid, "na") != 0) {
Bprint(bout, " assignee=\"%s\"", t->dir->uid);
}
- if (t->cgroup) {
+ if (t->cgroup && strcmp(t->dir->gid, "ng") != 0) {
Bprint(bout, " group=\"%s\"", t->dir->gid);
}
if (t->title) {
@@ -192,6 +202,18 @@
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)
{
@@ -281,7 +303,7 @@
if (status)
*status = i;
if (task)
- *task = j;
+ *task = t->id;
return t;
}
t = t->next;
@@ -308,6 +330,7 @@
t = t->next;
}
}
+ werrstr("task not found");
return nil;
}
@@ -342,6 +365,10 @@
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;
@@ -349,8 +376,34 @@
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)
{
@@ -372,6 +425,7 @@
}
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;
@@ -381,16 +435,58 @@
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);
- t->dir = dirstat(name);
+ fd = open(".", OREAD);
+ if (fd < 0)
+ sysfatal("unable to open dir: %r");
- t->dir->uid = strdup(assignee ? assignee : "na");
- t->cassignee = 1;
+ 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);
- t->dir->gid = strdup(group ? group : "ng");
- t->cgroup = 1;
+ 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);
}
@@ -516,9 +612,19 @@
}
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");
@@ -527,10 +633,10 @@
switch (qidtype(path)) {
case Qdir:
case Qstatus:
+ case Qtask:
type = QTDIR;
break;
case Qctl:
- case Qtask:
type = 0;
break;
}
@@ -543,6 +649,8 @@
d->length = 999;
}
+ t = getgtask(qidnum(path), nil);
+
switch (qidtype(path)) {
case Qdir:
d->name = estrdup9p(qnames[Qdir]);
@@ -552,6 +660,28 @@
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;
@@ -583,18 +713,15 @@
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->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;
@@ -603,10 +730,45 @@
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;
@@ -616,7 +778,10 @@
return 0;
}
- bin = Bopen(ultostr(t->id), OREAD);
+ 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;
@@ -635,7 +800,10 @@
fsread(Req *r)
{
Status *s;
+ Task *t;
+ ulong qnum = qidnum(r->fid->qid.path);
+
switch (qidtype(r->fid->qid.path)) {
case Qdir:
readstatuses();
@@ -644,7 +812,7 @@
break;
case Qstatus:
readstatuses();
- s = getnstatus(qidnum(r->fid->qid.path));
+ s = getnstatus(qnum);
readtasks();
dirread9p(r, statusgen, s);
respond(r, nil);
@@ -651,11 +819,53 @@
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;
@@ -710,7 +920,8 @@
if (n != 2)
goto Addstatuserr;
if (args[1] && *args[1]) {
- addstatus(args[1]);
+ if (addstatus(args[1]))
+ return savedata();
return 1;
}
Addstatuserr:
@@ -731,10 +942,68 @@
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)
{
- switch (qidtype(r->fid->qid.path)) {
+ 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);
@@ -741,12 +1010,28 @@
else
respond(r, nil);
break;
- case Qtask:
+ 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);
@@ -786,32 +1071,34 @@
void
fscreate(Req *r)
{
- Status *s, *s2;
+ Status *s, *sfrom;
Task *t;
- int tid, sid;
- ulong id;
+ int sid;
+ ulong id, qnum;
+ qnum = qidnum(r->fid->qid.path);
+
switch (qidtype(r->fid->qid.path)) {
case Qstatus:
- s = getnstatus(qidnum(r->fid->qid.path));
+ s = getnstatus(qnum);
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);
+ t = getgtask(id, &sfrom);
+ sid = getsid(sfrom);
if (t) {
/* move existing task */
- if (sid == qidnum(r->fid->qid.path)) {
+ if (sid == qnum) {
respond(r, "task already has status");
return;
}
- if (!taskmv(t, s2, s)) {
+ if (!taskmv(t, sfrom, s)) {
responderror(r);
return;
}
- r->fid->qid = (Qid){mkqid(Qtask, t->id), 0, 0};
+ r->fid->qid = (Qid){mkqid(Qtask, t->id), 0, QTDIR};
r->ofcall.qid = r->fid->qid;
} else {
/* create new task */
@@ -825,7 +1112,7 @@
respond(r, "error creating new task");
return;
}
- r->fid->qid = (Qid){mkqid(Qtask, t->id), 0, 0};
+ r->fid->qid = (Qid){mkqid(Qtask, t->id), 0, QTDIR};
r->ofcall.qid = r->fid->qid;
}
savedata();
@@ -853,8 +1140,10 @@
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:
@@ -877,10 +1166,43 @@
return nil;
}
if (t = getftask(name, &sid, &tid)) {
- *qid = (Qid){mkqid(Qtask, t->id), 0, 0};
+ if (qnum != sid) {
+ return "task has a different status";
+ }
+ *qid = (Qid){mkqid(Qtask, t->id), 0, QTDIR};
return nil;
}
- return "file not found";
+ 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";
}
@@ -952,6 +1274,8 @@
{
char *srvname = nil;
char *mtpt = "/mnt/todo";
+ int gen = 0;
+ int fd;
ARGBEGIN{
case 's':
@@ -960,6 +1284,9 @@
case 'm':
mtpt = EARGF(usage());
break;
+ case 'c':
+ gen++;
+ break;
case '9':
chatty9p++;
break;
@@ -978,6 +1305,15 @@
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)