Montando um Postgres como um filesystem

Sem ter um problema específico, queria fazer uma prova de conceito: Como seria montar um banco de dados Postgres como uma estrutura de diretório?

Um jeito simples de se fazer isto seria utilizando FUSE (Filesystem in userspace). Sendo assim criei uma prova-de-conceito primeiramente em C, visto que em Rust a crate está instável e quebrando constantemente.

Em resumo, crio um diretório onde será montada a estrutura do banco.

➤ mkdir testefs/
➤ ls -la testefs/
total 8
drwxr-xr-x 2 guedes guedes 4096 set  2 20:27 ./
drwxr-xr-x 9 guedes guedes 4096 set  2 20:55 ../

Para montar uso o programa compilado:

➤ ./src/pg_fuse testefs
guedes@betelgeuse:/d/g/r/pg_fs|master⚡?
➤ ls -la testefs/
total 4
drwxr-xr-x 2 root   root      0 dez 31  1969 ./
drwxr-xr-x 9 guedes guedes 4096 set  2 20:55 ../
drwxr-xr-x 2 root   root      0 dez 31  1969 information_schema/
drwxr-xr-x 2 root   root      0 dez 31  1969 pg_catalog/
drwxr-xr-x 2 root   root      0 dez 31  1969 pg_temp_1/
drwxr-xr-x 2 root   root      0 dez 31  1969 pg_toast/
drwxr-xr-x 2 root   root      0 dez 31  1969 pg_toast_temp_1/
drwxr-xr-x 2 root   root      0 dez 31  1969 public/

A ideia é que os esquemas sejam estrutura de diretorios na raiz, e dentro de cada esquema existe uma estrutura de diretorios para representar tabelas, visões, etc. Os objetos podem ser lidos no formato representado pela sua extensão. É como que, ao ler um arquivo pg_class.csv um COPY é feito e os dados são retornados.

A estrutura é mais ou menos assim:

guedes@betelgeuse:/d/g/r/pg_fs|master⚡?
➤ tree testefs/
testefs/
├── information_schema
│   └── tables
├── pg_catalog
│   └── tables
│       ├── pg_aggregate.csv
│       ├── pg_aggregate_fnoid_index.csv
│       ├── pg_am.csv
│       ├── pg_am_name_index.csv
│       ├── pg_am_oid_index.csv
...
...
│       ├── pg_user_mapping_user_server_index.csv
│       └── pg_views.csv
├── pg_temp_1
│   └── tables
├── pg_toast
│   └── tables
├── pg_toast_temp_1
│   └── tables
└── public
	└── tables
		├── first.csv
		├── first_pkey.csv
		├── second.csv
		└── second_pkey.csv

Para desmontar:

guedes@betelgeuse:/d/g/r/pg_fs|master⚡?
➤ sudo umount /disco/guedes/repos/pg_fs/testefs

O código não está organizado tão pouco postei no Github mas como é ainda uma prova-de-conceito, apenas posto aqui o código para futuramente, se isto virar algo útil, está aqui para refereência.

/*
 * PGFS - A PostgreSQL database exposed as filesystem
 * Copyright (c) 2014, 2015 Dickson S. Guedes <guedes@guedesoft.net>
 * MIT License
 *
 */
#define FUSE_USE_VERSION 26

#include <fuse.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <libpq-fe.h>

const char *conninfo;
PGconn     *conn;
PGresult   *res;
int        n_fields;

static void
destructure_path(const char *path, char **schema, char **type, char **object)
{

	char *token;
	char *mypath;

	*schema = NULL;
	*type   = NULL;
	*object = NULL;

	mypath = strdup(path);
	token = strtok(mypath, "/");

	if (token != NULL)
	{
		*schema = strdup(token);
	}

	token = strtok(NULL, "/");

	if (token != NULL)
	{
		*type = strdup(token);
	}

	token = strtok(NULL, "/");

	if (token != NULL)
	{
		*object = strdup(token);
	}

	free(mypath);
}

static void
exit_nicely(PGconn *conn)
{
	PQfinish(conn);
	exit(1);
}

static int
is_dir(const char *path)
{
	const char *param_values[3];

	char * schema;
	char * type;
	char * object;

	destructure_path(path, &schema, &type, &object);

	if (schema == NULL)
	{
		return 1;
	}

	param_values[0] = schema;
	param_values[1] = type;
	param_values[2] = object;

	res = PQexecParams(conn,
					   "SELECT 1 FROM pg_namespace WHERE nspname = $1 AND CAST($2 AS TEXT) IS NULL "
					   "UNION "
					   "SELECT 1 FROM pg_namespace WHERE nspname = $1 "
					   "        AND CAST($2 AS TEXT) = 'tables' "
					   "        AND CAST($3 AS TEXT) IS NULL ",
					   3,
					   NULL,
					   param_values,
					   NULL,
					   NULL,
					   0);

	if (PQntuples(res) == 1)
	{
		PQclear(res);
		return 1;
	}

	return 0;
}

