mirror of https://github.com/djcb/mu.git
lib: update Store & QueryResults in terms of Mu::Message
This commit is contained in:
parent
4b2812d25f
commit
9b8353fc37
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
**
|
**
|
||||||
** This program is free software; you can redistribute it and/or modify it
|
** 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
|
** under the terms of the GNU General Public License as published by the
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
#include <utils/mu-option.hh>
|
#include <utils/mu-option.hh>
|
||||||
#include <utils/mu-xapian-utils.hh>
|
#include <utils/mu-xapian-utils.hh>
|
||||||
|
|
||||||
#include "mu-msg.hh"
|
#include <message/mu-message.hh>
|
||||||
|
|
||||||
namespace Mu {
|
namespace Mu {
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ operator<<(std::ostream& os, const QueryMatch& qmatch)
|
||||||
class QueryResultsIterator {
|
class QueryResultsIterator {
|
||||||
public:
|
public:
|
||||||
using iterator_category = std::output_iterator_tag;
|
using iterator_category = std::output_iterator_tag;
|
||||||
using value_type = MuMsg*;
|
using value_type = Message;
|
||||||
using difference_type = void;
|
using difference_type = void;
|
||||||
using pointer = void;
|
using pointer = void;
|
||||||
using reference = void;
|
using reference = void;
|
||||||
|
@ -169,7 +169,6 @@ public:
|
||||||
: mset_it_{mset_it}, query_matches_{query_matches}
|
: mset_it_{mset_it}, query_matches_{query_matches}
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
~QueryResultsIterator() { g_clear_pointer(&msg_, mu_msg_unref); }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increment the iterator (we don't support post-increment)
|
* Increment the iterator (we don't support post-increment)
|
||||||
|
@ -304,36 +303,26 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the corresponding MuMsg for this iter; this instance is owned by
|
* get the corresponding Message for this iter; this instance is owned
|
||||||
* @this, and becomes invalid when iterating to the next, or @this is
|
* by @this, and becomes invalid when iterating to the next, or @this is
|
||||||
* destroyed.; it's a 'floating' reference.
|
* destroyed.; it's a 'floating' reference.
|
||||||
*
|
*
|
||||||
* @return a MuMsg* or NUL in case of error
|
* @return a Message or Nothing
|
||||||
*/
|
*/
|
||||||
MuMsg* floating_msg() G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT
|
Option<Message> message() const {
|
||||||
{
|
|
||||||
return xapian_try(
|
return xapian_try(
|
||||||
[&] {
|
[&]()->Option<Message> {
|
||||||
auto docp{reinterpret_cast<XapianDocument*>(
|
if (auto&& msg{Message::make_from_document(document())}; msg)
|
||||||
new Xapian::Document(document()))};
|
return Some(std::move(msg.value()));
|
||||||
GError* err{};
|
else
|
||||||
g_clear_pointer(&msg_, mu_msg_unref);
|
return Nothing;
|
||||||
if (!(msg_ = mu_msg_new_from_doc(docp, &err))) {
|
|
||||||
delete docp;
|
|
||||||
g_warning("failed to crate message for %s: %s",
|
|
||||||
path().value_or("<none>").c_str(),
|
|
||||||
err ? err->message : "somethng went wrong");
|
|
||||||
g_clear_error(&err);
|
|
||||||
}
|
|
||||||
return msg_;
|
|
||||||
},
|
},
|
||||||
(MuMsg*)NULL);
|
Nothing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Xapian::MSetIterator mset_it_;
|
Xapian::MSetIterator mset_it_;
|
||||||
QueryMatches& query_matches_;
|
QueryMatches& query_matches_;
|
||||||
MuMsg* msg_{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr auto MaxQueryResultsSize = std::numeric_limits<size_t>::max();
|
constexpr auto MaxQueryResultsSize = std::numeric_limits<size_t>::max();
|
||||||
|
|
520
lib/mu-store.cc
520
lib/mu-store.cc
|
@ -35,13 +35,12 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <xapian.h>
|
#include <xapian.h>
|
||||||
|
|
||||||
#include "mu-msg.hh"
|
#include "mu-maildir.hh"
|
||||||
#include "mu-store.hh"
|
#include "mu-store.hh"
|
||||||
#include "mu-query.hh"
|
#include "mu-query.hh"
|
||||||
#include "utils/mu-str.h"
|
#include "utils/mu-str.h"
|
||||||
#include "utils/mu-error.hh"
|
#include "utils/mu-error.hh"
|
||||||
|
|
||||||
#include "mu-msg-part.hh"
|
|
||||||
#include "utils/mu-utils.hh"
|
#include "utils/mu-utils.hh"
|
||||||
#include "utils/mu-xapian-utils.hh"
|
#include "utils/mu-xapian-utils.hh"
|
||||||
|
|
||||||
|
@ -63,36 +62,8 @@ constexpr auto DefaultMaxMessageSize = 100'000'000U;
|
||||||
constexpr auto ExpectedSchemaVersion = MU_STORE_SCHEMA_VERSION;
|
constexpr auto ExpectedSchemaVersion = MU_STORE_SCHEMA_VERSION;
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* calculate a 64-bit hash for the given string, based on a combination of the
|
|
||||||
* DJB and BKDR hash functions.
|
|
||||||
*
|
|
||||||
* @param a string
|
|
||||||
*
|
|
||||||
* @return the hash
|
|
||||||
*/
|
|
||||||
static uint64_t get_hash64 (const char* str)
|
|
||||||
{
|
|
||||||
guint32 djbhash;
|
|
||||||
guint32 bkdrhash;
|
|
||||||
guint32 bkdrseed;
|
|
||||||
guint64 hash;
|
|
||||||
|
|
||||||
djbhash = 5381;
|
|
||||||
bkdrhash = 0;
|
|
||||||
bkdrseed = 1313;
|
|
||||||
|
|
||||||
for(unsigned u = 0U; str[u]; ++u) {
|
|
||||||
djbhash = ((djbhash << 5) + djbhash) + str[u];
|
|
||||||
bkdrhash = bkdrhash * bkdrseed + str[u];
|
|
||||||
}
|
|
||||||
|
|
||||||
hash = djbhash;
|
|
||||||
return (hash<<32) | bkdrhash;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Store::Private {
|
struct Store::Private {
|
||||||
enum struct XapianOpts { ReadOnly, Open, CreateOverwrite, InMemory };
|
enum struct XapianOpts { ReadOnly, Open, CreateOverwrite };
|
||||||
|
|
||||||
Private(const std::string& path, bool readonly)
|
Private(const std::string& path, bool readonly)
|
||||||
: read_only_{readonly}, db_{make_xapian_db(path,
|
: read_only_{readonly}, db_{make_xapian_db(path,
|
||||||
|
@ -100,8 +71,7 @@ struct Store::Private {
|
||||||
: XapianOpts::Open)},
|
: XapianOpts::Open)},
|
||||||
properties_{make_properties(path)}, contacts_cache_{db().get_metadata(ContactsKey),
|
properties_{make_properties(path)}, contacts_cache_{db().get_metadata(ContactsKey),
|
||||||
properties_.personal_addresses}
|
properties_.personal_addresses}
|
||||||
{
|
{}
|
||||||
}
|
|
||||||
|
|
||||||
Private(const std::string& path,
|
Private(const std::string& path,
|
||||||
const std::string& root_maildir,
|
const std::string& root_maildir,
|
||||||
|
@ -110,17 +80,7 @@ struct Store::Private {
|
||||||
: read_only_{false}, db_{make_xapian_db(path, XapianOpts::CreateOverwrite)},
|
: read_only_{false}, db_{make_xapian_db(path, XapianOpts::CreateOverwrite)},
|
||||||
properties_{init_metadata(conf, path, root_maildir, personal_addresses)},
|
properties_{init_metadata(conf, path, root_maildir, personal_addresses)},
|
||||||
contacts_cache_{"", properties_.personal_addresses}
|
contacts_cache_{"", properties_.personal_addresses}
|
||||||
{
|
{}
|
||||||
}
|
|
||||||
|
|
||||||
Private(const std::string& root_maildir,
|
|
||||||
const StringVec& personal_addresses,
|
|
||||||
const Store::Config& conf)
|
|
||||||
: read_only_{false}, db_{make_xapian_db("", XapianOpts::InMemory)},
|
|
||||||
properties_{init_metadata(conf, "", root_maildir, personal_addresses)},
|
|
||||||
contacts_cache_{"", properties_.personal_addresses}
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
~Private()
|
~Private()
|
||||||
try {
|
try {
|
||||||
|
@ -142,10 +102,6 @@ struct Store::Private {
|
||||||
return std::make_unique<Xapian::WritableDatabase>(
|
return std::make_unique<Xapian::WritableDatabase>(
|
||||||
db_path,
|
db_path,
|
||||||
Xapian::DB_CREATE_OR_OVERWRITE);
|
Xapian::DB_CREATE_OR_OVERWRITE);
|
||||||
case XapianOpts::InMemory:
|
|
||||||
return std::make_unique<Xapian::WritableDatabase>(
|
|
||||||
std::string{},
|
|
||||||
Xapian::DB_BACKEND_INMEMORY);
|
|
||||||
default: throw std::logic_error("invalid xapian options");
|
default: throw std::logic_error("invalid xapian options");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,9 +128,6 @@ struct Store::Private {
|
||||||
// If not started yet, start a transaction. Otherwise, just update the transaction size.
|
// If not started yet, start a transaction. Otherwise, just update the transaction size.
|
||||||
void transaction_inc() noexcept
|
void transaction_inc() noexcept
|
||||||
{
|
{
|
||||||
if (properties_.in_memory)
|
|
||||||
return; // not supported
|
|
||||||
|
|
||||||
if (transaction_size_ == 0) {
|
if (transaction_size_ == 0) {
|
||||||
g_debug("starting transaction");
|
g_debug("starting transaction");
|
||||||
xapian_try([this] { writable_db().begin_transaction(); });
|
xapian_try([this] { writable_db().begin_transaction(); });
|
||||||
|
@ -186,9 +139,6 @@ struct Store::Private {
|
||||||
// filled up a batch, or with force.
|
// filled up a batch, or with force.
|
||||||
void transaction_maybe_commit(bool force = false) noexcept
|
void transaction_maybe_commit(bool force = false) noexcept
|
||||||
{
|
{
|
||||||
if (properties_.in_memory || transaction_size_ == 0)
|
|
||||||
return; // not supported or not in transaction
|
|
||||||
|
|
||||||
if (force || transaction_size_ >= properties_.batch_size) {
|
if (force || transaction_size_ >= properties_.batch_size) {
|
||||||
if (contacts_cache_.dirty()) {
|
if (contacts_cache_.dirty()) {
|
||||||
xapian_try([&] {
|
xapian_try([&] {
|
||||||
|
@ -196,6 +146,9 @@ struct Store::Private {
|
||||||
contacts_cache_.serialize());
|
contacts_cache_.serialize());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (transaction_size_ == 0)
|
||||||
|
return; // nothing more to do here.
|
||||||
|
|
||||||
g_debug("committing transaction (n=%zu,%zu)",
|
g_debug("committing transaction (n=%zu,%zu)",
|
||||||
transaction_size_, metadata_cache_.size());
|
transaction_size_, metadata_cache_.size());
|
||||||
xapian_try([this] {
|
xapian_try([this] {
|
||||||
|
@ -244,7 +197,6 @@ struct Store::Private {
|
||||||
props.read_only = read_only_;
|
props.read_only = read_only_;
|
||||||
props.batch_size = ::atoll(db().get_metadata(BatchSizeKey).c_str());
|
props.batch_size = ::atoll(db().get_metadata(BatchSizeKey).c_str());
|
||||||
props.max_message_size = ::atoll(db().get_metadata(MaxMessageSizeKey).c_str());
|
props.max_message_size = ::atoll(db().get_metadata(MaxMessageSizeKey).c_str());
|
||||||
props.in_memory = db_path.empty();
|
|
||||||
props.root_maildir = db().get_metadata(RootMaildirKey);
|
props.root_maildir = db().get_metadata(RootMaildirKey);
|
||||||
props.personal_addresses = Mu::split(db().get_metadata(PersonalAddressesKey), ",");
|
props.personal_addresses = Mu::split(db().get_metadata(PersonalAddressesKey), ",");
|
||||||
|
|
||||||
|
@ -281,9 +233,6 @@ struct Store::Private {
|
||||||
return make_properties(path);
|
return make_properties(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Xapian::docid add_or_update_msg(Xapian::docid docid, MuMsg* msg);
|
|
||||||
Xapian::Document new_doc_from_message(MuMsg* msg);
|
|
||||||
|
|
||||||
/* metadata to write as part of a transaction commit */
|
/* metadata to write as part of a transaction commit */
|
||||||
std::unordered_map<std::string, std::string> metadata_cache_;
|
std::unordered_map<std::string, std::string> metadata_cache_;
|
||||||
|
|
||||||
|
@ -298,19 +247,6 @@ struct Store::Private {
|
||||||
std::mutex lock_;
|
std::mutex lock_;
|
||||||
};
|
};
|
||||||
|
|
||||||
static void
|
|
||||||
hash_str(char* buf, size_t buf_size, const char* data)
|
|
||||||
{
|
|
||||||
g_snprintf(buf, buf_size, "016%" PRIx64, get_hash64(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string
|
|
||||||
get_uid_term(const char* path)
|
|
||||||
{
|
|
||||||
return field_from_id(Field::Id::Uid).xapian_term(
|
|
||||||
format("016%" PRIx64, get_hash64(path)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Store::Store(const std::string& path, bool readonly)
|
Store::Store(const std::string& path, bool readonly)
|
||||||
: priv_{std::make_unique<Private>(path, readonly)}
|
: priv_{std::make_unique<Private>(path, readonly)}
|
||||||
{
|
{
|
||||||
|
@ -330,11 +266,6 @@ Store::Store(const std::string& path,
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Store::Store(const std::string& maildir, const StringVec& personal_addresses, const Config& conf)
|
|
||||||
: priv_{std::make_unique<Private>(maildir, personal_addresses, conf)}
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Store::~Store() = default;
|
Store::~Store() = default;
|
||||||
|
|
||||||
const Store::Properties&
|
const Store::Properties&
|
||||||
|
@ -414,48 +345,44 @@ maildir_from_path(const std::string& root, const std::string& path)
|
||||||
return mdir;
|
return mdir;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned
|
Result<Store::Id>
|
||||||
Store::add_message(const std::string& path, bool use_transaction)
|
Store::add_message(const std::string& path, bool use_transaction)
|
||||||
{
|
{
|
||||||
std::lock_guard guard{priv_->lock_};
|
|
||||||
|
|
||||||
GError* gerr{};
|
|
||||||
const auto maildir{maildir_from_path(properties().root_maildir, path)};
|
const auto maildir{maildir_from_path(properties().root_maildir, path)};
|
||||||
auto msg{mu_msg_new_from_file(path.c_str(), maildir.c_str(), &gerr)};
|
auto msg{Message::make_from_path(Message::Options::None, path, maildir)};
|
||||||
if (G_UNLIKELY(!msg))
|
if (G_UNLIKELY(!msg))
|
||||||
throw Error{Error::Code::Message,
|
return Err(msg.error());
|
||||||
"failed to create message: %s",
|
|
||||||
gerr ? gerr->message : "something went wrong"};
|
std::lock_guard guard{priv_->lock_};
|
||||||
|
|
||||||
if (use_transaction)
|
if (use_transaction)
|
||||||
priv_->transaction_inc();
|
priv_->transaction_inc();
|
||||||
|
|
||||||
const auto docid{priv_->add_or_update_msg(0, msg)};
|
const auto docid = priv_->writable_db().add_document(
|
||||||
|
msg->document().xapian_document());
|
||||||
|
|
||||||
if (use_transaction) /* commit if batch is full */
|
if (use_transaction) /* commit if batch is full */
|
||||||
priv_->transaction_maybe_commit();
|
priv_->transaction_maybe_commit();
|
||||||
|
|
||||||
mu_msg_unref(msg);
|
|
||||||
|
|
||||||
if (G_UNLIKELY(docid == InvalidId))
|
if (G_UNLIKELY(docid == InvalidId))
|
||||||
throw Error{Error::Code::Message, "failed to add message"};
|
return Err(Error::Code::Message, "failed to add message");
|
||||||
|
|
||||||
g_debug("added message @ %s; docid = %u", path.c_str(), docid);
|
g_debug("added message @ %s; docid = %u", path.c_str(), docid);
|
||||||
|
|
||||||
return docid;
|
return Ok(static_cast<Store::Id>(docid));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
Store::update_message(MuMsg* msg, unsigned docid)
|
Store::update_message(const Message& msg, unsigned docid)
|
||||||
{
|
{
|
||||||
const auto docid2{priv_->add_or_update_msg(docid, msg)};
|
return xapian_try(
|
||||||
|
[&]{
|
||||||
|
priv_->writable_db().replace_document(
|
||||||
|
docid, msg.document().xapian_document());
|
||||||
|
|
||||||
if (G_UNLIKELY(docid != docid2))
|
g_debug("updated message %u @ %s", docid, msg.path().c_str());
|
||||||
throw Error{Error::Code::Internal, "failed to update message"};
|
return true;
|
||||||
|
}, false);
|
||||||
g_debug("updated message @ %s; docid = %u", mu_msg_get_path(msg), docid);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
@ -464,9 +391,8 @@ Store::remove_message(const std::string& path)
|
||||||
return xapian_try(
|
return xapian_try(
|
||||||
[&] {
|
[&] {
|
||||||
std::lock_guard guard{priv_->lock_};
|
std::lock_guard guard{priv_->lock_};
|
||||||
const std::string term{(get_uid_term(path.c_str()))};
|
const auto term{field_from_id(Field::Id::Path).xapian_term(path)};
|
||||||
priv_->writable_db().delete_document(term);
|
priv_->writable_db().delete_document(term);
|
||||||
|
|
||||||
g_debug("deleted message @ %s from store", path.c_str());
|
g_debug("deleted message @ %s from store", path.c_str());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -491,6 +417,54 @@ Store::remove_messages(const std::vector<Store::Id>& ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Result<Message>
|
||||||
|
Store::move_message(Store::Id id,
|
||||||
|
Option<const std::string&> target_mdir,
|
||||||
|
Option<Flags> new_flags, bool change_name)
|
||||||
|
{
|
||||||
|
auto msg = find_message(id);
|
||||||
|
if (!msg)
|
||||||
|
return Err(Error::Code::Store, "cannot find message <%u>", id);
|
||||||
|
|
||||||
|
const auto target_flags = new_flags.value_or(msg->flags());
|
||||||
|
const auto target_maildir = target_mdir.value_or(msg->maildir());
|
||||||
|
|
||||||
|
/* 1. first determine the file system path of the target */
|
||||||
|
const auto target_path =
|
||||||
|
mu_maildir_determine_target(msg->path(),
|
||||||
|
properties().root_maildir,
|
||||||
|
target_maildir,
|
||||||
|
target_flags,
|
||||||
|
change_name);
|
||||||
|
if (!target_path)
|
||||||
|
return Err(target_path.error());
|
||||||
|
|
||||||
|
/* 2. let's move it */
|
||||||
|
const auto move_res =
|
||||||
|
mu_maildir_move_message(msg->path(),
|
||||||
|
target_path.value(),
|
||||||
|
true/*ignore dups*/);
|
||||||
|
if (!move_res)
|
||||||
|
return Err(move_res.error());
|
||||||
|
|
||||||
|
/* 3. file move worked, now update the message with the new info.*/
|
||||||
|
const auto update_res = msg->update_after_move(target_path.value(),
|
||||||
|
target_maildir,
|
||||||
|
target_flags);
|
||||||
|
if (!update_res)
|
||||||
|
return Err(update_res.error());
|
||||||
|
|
||||||
|
/* 4. update message worked; re-store it */
|
||||||
|
if (!update_message(*msg, id))
|
||||||
|
return Err(Error::Code::Store, "failed to update message <%u>", id);
|
||||||
|
|
||||||
|
/* 5. Profit! */
|
||||||
|
return Ok(std::move(msg.value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
std::string
|
std::string
|
||||||
Store::metadata(const std::string& key) const
|
Store::metadata(const std::string& key) const
|
||||||
{
|
{
|
||||||
|
@ -540,24 +514,19 @@ Store::set_dirstamp(const std::string& path, time_t tstamp)
|
||||||
set_metadata(path, std::string{data.data(), len});
|
set_metadata(path, std::string{data.data(), len});
|
||||||
}
|
}
|
||||||
|
|
||||||
MuMsg*
|
Option<Message>
|
||||||
Store::find_message(unsigned docid) const
|
Store::find_message(Store::Id docid) const
|
||||||
{
|
{
|
||||||
return xapian_try(
|
return xapian_try(
|
||||||
[&] {
|
[&]()->Option<Message> {
|
||||||
std::lock_guard guard{priv_->lock_};
|
std::lock_guard guard{priv_->lock_};
|
||||||
Xapian::Document* doc{new Xapian::Document{priv_->db().get_document(docid)}};
|
auto res = Message::make_from_document(priv_->db().get_document(docid));
|
||||||
GError* gerr{};
|
if (res)
|
||||||
auto msg{mu_msg_new_from_doc(
|
return Some(std::move(res.value()));
|
||||||
reinterpret_cast<XapianDocument*>(doc), &gerr)};
|
else
|
||||||
if (!msg) {
|
return Nothing;
|
||||||
g_warning("could not create message: %s",
|
|
||||||
gerr ? gerr->message : "something went wrong");
|
|
||||||
g_clear_error(&gerr);
|
|
||||||
}
|
|
||||||
return msg;
|
|
||||||
},
|
},
|
||||||
(MuMsg*)nullptr);
|
Nothing);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
@ -566,7 +535,7 @@ Store::contains_message(const std::string& path) const
|
||||||
return xapian_try(
|
return xapian_try(
|
||||||
[&] {
|
[&] {
|
||||||
std::lock_guard guard{priv_->lock_};
|
std::lock_guard guard{priv_->lock_};
|
||||||
const std::string term(get_uid_term(path.c_str()));
|
const auto term{field_from_id(Field::Id::Path).xapian_term(path)};
|
||||||
return priv_->db().term_exists(term);
|
return priv_->db().term_exists(term);
|
||||||
},
|
},
|
||||||
false);
|
false);
|
||||||
|
@ -659,324 +628,3 @@ Store::parse_query(const std::string& expr, bool xapian) const
|
||||||
},
|
},
|
||||||
std::string{});
|
std::string{});
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
add_terms_values_date(Xapian::Document& doc, MuMsg* msg)
|
|
||||||
{
|
|
||||||
constexpr auto value_no{field_from_id(Field::Id::Date).value_no()};
|
|
||||||
const auto dstr = Mu::date_to_time_t_string(
|
|
||||||
static_cast<time_t>(mu_msg_get_field_numeric(msg, Field::Id::Date)));
|
|
||||||
|
|
||||||
doc.add_value(value_no, dstr);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_terms_values_size(Xapian::Document& doc, MuMsg* msg)
|
|
||||||
{
|
|
||||||
constexpr auto value_no{field_from_id(Field::Id::Size).value_no()};
|
|
||||||
const auto szstr = Mu::size_to_string(mu_msg_get_field_numeric(
|
|
||||||
msg, Field::Id::Size));
|
|
||||||
doc.add_value(value_no, szstr);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void // add term, truncate if needed.
|
|
||||||
add_term(Xapian::Document& doc, const std::string& term)
|
|
||||||
{
|
|
||||||
if (term.length() < Store::MaxTermLength)
|
|
||||||
doc.add_term(term);
|
|
||||||
else
|
|
||||||
doc.add_term(term.substr(0, Store::MaxTermLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_terms_values_number(Xapian::Document& doc, MuMsg* msg, const Field& field)
|
|
||||||
{
|
|
||||||
const auto num{mu_msg_get_field_numeric(msg, field.id)};
|
|
||||||
if (field.is_value()) {
|
|
||||||
const std::string numstr(Xapian::sortable_serialise((double)num));
|
|
||||||
doc.add_value(field.value_no(), numstr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.id == Field::Id::Flags) {
|
|
||||||
g_return_if_fail(num < static_cast<int64_t>(Flags::_final_));
|
|
||||||
const auto msgflag{static_cast<Flags>(num)};
|
|
||||||
flag_infos_for_each([&](auto&& info) {
|
|
||||||
if (any_of(info.flag & msgflag))
|
|
||||||
add_term(doc, field.xapian_term(info.shortcut_lower()));
|
|
||||||
});
|
|
||||||
} else if (field.id == Field::Id::Priority)
|
|
||||||
add_term(doc,
|
|
||||||
field.xapian_term(to_char(static_cast<Priority>(num))));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* for string and string-list */
|
|
||||||
static void
|
|
||||||
add_terms_values_str(Xapian::Document& doc, const char* val, const Field& field)
|
|
||||||
{
|
|
||||||
const auto flat = Mu::utf8_flatten(val);
|
|
||||||
if (field.is_indexable_term()) {
|
|
||||||
Xapian::TermGenerator termgen;
|
|
||||||
termgen.set_document(doc);
|
|
||||||
termgen.index_text(flat, 1, field.xapian_term());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.is_normal_term())
|
|
||||||
add_term(doc, field.xapian_term(flat));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_terms_values_string(Xapian::Document& doc, MuMsg* msg, const Field& field)
|
|
||||||
{
|
|
||||||
const char* orig{mu_msg_get_field_string(msg, field.id)};
|
|
||||||
if (!orig)
|
|
||||||
return; /* nothing to do */
|
|
||||||
|
|
||||||
/* the value is what we display in search results; the
|
|
||||||
* unchanged original */
|
|
||||||
if (field.is_value())
|
|
||||||
doc.add_value(field.value_no(), orig);
|
|
||||||
|
|
||||||
add_terms_values_str(doc, orig, field);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_terms_values_string_list(Xapian::Document& doc, MuMsg* msg, const Field& field)
|
|
||||||
{
|
|
||||||
const GSList* lst;
|
|
||||||
|
|
||||||
lst = mu_msg_get_field_string_list(msg, field.id);
|
|
||||||
if (!lst)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (field.is_value()) {
|
|
||||||
gchar* str;
|
|
||||||
str = mu_str_from_list(lst, ',');
|
|
||||||
if (str)
|
|
||||||
doc.add_value(field.value_no(), str);
|
|
||||||
g_free(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.is_normal_term()) {
|
|
||||||
for (; lst; lst = g_slist_next((GSList*)lst))
|
|
||||||
add_terms_values_str(doc, (const gchar*)lst->data, field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* index non-body text parts */
|
|
||||||
static void
|
|
||||||
maybe_index_text_part(Xapian::Document& doc, MuMsg* msg, MuMsgPart* part)
|
|
||||||
{
|
|
||||||
char* txt;
|
|
||||||
|
|
||||||
/* only deal with attachments/messages; inlines are indexed as
|
|
||||||
* body parts */
|
|
||||||
if (!(part->part_type & MU_MSG_PART_TYPE_ATTACHMENT) &&
|
|
||||||
!(part->part_type & MU_MSG_PART_TYPE_MESSAGE))
|
|
||||||
return;
|
|
||||||
|
|
||||||
txt = mu_msg_part_get_text(msg, part, MU_MSG_OPTION_NONE);
|
|
||||||
if (!txt)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Xapian::TermGenerator termgen;
|
|
||||||
termgen.set_document(doc);
|
|
||||||
|
|
||||||
const auto str = Mu::utf8_flatten(txt);
|
|
||||||
g_free(txt);
|
|
||||||
|
|
||||||
static const auto pfx{field_from_id(Field::Id::EmbeddedText).xapian_term()};
|
|
||||||
termgen.index_text(str, 1, pfx);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DocHolder {
|
|
||||||
Xapian::Document& doc;
|
|
||||||
};
|
|
||||||
|
|
||||||
static void
|
|
||||||
each_part(MuMsg* msg, MuMsgPart* part, DocHolder* doc_holder)
|
|
||||||
{
|
|
||||||
Xapian::Document& doc{doc_holder->doc};
|
|
||||||
|
|
||||||
/* save the mime type of any part */
|
|
||||||
if (part->type)
|
|
||||||
add_term(doc, field_from_id(Field::Id::Mime)
|
|
||||||
.xapian_term(format("%s/%s", part->type, part->subtype)));
|
|
||||||
|
|
||||||
if (char *fname = mu_msg_part_get_filename(part, FALSE); fname) {
|
|
||||||
const auto flat{Mu::utf8_flatten(fname)};
|
|
||||||
g_free(fname);
|
|
||||||
add_term(doc, field_from_id(Field::Id::File).xapian_term(flat));
|
|
||||||
}
|
|
||||||
|
|
||||||
maybe_index_text_part(doc, msg, part);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_terms_values_attach(Xapian::Document& doc, MuMsg* msg)
|
|
||||||
{
|
|
||||||
DocHolder holder{doc};
|
|
||||||
|
|
||||||
mu_msg_part_foreach(msg,
|
|
||||||
MU_MSG_OPTION_RECURSE_RFC822,
|
|
||||||
(MuMsgPartForeachFunc)each_part,
|
|
||||||
&holder);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_terms_values_body(Xapian::Document& doc, MuMsg* msg, const Field& field)
|
|
||||||
{
|
|
||||||
if (any_of(mu_msg_get_flags(msg) & Flags::Encrypted))
|
|
||||||
return; /* ignore encrypted bodies */
|
|
||||||
|
|
||||||
Xapian::TermGenerator termgen;
|
|
||||||
termgen.set_document(doc);
|
|
||||||
|
|
||||||
if (field.id == Field::Id::BodyText){
|
|
||||||
if (auto str{mu_msg_get_body_text(msg, MU_MSG_OPTION_NONE)}; str)
|
|
||||||
termgen.index_text(Mu::utf8_flatten(str), 1,
|
|
||||||
field.xapian_term());
|
|
||||||
} else if (field.id == Field::Id::BodyText) {
|
|
||||||
if (auto str{mu_msg_get_body_html(msg, MU_MSG_OPTION_NONE)}; str)
|
|
||||||
termgen.index_text(Mu::utf8_flatten(str), 1,
|
|
||||||
field.xapian_term());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_terms_values_default(Xapian::Document& doc, MuMsg *msg, const Field& field)
|
|
||||||
{
|
|
||||||
if (field.is_numerical())
|
|
||||||
add_terms_values_number(doc, msg, field);
|
|
||||||
else if (field.is_string())
|
|
||||||
add_terms_values_string(doc, msg, field);
|
|
||||||
else if (field.is_string_list())
|
|
||||||
add_terms_values_string_list(doc, msg, field);
|
|
||||||
else
|
|
||||||
g_return_if_reached();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void
|
|
||||||
add_contacts_terms_values(Xapian::Document& doc, MuMsg *msg,
|
|
||||||
ContactsCache& contacts_cache)
|
|
||||||
{
|
|
||||||
Xapian::TermGenerator termgen;
|
|
||||||
termgen.set_document(doc);
|
|
||||||
|
|
||||||
auto contacts{mu_msg_get_contacts(msg)};
|
|
||||||
|
|
||||||
for (auto&& contact: contacts) {
|
|
||||||
|
|
||||||
// e.g. Reply-To doesn't have a field connected.
|
|
||||||
const auto field_opt{contact.field_id};
|
|
||||||
if (!field_opt)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const auto field{field_from_id(*field_opt)};
|
|
||||||
doc.add_value(field.value_no(), contact.display_name());
|
|
||||||
add_term(doc, field.xapian_term(contact.email));
|
|
||||||
|
|
||||||
if (!contact.name.empty())
|
|
||||||
termgen.index_text(utf8_flatten(contact.name),
|
|
||||||
1, field.xapian_term());
|
|
||||||
|
|
||||||
|
|
||||||
// index name / domain separately, too.
|
|
||||||
if (const auto at = contact.email.find('@'); at != std::string::npos) {
|
|
||||||
add_term(doc, field.xapian_term(contact.email.substr(0, at)));
|
|
||||||
add_term(doc, field.xapian_term(contact.email.substr(at)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contacts_cache.add(std::move(contacts));
|
|
||||||
}
|
|
||||||
|
|
||||||
Xapian::Document
|
|
||||||
Store::Private::new_doc_from_message(MuMsg* msg)
|
|
||||||
{
|
|
||||||
Xapian::Document doc;
|
|
||||||
|
|
||||||
add_contacts_terms_values(doc, msg, this->contacts_cache_);
|
|
||||||
field_for_each([&](auto&& field) {
|
|
||||||
|
|
||||||
if (!field.is_searchable() && !field.is_value())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (field.is_contact())
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (field.id) {
|
|
||||||
case Field::Id::Date:
|
|
||||||
add_terms_values_date(doc, msg);
|
|
||||||
break;
|
|
||||||
case Field::Id::Size:
|
|
||||||
add_terms_values_size(doc, msg);
|
|
||||||
break;
|
|
||||||
case Field::Id::BodyHtml:
|
|
||||||
case Field::Id::BodyText:
|
|
||||||
add_terms_values_body(doc, msg, field);
|
|
||||||
break;
|
|
||||||
case Field::Id::File:
|
|
||||||
/* note: add_terms_values_attach handles _FILE, _MIME and
|
|
||||||
* _ATTACH_TEXT msgfields */
|
|
||||||
add_terms_values_attach(doc, msg);
|
|
||||||
break;
|
|
||||||
case Field::Id::Mime:
|
|
||||||
case Field::Id::EmbeddedText:
|
|
||||||
break;
|
|
||||||
case Field::Id::ThreadId:
|
|
||||||
case Field::Id::Uid:
|
|
||||||
break; /* already taken care of elsewhere */
|
|
||||||
default:
|
|
||||||
return add_terms_values_default(doc, msg, field);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
update_threading_info(MuMsg* msg, Xapian::Document& doc)
|
|
||||||
{
|
|
||||||
const GSList* refs;
|
|
||||||
const auto field{field_from_id(Field::Id::ThreadId)};
|
|
||||||
|
|
||||||
// refs contains a list of parent messages, with the oldest
|
|
||||||
// one first until the last one, which is the direct parent of
|
|
||||||
// the current message. of course, it may be empty.
|
|
||||||
//
|
|
||||||
// NOTE: there may be cases where the list is truncated; we happily
|
|
||||||
// ignore that case.
|
|
||||||
refs = mu_msg_get_references(msg);
|
|
||||||
|
|
||||||
char thread_id[16 + 1];
|
|
||||||
hash_str(thread_id,
|
|
||||||
sizeof(thread_id),
|
|
||||||
refs ? (const char*)refs->data : mu_msg_get_msgid(msg));
|
|
||||||
add_term(doc, field.xapian_term(std::string{thread_id}));
|
|
||||||
doc.add_value(field.value_no(), thread_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Xapian::docid
|
|
||||||
Store::Private::add_or_update_msg(Xapian::docid docid, MuMsg* msg)
|
|
||||||
{
|
|
||||||
g_return_val_if_fail(msg, InvalidId);
|
|
||||||
|
|
||||||
return xapian_try(
|
|
||||||
[&] {
|
|
||||||
Xapian::Document doc{new_doc_from_message(msg)};
|
|
||||||
const std::string term{get_uid_term(mu_msg_get_path(msg))};
|
|
||||||
add_term(doc, term);
|
|
||||||
|
|
||||||
// update the threading info if this message has a message id
|
|
||||||
if (mu_msg_get_msgid(msg))
|
|
||||||
update_threading_info(msg, doc);
|
|
||||||
|
|
||||||
if (docid == 0)
|
|
||||||
return writable_db().replace_document(term, doc);
|
|
||||||
|
|
||||||
writable_db().replace_document(docid, doc);
|
|
||||||
return docid;
|
|
||||||
},
|
|
||||||
InvalidId);
|
|
||||||
}
|
|
||||||
|
|
|
@ -20,8 +20,6 @@
|
||||||
#ifndef __MU_STORE_HH__
|
#ifndef __MU_STORE_HH__
|
||||||
#define __MU_STORE_HH__
|
#define __MU_STORE_HH__
|
||||||
|
|
||||||
#include <mu-msg.hh>
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
@ -75,15 +73,6 @@ public:
|
||||||
const StringVec& personal_addresses,
|
const StringVec& personal_addresses,
|
||||||
const Config& conf);
|
const Config& conf);
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct an in-memory, writeable store for testing
|
|
||||||
*
|
|
||||||
* @param maildir maildir to use for this store
|
|
||||||
* @param personal_addresses addresses that should be recognized as
|
|
||||||
* 'personal' for identifying personal messages.
|
|
||||||
*/
|
|
||||||
Store(const std::string& maildir, const StringVec& personal_addresses, const Config& conf);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTOR
|
* DTOR
|
||||||
*/
|
*/
|
||||||
|
@ -187,14 +176,15 @@ public:
|
||||||
/**
|
/**
|
||||||
* Add a message to the store. When planning to write many messages,
|
* Add a message to the store. When planning to write many messages,
|
||||||
* it's much faster to do so in a transaction. If so, set
|
* it's much faster to do so in a transaction. If so, set
|
||||||
* @in_transaction to true. When done with adding messages, call commit().
|
* @in_transaction to true. When done with adding messages, call
|
||||||
|
* commit().
|
||||||
*
|
*
|
||||||
* @param path the message path.
|
* @param path the message path.
|
||||||
* @param whether to bundle up to batch_size changes in a transaction
|
* @param whether to bundle up to batch_size changes in a transaction
|
||||||
*
|
*
|
||||||
* @return the doc id of the added message
|
* @return the doc id of the added message or an error.
|
||||||
*/
|
*/
|
||||||
Id add_message(const std::string& path, bool use_transaction = false);
|
Result<Id> add_message(const std::string& path, bool use_transaction = false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a message in the store.
|
* Update a message in the store.
|
||||||
|
@ -204,7 +194,7 @@ public:
|
||||||
*
|
*
|
||||||
* @return false in case of failure; true otherwise.
|
* @return false in case of failure; true otherwise.
|
||||||
*/
|
*/
|
||||||
bool update_message(MuMsg* msg, Id id);
|
bool update_message(const Message& msg, Id id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a message from the store. It will _not_ remove the message
|
* Remove a message from the store. It will _not_ remove the message
|
||||||
|
@ -237,9 +227,9 @@ public:
|
||||||
*
|
*
|
||||||
* @param id doc id for the message to find
|
* @param id doc id for the message to find
|
||||||
*
|
*
|
||||||
* @return a message (owned by caller), or nullptr
|
* @return a message (if found) or Nothing
|
||||||
*/
|
*/
|
||||||
MuMsg* find_message(Id id) const;
|
Option<Message> find_message(Id id) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* does a certain message exist in the store already?
|
* does a certain message exist in the store already?
|
||||||
|
@ -250,6 +240,22 @@ public:
|
||||||
*/
|
*/
|
||||||
bool contains_message(const std::string& path) const;
|
bool contains_message(const std::string& path) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a message both in the filesystem and in the store.
|
||||||
|
* After a successful move, the message is updated.
|
||||||
|
*
|
||||||
|
* @param id the id for some message
|
||||||
|
* @param target_mdir the target maildir (if any)
|
||||||
|
* @param new_flags new flags (if any)
|
||||||
|
* @param change_name whether to change the name
|
||||||
|
*
|
||||||
|
* @return Result, either the moved message or some error.
|
||||||
|
*/
|
||||||
|
Result<Message> move_message(Store::Id id,
|
||||||
|
Option<const std::string&> target_mdir = Nothing,
|
||||||
|
Option<Flags> new_flags = Nothing,
|
||||||
|
bool change_name = false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prototype for the ForEachMessageFunc
|
* Prototype for the ForEachMessageFunc
|
||||||
*
|
*
|
||||||
|
@ -301,7 +307,6 @@ public:
|
||||||
*/
|
*/
|
||||||
std::string metadata(const std::string& key) const;
|
std::string metadata(const std::string& key) const;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write metadata to the store.
|
* Write metadata to the store.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue