diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 299 |
1 files changed, 143 insertions, 156 deletions
diff --git a/src/main.rs b/src/main.rs index a061f5d..2dc0f2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,55 +26,27 @@ use std::env; 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::<Vec<String>>() - .join("") -} - +/// A connection to бирка-тян's tag database. #[derive(Debug)] -struct TagBase { - conn: Connection, -} - -/// Return a base64-encoded BLAKE2b hash identifying the file at `path`. -fn hash_file(path: &Path) -> Result<String> { - let mut file = std::fs::File::open(path)?; - let mut hasher = Blake2b::new(); - let _ = io::copy(&mut file, &mut hasher)?; - Ok(base64::encode(&hasher.result())) -} - -struct Image { - id: i64, - blake2: String, - filename: String, - orig_dir: String, -} +struct TagDatabase(Connection); -impl TagBase { - /// Instantiate a new `TagBase` whose backing database is at `path`. +impl TagDatabase { + /// Instantiate a new `TagDatabase` whose backing database is at `path`. pub fn new(path: &str) -> Result<Self> { - let tb = TagBase { - conn: Connection::open(path)?, - }; + let tb = TagDatabase(Connection::open(path)?); tb.initialize_tables()?; Ok(tb) } - fn make_temporary() -> Result<Self> { - let tb = TagBase { - conn: Connection::open_in_memory()?, - }; + fn new_mem() -> Result<Self> { + let tb = TagDatabase(Connection::open_in_memory()?); tb.initialize_tables()?; Ok(tb) } /// Create the table structure for the tag database. fn initialize_tables(&self) -> Result<()> { - self.conn.execute( + self.0.execute( "CREATE TABLE IF NOT EXISTS images ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, blake2 TEXT NOT NULL, @@ -83,14 +55,14 @@ impl TagBase { )", params![], )?; - self.conn.execute( + self.0.execute( "CREATE TABLE IF NOT EXISTS tags ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL )", params![], )?; - self.conn.execute( + self.0.execute( "CREATE TABLE IF NOT EXISTS mapping ( image INTEGER NOT NULL, tag INTEGER NOT NULL @@ -100,6 +72,28 @@ impl TagBase { Ok(()) } + /// Insert `tag` into the database, if it does not already exist. + fn add_tag(&self, tag: &str) -> Result<()> { + let exists = self + .0 + .prepare("SELECT * FROM tags WHERE name = ?")? + .exists(params![tag])?; + if !exists { + self.0 + .execute("INSERT INTO tags (name) VALUES(?)", params![tag])?; + } + Ok(()) + } + + /// Return the internal id for `tag`. + fn tag_id(&self, tag: &str) -> Result<i64> { + Ok(self + .0 + .prepare("SELECT * FROM tags WHERE name = ?")? + .query_row(params![tag], |row| Ok(row.get::<_, i64>(0)))??) + } + + /// Split the path into a tuple containing the parent directory and the filename. fn split_path(&self, path: &Path) -> Result<(String, String)> { if path.is_dir() { bail!("`path` does not name a file."); @@ -109,6 +103,9 @@ impl TagBase { .file_name() .and_then(|os| os.to_str()) .ok_or(anyhow!("Couldn't parse file name."))?; + + // Convert `path` to a string and strip off the `file_name` we've just + // obtained to yield the name of the parent directory. 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]; @@ -119,46 +116,24 @@ impl TagBase { /// Insert the image at `path` into the data. fn import_image(&self, path: &Path, hash: &str) -> Result<i64> { let (file_name, parent_directory) = self.split_path(path)?; - let id = self - .conn + Ok(self + .0 .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 !exists { - self.conn - .execute("INSERT INTO tags (name) VALUES(?)", params![tag])?; - } - Ok(()) + .insert(params![hash, file_name, parent_directory])?) } - /// Return the internal id for `path`. + /// Return the internal id for the image at `path`. fn image_id(&self, path: &Path) -> Result<i64> { let (file_name, path) = self.split_path(path)?; Ok(self - .conn - .prepare("SELECT * FROM images WHERE orig_dir = ? AND filename = ?")? + .0 + .prepare("SELECT id FROM images WHERE orig_dir = ? AND filename = ?")? .query_row(params![path, file_name], |row| Ok(row.get::<_, i64>(0)))??) } - /// Return the internal id for `tag`. - fn tag_id(&self, tag: &str) -> Result<i64> { - Ok(self - .conn - .prepare("SELECT * FROM tags WHERE name = ?")? - .query_row(params![tag], |row| Ok(row.get::<_, i64>(0)))??) - } - /// Associate `tag` with the image specified by `image_id`. fn tag_image(&self, image_id: i64, tag: &str) -> Result<()> { - self.conn.execute( + self.0.execute( "INSERT INTO mapping (image, tag) VALUES(?, ?)", params![image_id, self.tag_id(tag)?], )?; @@ -167,7 +142,7 @@ impl TagBase { /// Disassociate `tag` with the image specified by `image_id`. fn untag_image(&self, image_id: i64, tag: &str) -> Result<()> { - self.conn.execute( + self.0.execute( "DELETE FROM mapping WHERE image = ? AND tag = ?", params![image_id, self.tag_id(tag)?], )?; @@ -177,7 +152,7 @@ impl TagBase { /// Return the tags for the image specified by `image_id`. fn tags_for_image(&self, image_id: i64) -> Result<Vec<String>> { Ok(self - .conn + .0 .prepare( "SELECT tags.name, COUNT(mapping.tag) as tag_count FROM mapping @@ -203,7 +178,7 @@ impl TagBase { return Ok(vec![]); } - let union = if tags.len() == 0 { + let union = if tags.is_empty() { String::from( "INNER JOIN mapping ON images.id = mapping.image", @@ -236,7 +211,7 @@ impl TagBase { ); Ok(self - .conn + .0 .prepare(&query)? .query_map( params![], @@ -260,85 +235,21 @@ impl TagBase { } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn tables_exist() { - let tb = TagBase::make_temporary().unwrap(); - for table in ["images", "tags", "mapping"].iter() { - let exists = tb - .conn - .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?;") - .unwrap() - .exists(params![table]) - .unwrap(); - assert!(exists); - } - } - - #[test] - fn insert_file() { - let tb = TagBase::make_temporary().unwrap(); - let id = tb - .import_image(Path::new("/fake/path"), "fakehash") - .unwrap(); - let exists = tb - .conn - .prepare("SELECT filename, blake2 FROM images WHERE id = ?;") - .unwrap() - .exists(params![id]) - .unwrap(); - assert!(exists); - let exists = tb - .conn - .prepare("SELECT filename, blake2 FROM images WHERE id = ?;") - .unwrap() - .exists(params![id + 1]) - .unwrap(); - assert!(!exists); - } - - #[test] - fn add_tag() { - let tb = TagBase::make_temporary().unwrap(); - tb.add_tag("test").unwrap(); - tb.tag_id("test").unwrap(); - } - - #[test] - fn tag_image() { - let tb = TagBase::make_temporary().unwrap(); - let id = tb - .import_image(Path::new("/fake/path"), "fakehash") - .unwrap(); - tb.add_tag("test").unwrap(); - tb.tag_image(id, "test").unwrap(); - let tags = tb.tags_for_image(id).unwrap(); - assert_eq!(tags.len(), 1); - assert!(tags.contains(&String::from("test"))); - } +/// Return a base64-encoded BLAKE2b hash identifying the file at `path`. +fn hash_file(path: &Path) -> Result<String> { + let mut file = std::fs::File::open(path)?; + let mut hasher = Blake2b::new(); + io::copy(&mut file, &mut hasher)?; + Ok(base64::encode(&hasher.result())) +} - #[test] - fn query_by_tag() { - let tb = TagBase::make_temporary().unwrap(); - let ids: Vec<i64> = (1..4) - .map(|i| { - let i = i.to_string(); - tb.import_image(Path::new(&i), &i).unwrap() - }) - .collect(); - tb.add_tag("1").unwrap(); - tb.add_tag("2").unwrap(); - tb.tag_image(ids[0], "1").unwrap(); - tb.tag_image(ids[1], "2").unwrap(); - tb.tag_image(ids[2], "1").unwrap(); - tb.tag_image(ids[2], "2").unwrap(); - assert_eq!(tb.images_by_tags(&["1"]).unwrap().len(), 2); - assert_eq!(tb.images_by_tags(&["2"]).unwrap().len(), 2); - assert_eq!(tb.images_by_tags(&["1", "2"]).unwrap().len(), 1); - } +/// The result of an image query in бирка-тян's tag database. +#[derive(Debug)] +struct Image { + id: i64, + blake2: String, + filename: String, + orig_dir: String, } fn main() { @@ -349,13 +260,11 @@ fn main() { } let tb = if let Ok(val) = env::var("TB_PATH") { - TagBase::new(&val) + TagDatabase::new(&val) + } else if let Some(dir) = dirs::config_dir() { + TagDatabase::new(dir.join("birka.db").to_str().unwrap()) } else { - if let Some(dir) = dirs::config_dir() { - TagBase::new(dir.join("pometka.db").to_str().unwrap()) - } else { - TagBase::new(Path::new("/tmp").join("pometka.db").to_str().unwrap()) - } + TagDatabase::new(Path::new("/tmp").join("pometka.db").to_str().unwrap()) } .unwrap(); @@ -406,7 +315,7 @@ fn main() { "query" => { let tags = args[2..].iter().map(|s| s.as_str()).collect::<Vec<&str>>(); for tag in tags.iter() { - if let Err(_) = tb.tag_id(tag) { + if tb.tag_id(tag).is_err() { eprintln!("unknown tag: {}", tag); std::process::exit(1); } @@ -424,3 +333,81 @@ fn main() { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tables_exist() { + let tb = TagDatabase::new_mem().unwrap(); + for table in ["images", "tags", "mapping"].iter() { + let exists = + tb.0.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?;") + .unwrap() + .exists(params![table]) + .unwrap(); + assert!(exists); + } + } + + #[test] + fn insert_file() { + let tb = TagDatabase::new_mem().unwrap(); + let id = tb + .import_image(Path::new("/fake/path"), "fakehash") + .unwrap(); + let exists = + tb.0.prepare("SELECT filename, blake2 FROM images WHERE id = ?;") + .unwrap() + .exists(params![id]) + .unwrap(); + assert!(exists); + let exists = + tb.0.prepare("SELECT filename, blake2 FROM images WHERE id = ?;") + .unwrap() + .exists(params![id + 1]) + .unwrap(); + assert!(!exists); + } + + #[test] + fn add_tag() { + let tb = TagDatabase::new_mem().unwrap(); + tb.add_tag("test").unwrap(); + tb.tag_id("test").unwrap(); + } + + #[test] + fn tag_image() { + let tb = TagDatabase::new_mem().unwrap(); + let id = tb + .import_image(Path::new("/fake/path"), "fakehash") + .unwrap(); + tb.add_tag("test").unwrap(); + tb.tag_image(id, "test").unwrap(); + let tags = tb.tags_for_image(id).unwrap(); + assert_eq!(tags.len(), 1); + assert!(tags.contains(&String::from("test"))); + } + + #[test] + fn query_by_tag() { + let tb = TagDatabase::new_mem().unwrap(); + let ids: Vec<i64> = (1..4) + .map(|i| { + let i = i.to_string(); + tb.import_image(Path::new(&i), &i).unwrap() + }) + .collect(); + tb.add_tag("1").unwrap(); + tb.add_tag("2").unwrap(); + tb.tag_image(ids[0], "1").unwrap(); + tb.tag_image(ids[1], "2").unwrap(); + tb.tag_image(ids[2], "1").unwrap(); + tb.tag_image(ids[2], "2").unwrap(); + assert_eq!(tb.images_by_tags(&["1"]).unwrap().len(), 2); + assert_eq!(tb.images_by_tags(&["2"]).unwrap().len(), 2); + assert_eq!(tb.images_by_tags(&["1", "2"]).unwrap().len(), 1); + } +} |