diff --git a/meli/src/sqlite3.rs b/meli/src/sqlite3.rs index da66bbd3..9a6de9df 100644 --- a/meli/src/sqlite3.rs +++ b/meli/src/sqlite3.rs @@ -43,6 +43,7 @@ const DB: DatabaseDescription = DatabaseDescription { name: "index.db", identifier: None, application_prefix: "meli", + directory: None, init_script: Some( "CREATE TABLE IF NOT EXISTS envelopes ( id INTEGER PRIMARY KEY, diff --git a/melib/src/imap/mod.rs b/melib/src/imap/mod.rs index 054fe1ff..be6eb3cb 100644 --- a/melib/src/imap/mod.rs +++ b/melib/src/imap/mod.rs @@ -214,9 +214,10 @@ impl UIDStore { #[cfg(not(feature = "sqlite3"))] return Ok(None); #[cfg(feature = "sqlite3")] - return Ok(Some(sync::sqlite3_cache::Sqlite3Cache::get(Arc::clone( - self, - ))?)); + return Ok(Some(sync::sqlite3_cache::Sqlite3Cache::get( + Arc::clone(self), + None, + )?)); } pub fn reset_db(self: &Arc) -> Result<()> { @@ -228,7 +229,7 @@ impl UIDStore { #[cfg(feature = "sqlite3")] use crate::imap::sync::cache::ImapCacheReset; #[cfg(feature = "sqlite3")] - return sync::sqlite3_cache::Sqlite3Cache::reset_db(self); + return sync::sqlite3_cache::Sqlite3Cache::reset_db(self, None); } } diff --git a/melib/src/imap/sync/cache.rs b/melib/src/imap/sync/cache.rs index 58c86410..3568739d 100644 --- a/melib/src/imap/sync/cache.rs +++ b/melib/src/imap/sync/cache.rs @@ -19,7 +19,7 @@ * along with meli. If not, see . */ -use std::convert::TryFrom; +use std::{convert::TryFrom, path::Path}; use super::*; use crate::{ @@ -110,7 +110,7 @@ pub trait ImapCache: Send + std::fmt::Debug { } pub trait ImapCacheReset: Send + std::fmt::Debug { - fn reset_db(uid_store: &UIDStore) -> Result<()> + fn reset_db(uid_store: &UIDStore, data_dir: Option<&Path>) -> Result<()> where Self: Sized; } diff --git a/melib/src/imap/sync/sqlite3_cache.rs b/melib/src/imap/sync/sqlite3_cache.rs index 34632c11..f0b8830b 100644 --- a/melib/src/imap/sync/sqlite3_cache.rs +++ b/melib/src/imap/sync/sqlite3_cache.rs @@ -20,7 +20,11 @@ // // SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later -use std::{collections::BTreeSet, sync::Arc}; +use std::{ + collections::BTreeSet, + path::{Path, PathBuf}, + sync::Arc, +}; use smallvec::SmallVec; @@ -46,12 +50,14 @@ pub struct Sqlite3Cache { connection: Connection, loaded_mailboxes: BTreeSet, uid_store: Arc, + data_dir: Option, } const DB_DESCRIPTION: DatabaseDescription = DatabaseDescription { name: "header_cache.db", identifier: None, application_prefix: "meli", + directory: None, init_script: Some( "PRAGMA foreign_keys = true; PRAGMA encoding = 'UTF-8'; @@ -103,9 +109,11 @@ impl FromSql for ModSequence { } impl Sqlite3Cache { - pub fn get(uid_store: Arc) -> Result> { + pub fn get(uid_store: Arc, data_dir: Option<&Path>) -> Result> { + let data_dir = data_dir.map(|p| p.to_path_buf()); let db_desc = DatabaseDescription { identifier: Some(uid_store.account_name.to_string().into()), + directory: data_dir.clone().map(|p| p.into()), ..DB_DESCRIPTION.clone() }; let connection = match db_desc.open_or_create_db() { @@ -124,6 +132,7 @@ impl Sqlite3Cache { connection, loaded_mailboxes: BTreeSet::default(), uid_store, + data_dir, })) } @@ -141,9 +150,10 @@ impl Sqlite3Cache { } impl ImapCacheReset for Sqlite3Cache { - fn reset_db(uid_store: &UIDStore) -> Result<()> { + fn reset_db(uid_store: &UIDStore, data_dir: Option<&Path>) -> Result<()> { let db_desc = DatabaseDescription { identifier: Some(uid_store.account_name.to_string().into()), + directory: data_dir.map(|p| p.to_path_buf().into()), ..DB_DESCRIPTION.clone() }; db_desc.reset_db() @@ -152,7 +162,7 @@ impl ImapCacheReset for Sqlite3Cache { impl ImapCache for Sqlite3Cache { fn reset(&mut self) -> Result<()> { - Self::reset_db(&self.uid_store) + Self::reset_db(&self.uid_store, self.data_dir.as_deref()) } fn mailbox_state(&mut self, mailbox_hash: MailboxHash) -> Result> { @@ -427,6 +437,7 @@ impl ImapCache for Sqlite3Cache { ref mut connection, ref uid_store, loaded_mailboxes: _, + data_dir: _, } = self; let tx = connection.transaction()?; for item in fetches { @@ -485,6 +496,7 @@ impl ImapCache for Sqlite3Cache { ref mut connection, ref uid_store, loaded_mailboxes: _, + data_dir: _, } = self; let tx = connection.transaction()?; let values = std::rc::Rc::new(env_hashes.iter().map(Value::from).collect::>()); @@ -544,6 +556,7 @@ impl ImapCache for Sqlite3Cache { ref mut connection, ref uid_store, loaded_mailboxes: _, + data_dir: _, } = self; let tx = connection.transaction()?; let mut hash_index_lck = uid_store.hash_index.lock().unwrap(); diff --git a/melib/src/nntp/store.rs b/melib/src/nntp/store.rs index 6b88c7d1..d5287e95 100644 --- a/melib/src/nntp/store.rs +++ b/melib/src/nntp/store.rs @@ -37,6 +37,7 @@ mod inner { name: "nntp_store.db", application_prefix: "meli", identifier: None, + directory: None, init_script: Some( "PRAGMA foreign_keys = true; PRAGMA encoding = 'UTF-8'; diff --git a/melib/src/utils/sqlite3.rs b/melib/src/utils/sqlite3.rs index 24313e6b..ad391754 100644 --- a/melib/src/utils/sqlite3.rs +++ b/melib/src/utils/sqlite3.rs @@ -19,7 +19,12 @@ * along with meli. If not, see . */ -use std::{borrow::Cow, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc}; +use std::{ + borrow::Cow, + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, + sync::Arc, +}; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput}; pub use rusqlite::{self, config::DbConfig, params, Connection}; @@ -40,6 +45,9 @@ pub struct DatabaseDescription { /// directories, used for when the consumer application is not `meli` /// itself. pub application_prefix: &'static str, + /// Optionally override file system location instead of saving at `XDG` data + /// directory. + pub directory: Option>, /// A script that initializes the schema of the database. pub init_script: Option<&'static str>, /// The current value of the `user_version` `PRAGMA` of the `sqlite3` @@ -60,16 +68,65 @@ impl DatabaseDescription { || self.name.into(), |id| format!("{}_{}", id, self.name).into(), ); + + for (field_name, field_value) in [ + ("name", self.name), + ("identifier", self.identifier.as_deref().unwrap_or_default()), + ("application_prefix", self.application_prefix), + ] { + if field_value.contains(std::path::MAIN_SEPARATOR_STR) { + return Err(Error::new(format!( + "Database description for `{}{}{}` field {} cannot contain current platform's \ + path separator {}. Got: {}.", + self.identifier.as_deref().unwrap_or_default(), + if self.identifier.is_none() { "" } else { ":" }, + self.name, + field_name, + std::path::MAIN_SEPARATOR_STR, + field_value, + )) + .set_kind(ErrorKind::ValueError)); + } + } + + if let Some(directory) = self.directory.as_deref() { + if !directory.is_dir() { + return Err(Error::new(format!( + "Database description for `{}{}{}` expects a valid directory path value. Got: \ + {}.", + self.identifier.as_deref().unwrap_or_default(), + if self.identifier.is_none() { "" } else { ":" }, + self.name, + directory.display() + )) + .set_kind(ErrorKind::ValueError)); + } + return Ok(directory.join(name.as_ref())); + } let data_dir = xdg::BaseDirectories::with_prefix(self.application_prefix).map_err(|err| { Error::new(format!( + "Could not create sqlite3 database file for `{}{}{}` in XDG data directory.", + self.identifier.as_deref().unwrap_or_default(), + if self.identifier.is_none() { "" } else { ":" }, + self.name, + )) + .set_details(format!( "Could not open XDG data directory with prefix {}", self.application_prefix )) + .set_kind(ErrorKind::Platform) .set_source(Some(Arc::new(err))) })?; data_dir.place_data_file(name.as_ref()).map_err(|err| { - Error::new(format!("Could not create `{}`", name)).set_source(Some(Arc::new(err))) + Error::new(format!( + "Could not create sqlite3 database file for `{}{}{}` in XDG data directory.", + self.identifier.as_deref().unwrap_or_default(), + if self.identifier.is_none() { "" } else { ":" }, + self.name, + )) + .set_kind(ErrorKind::Platform) + .set_source(Some(Arc::new(err))) }) }