#[macro_use] extern crate anyhow; use anyhow::Result; use blake2::{Blake2b, Digest}; use rusqlite::{params, Connection}; use std::io; use std::path::Path; /// Return a hex-encoded string representing the contents of `hash`. fn hexlify(hash: &[u8]) -> String { hash.iter() .map(|b| format!("{:x}", b)) .collect::>() .join("") } #[derive(Debug)] struct TagBase { conn: Connection, } impl TagBase { /// Instantiate a new `TagBase` whose backing database is at `path`. fn new(path: &str) -> Result { let conn = Connection::open(path)?; conn.execute( "CREATE TABLE IF NOT EXISTS images ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, blake2 TEXT NOT NULL, filename TEXT NOT NULL, orig_dir TEXT NOT NULL )", params![], )?; conn.execute( "CREATE TABLE IF NOT EXISTS tags ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL )", params![], )?; conn.execute( "CREATE TABLE IF NOT EXISTS mapping ( image INTEGER NOT NULL, tag INTEGER NOT NULL )", params![], )?; Ok(TagBase { conn }) } /// Insert the image at `path` into the data. fn import_image(&self, path: &Path) -> Result { if path.is_dir() { bail!("`path` does not name a file."); } let mut file = std::fs::File::open(path)?; let mut hasher = Blake2b::new(); let _ = io::copy(&mut file, &mut hasher)?; let hash = base64::encode(&hasher.result()); let file_name = path .file_name() .and_then(|os| os.to_str()) .ok_or(anyhow!("Couldn't parse file name."))?; let path = path.to_str().ok_or(anyhow!("Couldn't parse path."))?; let split_index = path.len() - file_name.len(); let parent_directory = &path[0..split_index]; let id = self .conn .prepare("INSERT INTO images (blake2, filename, orig_dir) VALUES(?,?,?)")? .insert(params![hash, file_name, parent_directory])?; Ok(id) } /// Insert `tag` into the database, if it does not already exist. fn add_tag(&self, tag: &str) -> Result<()> { let exists = self .conn .prepare("SELECT * FROM tags WHERE name = ?")? .exists(params![tag]); if let Err(_) = exists { self.conn .execute("INSERT INTO tags (name) VALUES(?)", params![tag])?; } Ok(()) } /// Associate `tag` with the image specified by `image_id`. fn tag_image(&self, image_id: i64, tag: &str) -> Result<()> { let tag_id: i64 = self .conn .prepare("SELECT * FROM tags WHERE name = ?")? .query_row(params![tag], |row| Ok(row.get(0)))??; self.conn.execute( "INSERT INTO mapping (image, tag) VALUES(?, ?)", params![image_id, tag_id], )?; Ok(()) } fn tags_for_image(&self, image_id: i64) -> Result> { Ok(self .conn .prepare( "SELECT tags.name, COUNT(mapping.tag) as tag_count FROM mapping INNER JOIN images ON images.id = ? INNER JOIN tags GROUP BY tags.name", )? .query_map(params![image_id], |row| row.get(0))? .filter_map(|tag| tag.ok()) .collect()) } } fn main() { let tb = TagBase::new("/tmp/test.db").unwrap(); tb.import_image(Path::new("/home/jakob/Sync/06fc9ae71549eb4d.jpeg")) .unwrap(); println!("{:?}", tb); }