Thursday | 21 NOV 2024
[ previous ]
[ next ]

Rust, Fuse, Sqlite - Fuse

Title:
Date: 2023-02-11
Tags:  

About 400 lines of rust code. Half is the sqlite code and half is the fuse code.

This program will expose sqlite tables as a file system. Very poorly but it is cool! I only set it up to work with text values.

I also don't do any error handling. I need to properly send file system errors. That is probably the thing I work on next.

At this point though, I should be able to use this with webdav and sqlite in the browser to do some fun database stuff without actually writing a single line of backend web app code. Fucking A!

I was able to open and work with the sample databases northwind.db and chinook.db. This is pretty good so far. Lots of bugs but the idea works. A better programmer than me could probably take this to a production level utility.

use std::fs;
use std::collections::HashMap;
use std::time::{ Duration, SystemTime, UNIX_EPOCH };
use std::ffi::OsStr;

use rusqlite::Connection;
use rusqlite::types::ValueRef;

use fuser::{
    FileAttr, FileType, Filesystem, 
    ReplyAttr, ReplyDirectory, ReplyEntry, 
    ReplyData, ReplyWrite, 
    ReplyEmpty,
    Request, MountOption
};

const TTL: Duration = Duration::from_secs(1);

fn generate_attr(ino: u64, kind: FileType) -> FileAttr {
    FileAttr {
        ino,
        kind,
        size: 4096,
        blocks: 0,
        atime: UNIX_EPOCH,
        mtime: UNIX_EPOCH,
        ctime: UNIX_EPOCH,
        crtime: UNIX_EPOCH,
        perm: 0o755,
        nlink: 2,
        uid: 1000,
        gid: 1000,
        rdev: 0,
        flags: 0,
        blksize: 512,
        padding: 0,
    }
}

#[derive(Debug)]
enum FType {
    File, Directory
}

#[derive(Debug)]
struct Entry {
    ino: u64,
    parent: u64,
    id_col: String,
    table: String,
    name: String,
    col_count: usize,
    ftype: FType,
    attr: FileAttr,
}

#[derive(Debug)]
struct FS {
    db: String,
    modified: SystemTime,
    counter: u64,
    directories: HashMap<u64, Entry>,
    files: HashMap<u64, Entry>,
}

impl FS {
    fn new(db: String) -> FS {
        let metadata = fs::metadata(&db).unwrap();
        let modified = metadata.modified().unwrap();

        FS {
            db,
            modified,
            counter: 0,
            directories: HashMap::new(),
            files: HashMap::new(),
        }
    }

    fn get_directories(&mut self) {
        let conn = Connection::open(self.db.clone()).unwrap();

        let mut stmt = conn.prepare(
            "select * from sqlite_master
            where type = 'table'
            and name not like 'sqlite_%' order by name;"
        ).unwrap();

        self.counter = 1;

        let rows = stmt.query_map([], |row| {
            self.counter = self.counter + 1;

            let cols_text: String = row.get(4)?;
            let start = cols_text.find("(").unwrap();
            let end = cols_text.find(")").unwrap()+1;
            let cols_text = &cols_text[start..end];
            let col_count = cols_text.matches(",").count();

            Ok(Entry {
                ino: self.counter,
                parent: 1,
                id_col: "".to_string(),
                table: "".to_string(),
                name: row.get(1)?,
                col_count,
                ftype: FType::Directory,
                attr: generate_attr(self.counter, FileType::Directory)
            })
        }).unwrap();

        for row in rows {
            if let Ok(r) = row {
                self.directories.insert(r.ino, r);
            }
        }
    }

