diff --git a/lib/Makefile.am b/lib/Makefile.am index 76eecb2f..29116f39 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -27,6 +27,15 @@ json_srcs= \ json_flag="-DHAVE_JSON_GLIB" endif +TESTDEFS= \ + -DMU_TESTMAILDIR=\"${abs_srcdir}/testdir\" \ + -DMU_TESTMAILDIR2=\"${abs_srcdir}/testdir2\" \ + -DMU_TESTMAILDIR3=\"${abs_srcdir}/testdir3\" \ + -DMU_TESTMAILDIR4=\"${abs_srcdir}/testdir4\" \ + -DABS_CURDIR=\"${abs_builddir}\" \ + -DABS_SRCDIR=\"${abs_srcdir}\" + + AM_CFLAGS= \ $(WARN_CFLAGS) \ $(GMIME_CFLAGS) \ @@ -36,12 +45,7 @@ AM_CFLAGS= \ $(ASAN_CFLAGS) \ $(json_flag) \ $(CODE_COVERAGE_CFLAGS) \ - -DMU_TESTMAILDIR=\"${abs_srcdir}/testdir\" \ - -DMU_TESTMAILDIR2=\"${abs_srcdir}/testdir2\" \ - -DMU_TESTMAILDIR3=\"${abs_srcdir}/testdir3\" \ - -DMU_TESTMAILDIR4=\"${abs_srcdir}/testdir4\" \ - -DABS_CURDIR=\"${abs_builddir}\" \ - -DABS_SRCDIR=\"${abs_srcdir}\" \ + $(TESTDEFS) \ -Wno-format-nonliteral \ -Wno-switch-enum \ -Wno-deprecated-declarations \ @@ -57,7 +61,7 @@ AM_CXXFLAGS= \ $(XAPIAN_CXXFLAGS) \ $(ASAN_CXXFLAGS) \ $(CODE_COVERAGE_CFLAGS) \ - -DMU_TESTMAILDIR=\"${abs_srcdir}/testdir\" + $(TESTDEFS) AM_CPPFLAGS= \ $(CODE_COVERAGE_CPPFLAGS) @@ -81,8 +85,6 @@ libmu_la_SOURCES= \ mu-container.h \ mu-flags.h \ mu-flags.c \ - mu-index.c \ - mu-index.h \ mu-maildir.c \ mu-maildir.h \ mu-msg-crypto.c \ @@ -123,6 +125,7 @@ libmu_la_LIBADD= \ $(JSON_GLIB_LIBS) \ ${builddir}/utils/libmu-utils.la \ ${builddir}/query/libmu-query.la \ + ${builddir}/index/libmu-index.la \ $(CODE_COVERAGE_LIBS) libmu_la_LDFLAGS= \ @@ -150,7 +153,7 @@ test_mu_msg_SOURCES= test-mu-msg.c dummy.cc test_mu_msg_LDADD= libtestmucommon.la TEST_PROGS += test-mu-store -test_mu_store_SOURCES= test-mu-store.c dummy.cc +test_mu_store_SOURCES= test-mu-store.cc test_mu_store_LDADD= libtestmucommon.la TEST_PROGS += test-mu-flags diff --git a/lib/mu-store.cc b/lib/mu-store.cc index a09e1ec2..cc3b9eac 100644 --- a/lib/mu-store.cc +++ b/lib/mu-store.cc @@ -19,14 +19,20 @@ #include "config.h" +#include #include #include #include -#include +#include #include #include +#include #include #include + +#include + + #include "mu-store.hh" #include "utils/mu-str.h" #include "utils/mu-error.hh" @@ -36,17 +42,22 @@ using namespace Mu; +static_assert(std::is_same::value, + "wrong type for Store::Id"); + constexpr auto SchemaVersionKey = "schema-version"; constexpr auto RootMaildirKey = "maildir"; // XXX: make this 'root-maildir' constexpr auto ContactsKey = "contacts"; constexpr auto PersonalAddressesKey = "personal-addresses"; constexpr auto CreatedKey = "created"; -constexpr auto BatchSize = 150'000; +constexpr auto BatchSizeKey = "batch-size"; +constexpr auto DefaultBatchSize = 250'000U; + +constexpr auto MaxMessageSizeKey = "max-message-size"; +constexpr auto DefaultMaxMessageSize = 100'000'000U; constexpr auto ExpectedSchemaVersion = MU_STORE_SCHEMA_VERSION; - - extern "C" { static unsigned add_or_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err); } @@ -98,52 +109,58 @@ struct Store::Private { #define LOCKED std::lock_guard l(lock_); + enum struct XapianOpts {ReadOnly, Open, CreateOverwrite }; + Private (const std::string& path, bool readonly): - db_path_{path}, - db_{readonly? - std::make_shared(db_path_) : - std::make_shared(db_path_, Xapian::DB_OPEN)}, - root_maildir_{db()->get_metadata(RootMaildirKey)}, - created_{atoll(db()->get_metadata(CreatedKey).c_str())}, - schema_version_{db()->get_metadata(SchemaVersionKey)}, - personal_addresses_{Mu::split(db()->get_metadata(PersonalAddressesKey),",")}, + db_{make_xapian(path, readonly ? XapianOpts::ReadOnly : XapianOpts::Open)}, + mdata_{make_metadata(path)}, contacts_{db()->get_metadata(ContactsKey)} { + + if (!readonly) + wdb()->begin_transaction(); } Private (const std::string& path, const std::string& root_maildir, - const StringVec& personal_addresses): - db_path_{path}, - db_{std::make_shared( - db_path_, Xapian::DB_CREATE_OR_OVERWRITE)}, - root_maildir_{root_maildir}, - created_{time({})}, - schema_version_{MU_STORE_SCHEMA_VERSION}, - personal_addresses_{personal_addresses} { - - writable_db()->set_metadata(SchemaVersionKey, schema_version_); - writable_db()->set_metadata(RootMaildirKey, root_maildir_); - writable_db()->set_metadata(CreatedKey, Mu::format("%" PRId64, (int64_t)created_)); - - std::string addrs; - for (const auto& addr : personal_addresses_) { // _very_ minimal check. - if (addr.find(",") != std::string::npos) - throw Mu::Error(Error::Code::InvalidArgument, - "e-mail address '%s' contains comma", addr.c_str()); - addrs += (addrs.empty() ? "": ",") + addr; - } - writable_db()->set_metadata (PersonalAddressesKey, addrs); + const StringVec& personal_addresses, const Store::Config& conf): + db_{make_xapian(path, XapianOpts::CreateOverwrite)}, + mdata_{init_metadata(conf, path, root_maildir, personal_addresses)} { + wdb()->begin_transaction(); } ~Private() try { LOCKED; + g_debug("closing store @ %s", mdata_.database_path.c_str()); if (wdb()) { wdb()->set_metadata (ContactsKey, contacts_.serialize()); - if (in_transaction_) // auto-commit. - wdb()->commit_transaction(); + commit(); } } MU_XAPIAN_CATCH_BLOCK; + std::shared_ptr make_xapian (const std::string db_path, + XapianOpts opts) try { + switch (opts) { + case XapianOpts::ReadOnly: + return std::make_shared(db_path); + case XapianOpts::Open: + return std::make_shared( + db_path, Xapian::DB_OPEN); + case XapianOpts::CreateOverwrite: + return std::make_shared( + db_path, Xapian::DB_CREATE_OR_OVERWRITE); + default: + throw std::logic_error ("invalid xapian options"); + } + + } catch (const Xapian::DatabaseError& xde) { + throw Mu::Error(Error::Code::Store, "failed to open store @ %s: %s", + db_path.c_str(), xde.get_msg().c_str()); + } catch (...) { + throw Mu::Error(Error::Code::Internal, + "something went wrong when opening store @ %s", + db_path.c_str()); + } + std::shared_ptr db() const { if (!db_) throw Mu::Error(Error::Code::NotFound, "no database found"); @@ -162,16 +179,16 @@ struct Store::Private { return w_db; } - void begin_transaction () try { - wdb()->begin_transaction(); - in_transaction_ = true; - dirtiness_ = 0; + void dirty () try { + if (++dirtiness_ > mdata_.batch_size) + commit(); } MU_XAPIAN_CATCH_BLOCK; - void commit_transaction () try { - in_transaction_ = false; + void commit () try { + g_debug("committing %zu modification(s)", dirtiness_); dirtiness_ = 0; wdb()->commit_transaction(); + wdb()->begin_transaction(); } MU_XAPIAN_CATCH_BLOCK; void add_synonyms () { @@ -186,19 +203,58 @@ struct Store::Private { return (time_t)atoll(db()->get_metadata(key).c_str()); } + Store::Metadata make_metadata(const std::string& db_path) { + Store::Metadata mdata; + mdata.database_path = db_path; + mdata.schema_version = db()->get_metadata(SchemaVersionKey); + mdata.created = ::atoll(db()->get_metadata(CreatedKey).c_str()); + mdata.read_only = !wdb(); + + mdata.batch_size = ::atoll(db()->get_metadata(BatchSizeKey).c_str()); + mdata.max_message_size = ::atoll(db()->get_metadata(MaxMessageSizeKey).c_str()); + + mdata.root_maildir = db()->get_metadata(RootMaildirKey); + mdata.personal_addresses = Mu::split(db()->get_metadata(PersonalAddressesKey),","); + + return mdata; + } + + Store::Metadata init_metadata(const Store::Config& conf, + const std::string& path, const std::string& root_maildir, + const StringVec& personal_addresses) { + + wdb()->set_metadata(SchemaVersionKey, ExpectedSchemaVersion); + wdb()->set_metadata(CreatedKey, Mu::format("%" PRId64, (int64_t)::time({}))); + + const size_t batch_size = conf.batch_size ? conf.batch_size : DefaultBatchSize; + wdb()->set_metadata(BatchSizeKey, Mu::format("%zu", batch_size)); + + const size_t max_msg_size = conf.max_message_size ? + conf.max_message_size : DefaultMaxMessageSize; + wdb()->set_metadata(MaxMessageSizeKey, Mu::format("%zu", max_msg_size)); + + wdb()->set_metadata(RootMaildirKey, root_maildir); + + std::string addrs; + for (const auto& addr : personal_addresses) { // _very_ minimal check. + if (addr.find(",") != std::string::npos) + throw Mu::Error(Error::Code::InvalidArgument, + "e-mail address '%s' contains comma", addr.c_str()); + addrs += (addrs.empty() ? "": ",") + addr; + } + wdb()->set_metadata (PersonalAddressesKey, addrs); + + return make_metadata(path); + } - const std::string db_path_; std::shared_ptr db_; - const std::string root_maildir_; - const time_t created_{}; - const std::string schema_version_; - const StringVec personal_addresses_; + const Store::Metadata mdata_; Contacts contacts_; + std::unique_ptr indexer_; std::atomic in_transaction_{}; std::mutex lock_; - size_t dirtiness_{}; mutable std::atomic ref_count_{1}; @@ -222,48 +278,30 @@ get_uid_term (const char* path) return std::string{uid_term, sizeof(uid_term)}; } - #undef LOCKED -#define LOCKED std::lock_guard l(priv_->lock_); +#define LOCKED std::lock_guard l__(priv_->lock_) Store::Store (const std::string& path, bool readonly): priv_{std::make_unique(path, readonly)} { - if (ExpectedSchemaVersion != schema_version()) + if (metadata().schema_version != ExpectedSchemaVersion) throw Mu::Error(Error::Code::SchemaMismatch, "expected schema-version %s, but got %s", - ExpectedSchemaVersion, schema_version().c_str()); + ExpectedSchemaVersion, + metadata().schema_version.c_str()); } Store::Store (const std::string& path, const std::string& maildir, - const StringVec& personal_addresses): - priv_{std::make_unique(path, maildir, personal_addresses)} + const StringVec& personal_addresses, const Store::Config& conf): + priv_{std::make_unique(path, maildir, personal_addresses, conf)} {} Store::~Store() = default; -bool -Store::read_only() const +const Store::Metadata& +Store::metadata() const { - return !priv_->wdb(); -} - -const std::string& -Store::root_maildir () const -{ - return priv_->root_maildir_; -} - -const StringVec& -Store::personal_addresses(void) const -{ - return priv_->personal_addresses_; -} - -const std::string& -Store::database_path() const -{ - return priv_->db_path_; + return priv_->mdata_; } const Contacts& @@ -273,9 +311,23 @@ Store::contacts() const return priv_->contacts_; } +Indexer& +Store::indexer() +{ + LOCKED; + + if (metadata().read_only) + throw Error{Error::Code::Store, "no indexer for read-only store"}; + else if (!priv_->indexer_) + priv_->indexer_ = std::make_unique(*this); + + return *priv_->indexer_.get(); +} + std::size_t Store::size() const { + LOCKED; return priv_->db()->get_doccount(); } @@ -285,19 +337,6 @@ Store::empty() const return size() == 0; } - -const std::string& -Store::schema_version() const -{ - return priv_->schema_version_; -} - -time_t -Store::created() const -{ - return priv_->created_; -} - static std::string maildir_from_path (const std::string& root, const std::string& path) { @@ -333,7 +372,7 @@ Store::add_message (const std::string& path) LOCKED; GError *gerr{}; - const auto maildir{maildir_from_path(root_maildir(), path)}; + const auto maildir{maildir_from_path(metadata().root_maildir, path)}; auto msg{mu_msg_new_from_file (path.c_str(), maildir.c_str(), &gerr)}; if (G_UNLIKELY(!msg)) throw Error{Error::Code::Message, "failed to create message: %s", @@ -347,6 +386,7 @@ Store::add_message (const std::string& path) gerr ? gerr->message : "something went wrong"}; g_debug ("added message @ %s; docid = %u", path.c_str(), docid); + priv_->dirty(); return docid; } @@ -366,6 +406,7 @@ Store::update_message (MuMsg *msg, unsigned docid) g_debug ("updated message @ %s; docid = %u", mu_msg_get_path(msg), docid); + priv_->dirty(); return true; } @@ -384,13 +425,26 @@ Store::remove_message (const std::string& path) } MU_XAPIAN_CATCH_BLOCK_RETURN (false); - g_debug ("deleted message @ %s from database", path.c_str()); - + g_debug ("deleted message @ %s from store", path.c_str()); + priv_->dirty(); return true; } +void +Store::remove_messages (const std::vector& ids) +{ + LOCKED; + + try { + for (auto&& id: ids) { + priv()->wdb()->delete_document(id); + priv_->dirty(); + } + + } MU_XAPIAN_CATCH_BLOCK; +} time_t Store::dirstamp (const std::string& path) const @@ -413,6 +467,7 @@ Store::set_dirstamp (const std::string& path, time_t tstamp) const std::size_t len = g_snprintf (data.data(), data.size(), "%zx", tstamp); priv_->writable_db()->set_metadata(path, std::string{data.data(), len}); + priv_->dirty(); } @@ -449,29 +504,38 @@ Store::contains_message (const std::string& path) const } MU_XAPIAN_CATCH_BLOCK_RETURN(false); } -void -Store::begin_transaction () try + +std::size_t +Store::for_each (Store::ForEachFunc func) { LOCKED; - priv_->begin_transaction(); -} MU_XAPIAN_CATCH_BLOCK; + size_t n{}; -void -Store::commit_transaction () try -{ - LOCKED; - priv_->commit_transaction(); + try { + Xapian::Enquire enq (*priv_->db().get()); -} MU_XAPIAN_CATCH_BLOCK; + enq.set_query (Xapian::Query::MatchAll); + enq.set_cutoff (0,0); -bool -Store::in_transaction () const -{ - return priv_->in_transaction_; + Xapian::MSet matches(enq.get_mset (0, priv_->db()->get_doccount())); + + for (auto&& it = matches.begin(); it != matches.end(); ++it, ++n) + if (!func (*it, it.get_document().get_value(MU_MSG_FIELD_ID_PATH))) + break; + + } MU_XAPIAN_CATCH_BLOCK; + + return n; } +void +Store::commit () try +{ + LOCKED; + priv_->commit(); +} MU_XAPIAN_CATCH_BLOCK; //////////////////////////////////////////////////////////////////////////////// // C compat @@ -501,7 +565,7 @@ mutable_self (MuStore *store) } auto s = reinterpret_cast(store); - if (s->read_only()) { + if (s->metadata().read_only) { g_error ("store is read-only"); // terminates return {}; } @@ -533,66 +597,6 @@ mu_store_new_readable (const char* xpath, GError **err) return NULL; } -MuStore* -mu_store_new_writable (const char* xpath, GError **err) -{ - g_return_val_if_fail (xpath, NULL); - - g_debug ("opening database at %s (writable)", xpath); - - try { - return reinterpret_cast(new Store (xpath, false/*!readonly*/)); - - } catch (const Mu::Error& me) { - if (me.code() == Mu::Error::Code::SchemaMismatch) { - g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_SCHEMA_MISMATCH, - "%s", me.what()); - return NULL; - } - } catch (const Xapian::DatabaseLockError& dle) { - g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK, - "database @ %s is write-locked", xpath); - return NULL; - } catch (const Xapian::Error& dbe) { - g_warning ("failed to open database @ %s: %s", xpath, - dbe.get_error_string() ? dbe.get_error_string() : "something went wrong"); - } - - g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_OPEN, - "failed to open database @ %s", xpath); - - return NULL; -} - - -MuStore* -mu_store_new_create (const char* xpath, const char *root_maildir, - const char **personal_addresses, GError **err) -{ - g_return_val_if_fail (xpath, NULL); - g_return_val_if_fail (root_maildir, NULL); - - g_debug ("create database at %s (root-maildir=%s)", xpath, root_maildir); - - try { - StringVec addrs; - for (auto i = 0; personal_addresses && personal_addresses[i]; ++i) - addrs.emplace_back(personal_addresses[i]); - - return reinterpret_cast( - new Store (xpath, std::string{root_maildir}, addrs)); - - } catch (const Xapian::DatabaseLockError& dle) { - g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK, - "database @ %s is write-locked already", xpath); - } catch (...) { - g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN, - "error opening database @ %s", xpath); - } - - return NULL; -} - MuStore* mu_store_ref (MuStore* store) @@ -619,30 +623,6 @@ mu_store_unref (MuStore* store) return NULL; } -gboolean -mu_store_is_read_only (const MuStore *store) -{ - g_return_val_if_fail (store, FALSE); - - try { - return self(store)->read_only() ? TRUE : FALSE; - - } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE); - -} - - -const MuContacts* -mu_store_contacts (MuStore *store) -{ - g_return_val_if_fail (store, FALSE); - - try { - return self(store)->contacts().mu_contacts(); - - } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE); -} - unsigned mu_store_count (const MuStore *store, GError **err) { @@ -660,151 +640,16 @@ mu_store_schema_version (const MuStore *store) { g_return_val_if_fail (store, NULL); - return self(store)->schema_version().c_str(); + return self(store)->metadata().schema_version.c_str(); } XapianDatabase* mu_store_get_read_only_database (MuStore *store) { g_return_val_if_fail (store, NULL); - return (XapianWritableDatabase*)self(store)->priv()->db().get(); + return (XapianDatabase*)self(store)->priv()->db().get(); } - - - -gboolean -mu_store_contains_message (const MuStore *store, const char* path) -{ - g_return_val_if_fail (store, FALSE); - g_return_val_if_fail (path, FALSE); - - try { - return self(store)->contains_message(path) ? TRUE : FALSE; - - } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE); -} - -unsigned -mu_store_get_docid_for_path (const MuStore *store, const char* path, GError **err) -{ - g_return_val_if_fail (store, FALSE); - g_return_val_if_fail (path, FALSE); - - try { - const std::string term (get_uid_term(path)); - Xapian::Query query (term); - Xapian::Enquire enq (*self(store)->priv()->db().get()); - - enq.set_query (query); - - Xapian::MSet mset (enq.get_mset (0,1)); - if (mset.empty()) - throw Mu::Error(Error::Code::NotFound, - "message @ %s not found in store", path); - - return *mset.begin(); - - } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(err, MU_ERROR_XAPIAN, - MU_STORE_INVALID_DOCID); -} - - -MuError -mu_store_foreach (MuStore *store, - MuStoreForeachFunc func, void *user_data, GError **err) -{ - g_return_val_if_fail (store, MU_ERROR); - g_return_val_if_fail (func, MU_ERROR); - - try { - Xapian::Enquire enq (*self(store)->priv()->db().get()); - - enq.set_query (Xapian::Query::MatchAll); - enq.set_cutoff (0,0); - - Xapian::MSet matches(enq.get_mset (0, self(store)->size())); - if (matches.empty()) - return MU_OK; /* database is empty */ - - for (Xapian::MSet::iterator iter = matches.begin(); - iter != matches.end(); ++iter) { - Xapian::Document doc (iter.get_document()); - const std::string path(doc.get_value(MU_MSG_FIELD_ID_PATH)); - MuError res = func (path.c_str(), user_data); - if (res != MU_OK) - return res; - } - - } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(err, MU_ERROR_XAPIAN, - MU_ERROR_XAPIAN); - - return MU_OK; -} - - -MuMsg* -mu_store_get_msg (const MuStore *store, unsigned docid, GError **err) -{ - g_return_val_if_fail (store, NULL); - g_return_val_if_fail (docid != 0, NULL); - - return self(store)->find_message(docid); -} - - -const char* -mu_store_database_path (const MuStore *store) -{ - g_return_val_if_fail (store, NULL); - - return self(store)->database_path().c_str(); -} - - -const char* -mu_store_root_maildir (const MuStore *store) -{ - g_return_val_if_fail (store, NULL); - - return self(store)->root_maildir().c_str(); -} - - -time_t -mu_store_created (const MuStore *store) -{ - g_return_val_if_fail (store, (time_t)0); - - return self(store)->created(); -} - -char** -mu_store_personal_addresses (const MuStore *store) -{ - g_return_val_if_fail (store, NULL); - - const auto size = self(store)->personal_addresses().size(); - auto addrs = g_new0 (char*, 1 + size); - for (size_t i = 0; i != size; ++i) - addrs[i] = g_strdup(self(store)->personal_addresses()[i].c_str()); - - return addrs; -} - -void -mu_store_flush (MuStore *store) try { - - g_return_if_fail (store); - - if (self(store)->priv()->in_transaction_) - mutable_self(store)->commit_transaction (); - - mutable_self(store)->priv()->wdb()->set_metadata( - ContactsKey, self(store)->priv()->contacts_.serialize()); - -} MU_XAPIAN_CATCH_BLOCK; - static void add_terms_values_date (Xapian::Document& doc, MuMsg *msg, MuMsgFieldId mfid) { @@ -1237,7 +1082,7 @@ new_doc_from_message (MuStore *store, MuMsg *msg) /* determine whether this is 'personal' email, ie. one of my * e-mail addresses is explicitly mentioned -- it's not a * mailing list message. Callback will update docinfo->_personal */ - const auto& personal_addresses = self(store)->personal_addresses(); + const auto& personal_addresses = self(store)->metadata().personal_addresses; if (personal_addresses.size()) { docinfo._my_addresses = &personal_addresses; mu_msg_contact_foreach @@ -1293,9 +1138,6 @@ add_or_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err) auto self = mutable_self(store); auto wdb = self->priv()->wdb(); - if (!self->in_transaction()) - self->priv()->begin_transaction(); - add_term (doc, term); // update the threading info if this message has a message id @@ -1309,9 +1151,6 @@ add_or_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err) id = docid; } - if (++self->priv()->dirtiness_ >= BatchSize) - self->priv()->commit_transaction(); - return id; } MU_XAPIAN_CATCH_BLOCK_G_ERROR (err, MU_ERROR_XAPIAN_STORE_FAILED); @@ -1319,101 +1158,4 @@ add_or_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err) return MU_STORE_INVALID_DOCID; } -unsigned -mu_store_add_msg (MuStore *store, MuMsg *msg, GError **err) -{ - g_return_val_if_fail (store, MU_STORE_INVALID_DOCID); - g_return_val_if_fail (msg, MU_STORE_INVALID_DOCID); - - return add_or_update_msg (store, 0, msg, err); -} - -unsigned -mu_store_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err) -{ - g_return_val_if_fail (store, MU_STORE_INVALID_DOCID); - g_return_val_if_fail (msg, MU_STORE_INVALID_DOCID); - g_return_val_if_fail (docid != 0, MU_STORE_INVALID_DOCID); - - return add_or_update_msg (store, docid, msg, err); -} - -unsigned -mu_store_add_path (MuStore *store, const char *path, GError **err) try { - - MuMsg *msg; - unsigned docid; - - g_return_val_if_fail (store, FALSE); - g_return_val_if_fail (path, FALSE); - - const auto maildir{maildir_from_path(self(store)->root_maildir(), path)}; - msg = mu_msg_new_from_file (path, maildir.c_str(), err); - if (!msg) - return MU_STORE_INVALID_DOCID; - - docid = add_or_update_msg (store, 0, msg, err); - mu_msg_unref (msg); - - return docid; - -} catch (const Mu::Error& me) { - g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN, - "%s", me.what()); - return MU_STORE_INVALID_DOCID; -} catch (...) { - g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_INTERNAL, - "caught exception"); - return MU_STORE_INVALID_DOCID; -} - -XapianWritableDatabase* -mu_store_get_writable_database (MuStore *store) -{ - g_return_val_if_fail (store, NULL); - - return (XapianWritableDatabase*)mutable_self(store)->priv()->wdb().get(); -} - - -gboolean -mu_store_remove_path (MuStore *store, const char *msgpath) -{ - g_return_val_if_fail (store, FALSE); - g_return_val_if_fail (msgpath, FALSE); - - try { - const std::string term{(get_uid_term(msgpath))}; - auto wdb = mutable_self(store)->priv()->wdb(); - - wdb->delete_document (term); - //store->inc_processed(); - - return TRUE; - - } MU_XAPIAN_CATCH_BLOCK_RETURN (FALSE); -} - - -gboolean -mu_store_set_dirstamp (MuStore *store, const char* dirpath, - time_t stamp, GError **err) -{ - g_return_val_if_fail (store, FALSE); - g_return_val_if_fail (dirpath, FALSE); - - mutable_self(store)->set_dirstamp(dirpath, stamp); - - return TRUE; -} - -time_t -mu_store_get_dirstamp (const MuStore *store, const char *dirpath, GError **err) -{ - g_return_val_if_fail (store, 0); - g_return_val_if_fail (dirpath, 0); - - return self(store)->dirstamp(dirpath); -} - -} +} // extern C diff --git a/lib/mu-store.hh b/lib/mu-store.hh index 9eefb718..92b1bccd 100644 --- a/lib/mu-store.hh +++ b/lib/mu-store.hh @@ -24,20 +24,26 @@ #ifdef __cplusplus -#include "mu-contacts.hh" - -#include - #include #include +#include #include + +#include "mu-contacts.hh" +#include + #include +#include namespace Mu { class Store { public: + using Id = unsigned; /**< Id for a message in the store (internally, + * corresponds to a Xapian document-id) */ + static constexpr Id InvalidId = 0; /**< Invalid store id */ + /** * Construct a store for an existing document database * @@ -46,65 +52,51 @@ public: */ Store (const std::string& path, bool readonly=true); + + struct Config { + size_t max_message_size{}; + /**< maximum size (in bytes) for a message, or 0 for default */ + size_t batch_size{}; + /**< size of batches before committing, or 0 for default */ + }; + /** * Construct a store for a not-yet-existing document database * * @param path path to the database * @param maildir maildir to use for this store - * @param personal_addressesaddresses that should be recognized as + * @param personal_addresses addresses that should be recognized as * 'personal' for identifying personal messages. */ Store (const std::string& path, const std::string& maildir, - const StringVec& personal_addresses); + const StringVec& personal_addresses, const Config& conf); /** * DTOR */ ~Store(); - /** - * Is the store read-only? - * - * @return true or false - */ - bool read_only() const; + struct Metadata { + std::string database_path; /**< Full path to the Xapian database */ + std::string schema_version; /**< Database schema version */ + std::time_t created; /**< database creation time */ + + bool read_only; /**< Is the database opened read-only? */ + size_t batch_size; /**< Maximum database tranasction batch size */ + + std::string root_maildir; /**< Absolute path to the top-level maildir */ + + StringVec personal_addresses; /**< Personal e-mail addresses */ + size_t max_message_size; /**< Maximus allowed message size */ + }; /** - * Path to the database; this is some subdirectory of the path - * passed to the constructor. + * Get metadata about this store. * - * @return the database path + * @return the metadata */ - const std::string& database_path() const; + const Metadata& metadata() const; - /** - * Path to the top-level Maildir - * - * @return the maildir - */ - const std::string& root_maildir() const; - - /** - * Version of the database-schema - * - * @return the maildir - */ - const std::string& schema_version() const; - - - /** - * Time of creation of the store - * - * @return creation time - */ - std::time_t created() const; - - /** - * Get a vec with the personal addresses - * - * @return personal addresses - */ - const StringVec& personal_addresses() const; /** * Get the Contacts object for this store @@ -113,6 +105,15 @@ public: */ const Contacts& contacts() const; + + /** + * Get the Indexer associated with this store. It is an error + * to call this on a read-only store. + * + * @return the indexer. + */ + Indexer& indexer(); + /** * Add a message to the store. * @@ -120,20 +121,21 @@ public: * * @return the doc id of the added message */ - unsigned add_message (const std::string& path); + Id add_message (const std::string& path); /** * Update a message in the store. * * @param msg a message - * @param docid a docid + * @param id the id for this message * * @return false in case of failure; true ottherwise. */ - bool update_message (MuMsg *msg, unsigned docid); + bool update_message (MuMsg *msg, Id id); /** - * Add a message to the store. + * Remove a message from the store. It will _not_ remove the message + * fromt he file system. * * @param path the message path. * @@ -142,13 +144,29 @@ public: bool remove_message (const std::string& path); /** - * Fina message in the store. + * Remove a number if messages from the store. It will _not_ remove the + * message fromt he file system. * - * @param docid doc id for the message to find + * @param ids vector with store ids for the message + */ + void remove_messages (const std::vector& ids); + + /** + * Remove a message from the store. It will _not_ remove the message + * fromt he file system. + * + * @param id the store id for the message + */ + void remove_message (Id id) { remove_messages({id}); } + + /** + * Find message in the store. + * + * @param id doc id for the message to find * * @return a message (owned by caller), or nullptr */ - MuMsg* find_message (unsigned docid) const; + MuMsg* find_message (Id id) const; /** * does a certain message exist in the store already? @@ -159,6 +177,36 @@ public: */ bool contains_message (const std::string& path) const; + + /** + * Prototype for the ForEachFunc + * + * @param id :t store Id for the message + * @param path: the absolute path to the message + * + * @return true if for_each should continue; false to quit + */ + using ForEachFunc = std::function; + + /** + * Call @param func for each document in the store. This takes a lock on + * the store, so the func should _not_ call any other Store:: methods. + * + * @param func a functio + * + * @return the number of times func was invoked + */ + size_t for_each (ForEachFunc func); + + /** + * Get the timestamp for some message, or 0 if not found + * + * @param path the path + * + * @return the timestamp, or 0 if not found + */ + time_t message_tstamp (const std::string& path) const; + /** * Get the timestamp for some directory * @@ -191,30 +239,18 @@ public: bool empty() const; /** - * Begin a database transaction + * Commit the current group of modifcations (i.e., transaction) to disk; + * This rarely needs to be called explicitly, as Store will take care of + * it. */ - void begin_transaction(); - - /** - * Commit a database transaction - * - */ - void commit_transaction(); - - /** - * Are we in a transaction? - * - * @return true or false - */ - bool in_transaction() const; - + void commit(); /** * Get a reference to the private data. For internal use. * * @return private reference. */ - struct Private; + struct Private; std::unique_ptr& priv() { return priv_; } const std::unique_ptr& priv() const { return priv_; } @@ -240,7 +276,6 @@ typedef struct MuStore_ MuStore; /* http://article.gmane.org/gmane.comp.search.xapian.general/3656 */ #define MU_STORE_MAX_TERM_LENGTH (240) - /** * create a new read-only Xapian store, for querying documents * @@ -252,34 +287,6 @@ typedef struct MuStore_ MuStore; */ MuStore* mu_store_new_readable (const char* xpath, GError **err) G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; -/** - * create a new writable Xapian store, a place to store documents - * - * @param path the path to the database - * @param err to receive error info or NULL. err->code is MuError value - * - * @return a new MuStore object with ref count == 1, or NULL in case - * of error; free with mu_store_unref - */ -MuStore* mu_store_new_writable (const char *xpath, GError **err) - G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; - -/** - * create a new writable Xapian store, a place to store documents, and - * create/overwrite the existing database. - * - * @param path the path to the database - * @param path to the maildir - * @param personal_addressesaddresses that should be recognized as - * 'personal' for identifying personal messages. - * @param err to receive error info or NULL. err->code is MuError value - * - * @return a new MuStore object with ref count == 1, or NULL in case - * of error; free with mu_store_unref - */ -MuStore* mu_store_new_create (const char *xpath, const char *maildir, - const char **personal_addresses, GError **err) - G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; /** * increase the reference count for this store with 1 @@ -304,22 +311,8 @@ MuStore* mu_store_unref (MuStore *store); /** * we need this when using Xapian::(Writable)Database* from C */ -typedef gpointer XapianWritableDatabase; typedef gpointer XapianDatabase; - -/** - * get the underlying writable database object for this store; not - * that this pointer becomes in valid after mu_store_destroy - * - * @param store a valid store - * - * @return a Xapian::WritableDatabase (you'll need to cast in C++), or - * NULL in case of error. - */ -XapianWritableDatabase* mu_store_get_writable_database (MuStore *store); - - /** * get the underlying read-only database object for this store; not that this * pointer becomes in valid after mu_store_destroy @@ -344,56 +337,6 @@ XapianDatabase* mu_store_get_read_only_database (MuStore *store); const char* mu_store_schema_version (const MuStore* store); -/** - * Get the database-path for this message store - * - * @param store the store to inspetc - * - * @return the database-path - */ -const char *mu_store_database_path (const MuStore *store); - - -/** - * Get the root-maildir for this message store. - * - * @param store the store - * - * @return the maildir. - */ -const char *mu_store_root_maildir(const MuStore *store); - - -/** - * Get the time this database was created - * - * @param store the store - * - * @return the maildir. - */ -time_t mu_store_created(const MuStore *store); - -/** - * Get the list of personal addresses from the store - * - * @param store the message store - * - * @return the list of personal addresses, or NULL in case of error. - * - * Free with g_strfreev(). - */ -char** mu_store_personal_addresses (const MuStore *store); - -/** - * Get the a MuContacts* ptr for this store. - * - * @param store a store - * - * @return the contacts ptr - */ -const MuContacts* mu_store_contacts (MuStore *store); - - /** * get the numbers of documents in the database * @@ -405,174 +348,8 @@ const MuContacts* mu_store_contacts (MuStore *store); */ unsigned mu_store_count (const MuStore *store, GError **err); - -/** - * try to flush/commit all outstanding work to the database and the contacts - * cache. - * - * @param store a valid xapian store - */ -void mu_store_flush (MuStore *store); - #define MU_STORE_INVALID_DOCID 0 -/** - * store an email message in the XapianStore - * - * @param store a valid store - * @param msg a valid message - * @param err receives error information, if any, or NULL - * - * @return the docid of the stored message, or 0 - * (MU_STORE_INVALID_DOCID) in case of error - */ -unsigned mu_store_add_msg (MuStore *store, MuMsg *msg, GError **err); - - -/** - * update an email message in the XapianStore - * - * @param store a valid store - * @param the docid for the message - * @param msg a valid message - * @param err receives error information, if any, or NULL - * - * @return the docid of the stored message, or 0 - * (MU_STORE_INVALID_DOCID) in case of error - */ -unsigned mu_store_update_msg (MuStore *store, unsigned docid, MuMsg *msg, - GError **err); - -/** - * store an email message in the XapianStore; similar to - * mu_store_store, but instead takes a path as parameter instead of a - * MuMsg* - * - * @param store a valid store - * @param path full filesystem path to a valid message - * @param err receives error information, if any, or NULL - * - * @return the docid of the stored message, or 0 - * (MU_STORE_INVALID_DOCID) in case of error - */ -unsigned mu_store_add_path (MuStore *store, const char *path, GError **err); - -/** - * remove a message from the database based on its path - * - * @param store a valid store - * @param msgpath path of the message (note, this is only used to - * *identify* the message; a common use of this function is to remove - * a message from the database, for which there is no message anymore - * in the filesystem. - * - * @return TRUE if it succeeded, FALSE otherwise - */ -gboolean mu_store_remove_path (MuStore *store, const char* msgpath); - -/** - * does a certain message exist in the database already? - * - * @param store a store - * @param path the message path - * - * @return TRUE if the message exists, FALSE otherwise - */ -gboolean mu_store_contains_message (const MuStore *store, const char* path); - -/** - * get the docid for message at path - * - * @param store a store - * @param path the message path - * @param err to receive error info or NULL. err->code is MuError value - * - * @return the docid if the message was found, MU_STORE_INVALID_DOCID (0) otherwise - * */ -unsigned mu_store_get_docid_for_path (const MuStore *store, const char* path, - GError **err); - -/** - * store a timestamp for a directory - * - * @param store a valid store - * @param dirpath path to some directory - * @param stamp a timestamp - * @param err to receive error info or NULL. err->code is MuError value - * - * @return TRUE if setting the timestamp succeeded, FALSE otherwise - */ -gboolean mu_store_set_dirstamp (MuStore *store, const char* dirpath, - time_t stamp, GError **err); - -/** - * get the timestamp for a directory - * - * @param store a valid store - * @param msgpath path to some directory - * @param err to receive error info or NULL. err->code is MuError value - * - * @return the timestamp, or 0 in case of error - */ -time_t mu_store_get_dirstamp (const MuStore *store, const char* dirpath, - GError **err); - -/** - * check whether this store is read-only - * - * @param store a store - * - * @return TRUE if the store is read-only, FALSE otherwise (and in - * case of error) - */ -gboolean mu_store_is_read_only (const MuStore *store); - -/** - * call a function for each document in the database - * - * @param self a valid store - * @param func a callback function to to call for each document - * @param user_data a user pointer passed to the callback function - * @param err to receive error info or NULL. err->code is MuError value - * - * @return MU_OK if all went well, MU_STOP if the foreach was interrupted, - * MU_ERROR in case of error - */ -typedef MuError (*MuStoreForeachFunc) (const char* path, gpointer user_data); -MuError mu_store_foreach (MuStore *self, MuStoreForeachFunc func, - void *user_data, GError **err); - -/** - * check if the database is locked for writing - * - * @param xpath path to a xapian database - * - * @return TRUE if it is locked, FALSE otherwise (or in case of error) - */ -gboolean mu_store_database_is_locked (const gchar *xpath); - -/** - * get a specific message, based on its Xapian docid - * - * @param self a valid MuQuery instance - * @param docid the Xapian docid for the wanted message - * @param err receives error information, or NULL - * - * @return a MuMsg instance (use mu_msg_unref when done with it), or - * NULL in case of error - */ -MuMsg* mu_store_get_msg (const MuStore *self, unsigned docid, GError **err) - G_GNUC_WARN_UNUSED_RESULT; - -/** - * Print some information about the store - * - * @param store a store - * @param nocolor whether to _not_ show color - */ -void mu_store_print_info (const MuStore *store, gboolean nocolor); - - G_END_DECLS #endif /* __MU_STORE_HH__ */ diff --git a/lib/test-mu-store.c b/lib/test-mu-store.c deleted file mode 100644 index 3276d575..00000000 --- a/lib/test-mu-store.c +++ /dev/null @@ -1,206 +0,0 @@ -/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ - -/* -** Copyright (C) 2008-2013 Dirk-Jan C. Binnema -** -** 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. -** -*/ - -#if HAVE_CONFIG_H -#include "config.h" -#endif /*HAVE_CONFIG_H*/ - -#include -#include -#include -#include - -#include - -#include "test-mu-common.h" -#include "mu-store.hh" - -static void -test_mu_store_new_destroy (void) -{ - MuStore *store; - gchar* tmpdir; - GError *err; - - tmpdir = test_mu_common_get_random_tmpdir(); - g_assert (tmpdir); - - err = NULL; - store = mu_store_new_create (tmpdir, "/tmp", NULL, &err); - g_assert_no_error (err); - g_assert (store); - - g_assert_cmpuint (0,==,mu_store_count (store, NULL)); - - mu_store_flush (store); - mu_store_unref (store); - - g_free (tmpdir); -} - - -static void -test_mu_store_version (void) -{ - MuStore *store; - gchar* tmpdir; - GError *err; - - tmpdir = test_mu_common_get_random_tmpdir(); - g_assert (tmpdir); - - err = NULL; - store = mu_store_new_create (tmpdir, "/tmp", NULL, &err); - g_assert (store); - mu_store_unref (store); - store = mu_store_new_readable (tmpdir, &err); - g_assert (store); - - g_assert (err == NULL); - - g_assert_cmpuint (0,==,mu_store_count (store, NULL)); - g_assert_cmpstr (MU_STORE_SCHEMA_VERSION,==, - mu_store_schema_version(store)); - - mu_store_unref (store); - g_free (tmpdir); -} - - -G_GNUC_UNUSED static void -test_mu_store_store_msg_and_count (void) -{ - MuMsg *msg; - MuStore *store; - gchar* tmpdir; - - tmpdir = test_mu_common_get_random_tmpdir(); - g_assert (tmpdir); - - store = mu_store_new_create (tmpdir, MU_TESTMAILDIR, NULL, NULL); - g_assert (store); - g_free (tmpdir); - - g_assert_cmpuint (0,==,mu_store_count (store, NULL)); - - /* add one */ - /* XXX this passes, but not make-dist; investigate */ - msg = mu_msg_new_from_file ( - MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,", - NULL, NULL); - g_assert (msg); - g_assert_cmpuint (mu_store_add_msg (store, msg, NULL), - !=, MU_STORE_INVALID_DOCID); - g_assert_cmpuint (1,==,mu_store_count (store, NULL)); - g_assert_cmpuint (TRUE,==,mu_store_contains_message - (store, - MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,")); - mu_msg_unref (msg); - - /* add another one */ - msg = mu_msg_new_from_file (MU_TESTMAILDIR2 - "/bar/cur/mail3", NULL, NULL); - g_assert (msg); - g_assert_cmpuint (mu_store_add_msg (store, msg, NULL), - !=, MU_STORE_INVALID_DOCID); - g_assert_cmpuint (2,==,mu_store_count (store, NULL)); - g_assert_cmpuint (TRUE,==, - mu_store_contains_message (store, MU_TESTMAILDIR2 - "/bar/cur/mail3")); - mu_msg_unref (msg); - - /* try to add the first one again. count should be 2 still */ - msg = mu_msg_new_from_file - (MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,", - NULL, NULL); - g_assert (msg); - g_assert_cmpuint (mu_store_add_msg (store, msg, NULL), - !=, MU_STORE_INVALID_DOCID); - g_assert_cmpuint (2,==,mu_store_count (store, NULL)); - - mu_msg_unref (msg); - mu_store_unref (store); -} - - -G_GNUC_UNUSED static void -test_mu_store_store_msg_remove_and_count (void) -{ - MuMsg *msg; - MuStore *store; - gchar* tmpdir; - GError *err; - - tmpdir = test_mu_common_get_random_tmpdir(); - g_assert (tmpdir); - - store = mu_store_new_create (tmpdir, MU_TESTMAILDIR, NULL, NULL); - g_assert (store); - - g_assert_cmpuint (0,==,mu_store_count (store, NULL)); - - /* add one */ - err = NULL; - msg = mu_msg_new_from_file ( - MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,", - NULL, &err); - g_assert (msg); - g_assert_cmpuint (mu_store_add_msg (store, msg, NULL), - !=, MU_STORE_INVALID_DOCID); - g_assert_cmpuint (1,==,mu_store_count (store, NULL)); - mu_msg_unref (msg); - - /* remove one */ - mu_store_remove_path (store, - MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,"); - g_assert_cmpuint (0,==,mu_store_count (store, NULL)); - g_assert_cmpuint (FALSE,==,mu_store_contains_message - (store, - MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,")); - g_free (tmpdir); - mu_store_unref (store); -} - - -int -main (int argc, char *argv[]) -{ - g_test_init (&argc, &argv, NULL); - - /* mu_runtime_init/uninit */ - g_test_add_func ("/mu-store/mu-store-new-destroy", - test_mu_store_new_destroy); - g_test_add_func ("/mu-store/mu-store-version", - test_mu_store_version); -#if 0 - /* XXX this passes, but not make-dist; investigate */ - g_test_add_func ("/mu-store/mu-store-store-and-count", - test_mu_store_store_msg_and_count); - g_test_add_func ("/mu-store/mu-store-store-remove-and-count", - test_mu_store_store_msg_remove_and_count); -#endif - if (!g_test_verbose()) - g_log_set_handler (NULL, - G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION, - (GLogFunc)black_hole, NULL); - - return g_test_run (); -} diff --git a/lib/test-mu-store.cc b/lib/test-mu-store.cc new file mode 100644 index 00000000..cdafd5fe --- /dev/null +++ b/lib/test-mu-store.cc @@ -0,0 +1,98 @@ +/* +** Copyright (C) 2008-2020 Dirk-Jan C. Binnema +** +** 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 "config.h" + +#include +#include +#include +#include + +#include + +#include "test-mu-common.h" +#include "mu-store.hh" + +static std::string MuTestMaildir = Mu::canonicalize_filename(MU_TESTMAILDIR, "/"); +static std::string MuTestMaildir2 = Mu::canonicalize_filename(MU_TESTMAILDIR2, "/"); + +static void +test_store_ctor_dtor () +{ + char *tmpdir = test_mu_common_get_random_tmpdir(); + g_assert (tmpdir); + + Mu::Store store{tmpdir, "/tmp", {}, {}}; + g_free (tmpdir); + g_assert_true(store.empty()); + g_assert_cmpuint (0,==,store.size()); + + g_assert_cmpstr (MU_STORE_SCHEMA_VERSION,==, + store.metadata().schema_version.c_str()); +} + +static void +test_store_add_count_remove () +{ + char *tmpdir = test_mu_common_get_random_tmpdir(); + g_assert (tmpdir); + + Mu::Store store{tmpdir, MuTestMaildir, {}, {}}; + g_free (tmpdir); + + const auto id1 = store.add_message(MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"); + + g_assert_cmpuint(id1, !=, Mu::Store::InvalidId); + + g_assert_cmpuint(store.size(), ==, 1); + g_assert_true(store.contains_message(MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,")); + + g_assert_cmpuint(store.add_message(MuTestMaildir2 + "/bar/cur/mail3"), + !=, Mu::Store::InvalidId); + + g_assert_cmpuint(store.size(), ==, 2); + g_assert_true(store.contains_message(MuTestMaildir2 + "/bar/cur/mail3")); + + store.remove_message(id1); + g_assert_cmpuint(store.size(), ==, 1); + g_assert_false(store.contains_message(MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,")); + + store.remove_message (MuTestMaildir2 + "/bar/cur/mail3"); + g_assert_true(store.empty()); + g_assert_false(store.contains_message(MuTestMaildir2 + "/bar/cur/mail3")); +} + + + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + /* mu_runtime_init/uninit */ + g_test_add_func ("/mu-store/ctor-dtor", test_store_ctor_dtor); + g_test_add_func ("/mu-store/add-count-remove", test_store_add_count_remove); + + // if (!g_test_verbose()) + // g_log_set_handler (NULL, + // G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION, + // (GLogFunc)black_hole, NULL); + + return g_test_run (); +}