summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs299
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);
+ }
+}