    fn get_all_files(&mut self) {
        for (ino, entry) in &self.directories {
            let table = &entry.name;
            let conn = Connection::open(self.db.clone()).unwrap();

            let query = format!("select * from \"{}\";",table);
            let mut stmt = conn.prepare(&query).unwrap();

            let mut id_col = "".to_string();

            let rows = stmt.query_map([], |row| {
                self.counter = self.counter + 1;

                if id_col == "" {
                    id_col = row.as_ref().column_name(0).unwrap().to_string();
                }

                let name = match row.get_raw(0) {
                    ValueRef::Null => "".to_string(),
                    ValueRef::Text(t) => std::str::from_utf8(t).unwrap().to_string(),
                    ValueRef::Integer(i) => i.to_string(),
                    ValueRef::Real(r) => r.to_string(),
                    _ => "".to_string(),
                };

                Ok(Entry {
                    ino: self.counter,
                    parent: ino.clone(),
                    name,
                    col_count: 0,
                    id_col: id_col.clone(),
                    table: table.clone(),
                    ftype: FType::File,
                    attr: generate_attr(self.counter, FileType::RegularFile)
                })
            }).unwrap();

            for row in rows {
                match row {
                    Ok(r) => {
                        self.files.insert(r.ino, r);
                    },
                    Err(err) => {
                        println!("rusqlite error: {}", err);
                    }
                }
            }
        }
    }

    fn fetch(&mut self) {
        self.directories = HashMap::new();
        self.files = HashMap::new();
        self.get_directories();
        self.get_all_files();
    }

    fn fs_read(&self, table: &str, id_col: &str, id: &str) -> String{
        let query = format!("select * from {} where {} = \"{}\";", table, id_col, id);

        let conn = Connection::open(self.db.clone()).unwrap();
        let mut stmt = conn.prepare(&query).unwrap();
        let column_count = stmt.column_count();

        let mut rows = stmt.query(()).unwrap();
        let row = rows.next().unwrap().unwrap();

        let mut row_map = serde_json::map::Map::new();

        for column_index in 0..column_count {
            let key = row.as_ref().column_name(column_index).unwrap().to_string();

                let str_val: String = match row.get_raw(column_index) {
                    ValueRef::Null => "".to_string(),
                    ValueRef::Text(t) => std::str::from_utf8(t).unwrap().to_string(),
                    ValueRef::Integer(i) => i.to_string(),
                    ValueRef::Real(r) => r.to_string(),
                    _ => "".to_string(),
                };


            let val = serde_json::Value::from(str_val);
            row_map.insert(key, val);
        }

        row_map.remove(id_col);

        serde_json::to_string_pretty(&row_map).unwrap()
    }

    fn fs_write(&self, table: &str, id_col: &str, id: &str, text :&str) {
        let mut columns = vec!();
        let mut values = vec!();
        let json: serde_json::Value = match serde_json::from_str(text) {
            Ok(json) => json,
            Err(err) => {
                println!("Failed to generate json - {} on {}: {}", id, table,err);
                return;
            }
        };

        for (key, val) in json.as_object().unwrap() {
            columns.push(key.to_string());
            values.push(val.to_string());
        }

        let columns = columns.join(",");
        let values = values.join(",");
        let query = format!(r#"REPLACE INTO {} ({},{}) VALUES ("{}",{});"#, table, id_col, columns, id, values);
        let conn = Connection::open(self.db.clone()).unwrap();
        let res = conn.execute(&query, ());

        match res {
            Ok(_) => {
                println!("Successfully written: {} on {}.", id, table);
            },
            Err(err) => {
                println!("Failed to write - {} on {}: {}", id, table,err);
            }
        }
    }

    fn fs_delete(&self, table: &str, id_col: &str, id: &str) {
        let query = format!(r#"DELETE FROM {} WHERE {} = "{}";"#, table, id_col, id);
        let conn = Connection::open(self.db.clone()).unwrap();
        let res = conn.execute(&query, ());

        match res {
            Ok(_) => {
                println!("Successfully deleted: {} on {}.", id, table);
            },
            Err(err) => {
                println!("Failed to delete - {} on {}: {}", id, table,err);
            }
        }
    }

    fn print_directories(&self) {
        for (ino, entry) in &self.directories {
            println!("{:?}: {:?}", ino, entry);
        }
    }

    fn print_files(&self) {
        for (ino, entry) in &self.files {
            println!("{:?}: {:?}", ino, entry);
        }
    }

    fn info(&self) {
        let dirs = self.directories.keys().len();
        let files = self.files.keys().len();

        println!("Last modified {:?}", self.modified);
        println!("{} directoriess found", dirs);
        println!("{} files found", files);
        println!("{} is the last inode", self.counter);
    }
}

impl Filesystem for FS {
    fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) {
        println!("Calling getattr on: {}", ino);
        let x = generate_attr(1, FileType::Directory);
        reply.attr(&TTL, &x);
    }

    fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
        println!("Calling lookup: {:?}, {:?}", name, parent);

        let name = name.to_string_lossy().to_string();

        for (_, entry) in &self.directories {
            if entry.parent == parent && name == entry.name {
                reply.entry(&TTL, &entry.attr, 0);
                return;
            }
        }

        for (_, entry) in &self.files {
            if entry.parent == parent && name == entry.name {
                reply.entry(&TTL, &entry.attr, 0);
                return;
            }
        }

        for (_, entry) in &self.directories {
            if entry.ino == parent {
                let cols = vec![""; entry.col_count-1].join(",");
                let query = format!("INSERT INTO {} VALUES (\"{}\",\"{}\" );", entry.name, name, cols);
                let conn = Connection::open(self.db.clone()).unwrap();
                let res = conn.execute(&query, ());

                match res {
                    Ok(_) => {
                        self.fetch();
                        for (_, e) in &self.files {
                            if e.parent == parent && name == e.name {
                                reply.entry(&TTL, &e.attr, 0);
                                println!("Successfully created: {} on {}.", name, e.table);
                                return;
                            }
                        }
                    },
                    Err(err) => {
                        println!("query: {}", query);
                        println!("Failed to create - {} on {}", name, err);
                    }
                }

                return;
            }
        }
    }

