use super::attrs::{self, Attr}; use super::blobref::BlobRef; use super::object::Chunk; use super::object::{Attrs, Object}; use crate::common::{json, sqlite}; use log::error; use rusqlite::{params, Connection, Row}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use time::OffsetDateTime; #[derive(Serialize, Deserialize)] pub struct Config { pub root: PathBuf, } const DB_NAME: &str = "index.sqlite3"; const SQL_INIT: &str = " create table objects ( anchor text primary key, size integer not null, chunks text not null, -- JSON array created_at datetime not null, updated_at datetime not null, x_name text, x_group text, x_mime text, x_created_at text, x_updated_at text, x_deleted_at text, x_tags text, x_note text ); "; #[derive(Debug)] pub enum Error { Internal(rusqlite::Error), } type Result = std::result::Result; pub struct Index { db: Connection, } impl Index { pub fn new(config: &Config) -> Result { let db = match Connection::open(config.root.join(DB_NAME)) { Ok(conn) => conn, Err(e) => { error!("Couldn't open connection to the database: {e:?}"); return Err(Error::Internal(e)); } }; Ok(Self { db }) } pub fn put(&self, anchor: &BlobRef, chunks: &[Chunk]) -> Result<()> { // Calculate the overall size let size: usize = chunks.iter().map(|c| c.size).sum(); // Insert or update the object for `anchor` let query = " insert into objects (anchor, size, chunks, created_at, updated_at) values (?, ?, ?, ?, ?) on conflict do update set size = excluded.size, chunks = excluded.chunks, updated_at = excluded.updated_at "; let now = OffsetDateTime::now_utc(); let mut stmt = self.db.prepare(query).map_err(Error::Internal)?; stmt.execute(params![anchor, size, json::serialize(chunks), now, now]) .map_err(Error::Internal)?; Ok(()) } pub fn set_attrs(&mut self, anchor: &BlobRef, attrs: &[Attr]) -> Result<()> { let now = OffsetDateTime::now_utc(); let tx = self.db.transaction().map_err(Error::Internal)?; for attr in attrs { let query = format!( r#"update objects set "x_{}" = ? where anchor = ?"#, attrs::key(attr) ); let mut stmt = tx.prepare(&query).map_err(Error::Internal)?; stmt.execute(params![attr, anchor]) .map_err(Error::Internal)?; } { let mut stmt = tx .prepare("update objects set updated_at = ? where anchor = ?") .map_err(Error::Internal)?; stmt.execute(params![now, anchor]) .map_err(Error::Internal)?; } tx.commit().map_err(Error::Internal)?; Ok(()) } pub fn get(&self, anchor: &BlobRef) -> Result { let query = "select anchor, size, chunks, created_at, updated_at, x_name, x_group, x_mime, x_created_at, x_updated_at, x_deleted_at, x_tags, x_note from objects where anchor = ?"; sqlite::get(&self.db, query, (anchor,), |r| Object::try_from(r)).map_err(Error::Internal) } pub fn list(&self, limit: usize, offset: usize, filter: Option) -> Result> { let where_clause = if let Some(ref filter) = filter { format!(r#""x_{}" = ?"#, attrs::key(filter)) } else { "1=1".into() }; let query = format!( r#"select anchor, size, chunks, created_at, updated_at, x_name, x_group, x_mime, x_created_at, x_updated_at, x_deleted_at, x_tags, x_note from objects where {where_clause} order by rowid limit {limit} offset {offset} "# ); if let Some(filter) = filter { sqlite::list(&self.db, &query, (filter,), |r| Object::try_from(r)) .map_err(Error::Internal) } else { sqlite::list(&self.db, &query, (), |r| Object::try_from(r)).map_err(Error::Internal) } } } // // impl Object { // fn new(blobref: BlobRef) -> Self { // Object { // blobref, // group: None, // is_group: false, // name: None, // kind: None, // size: None, // created_at: None, // updated_at: None, // deleted_at: None, // tags: Vec::new(), // } // } // // pub fn get_name(&self) -> String { // self.name.clone().unwrap_or(self.blobref.to_string()) // } // // pub fn get_size(&self) -> u64 { // self.size.unwrap_or(0) // } // // pub fn is_group(&self) -> bool { // self.is_group // } // // fn apply_patch(&mut self, patch: Patch) { // for change in patch.changes { // self.apply_change(change); // } // } // // fn apply_change(&mut self, change: Change) { // match change { // Change::MarkAsGroup => self.is_group = true, // Change::SetName { name } => self.name = Some(name), // Change::SetGroup { group } => self.group = Some(group), // Change::SetSize { size } => self.size = Some(size), // Change::SetCreatedAt { created_at } => self.created_at = Some(created_at), // Change::SetUpdatedAt { updated_at } => self.updated_at = Some(updated_at), // Change::SetDeletedAt { deleted_at } => self.deleted_at = Some(deleted_at), // Change::SetTags { tags } => self.tags = tags, // }; // } // } // // fn row_to_object(row: &Row) -> rusqlite::Result { // Ok(Object { // blobref: row.get(0)?, // group: row.get(1)?, // is_group: row.get(2)?, // name: row.get(3)?, // size: row.get(4)?, // kind: row.get(5)?, // created_at: row.get(6)?, // updated_at: row.get(7)?, // deleted_at: row.get(8)?, // tags: vec![], // }) // } impl TryFrom<&Row<'_>> for Object { type Error = rusqlite::Error; fn try_from(row: &Row) -> std::result::Result { let chunks_string: Vec = row.get(2)?; let chunks = serde_json::from_slice::>(&chunks_string) .map_err(|_| rusqlite::Error::InvalidQuery)?; let attrs = Attrs { name: row.get(5)?, group: row.get(6)?, mime: row.get(7)?, created_at: row.get(8)?, updated_at: row.get(9)?, deleted_at: row.get(10)?, tags: Vec::new(), note: row.get(12)?, }; Ok(Object { blobref: row.get(0)?, size: row.get(1)?, chunks, attrs, created_at: row.get(3)?, updated_at: row.get(4)?, }) } }