mirror of https://github.com/djcb/mu.git
lib: create mu-xapian-db, mu-config
XapianDb is a fairly thing wrapper around Xapian, which handles locking, exception handling and some tracking. On top of that, Config add a configuration database for type / introspectable configuration info.
This commit is contained in:
parent
426c1d35d0
commit
c28fde9155
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
**
|
||||
** This program 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, or (at your option) any
|
||||
** later version.
|
||||
**
|
||||
** This program 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 this program; if not, write to the Free Software Foundation,
|
||||
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
**
|
||||
*/
|
||||
|
||||
#include "mu-config.hh"
|
||||
|
||||
using namespace Mu;
|
||||
|
||||
constexpr /*static*/ bool
|
||||
validate_props()
|
||||
{
|
||||
size_t id{0};
|
||||
for (auto&& prop: Config::properties) {
|
||||
|
||||
// ids must match
|
||||
if (static_cast<size_t>(prop.id) != id)
|
||||
return false;
|
||||
++id;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef BUILD_TESTS
|
||||
#define static_assert g_assert_true
|
||||
#endif /*BUILD_TESTS*/
|
||||
|
||||
[[maybe_unused]]
|
||||
static void
|
||||
test_props()
|
||||
{
|
||||
static_assert(validate_props());
|
||||
}
|
||||
|
||||
#ifdef BUILD_TESTS
|
||||
/*
|
||||
* Tests.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "utils/mu-test-utils.hh"
|
||||
|
||||
static void
|
||||
test_basic()
|
||||
{
|
||||
MemDb db;
|
||||
Config conf_db{db};
|
||||
|
||||
using Id = Config::Id;
|
||||
|
||||
{
|
||||
const auto rmd = conf_db.get<Id::RootMaildir>();
|
||||
g_assert_true(rmd.empty());
|
||||
}
|
||||
|
||||
{
|
||||
conf_db.set<Id::RootMaildir>("/home/djcb/Maildir");
|
||||
const auto rmd = conf_db.get<Id::RootMaildir>();
|
||||
assert_equal(rmd, "/home/djcb/Maildir");
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char* argv[])
|
||||
{
|
||||
mu_test_init(&argc, &argv);
|
||||
|
||||
g_test_add_func("/config-db/props", test_props);
|
||||
g_test_add_func("/config-db/basic", test_basic);
|
||||
|
||||
return g_test_run();
|
||||
}
|
||||
#endif /*BUILD_TESTS*/
|
|
@ -0,0 +1,293 @@
|
|||
/*
|
||||
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
**
|
||||
** This program 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, or (at your option) any
|
||||
** later version.
|
||||
**
|
||||
** This program 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 this program; if not, write to the Free Software Foundation,
|
||||
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
**
|
||||
*/
|
||||
|
||||
#ifndef MU_CONFIG_HH__
|
||||
#define MU_CONFIG_HH__
|
||||
|
||||
#include <cstdint>
|
||||
#include <cinttypes>
|
||||
#include <string_view>
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <variant>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <xapian.h>
|
||||
|
||||
#include "mu-xapian-db.hh"
|
||||
|
||||
#include <utils/mu-utils.hh>
|
||||
#include <utils/mu-result.hh>
|
||||
#include <utils/mu-option.hh>
|
||||
|
||||
namespace Mu {
|
||||
|
||||
struct Property {
|
||||
enum struct Id {
|
||||
BatchSize, /**< Xapian batch-size */
|
||||
Contacts, /**< Cache of contact information */
|
||||
Created, /** Time of creation */
|
||||
LastChange, /**< Time of last change */
|
||||
LastIndex, /**< Time of last index */
|
||||
MaxMessageSize, /**< Maximum message size (in bytes) */
|
||||
PersonalAddresses, /**< List of personal e-mail addresses */
|
||||
RootMaildir, /**< Root maildir path */
|
||||
SchemaVersion, /**< Xapian DB schema version */
|
||||
/* <private> */
|
||||
_count_ /* Number of Ids */
|
||||
};
|
||||
|
||||
static constexpr size_t id_size = static_cast<size_t>(Id::_count_);
|
||||
/**< Number of Property::Ids */
|
||||
|
||||
enum struct Flags {
|
||||
None = 0, /**< Nothing in particular */
|
||||
ReadOnly = 1 << 0, /**< Property is read-only for external use
|
||||
* (but can change from within the store) */
|
||||
Configurable = 1 << 1, /**< A user-configurable parameter; name
|
||||
* starts with 'conf-' */
|
||||
Internal = 1 << 2, /**< Mu-internal field */
|
||||
};
|
||||
enum struct Type {
|
||||
Number, /**< Some number */
|
||||
Timestamp, /**< Timestamp number */
|
||||
Path, /**< Path string */
|
||||
String, /**< A string */
|
||||
StringList, /**< A list of strings */
|
||||
};
|
||||
|
||||
using Value = std::variant<int64_t, std::string, std::vector<std::string> >;
|
||||
|
||||
Id id;
|
||||
Type type;
|
||||
Flags flags;
|
||||
std::string_view name;
|
||||
std::string_view default_val;
|
||||
std::string_view description;
|
||||
};
|
||||
|
||||
MU_ENABLE_BITOPS(Property::Flags);
|
||||
|
||||
class Config {
|
||||
public:
|
||||
using Id = Property::Id;
|
||||
using Type = Property::Type;
|
||||
using Flags = Property::Flags;
|
||||
using Value = Property::Value;
|
||||
|
||||
static constexpr std::array<Property, Property::id_size>
|
||||
properties = {{
|
||||
{
|
||||
Id::BatchSize,
|
||||
Type::Number,
|
||||
Flags::Configurable,
|
||||
"batch-size",
|
||||
"250000",
|
||||
"Version of the Xapian database schema"
|
||||
},
|
||||
{
|
||||
Id::Contacts,
|
||||
Type::String,
|
||||
Flags::Internal,
|
||||
"contacts",
|
||||
{},
|
||||
"Serialized contact information"
|
||||
},
|
||||
{
|
||||
Id::Created,
|
||||
Type::Timestamp,
|
||||
Flags::ReadOnly,
|
||||
MetadataIface::created_key,
|
||||
{},
|
||||
"Database creation time"
|
||||
},
|
||||
{
|
||||
Id::LastChange,
|
||||
Type::Timestamp,
|
||||
Flags::ReadOnly,
|
||||
MetadataIface::last_change_key,
|
||||
{},
|
||||
"Time when last change occurred"
|
||||
},
|
||||
{
|
||||
Id::LastIndex,
|
||||
Type::Timestamp,
|
||||
Flags::ReadOnly,
|
||||
"last-index",
|
||||
{},
|
||||
"Time when last indexing operation was completed"
|
||||
},
|
||||
{
|
||||
Id::MaxMessageSize,
|
||||
Type::Number,
|
||||
Flags::Configurable,
|
||||
"max-message-size",
|
||||
"100000000", // default max: 100M bytes
|
||||
"Maximum message size (in bytes); bigger messages are skipped"
|
||||
},
|
||||
{
|
||||
Id::PersonalAddresses,
|
||||
Type::StringList,
|
||||
Flags::Configurable,
|
||||
"personal-addresses",
|
||||
"",
|
||||
"List of personal e-mail addresses, literal or /regexp/"
|
||||
},
|
||||
{
|
||||
Id::RootMaildir,
|
||||
Type::Path,
|
||||
Flags::ReadOnly,
|
||||
"root-maildir",
|
||||
{},
|
||||
"Absolute path of the top of the Maildir tree"
|
||||
},
|
||||
{
|
||||
Id::SchemaVersion,
|
||||
Type::Number,
|
||||
Flags::ReadOnly,
|
||||
"schema-version",
|
||||
{},
|
||||
"Version of the Xapian database schema"
|
||||
},
|
||||
}};
|
||||
|
||||
/**
|
||||
* Construct a new Config object.
|
||||
*
|
||||
* @param db The config-store (database); must stay valid for the
|
||||
* lifetime of this config.
|
||||
*/
|
||||
Config(MetadataIface& cstore): cstore_{cstore}{}
|
||||
|
||||
/**
|
||||
* Get the property by its id
|
||||
*
|
||||
* @param id a property id (!= Id::_count_)
|
||||
*
|
||||
* @return the property
|
||||
*/
|
||||
template <Id ID>
|
||||
constexpr static const Property& property() {
|
||||
return properties[static_cast<size_t>(ID)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Property by its name.
|
||||
*
|
||||
* @param name The name
|
||||
*
|
||||
* @return the property or Nothing if not found
|
||||
*/
|
||||
static Option<const Property&> property(const std::string& name) {
|
||||
const auto pname{std::string_view(name.data(), name.size())};
|
||||
for(auto&& prop: properties)
|
||||
if (prop.name == pname)
|
||||
return prop;
|
||||
return Nothing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the property value of the correct type
|
||||
*
|
||||
* @param prop_id a property id
|
||||
*
|
||||
* @return the value or Nothing
|
||||
*/
|
||||
template<Id ID>
|
||||
auto get() const {
|
||||
constexpr auto&& prop{property<ID>()};
|
||||
const auto str = std::invoke([&]()->std::string {
|
||||
const auto str = cstore_.metadata(std::string{prop.name});
|
||||
return str.empty() ? std::string{prop.default_val} : str;
|
||||
});
|
||||
if constexpr (prop.type == Type::Number)
|
||||
return static_cast<size_t>(str.empty() ? 0 : std::atoll(str.c_str()));
|
||||
else if constexpr (prop.type == Type::Timestamp)
|
||||
return static_cast<time_t>(str.empty() ? 0 : std::atoll(str.c_str()));
|
||||
else if constexpr (prop.type == Type::Path || prop.type == Type::String)
|
||||
return str;
|
||||
else if constexpr (prop.type == Type::StringList)
|
||||
return split(str, SepaChar1);
|
||||
|
||||
throw std::logic_error("invalid prop " + std::string{prop.name});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a new value for some property
|
||||
*
|
||||
* @param prop_id property-id
|
||||
* @param val the new value (of the correct type)
|
||||
*
|
||||
* @return Ok() or some error
|
||||
*/
|
||||
template<Id ID, typename T>
|
||||
Result<void> set(const T& val) {
|
||||
constexpr auto&& prop{property<ID>()};
|
||||
if (read_only())
|
||||
return Err(Error::Code::AccessDenied,
|
||||
"cannot write to read-only db");
|
||||
|
||||
const auto strval = std::invoke([&]{
|
||||
if constexpr (prop.type == Type::Number || prop.type == Type::Timestamp)
|
||||
return format("%" PRIi64, static_cast<int64_t>(val));
|
||||
else if constexpr (prop.type == Type::Path || prop.type == Type::String)
|
||||
return std::string{val};
|
||||
else if constexpr (prop.type == Type::StringList)
|
||||
return join(val, SepaChar1);
|
||||
|
||||
throw std::logic_error("invalid prop " + std::string{prop.name});
|
||||
});
|
||||
|
||||
cstore_.set_metadata(std::string{prop.name}, strval);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a read-only Config?
|
||||
*
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
bool read_only() const { return cstore_.read_only();};
|
||||
|
||||
/**
|
||||
* Import configurable settings to some other MetadataIface
|
||||
*
|
||||
* @param target some other metadata interface
|
||||
*/
|
||||
void import_configurable(const Config& src) const {
|
||||
for (auto&& prop: properties) {
|
||||
if (any_of(prop.flags & Flags::Configurable)) {
|
||||
const auto&& key{std::string{prop.name}};
|
||||
if (auto&& val{src.cstore_.metadata(key)}; !val.empty())
|
||||
cstore_.set_metadata(key, std::string{val});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t SepaChar1 = 0xfe;
|
||||
MetadataIface& cstore_;
|
||||
};
|
||||
|
||||
|
||||
} // namespace Mu
|
||||
|
||||
#endif /* MU_CONFIG_DB_HH__ */
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
**
|
||||
** This program 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, or (at your option) any
|
||||
** later version.
|
||||
**
|
||||
** This program 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 this program; if not, write to the Free Software Foundation,
|
||||
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
**
|
||||
*/
|
||||
|
||||
|
||||
#include "mu-xapian-db.hh"
|
||||
#include "utils/mu-utils.hh"
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
using namespace Mu;
|
||||
|
||||
struct XapianDb::Private {
|
||||
using DbType = std::variant<Xapian::Database, Xapian::WritableDatabase>;
|
||||
|
||||
Private(Xapian::Database&& db, const std::string& path):
|
||||
db_{std::move(db)}, path_{path} {}
|
||||
Private(Xapian::WritableDatabase&& wdb, const std::string& path):
|
||||
db_{std::move(wdb)}, path_{path} {}
|
||||
|
||||
DbType db_;
|
||||
const std::string path_;
|
||||
mutable std::mutex lock_;
|
||||
};
|
||||
|
||||
|
||||
XapianDb::XapianDb(Xapian::Database&& db, const std::string& path):
|
||||
priv_{std::make_unique<Private>(std::move(db), path)}
|
||||
{}
|
||||
XapianDb::XapianDb(Xapian::WritableDatabase&& wdb, const std::string& path):
|
||||
priv_{std::make_unique<Private>(std::move(wdb), path)}
|
||||
{}
|
||||
|
||||
XapianDb::XapianDb(XapianDb&& rhs) = default;
|
||||
XapianDb::~XapianDb() = default;
|
||||
|
||||
|
||||
const Xapian::Database&
|
||||
XapianDb::db() const
|
||||
{
|
||||
if (std::holds_alternative<Xapian::WritableDatabase>(priv_->db_))
|
||||
return std::get<Xapian::WritableDatabase>(priv_->db_);
|
||||
else
|
||||
return std::get<Xapian::Database>(priv_->db_);
|
||||
}
|
||||
|
||||
Xapian::WritableDatabase&
|
||||
XapianDb::wdb()
|
||||
{
|
||||
if (read_only())
|
||||
throw std::runtime_error("database is read-only");
|
||||
return std::get<Xapian::WritableDatabase>(priv_->db_);
|
||||
}
|
||||
|
||||
bool
|
||||
XapianDb::read_only() const
|
||||
{
|
||||
return !std::holds_alternative<Xapian::WritableDatabase>(priv_->db_);
|
||||
}
|
||||
|
||||
const std::string&
|
||||
XapianDb::path() const
|
||||
{
|
||||
return priv_->path_;
|
||||
}
|
||||
|
||||
std::mutex&
|
||||
XapianDb::lock() const
|
||||
{
|
||||
return priv_->lock_;
|
||||
}
|
||||
|
||||
void
|
||||
XapianDb::set_timestamp(const std::string_view key)
|
||||
{
|
||||
wdb().set_metadata(std::string{key},
|
||||
format("%" PRIi64, static_cast<int64_t>(::time({}))));
|
||||
}
|
||||
|
||||
Result<XapianDb>
|
||||
XapianDb::make(const std::string& db_path, Flavor flavor) noexcept try {
|
||||
|
||||
if (flavor != Flavor::ReadOnly) {
|
||||
/* we do our own flushing, set Xapian's internal one as
|
||||
* the backstop*/
|
||||
g_setenv("XAPIAN_FLUSH_THRESHOLD", "500000", 1);
|
||||
/* create path if needed */
|
||||
if (g_mkdir_with_parents(db_path.c_str(), 0700) != 0)
|
||||
return Err(Error::Code::File, "failed to create database dir %s: %s",
|
||||
db_path.c_str(), ::strerror(errno));
|
||||
}
|
||||
|
||||
switch (flavor) {
|
||||
|
||||
case Flavor::ReadOnly:
|
||||
return Ok(XapianDb(Xapian::Database(db_path), db_path));
|
||||
case Flavor::Open:
|
||||
return Ok(XapianDb(Xapian::WritableDatabase(
|
||||
db_path, Xapian::DB_OPEN), db_path));
|
||||
case Flavor::CreateOverwrite: {
|
||||
auto&& xdb{XapianDb(Xapian::WritableDatabase(
|
||||
db_path,
|
||||
Xapian::DB_CREATE_OR_OVERWRITE), db_path)};
|
||||
xdb.set_timestamp(MetadataIface::created_key);
|
||||
return Ok(std::move(xdb));
|
||||
}
|
||||
default:
|
||||
return Err(Error::Code::Internal, "invalid xapian options");
|
||||
}
|
||||
|
||||
} catch (const Xapian::DatabaseLockError& xde) {
|
||||
return Err(Error::Code::StoreLock, "%s", xde.get_msg().c_str());
|
||||
} catch (const Xapian::DatabaseError& xde) {
|
||||
return Err(Error::Code::Store, "%s", xde.get_msg().c_str());
|
||||
} catch (const Mu::Error& me) {
|
||||
return Err(me);
|
||||
} catch (...) {
|
||||
return Err(Error::Code::Internal,
|
||||
"something went wrong when opening store @ %s", db_path.c_str());
|
||||
}
|
|
@ -0,0 +1,404 @@
|
|||
/*
|
||||
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
**
|
||||
** This program 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, or (at your option) any
|
||||
** later version.
|
||||
**
|
||||
** This program 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 this program; if not, write to the Free Software Foundation,
|
||||
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
**
|
||||
*/
|
||||
|
||||
#ifndef MU_XAPIAN_DB_HH__
|
||||
#define MU_XAPIAN_DB_HH__
|
||||
|
||||
#include <variant>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <functional>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#include <xapian.h>
|
||||
#include <utils/mu-result.hh>
|
||||
|
||||
namespace Mu {
|
||||
|
||||
// LCOV_EXCL_START
|
||||
|
||||
// avoid exception-handling boilerplate.
|
||||
template <typename Func> void
|
||||
xapian_try(Func&& func) noexcept
|
||||
try {
|
||||
func();
|
||||
} catch (const Xapian::Error& xerr) {
|
||||
g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str());
|
||||
} catch (const std::runtime_error& re) {
|
||||
g_critical("%s: runtime error: %s", __func__, re.what());
|
||||
} catch (const std::exception& e) {
|
||||
g_critical("%s: caught std::exception: %s", __func__, e.what());
|
||||
} catch (...) {
|
||||
g_critical("%s: caught exception", __func__);
|
||||
}
|
||||
|
||||
template <typename Func, typename Default = std::invoke_result<Func>> auto
|
||||
xapian_try(Func&& func, Default&& def) noexcept -> std::decay_t<decltype(func())>
|
||||
try {
|
||||
return func();
|
||||
} catch (const Xapian::DocNotFoundError& xerr) {
|
||||
return static_cast<Default>(def);
|
||||
} catch (const Xapian::Error& xerr) {
|
||||
g_warning("%s: xapian error '%s'", __func__, xerr.get_msg().c_str());
|
||||
return static_cast<Default>(def);
|
||||
} catch (const std::runtime_error& re) {
|
||||
g_critical("%s: runtime error: %s", __func__, re.what());
|
||||
return static_cast<Default>(def);
|
||||
} catch (const std::exception& e) {
|
||||
g_critical("%s: caught std::exception: %s", __func__, e.what());
|
||||
return static_cast<Default>(def);
|
||||
} catch (...) {
|
||||
g_critical("%s: caught exception", __func__);
|
||||
return static_cast<Default>(def);
|
||||
}
|
||||
|
||||
|
||||
template <typename Func> auto
|
||||
xapian_try_result(Func&& func) noexcept -> std::decay_t<decltype(func())>
|
||||
try {
|
||||
return func();
|
||||
} catch (const Xapian::Error& xerr) {
|
||||
return Err(Error::Code::Xapian, "%s", xerr.get_error_string());
|
||||
} catch (const std::runtime_error& re) {
|
||||
return Err(Error::Code::Internal, "runtime error: %s", re.what());
|
||||
} catch (const std::exception& e) {
|
||||
return Err(Error::Code::Internal, "caught std::exception: %s", e.what());
|
||||
} catch (...) {
|
||||
return Err(Error::Code::Internal, "caught exception");
|
||||
}
|
||||
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
/// abstract base
|
||||
struct MetadataIface {
|
||||
virtual ~MetadataIface(){}
|
||||
virtual void set_metadata(const std::string& name, const std::string& val) = 0;
|
||||
virtual std::string metadata(const std::string& name) const = 0;
|
||||
virtual bool read_only() const = 0;
|
||||
|
||||
using each_func = std::function<void(const std::string&, const std::string&)>;
|
||||
virtual void for_each(each_func&& func) const =0;
|
||||
|
||||
/*
|
||||
* These are special: handled on the Xapian db level
|
||||
* rather than Config
|
||||
*/
|
||||
static inline constexpr std::string_view created_key = "created";
|
||||
static inline constexpr std::string_view last_change_key = "last-change";
|
||||
};
|
||||
|
||||
|
||||
/// In-memory db
|
||||
struct MemDb: public MetadataIface {
|
||||
/**
|
||||
* Set some metadata
|
||||
*
|
||||
* @param name key name
|
||||
* @param val value
|
||||
*/
|
||||
void set_metadata(const std::string& name, const std::string& val) override {
|
||||
map_.erase(name);
|
||||
map_[name] = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for given key, empty if not found
|
||||
*
|
||||
* @param name key name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
std::string metadata(const std::string& name) const override {
|
||||
if (auto&& it = map_.find(name); it != map_.end())
|
||||
return it->second;
|
||||
else
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this db read-only?
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
bool read_only() const override { return false; }
|
||||
|
||||
|
||||
/**
|
||||
* Invoke function for each key/value pair. Do not call
|
||||
* @this from each_func().
|
||||
*
|
||||
* @param func a function
|
||||
*/
|
||||
void for_each(MetadataIface::each_func&& func) const override {
|
||||
for (const auto& [key, value] : map_)
|
||||
func(key, value);
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::string> map_;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fairly thin wrapper around Xapian::Database and Xapian::WritableDatabase
|
||||
* with just the things we need + locking + exception handling
|
||||
*/
|
||||
class XapianDb: public MetadataIface {
|
||||
#define DB_LOCKED std::unique_lock l{lock()}
|
||||
public:
|
||||
/**
|
||||
* Type of database to create.
|
||||
*
|
||||
*/
|
||||
enum struct Flavor {
|
||||
ReadOnly, /**< Read-only database */
|
||||
Open, /**< Open existing read-write */
|
||||
CreateOverwrite, /**< Create new or overwrite existing */
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a new Xapian Database wrapper object
|
||||
*
|
||||
* @param db_path path to the database
|
||||
* @param flavor type of database to created
|
||||
*
|
||||
* @return a result; either a XapianDb or an Error.
|
||||
*/
|
||||
static Result<XapianDb> make(const std::string& db_path, Flavor flavor) noexcept;
|
||||
|
||||
/**
|
||||
* Is the database read-only?
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
bool read_only() const override;
|
||||
|
||||
/**
|
||||
* Path to the database; empty for in-memory database
|
||||
*
|
||||
* @return path to database
|
||||
*/
|
||||
const std::string& path() const;
|
||||
|
||||
/**
|
||||
* Get the number of documents (messages) in the database
|
||||
*
|
||||
* @return number
|
||||
*/
|
||||
size_t size() const noexcept {
|
||||
return xapian_try([this]{
|
||||
DB_LOCKED; return db().get_doccount(); }, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the the base empty?
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
size_t empty() const noexcept { return size() == 0; }
|
||||
|
||||
/**
|
||||
* Get a database enquire object for queries.
|
||||
*
|
||||
* @return an enquire object
|
||||
*/
|
||||
Xapian::Enquire enquire() const {
|
||||
DB_LOCKED; return Xapian::Enquire(db());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a document from the database if there is one
|
||||
*
|
||||
* @param id id of the document
|
||||
*
|
||||
* @return the document or an error
|
||||
*/
|
||||
Result<Xapian::Document> document(Xapian::docid id) const {
|
||||
return xapian_try_result([&]{
|
||||
DB_LOCKED; return Ok(db().get_document(id)); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata for the given key
|
||||
*
|
||||
* @param key key (non-empty)
|
||||
*
|
||||
* @return the value or empty
|
||||
*/
|
||||
std::string metadata(const std::string& key) const override {
|
||||
return xapian_try([&]{
|
||||
DB_LOCKED; return db().get_metadata(key);}, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set metadata for the given key
|
||||
*
|
||||
* @param key key (non-empty)
|
||||
* @param val new value for key
|
||||
*/
|
||||
void set_metadata(const std::string& key, const std::string& val) override {
|
||||
xapian_try([&] { DB_LOCKED; wdb().set_metadata(key, val); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke function for each key/value pair. This is called with the lock
|
||||
* held, so do not call functions on @this is each_func().
|
||||
*
|
||||
* @param each_func a function
|
||||
*/
|
||||
//using each_func = MetadataIface::each_func;
|
||||
void for_each(MetadataIface::each_func&& func) const override {
|
||||
xapian_try([&]{
|
||||
DB_LOCKED;
|
||||
for (auto&& it = db().metadata_keys_begin();
|
||||
it != db().metadata_keys_end(); ++it)
|
||||
func(*it, db().get_metadata(*it));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Does the given term exist in the database?
|
||||
*
|
||||
* @param term some term
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
bool term_exists(const std::string& term) const {
|
||||
return xapian_try([&]{
|
||||
DB_LOCKED; return db().term_exists(term);}, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace document in database
|
||||
*
|
||||
* @param term unique term
|
||||
* @param id docid
|
||||
* @param doc replacement document
|
||||
*
|
||||
* @return new docid or nothing.
|
||||
*/
|
||||
Xapian::docid replace_document(const std::string& term, const Xapian::Document& doc) {
|
||||
return xapian_try([&]{
|
||||
DB_LOCKED;
|
||||
auto&& id= wdb().replace_document(term, doc);
|
||||
set_timestamp(MetadataIface::last_change_key);
|
||||
return id;
|
||||
}, 0);
|
||||
}
|
||||
void replace_document(const Xapian::docid id, const Xapian::Document& doc) {
|
||||
xapian_try([&]{
|
||||
DB_LOCKED;
|
||||
wdb().replace_document(id, doc);
|
||||
set_timestamp(MetadataIface::last_change_key);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete document(s) for the given term or id
|
||||
*
|
||||
* @param term a term
|
||||
*/
|
||||
void delete_document(const std::string& term) {
|
||||
xapian_try([&]{
|
||||
DB_LOCKED;
|
||||
wdb().delete_document(term);
|
||||
set_timestamp(MetadataIface::last_change_key);
|
||||
});
|
||||
}
|
||||
|
||||
void delete_document(Xapian::docid id) {
|
||||
xapian_try([&]{
|
||||
DB_LOCKED;
|
||||
wdb().delete_document(id);
|
||||
set_timestamp(MetadataIface::last_change_key);
|
||||
});
|
||||
}
|
||||
|
||||
template<typename Func>
|
||||
size_t all_terms(const std::string& prefix, Func&& func) const {
|
||||
DB_LOCKED;
|
||||
size_t n{};
|
||||
for (auto it = db().allterms_begin(prefix); it != db().allterms_end(prefix); ++it) {
|
||||
if (!func(*it))
|
||||
break;
|
||||
++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/*
|
||||
* transactions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Start a transaction
|
||||
*
|
||||
* @param flushed
|
||||
*/
|
||||
void begin_transaction(bool flushed=true) {
|
||||
xapian_try([&]{ DB_LOCKED; return wdb().begin_transaction(flushed);});
|
||||
}
|
||||
/**
|
||||
* Commit a transaction
|
||||
*/
|
||||
void commit_transaction() {
|
||||
xapian_try([&]{ DB_LOCKED; return wdb().commit_transaction();});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a reference to the underlying database
|
||||
*
|
||||
* @return db database reference
|
||||
*/
|
||||
const Xapian::Database& db() const;
|
||||
/**
|
||||
* Get a reference to the underlying writable database. It is
|
||||
* an error to call this on a read-only database.
|
||||
*
|
||||
* @return db writable database reference
|
||||
*/
|
||||
Xapian::WritableDatabase& wdb();
|
||||
|
||||
/**
|
||||
* DTOR
|
||||
*/
|
||||
~XapianDb();
|
||||
|
||||
/**
|
||||
* Move CTOR
|
||||
*
|
||||
* @param rhs
|
||||
*/
|
||||
XapianDb(XapianDb&& rhs);
|
||||
|
||||
private:
|
||||
XapianDb(Xapian::Database&& db, const std::string& path);
|
||||
XapianDb(Xapian::WritableDatabase&& wdb, const std::string& path);
|
||||
void set_timestamp(const std::string_view key);
|
||||
std::mutex& lock() const;
|
||||
|
||||
struct Private;
|
||||
std::unique_ptr<Private> priv_;
|
||||
};
|
||||
|
||||
} // namespace Mu
|
||||
|
||||
#endif /* MU_XAPIAN_DB_HH__ */
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
** Copyright (C) 2021-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||
**
|
||||
** This program 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, or (at your option) any
|
||||
** later version.
|
||||
**
|
||||
** This program 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 this program; if not, write to the Free Software Foundation,
|
||||
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
**
|
||||
*/
|
||||
|
||||
#ifndef MU_XAPIAN_UTILS_HH__
|
||||
#define MU_XAPIAN_UTILS_HH__
|
||||
|
||||
#include <xapian.h>
|
||||
#include <glib.h>
|
||||
#include "mu-result.hh"
|
||||
|
||||
namespace Mu {
|
||||
|
||||
// LCOV_EXCL_START
|
||||
|
||||
// avoid exception-handling boilerplate.
|
||||
template <typename Func> void
|
||||
xapian_try(Func&& func) noexcept
|
||||
try {
|
||||
func();
|
||||
} catch (const Xapian::Error& xerr) {
|
||||
g_critical("%s: xapian error '%s'", __func__, xerr.get_msg().c_str());
|
||||
} catch (const std::runtime_error& re) {
|
||||
g_critical("%s: runtime error: %s", __func__, re.what());
|
||||
} catch (const std::exception& e) {
|
||||
g_critical("%s: caught std::exception: %s", __func__, e.what());
|
||||
} catch (...) {
|
||||
g_critical("%s: caught exception", __func__);
|
||||
}
|
||||
|
||||
template <typename Func, typename Default = std::invoke_result<Func>> auto
|
||||
xapian_try(Func&& func, Default&& def) noexcept -> std::decay_t<decltype(func())>
|
||||
try {
|
||||
return func();
|
||||
} catch (const Xapian::DocNotFoundError& xerr) {
|
||||
return static_cast<Default>(def);
|
||||
} catch (const Xapian::Error& xerr) {
|
||||
g_warning("%s: xapian error '%s'", __func__, xerr.get_msg().c_str());
|
||||
return static_cast<Default>(def);
|
||||
} catch (const std::runtime_error& re) {
|
||||
g_critical("%s: runtime error: %s", __func__, re.what());
|
||||
return static_cast<Default>(def);
|
||||
} catch (const std::exception& e) {
|
||||
g_critical("%s: caught std::exception: %s", __func__, e.what());
|
||||
return static_cast<Default>(def);
|
||||
} catch (...) {
|
||||
g_critical("%s: caught exception", __func__);
|
||||
return static_cast<Default>(def);
|
||||
}
|
||||
|
||||
|
||||
template <typename Func> auto
|
||||
xapian_try_result(Func&& func) noexcept -> std::decay_t<decltype(func())>
|
||||
try {
|
||||
return func();
|
||||
} catch (const Xapian::Error& xerr) {
|
||||
return Err(Error::Code::Xapian, "%s", xerr.get_error_string());
|
||||
} catch (const std::runtime_error& re) {
|
||||
return Err(Error::Code::Internal, "runtime error: %s", re.what());
|
||||
} catch (const std::exception& e) {
|
||||
return Err(Error::Code::Internal, "caught std::exception: %s", e.what());
|
||||
} catch (...) {
|
||||
return Err(Error::Code::Internal, "caught exception");
|
||||
}
|
||||
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
} // namespace Mu
|
||||
|
||||
#endif /* MU_ XAPIAN_UTILS_HH__ */
|
Loading…
Reference in New Issue