Sunday | 23 JUN 2024
[ previous ]
[ next ]

Fuse Tutorial - 07 read - Learning to Read

Title:
Date: 2023-02-06
Tags:  

We can now see sql tables as directories and we can see keys as files. The next thing we need to do is make it so we can see rows as a text file. For this we are going to keep things simple. We are going to make each row look like a json file.

Now for the first step, we will write a read function. This is the function will get run when we try to read a file when our fuse program is running.

static int sfs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
    printf("Reading: %s\n",path);

    char *filecontent = "Hello, World!";

    size_t len = strlen(filecontent);
    if (offset >= len) {
        return 0;
    }

    if (offset + size > len) {
        memcpy(buf, filecontent + offset, len - offset);
        return len - offset;
    }

    memcpy(buf, filecontent + offset, size);
    return size;
}

We are hardcoding our file content here. This is just for testing purposes. The goal is that we should now be able to open one our files and see Hello, World! inside it.

Before we compile and run our fuse program, we first need to register our read function:

static struct fuse_operations sfs_ops = {
    .getattr = sfs_getattr,
    .readdir = sfs_readdir,
    .read = sfs_read,
};

Now that we updated our ops, we can now compile and run it:

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

In another terminal we can do:

vim dir/mytable/1

Voila! We should see our hello, world! in our file.

It's time to add in our sqlite portion now. We need to read in the path, get the table and key as separate pieces. We can then execute a sql query to get the data and finally we can write it into our buffer. This will have the effect of appearing when we try to read a file in our fuse file system.

Let's get started!

static int sfs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
    printf("Reading: %s\n",path);

    int slashes = count_slashes(path);
    if (slashes != 2) {
        return 0;
    }

    char *new_path = malloc(strlen(path) + 1);
    strcpy(new_path, path);

    char *tableName = strtok(new_path, "/");
    char *key = strtok(NULL, "/");

    char* filecontent = sq_getData(tableName, key, buf, offset);

    size_t len = strlen(filecontent);
    if (offset >= len) {
        return 0;
    }

    if (offset + size > len) {
        memcpy(buf, filecontent + offset, len - offset);
        return len - offset;
    }

    memcpy(buf, filecontent + offset, size);

    free(new_path);
    return size;
}

In our read function, we check how many slashes are in the path. If there is any number besides 2, we can go ahead and skip the read.

The next thing we do is make a copy of our path. We will then use strok to get the 2 pieces of information we need from the path. We need the table name and the key.

Once we have this, we can then call our getData function which is going to do the work of opening sqlite, executing the query, combining the data and then returning a string. We then copy the string into our buffer where it will appear when we read a file.

Let's look at our sq_getData function:

char *sq_getData(char *tableName, char *key, char *buffer, off_t offset) {
    sqlite3 *db;
    int rc = sqlite3_open("db.sqlite", &db);

    char *sql = "select * from ";
    char *where = " where title = '";
    char *sc = "';";

    int length = strlen(sql) + strlen(tableName) + strlen(where) + strlen(key) + strlen(sc);
    char *query = malloc(length + 1);

    strcpy(query, sql);
    strcat(query, tableName);
    strcat(query, where);
    strcat(query, key);
    strcat(query, sc);

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

    rc = sqlite3_step(stmt);

    int count = sqlite3_column_count(stmt);
    char **strings = (char**) malloc(count * sizeof(char*));

    int i;
    for (i = 0; i<count; i++) {
        const char * col = sqlite3_column_name(stmt, i);
        const unsigned char *val = sqlite3_column_text(stmt,i);

        int length = snprintf(NULL, 0, "%s = '%s'", col, val);
        strings[i] = (char*) malloc(length + 1);
        snprintf(strings[i], length + 1, "%s = '%s'", col,val);
    }

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

    free(query);

    int total_length = 0;
    for (i = 0; i < count; i++) {
        total_length += strlen(strings[i]);
    }

    char *result = (char*) malloc(total_length + count - 1);
    result[0] = '\0';
    for (i = 0; i < count; i++) {
        strcat(result, strings[i]);
        if (i < count - 1) {
            strcat(result, "\n");
        }
    }

    for (i = 0; i < count; i++) {
        free(strings[i]);
    }
    free(strings);

    return result;
}

One big note: We hardcoded the where to go off title, this will need to be changed so that it is dynamic. I might just make it a rule that all tables must have id and that it is the first column.

We first create our query string and then we execute it. Once we execute it, we will have an inner loop to get every columns value. We are going to batch the column name and column value together and stick it inside our array. We then will go through the process of concatenating everything in this array so that we have a single string we can return.

This function will join together column names and data into a single string that we can then use in our read function.

Now 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 we can do:

vim dir/mytable/1

Which should result in a successful open and we should see the content:

title = '1'
body = 'post'

With that we are done!

We should now be able to read sqlite rows as files! The final thing we need to do is our writes. We will first focus on updating existing rows. We will then look at creating new rows.