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