You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
meli/melib/src/utils/sqlite3.rs

185 lines
7.1 KiB
Rust

/*
* meli - melib
*
* Copyright 2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::{borrow::Cow, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput};
pub use rusqlite::{self, config::DbConfig, params, Connection};
use crate::{error::*, log, Envelope};
/// A description for creating, opening and handling application databases.
#[derive(Clone, Debug)]
pub struct DatabaseDescription {
/// A name that represents the function of this database, e.g.
/// `headers_cache`, `contacts`, `settings`, etc.
pub name: &'static str,
/// An optional identifier string that along with
/// [`DatabaseDescription::name`] makes a specialized identifier for the
/// database. E.g. an account name, a date, etc.
pub identifier: Option<Cow<'static, str>>,
/// The name of the application to use when storing the database in `XDG`
/// directories, used for when the consumer application is not `meli`
/// itself.
pub application_prefix: &'static str,
/// 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`
/// database, used for schema versioning.
pub version: u32,
}
impl DatabaseDescription {
/// Returns whether the computed database path for this description exist.
pub fn exists(&self) -> Result<bool> {
let path = self.db_path()?;
Ok(path.exists())
}
/// Returns the computed database path for this description.
pub fn db_path(&self) -> Result<PathBuf> {
let name: Cow<'static, str> = self.identifier.as_ref().map_or_else(
|| self.name.into(),
|id| format!("{}_{}", id, self.name).into(),
);
let data_dir =
xdg::BaseDirectories::with_prefix(self.application_prefix).map_err(|err| {
Error::new(format!(
"Could not open XDG data directory with prefix {}",
self.application_prefix
))
.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)))
})
}
/// Returns an [`rusqlite::Connection`] for this description.
pub fn open_or_create_db(&self) -> Result<Connection> {
let mut second_try: bool = false;
let db_path = self.db_path()?;
let set_mode = !db_path.exists();
if set_mode {
log::info!("Creating {} database in {}", self.name, db_path.display());
}
loop {
let mut inner_fn = || {
let conn = Connection::open(&db_path)?;
conn.busy_timeout(std::time::Duration::new(10, 0))?;
for conf_flag in [
DbConfig::SQLITE_DBCONFIG_ENABLE_FKEY,
DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER,
]
.into_iter()
{
conn.set_db_config(conf_flag, true)?;
}
rusqlite::vtab::array::load_module(&conn)?;
if set_mode {
let file = std::fs::File::open(&db_path)?;
let metadata = file.metadata()?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o600); // Read/write for owner only.
file.set_permissions(permissions)?;
}
let _: String =
conn.pragma_update_and_check(None, "journal_mode", "WAL", |row| row.get(0))?;
let version: i32 =
conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
if version != 0_i32 && version as u32 != self.version {
log::info!(
"Database version mismatch, is {} but expected {}. Attempting to recreate \
database.",
version,
self.version
);
if second_try {
return Err(Error::new(format!(
"Database version mismatch, is {} but expected {}. Could not recreate \
database.",
version, self.version
)));
}
self.reset_db()?;
second_try = true;
return Ok(conn);
}
if version == 0 {
conn.pragma_update(None, "user_version", self.version)?;
}
if let Some(s) = self.init_script {
conn.execute_batch(s)
.map_err(|err| Error::new(err.to_string()))?;
}
Ok(conn)
};
inner_fn().unwrap();
match inner_fn() {
Ok(_) if second_try => continue,
Ok(conn) => return Ok(conn),
Err(err) => {
return Err(Error::new(format!(
"{}: Could not open or create database",
db_path.display()
))
.set_source(Some(Arc::new(err))))
}
}
}
}
/// Reset database to a clean slate.
pub fn reset_db(&self) -> Result<()> {
let db_path = self.db_path()?;
if !db_path.exists() {
return Ok(());
}
log::info!("Resetting {} database in {}", self.name, db_path.display());
std::fs::remove_file(&db_path).map_err(|err| {
Error::new(format!("{}: could not remove file", db_path.display()))
.set_kind(ErrorKind::from(err.kind()))
.set_source(Some(Arc::new(err)))
})?;
Ok(())
}
}
impl ToSql for Envelope {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
let v: Vec<u8> = serde_json::to_vec(self).map_err(|e| {
rusqlite::Error::ToSqlConversionFailure(Box::new(Error::new(e.to_string())))
})?;
Ok(ToSqlOutput::from(v))
}
}
impl FromSql for Envelope {
fn column_result(value: rusqlite::types::ValueRef) -> FromSqlResult<Self> {
let b: Vec<u8> = FromSql::column_result(value)?;
serde_json::from_slice(&b).map_err(|e| FromSqlError::Other(Box::new(e)))
}
}