diff options
Diffstat (limited to 'src/index.rs')
-rw-r--r-- | src/index.rs | 188 |
1 files changed, 86 insertions, 102 deletions
diff --git a/src/index.rs b/src/index.rs index d527839..42f1a95 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,65 +1,81 @@ +//! An on-disk index of chunks for the server. + use crate::chunkid::ChunkId; use crate::chunkmeta::ChunkMeta; +use crate::label::Label; use rusqlite::Connection; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; +use std::path::Path; -/// A chunk index. +/// A chunk index stored on the disk. /// /// A chunk index lets the server quickly find chunks based on a -/// string key/value pair, or whether they are generations. +/// string key/value pair. #[derive(Debug)] pub struct Index { - filename: PathBuf, conn: Connection, - map: HashMap<(String, String), Vec<ChunkId>>, - generations: Vec<ChunkId>, - metas: HashMap<ChunkId, ChunkMeta>, +} + +/// All the errors that may be returned for `Index`. +#[derive(Debug, thiserror::Error)] +pub enum IndexError { + /// Index does not have a chunk. + #[error("The repository index does not have chunk {0}")] + MissingChunk(ChunkId), + + /// Index has chunk more than once. + #[error("The repository index duplicates chunk {0}")] + DuplicateChunk(ChunkId), + + /// An error from SQLite. + #[error(transparent)] + SqlError(#[from] rusqlite::Error), } impl Index { - pub fn new<P: AsRef<Path>>(dirname: P) -> anyhow::Result<Self> { + /// Create a new index. + pub fn new<P: AsRef<Path>>(dirname: P) -> Result<Self, IndexError> { let filename = dirname.as_ref().join("meta.db"); let conn = if filename.exists() { sql::open_db(&filename)? } else { sql::create_db(&filename)? }; - Ok(Self { - filename, - conn, - map: HashMap::new(), - generations: vec![], - metas: HashMap::new(), - }) + Ok(Self { conn }) } - pub fn insert_meta(&mut self, id: ChunkId, meta: ChunkMeta) -> anyhow::Result<()> { + /// Insert metadata for a new chunk into index. + pub fn insert_meta(&mut self, id: ChunkId, meta: ChunkMeta) -> Result<(), IndexError> { let t = self.conn.transaction()?; sql::insert(&t, &id, &meta)?; t.commit()?; Ok(()) } - pub fn get_meta(&self, id: &ChunkId) -> anyhow::Result<ChunkMeta> { + /// Look up metadata for a chunk, given its id. + pub fn get_meta(&self, id: &ChunkId) -> Result<ChunkMeta, IndexError> { sql::lookup(&self.conn, id) } - pub fn remove_meta(&mut self, id: &ChunkId) -> anyhow::Result<()> { + /// Remove a chunk's metadata. + pub fn remove_meta(&mut self, id: &ChunkId) -> Result<(), IndexError> { sql::remove(&self.conn, id) } - pub fn find_by_sha256(&self, sha256: &str) -> anyhow::Result<Vec<ChunkId>> { - sql::find_by_256(&self.conn, sha256) + /// Find chunks with a client-assigned label. + pub fn find_by_label(&self, label: &str) -> Result<Vec<ChunkId>, IndexError> { + sql::find_by_label(&self.conn, label) } - pub fn find_generations(&self) -> anyhow::Result<Vec<ChunkId>> { - sql::find_generations(&self.conn) + /// Find all chunks. + pub fn all_chunks(&self) -> Result<Vec<ChunkId>, IndexError> { + sql::find_chunk_ids(&self.conn) } } #[cfg(test)] mod test { + use super::Label; + use super::{ChunkId, ChunkMeta, Index}; use std::path::Path; use tempfile::tempdir; @@ -71,140 +87,113 @@ mod test { #[test] fn remembers_inserted() { let id: ChunkId = "id001".parse().unwrap(); - let meta = ChunkMeta::new("abc"); + let sum = Label::sha256(b"abc"); + let meta = ChunkMeta::new(&sum); let dir = tempdir().unwrap(); let mut idx = new_index(dir.path()); idx.insert_meta(id.clone(), meta.clone()).unwrap(); assert_eq!(idx.get_meta(&id).unwrap(), meta); - let ids = idx.find_by_sha256("abc").unwrap(); + let ids = idx.find_by_label(&sum.serialize()).unwrap(); assert_eq!(ids, vec![id]); } #[test] fn does_not_find_uninserted() { let id: ChunkId = "id001".parse().unwrap(); - let meta = ChunkMeta::new("abc"); + let sum = Label::sha256(b"abc"); + let meta = ChunkMeta::new(&sum); let dir = tempdir().unwrap(); let mut idx = new_index(dir.path()); idx.insert_meta(id, meta).unwrap(); - assert_eq!(idx.find_by_sha256("def").unwrap().len(), 0) + assert_eq!(idx.find_by_label("def").unwrap().len(), 0) } #[test] fn removes_inserted() { let id: ChunkId = "id001".parse().unwrap(); - let meta = ChunkMeta::new("abc"); + let sum = Label::sha256(b"abc"); + let meta = ChunkMeta::new(&sum); let dir = tempdir().unwrap(); let mut idx = new_index(dir.path()); idx.insert_meta(id.clone(), meta).unwrap(); idx.remove_meta(&id).unwrap(); - let ids: Vec<ChunkId> = idx.find_by_sha256("abc").unwrap(); + let ids: Vec<ChunkId> = idx.find_by_label(&sum.serialize()).unwrap(); assert_eq!(ids, vec![]); } - - #[test] - fn has_no_generations_initially() { - let dir = tempdir().unwrap(); - let idx = new_index(dir.path()); - assert_eq!(idx.find_generations().unwrap(), vec![]); - } - - #[test] - fn remembers_generation() { - let id: ChunkId = "id001".parse().unwrap(); - let meta = ChunkMeta::new_generation("abc", "timestamp"); - let dir = tempdir().unwrap(); - let mut idx = new_index(dir.path()); - idx.insert_meta(id.clone(), meta.clone()).unwrap(); - assert_eq!(idx.find_generations().unwrap(), vec![id]); - } - - #[test] - fn removes_generation() { - let id: ChunkId = "id001".parse().unwrap(); - let meta = ChunkMeta::new_generation("abc", "timestamp"); - let dir = tempdir().unwrap(); - let mut idx = new_index(dir.path()); - idx.insert_meta(id.clone(), meta.clone()).unwrap(); - idx.remove_meta(&id).unwrap(); - assert_eq!(idx.find_generations().unwrap(), vec![]); - } } mod sql { + use super::{IndexError, Label}; use crate::chunkid::ChunkId; use crate::chunkmeta::ChunkMeta; - use crate::error::ObnamError; use log::error; use rusqlite::{params, Connection, OpenFlags, Row, Transaction}; use std::path::Path; - pub fn create_db(filename: &Path) -> anyhow::Result<Connection> { + /// Create a database in a file. + pub fn create_db(filename: &Path) -> Result<Connection, IndexError> { let flags = OpenFlags::SQLITE_OPEN_CREATE | OpenFlags::SQLITE_OPEN_READ_WRITE; let conn = Connection::open_with_flags(filename, flags)?; conn.execute( - "CREATE TABLE chunks (id TEXT PRIMARY KEY, sha256 TEXT, generation INT, ended TEXT)", + "CREATE TABLE chunks (id TEXT PRIMARY KEY, label TEXT)", params![], )?; - conn.execute("CREATE INDEX sha256_idx ON chunks (sha256)", params![])?; - conn.execute( - "CREATE INDEX generation_idx ON chunks (generation)", - params![], - )?; - conn.pragma_update(None, "journal_mode", &"WAL")?; + conn.execute("CREATE INDEX label_idx ON chunks (label)", params![])?; + conn.pragma_update(None, "journal_mode", "WAL")?; Ok(conn) } - pub fn open_db(filename: &Path) -> anyhow::Result<Connection> { + /// Open an existing database in a file. + pub fn open_db(filename: &Path) -> Result<Connection, IndexError> { let flags = OpenFlags::SQLITE_OPEN_READ_WRITE; let conn = Connection::open_with_flags(filename, flags)?; - conn.pragma_update(None, "journal_mode", &"WAL")?; + conn.pragma_update(None, "journal_mode", "WAL")?; Ok(conn) } - pub fn insert(t: &Transaction, chunkid: &ChunkId, meta: &ChunkMeta) -> anyhow::Result<()> { + /// Insert a new chunk's metadata into database. + pub fn insert(t: &Transaction, chunkid: &ChunkId, meta: &ChunkMeta) -> Result<(), IndexError> { let chunkid = format!("{}", chunkid); - let sha256 = meta.sha256(); - let generation = if meta.is_generation() { 1 } else { 0 }; - let ended = meta.ended(); + let label = meta.label(); t.execute( - "INSERT INTO chunks (id, sha256, generation, ended) VALUES (?1, ?2, ?3, ?4)", - params![chunkid, sha256, generation, ended], + "INSERT INTO chunks (id, label) VALUES (?1, ?2)", + params![chunkid, label], )?; Ok(()) } - pub fn remove(conn: &Connection, chunkid: &ChunkId) -> anyhow::Result<()> { + /// Remove a chunk's metadata from the database. + pub fn remove(conn: &Connection, chunkid: &ChunkId) -> Result<(), IndexError> { conn.execute("DELETE FROM chunks WHERE id IS ?1", params![chunkid])?; Ok(()) } - pub fn lookup(conn: &Connection, id: &ChunkId) -> anyhow::Result<ChunkMeta> { + /// Look up a chunk using its id. + pub fn lookup(conn: &Connection, id: &ChunkId) -> Result<ChunkMeta, IndexError> { let mut stmt = conn.prepare("SELECT * FROM chunks WHERE id IS ?1")?; - let iter = stmt.query_map(params![id], |row| row_to_meta(row))?; + let iter = stmt.query_map(params![id], row_to_meta)?; let mut metas: Vec<ChunkMeta> = vec![]; for meta in iter { let meta = meta?; if metas.is_empty() { - eprintln!("lookup: meta={:?}", meta); metas.push(meta); } else { - let err = ObnamError::DuplicateChunk(id.clone()); + let err = IndexError::DuplicateChunk(id.clone()); error!("{}", err); - return Err(err.into()); + return Err(err); } } - if metas.len() == 0 { - eprintln!("lookup: no hits"); - return Err(ObnamError::MissingChunk(id.clone()).into()); + if metas.is_empty() { + return Err(IndexError::MissingChunk(id.clone())); } let r = metas[0].clone(); Ok(r) } - pub fn find_by_256(conn: &Connection, sha256: &str) -> anyhow::Result<Vec<ChunkId>> { - let mut stmt = conn.prepare("SELECT id FROM chunks WHERE sha256 IS ?1")?; - let iter = stmt.query_map(params![sha256], |row| row_to_id(row))?; + /// Find chunks with a given checksum. + pub fn find_by_label(conn: &Connection, label: &str) -> Result<Vec<ChunkId>, IndexError> { + let mut stmt = conn.prepare("SELECT id FROM chunks WHERE label IS ?1")?; + let iter = stmt.query_map(params![label], row_to_id)?; let mut ids = vec![]; for x in iter { let x = x?; @@ -213,9 +202,10 @@ mod sql { Ok(ids) } - pub fn find_generations(conn: &Connection) -> anyhow::Result<Vec<ChunkId>> { - let mut stmt = conn.prepare("SELECT id FROM chunks WHERE generation IS 1")?; - let iter = stmt.query_map(params![], |row| row_to_id(row))?; + /// Find ids of all chunks. + pub fn find_chunk_ids(conn: &Connection) -> Result<Vec<ChunkId>, IndexError> { + let mut stmt = conn.prepare("SELECT id FROM chunks")?; + let iter = stmt.query_map(params![], row_to_id)?; let mut ids = vec![]; for x in iter { let x = x?; @@ -224,20 +214,14 @@ mod sql { Ok(ids) } - pub fn row_to_meta(row: &Row) -> rusqlite::Result<ChunkMeta> { - let sha256: String = row.get(row.column_index("sha256")?)?; - let generation: i32 = row.get(row.column_index("generation")?)?; - let meta = if generation == 0 { - ChunkMeta::new(&sha256) - } else { - let ended: String = row.get(row.column_index("ended")?)?; - ChunkMeta::new_generation(&sha256, &ended) - }; - Ok(meta) + fn row_to_meta(row: &Row) -> rusqlite::Result<ChunkMeta> { + let hash: String = row.get("label")?; + let sha256 = Label::deserialize(&hash).expect("deserialize checksum from database"); + Ok(ChunkMeta::new(&sha256)) } - pub fn row_to_id(row: &Row) -> rusqlite::Result<ChunkId> { - let id: String = row.get(row.column_index("id")?)?; - Ok(ChunkId::from_str(&id)) + fn row_to_id(row: &Row) -> rusqlite::Result<ChunkId> { + let id: String = row.get("id")?; + Ok(ChunkId::recreate(&id)) } } |