## A filesystem for viewing trello Since my last note [here](http://pmikkelsen.com/plan9/basic_9p_server) was very simple, here comes a small program (around 250 lines of C), which makes it possible to view trello as a read-only filesystem. ## Usage example First set the `trellokey` and `trellotoken` environment variables to the API key and token from the trello website. term% trellokey=1298791723987937123..... term% trellotoken=923748237497ab798798999999..... Then `webfs` must be started so the program can make requests to the trello REST api. term% webfs Then `trellofs` itself must be started. It mounts the filesystem under `/mnt/trello`. term% trellofs If I now go and see which boards I have, I get the following term% cd /mnt/trello term% ls Beaglebone_Black Fly Fysik_Rapport Guld_kobber Ideer_til_2._produkt Lektier P0 P1 P2 Projekt_Materialer Spil Welcome_Board The names of the folders are almost like the original board names, except that that some characters have been replaced by `_`. Lets look at `P1` term% cd P1 term% ls Backlog Done Estimat In-progress Info Master.C_Top_Down_Ting Sprint_1 Test Test_sprint_1 Todo Todo must have something worth looking at term% ls Færdig_rapport_aflevering Korrekturlæsning Those two items are the actual trello "cards", which I think of as tasks. The first one means "Final report hand in", so let's have a look at that. term% cd Færdig_rapport_aflevering term% ls description duedate url There are always three files inside each card folder, but since the information in them is not mandatory on trello, some of them will be empty. term% cat description term% cat duedate 2019-12-18T13:00:00.000Z term% cat url https://trello.com/c/u23ZcNAm/9-f%C3%A6rdig-rapport-aflevering ## Limitations The key and token are right now in environment variables, which is somewhat awkward. Also I would like to be able to actually create and move cards directly from the filesystem just by creating files, but that is not something I will implement right now. ## The code The code for the `trellofs` program is listed below. #include #include #include #include #include <9p.h> #include #define MAX_RESPONSE_SIZE (1024 * 1024 * 8) char *key; char *token; void fsread(Req *r) { char *str; if (r->fid->file->aux == nil) { readstr(r, ""); } else { str = malloc(strlen(r->fid->file->aux) + 2); sprint(str, "%s\n", r->fid->file->aux); readstr(r, str); } respond(r, nil); } Srv fs = { .read = fsread, }; JSON * trelloget(char *endpoint, char *params) { int ctlfd, bodyfd; int n; int err; char *buf; JSON *res; res = nil; bodyfd = -1; buf = malloc(MAX_RESPONSE_SIZE + 1); if(buf == nil) { perror("malloc"); return nil; } ctlfd = open("/mnt/web/clone", ORDWR); if(ctlfd < 0) { perror("open"); goto fail; } if(read(ctlfd, buf, 32) < 0) { perror("read"); goto fail; } n = atoi(buf); sprint(buf, "url https://api.trello.com/%s?key=%s&token=%s", endpoint, key, token); if(params) sprint(buf + strlen(buf), "\&%s\n", params); else sprint(buf + strlen(buf), "\n"); err = write(ctlfd, buf, strlen(buf)); if(err < 0){ perror("write"); goto fail; } sprint(buf, "/mnt/web/%d/body", n); bodyfd = open(buf, OREAD); if(bodyfd < 0){ perror("open"); goto fail; } err = readn(bodyfd, buf, MAX_RESPONSE_SIZE); if (err < 0) { perror("read"); goto fail; } res = jsonparse(buf); fail: close(ctlfd); close(bodyfd); free(buf); return res; } char * escapename(char *str) { char *new; int i, len; len = strlen(str); new = malloc(len + 1); for(i = 0; i <= len; i++){ switch(str[i]){ case '/': case ' ': case ',': case ':': case '"': case '\'': new[i] = '_'; break; default: new[i] = str[i]; } } return new; } void addcard(File *dir, JSON *card) { JSON *element; char *filename; File *carddir; char *description; char *url; char *duedate; element = jsonbyname(card, "name"); filename = escapename(element->s); carddir = createfile(dir, filename, nil, DMDIR|0555, nil); if(carddir == nil){ perror("createfile"); return; } else { element = jsonbyname(card, "desc"); description = strdup(element->s); element = jsonbyname(card, "url"); url = strdup(element->s); element = jsonbyname(card, "due"); if (element->t == JSONString) duedate = strdup(element->s); else duedate = nil; createfile(carddir, "description", nil, 0444, description); createfile(carddir, "url", nil, 0444, url); createfile(carddir, "duedate", nil, 0444, duedate); } free(filename); } void addlist(File *dir, JSON *list) { JSON *element; char *filename; JSON *cards; JSONEl *card; char cardsEndpoint[128]; File *listdir; element = jsonbyname(list, "name"); filename = escapename(element->s); listdir = createfile(dir, filename, nil, DMDIR|0555, nil); if(listdir == nil){ perror("createfile"); return; } else { element = jsonbyname(list, "id"); sprint(cardsEndpoint, "1/lists/%s/cards", element->s); cards = trelloget(cardsEndpoint, "fields=desc,name,url,due"); if(cards == nil) return; for(card = cards->first; card != nil; card = card->next){ addcard(listdir, card->val); } } jsonfree(cards); free(filename); } void addboard(File *root, JSON *board) { JSON *element; JSONEl *list; char *filename; File *boarddir; element = jsonbyname(board, "name"); filename = escapename(element->s); boarddir = createfile(root, filename, nil, DMDIR|0555, nil); if(boarddir == nil) { perror("createfile"); return; } element = jsonbyname(board, "lists"); for(list = element->first; list != nil; list = list->next){ addlist(boarddir, list->val); } free(filename); } void trelloinit(File *root) { JSON *result; JSONEl *board; result = trelloget("1/members/me/boards", "fields=name,lists&lists=open"); for(board = result->first; board != nil; board = board->next){ addboard(root, board->val); } jsonfree(result); } void main(void) { JSONfmtinstall(); key = getenv("trellokey"); token = getenv("trellotoken"); Tree *tree; tree = alloctree(nil, nil, DMDIR|0555, nil); fs.tree = tree; trelloinit(tree->root); postmountsrv(&fs, nil, "/mnt/trello", MREPL | MCREATE); } The program can be compiled by hand or by using the following `mkfile`. BIN=/usr/glenda/bin/amd64 TARG=trellofs OFILES=\ main.$O\