summaryrefslogtreecommitdiff
path: root/src/index.rs
blob: 11f348049332d5e563056256566f4dc09386a103 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//! An on-disk index of chunks for the server.

use crate::checksummer::Checksum;
use crate::chunkid::ChunkId;
use crate::chunkmeta::ChunkMeta;
use rusqlite::Connection;
use std::path::Path;

/// A chunk index stored on the disk.
///
/// A chunk index lets the server quickly find chunks based on a
/// string key/value pair.
#[derive(Debug)]
pub struct Index {
    conn: Connection,
}

/// 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 {
    /// 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 { conn })
    }

    /// 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(())
    }

    /// Look up metadata for a chunk, given its id.
    pub fn get_meta(&self, id: &ChunkId) -> Result<ChunkMeta, IndexError> {
        sql::lookup(&self.conn, id)
    }

    /// Remove a chunk's metadata.
    pub fn remove_meta(&mut self, id: &ChunkId) -> Result<(), IndexError> {
        sql::remove(&self.conn, id)
    }

    /// 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)
    }

    /// Find all chunks.
    pub fn all_chunks(&self) -> Result<Vec<ChunkId>, IndexError> {
        sql::find_chunk_ids(&self.conn)
    }
}

#[cfg(test)]
mod test {
    use crate::checksummer::Checksum;

    use super::{ChunkId, ChunkMeta, Index};
    use std::path::Path;
    use tempfile::tempdir;

    fn new_index(dirname: &Path) -> Index {
        Index::new(dirname).unwrap()
    }

    #[test]
    fn remembers_inserted() {
        let id: ChunkId = "id001".parse().unwrap();
        let sum = Checksum::sha256_from_str_unchecked("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_label("abc").unwrap();
        assert_eq!(ids, vec![id]);
    }

    #[test]
    fn does_not_find_uninserted() {
        let id: ChunkId = "id001".parse().unwrap();
        let sum = Checksum::sha256_from_str_unchecked("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_label("def").unwrap().len(), 0)
    }

    #[test]
    fn removes_inserted() {
        let id: ChunkId = "id001".parse().unwrap();
        let sum = Checksum::sha256_from_str_unchecked("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_label("abc").unwrap();
        assert_eq!(ids, vec![]);
    }
}

mod sql {
    use super::{Checksum, IndexError};
    use crate::chunkid::ChunkId;
    use crate::chunkmeta::ChunkMeta;
    use log::error;
    use rusqlite::{params, Connection, OpenFlags, Row, Transaction};
    use std::path::Path;

    /// 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, label TEXT)",
            params![],
        )?;
        conn.execute("CREATE INDEX label_idx ON chunks (label)", params![])?;
        conn.pragma_update(None, "journal_mode", &"WAL")?;
        Ok(conn)
    }

    /// 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")?;
        Ok(conn)
    }

    /// Insert a new chunk's metadata into database.
    pub fn insert(t: &Transaction, chunkid: &ChunkId, meta: &ChunkMeta) -> Result<(), IndexError> {
        let chunkid = format!("{}", chunkid);
        let label = meta.label();
        t.execute(
            "INSERT INTO chunks (id, label) VALUES (?1, ?2)",
            params![chunkid, label],
        )?;
        Ok(())
    }

    /// 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(())
    }

    /// 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_to_meta)?;
        let mut metas: Vec<ChunkMeta> = vec![];
        for meta in iter {
            let meta = meta?;
            if metas.is_empty() {
                metas.push(meta);
            } else {
                let err = IndexError::DuplicateChunk(id.clone());
                error!("{}", err);
                return Err(err);
            }
        }
        if metas.is_empty() {
            return Err(IndexError::MissingChunk(id.clone()));
        }
        let r = metas[0].clone();
        Ok(r)
    }

    /// 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?;
            ids.push(x);
        }
        Ok(ids)
    }

    /// 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?;
            ids.push(x);
        }
        Ok(ids)
    }

    fn row_to_meta(row: &Row) -> rusqlite::Result<ChunkMeta> {
        let hash: String = row.get("label")?;
        let sha256 = Checksum::sha256_from_str_unchecked(&hash);
        Ok(ChunkMeta::new(&sha256))
    }

    fn row_to_id(row: &Row) -> rusqlite::Result<ChunkId> {
        let id: String = row.get("id")?;
        Ok(ChunkId::recreate(&id))
    }
}