    fn readdir(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectory) {
        println!("Calling readdir: {}", ino);

        let mut entries = vec![
            (1, FileType::Directory, String::from(".")),
            (1, FileType::Directory, String::from("..")),
        ];

        if ino == 1 {
            for (inode, entry) in &self.directories {
                entries.push((inode.clone(), entry.attr.kind, entry.name.to_string()));
            }
        } else {
            for (inode, entry) in &self.files {
                if entry.parent == ino {
                    entries.push((inode.clone(), entry.attr.kind, entry.name.to_string()));
                }
            }
        }

        for (i, entry) in entries.into_iter().enumerate().skip(offset as usize) {
            if reply.add(entry.0, (i + 1) as i64, entry.1, entry.2) {
                break;
            }
        }

        reply.ok();
    }

    fn read(&mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, _size: u32, _flags: i32, _lock_owner: Option<u64>, reply: ReplyData,) {
        println!("Reading: {:?}", ino);

        let entry = match self.files.get(&ino) {
            Some(entry) => entry,
            _ => {
                println!("Failed to find inode: {}.", ino);
                return;
            }
        };

        let text = self.fs_read(&entry.table, &entry.id_col, &entry.name);
        reply.data(&text.as_bytes());
    }

    fn write(&mut self, _req: &Request, ino: u64, _fh: u64, _offset: i64, data: &[u8], _write_flags: u32, _flags: i32, _lock_owner: Option<u64>, reply: ReplyWrite,) {
        let entry = match self.files.get(&ino) {
            Some(entry) => entry,
            _ => {
                println!("Failed to find inode: {}.", ino);
                return;
            }
        };

        let text = match std::str::from_utf8(data) {
            Ok(v) => v,
            Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
        };

        self.fs_write(&entry.table, &entry.id_col, &entry.name, text);
        reply.written(data.len() as u32);
    }

    fn unlink(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) {
        println!("Deleting...: {:?}", name);
        let name = name.to_string_lossy().to_string();
        for (inode, entry) in &self.files {
            if entry.parent == parent && entry.name == name {
                self.fs_delete(&entry.table, &entry.id_col, &entry.name);
                self.fetch();
                reply.ok();
                return;
            }
        }
        println!("Failed to delete {}.", name);
    }
}

fn main() {
    let db = "example.db".to_string();
    let db = "northwind.db".to_string();
    let db = "chinook.db".to_string();
    let mut fs = FS::new(db);
    fs.fetch();

    let mountpoint = "dir";
    let mut options = vec![];
    options.push(MountOption::AutoUnmount);

    fuser::mount2(fs, mountpoint, &options).unwrap();
}