Peter's website - random notes

A filesystem for viewing trello

Since my last note here 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 <u.h>
#include <libc.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include <json.h>

#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\

</sys/src/cmd/mkone