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();
}