static int
is_file(const char *path)
{
	const char *param_values[3];

	char * schema;
	char * type;
	char * object;

	destructure_path(path, &schema, &type, &object);

	if (object == NULL)
	{
		return 0;
	}

	param_values[0] = schema;
	param_values[1] = type;
	param_values[2] = object;

	res = PQexecParams(conn,
					   "SELECT 1 "
					   "FROM pg_catalog.pg_class c "
					   "LEFT JOIN pg_catalog.pg_namespace n "
					   "	  ON n.oid = c.relnamespace "
					   "WHERE c.relname = split_part($3,'.',1) "
					   "  AND 'tables' = $2 "
					   "  AND n.nspname = $1 "
					   "  AND pg_catalog.pg_table_is_visible(c.oid) ",
					   3,
					   NULL,
					   param_values,
					   NULL,
					   NULL,
					   0);

	if (PQntuples(res) == 1)
	{
		PQclear(res);
		return 1;
	}

	return 0;
}

static int
pgfs_getattr(const char *path, struct stat *stbuf)
{
	int result = 0;

	memset(stbuf, 0, sizeof(struct stat));

	if (is_dir(path))
	{
		stbuf->st_mode = S_IFDIR | 0755;
		stbuf->st_nlink = 2;
	}
	else if (is_file(path))
	{
		stbuf->st_mode = S_IFREG | 0444;
		stbuf->st_nlink = 1;
		stbuf->st_size = 1;
	}
	else
	{
		result = -ENOENT;
	}

	return result;
}

static int
pgfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
			 off_t offset, struct fuse_file_info *fi)
{
	(void) offset;
	(void) fi;
	int i;

	const char *param_values[3];

	char * schema;
	char * type;
	char * object;

	destructure_path(path, &schema, &type, &object);

	if (strcmp(path, "/") == 0)
	{
		filler(buf, ".", NULL, 0);
		filler(buf, "..", NULL, 0);

		//res = PQexec(conn, "SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.+$");
		res = PQexec(conn, "SELECT nspname FROM pg_namespace ");

		for (i = 0; i < PQntuples(res); i++)
		{
			filler(buf, PQgetvalue(res, i, 0), NULL, 0);
		}

		PQclear(res);
	}
	else if (schema != NULL && type == NULL)
	{
		filler(buf, ".", NULL, 0);
		filler(buf, "..", NULL, 0);
		filler(buf, "tables", NULL, 0);
	}
	else if (schema != NULL && (strcmp(type, "tables") == 0))
	{
		filler(buf, ".", NULL, 0);
		filler(buf, "..", NULL, 0);

		param_values[0] = schema;
		param_values[1] = type;
		param_values[2] = object;

		res = PQexecParams(conn,
						   "SELECT relname ||'.csv' "
						   "FROM pg_catalog.pg_class c "
						   "LEFT JOIN pg_catalog.pg_namespace n "
						   "	  ON n.oid = c.relnamespace "
						   "WHERE 'tables' = $2 "
						   "  AND n.nspname = $1 "
						   "  AND pg_catalog.pg_table_is_visible(c.oid) ",
						   2,
						   NULL,
						   param_values,
						   NULL,
						   NULL,
						   0);

		for (i = 0; i < PQntuples(res); i++)
		{
			filler(buf, PQgetvalue(res, i, 0), NULL, 0);
		}

		PQclear(res);
	}

	return 0;
}

static int
pgfs_open(const char *path, struct fuse_file_info *fi)
{
	//if (strcmp(path, "/my/path") != 0)
	//	return -ENOENT;

	if ((fi->flags & 3) != O_RDONLY)
		return -EACCES;

	return 0;
}

static int
pgfs_read(const char *path, char *buf, size_t size, off_t offset,
			  struct fuse_file_info *fi)
{
	size_t len;
	(void) fi;
	int i;

	const char *param_values[3];

	char * schema;
	char * type;
	char * object;

	destructure_path(path, &schema, &type, &object);

	printf("size=%d offset=%d\n", size, offset);

	param_values[0] = schema;
	param_values[1] = type;
	param_values[2] = object;

	res = PQexecParams(conn,
					   "COPY (SELECT 1, 2) TO STDOUT ",
					   0,
					   NULL,
					   param_values,
					   NULL,
					   NULL,
					   0);

	for (i = 0; i < PQntuples(res); i++)
	{
		printf(">>>>> %s\n", PQgetvalue(res, i, 0));
	}

	PQclear(res);

	//if(strcmp(path, "/my/path") != 0)
	//	return -ENOENT;

	//len = strlen(hello_str);
	//if (offset < len) {
	//	if (offset + size > len)
	//		size = len - offset;
	//	memcpy(buf, hello_str + offset, size);
	//} else
	//	size = 0;

	return -ENOENT;
}

static struct fuse_operations pgfs_oper = {
	.getattr	= pgfs_getattr,
	.readdir	= pgfs_readdir,
	.open		= pgfs_open,
	.read		= pgfs_read,
};

int
main(int argc, char *argv[])
{
	conninfo = "dbname=postgres user=postgres port=5433";

	conn = PQconnectdb(conninfo);

	if (PQstatus(conn) != CONNECTION_OK)
	{
		fprintf(stderr, "connection failed: %s",
				PQerrorMessage(conn));
		exit_nicely(conn);
	}

	return fuse_main(argc, argv, &pgfs_oper, NULL);
}

comments powered by Disqus