Monday | 15 APR 2024
[ previous ]
[ next ]

FUSE Tutorial - 06 readdir - Surely

Title:
Date: 2023-02-05
Tags:  

Now back to your regularly scheduled programming. We have a sqlite3 function that can connect to a database, execute a query and give us a listing of the tables. We also have a function that can hook into when a directory is being listed. The goal is to on a listing of a directory, instead of actually looking at a file system, we'll execute an sql query and display that.

The first thing we need to do is update our includes in our sfs.c file:

#define FUSE_USE_VERSION 29
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sqlite3.h>

We've added our sqlite3 file here.

The next thing is to add our sq_getTables function:

int sq_getTables(void *buffer, fuse_fill_dir_t filler) {
    sqlite3 *db;
    int rc = sqlite3_open("db.sqlite", &db);

    char *sql = "select name from sqlite_master where type = 'table' and name not like 'sqlite_%';";

    sqlite3_stmt *stmt = NULL;
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

    rc = sqlite3_step(stmt);

    while (rc != SQLITE_DONE && rc != SQLITE_OK) {
        const unsigned char * table = sqlite3_column_text(stmt,0);
        filler(buffer, table, NULL, 0);
        rc = sqlite3_step(stmt);
    }

    rc = sqlite3_finalize(stmt);
    rc = sqlite3_close(db);

    return rc;
}

This function takes in a buffer and a filler. These are fuse variables that we need to pass in so we can update the fuse system.

Very similar to what we did before, we are simply populating the filler with the tables we have in our database.

Now let's update our readdir function:

int sfs_readdir(const char *path, void *buffer, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi)
{
    printf("Calling readdir on: %s\n",path);

    filler(buffer, ".", NULL, 0);
    filler(buffer, "..", NULL, 0);
    sq_getTables(buffer, filler);
    return 0;
}

This is a very simple update, we simply pass the buffer and filler to our sq_getTables function and it will populate FUSE with out table names.

Now let's compile and run our application:

gcc -lsqlite3 -lfuse -D_FILE_OFFSET_BITS=64 sfs.c -o sfs
./sfs -f db.sqlite dir

Here we now have a new include when we compile our program, we need to compile in sqlite3 headers as well as the fuse headers.

In another terminal we can do a listing of dir and we should see our tables:

ls -l dir/
total 0
-rw-r--r-- 2 nivethan nivethan 4096 Dec 31  1969 mytable

Beautiful! One thing to note here is that our table is appearing as file, we will need to fix this so that it is seen as a folder.

The following is the messages we should be seeing in our fuse program:

Lighting FUSE...
Getting attributes for: /
Calling readdir on: /
Getting attributes for: /mytable

We can now expose sql tables as part of the file system.

Now let's add some logic to show our table a directory. We are also going to set it up so that we can see inside our table and see the keys as an individual file.

The first thing we need to do is write a function to count forward slashes. We are going to use a very simple heuristic to decide when to show a folder vs a file. If the path being requested has a single slash then we know we are trying to look at a table. If we get 2 slashes, then it means we are look at a key inside of table.

This means that our sql file will only ever go two levels which makes sense. You can tables inside sqlite and inside a table you can only have keys. You can't arbitrarily nest things in sqlite. This will help us keep our fuse program simple.

Now for the new function we will write:

int count_slashes(const char *str) {
    int count = 0;
    int i;
    for (i = 0; i < strlen(str); i++) {
        if (str[i] == '/') {
            count++;
        }
    }
    return count;
}

This is a very simple function.

Now we can update our getattr function accordingly:

int sfs_getattr(const char *path, struct stat *st)
{
    printf("Getting attributes for: %s\n",path);

    int slashes = count_slashes(path);

    if (slashes == 1) {
        st->st_mode = S_IFDIR | 0755;
        st->st_nlink = 2;
        st->st_size = 4096;           // file size

    } else {
        st->st_mode = S_IFREG | 0644;
        st->st_nlink = 2;
        st->st_size = 4096;
    }

    st->st_uid = getuid();
    st->st_gid = getgid();

    return 0;
}

We now count the forward slashes and anything where it is a single one, we know that it will be a directory. This will work for "/" and for "/mytable". Everything besides these, for example "/mytable/1" will be treated as a file.

We can compile and run our program and we should see mytable as a directory.

Next, we will add the ability to see inside our mytable directory. To do this, we will update our readdir function. Our readdir function only needs to work with a single slash. Anymore than that it doesn't count as a directory so there is nothing to list.

We do need to handle a single slash as a path vs a single slash followed by some text. This is because a single slash means we are listing our tables. If there is text then it means we need to get the text and treat it as the table name. Once we have the table name we can then call a new function we will write called getKeys.

int sfs_readdir(const char *path, void *buffer, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi)
{
    printf("Calling readdir on: %s\n",path);
    int slashes = count_slashes(path);

    filler(buffer, ".", NULL, 0);
    filler(buffer, "..", NULL, 0);

    if (slashes == 1) {
        if (!strcmp(path, "/")) {
            sq_getTables(buffer, filler);

        } else {
            int length = strlen(path);
            char *new_path = malloc(length + 1);
            strcpy(new_path, path);
                
            char *tableName = strtok(new_path, "/");
            sq_getKeys(tableName, buffer, filler);

            free(new_path);
        }
    }
    return 0;
}

As you can see, we first check if there is a single slashes. If there is we can then check to see what the path is. If the path is a single forward slash, then we call getTables to get a list of tables.

Otherwise, we will call our getKeys function which will pass the table name to our new function. This function will execute the query and populate the filler with a list of keys.

int sq_getKeys(char *tableName, void *buffer, fuse_fill_dir_t filler) {
    sqlite3 *db;
    int rc = sqlite3_open("db.sqlite", &db);

    char *sql = "select * from ";
    char *sc = ";";

    int length = strlen(sql) + strlen(tableName) + strlen(sc);
    char *result = malloc(length + 1);

    strcpy(result, sql);
    strcat(result, tableName);
    strcat(result, sc);

    sqlite3_stmt *stmt = NULL;
    sqlite3_prepare_v2(db, result, -1, &stmt, NULL);

    rc = sqlite3_step(stmt);

    while (rc != SQLITE_DONE && rc != SQLITE_OK) {
        const unsigned char *key = sqlite3_column_text(stmt,0);
        filler(buffer, key, NULL, 0);
        rc = sqlite3_step(stmt);
    }

    rc = sqlite3_finalize(stmt);
    rc = sqlite3_close(db);

    free(result);

    return rc;
}

We concatenate our table name with a select statement then we execute our query and populate our fuse listing using the filler program.

With that we are done!

We can compile and run our program:

gcc -lsqlite3 -lfuse -D_FILE_OFFSET_BITS=64 sfs.c -o sfs
./sfs -f db.sqlite dir

In another terminal window we can do:

ls -l dir/mytable
total 0
-rw-r--r-- 2 nivethan nivethan 4096 Dec 31  1969 1
-rw-r--r-- 2 nivethan nivethan 4096 Dec 31  1969 2
-rw-r--r-- 2 nivethan nivethan 4096 Dec 31  1969 3

In my table 1, 2, 3 are the keys in mytable.

The output in our fuse program is:

./sfs -f db.sqlite dir
Lighting FUSE...
Getting attributes for: /mytable
Calling readdir on: /mytable
Getting attributes for: /mytable/1
Getting attributes for: /mytable/2
Getting attributes for: /mytable/3

This is very cool! We are done our directory logic! The next step will be to read in a file. If you tried to use vim on one of these "files", you would get a read error. That will be what we resolve next.