summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk-Jan C. Binnema <djcb@djcbsoftware.nl>2025-07-27 09:19:48 +0300
committerDirk-Jan C. Binnema <djcb@djcbsoftware.nl>2025-08-15 21:02:24 +0300
commit1e628dfcaba241a1d9dadda91a9546eca32806d0 (patch)
tree211dc2f2a56e1d4248a2f32a7faf1922964dc286
parent552bb3a7c82fc7472ab8d3a5a0fd32fd9ea72909 (diff)
store: add support for modifying and listing labels and caching
Add methods update_labels, clear_labels which update or clear the labels for a message in the store, and update the cache with the overall counts of labels. Add a LabelsCache to keep track of the counts and labels_map() to retrieve that map.
-rw-r--r--lib/mu-config.hh13
-rw-r--r--lib/mu-labels-cache.hh156
-rw-r--r--lib/mu-store.cc60
-rw-r--r--lib/mu-store.hh31
4 files changed, 257 insertions, 3 deletions
diff --git a/lib/mu-config.hh b/lib/mu-config.hh
index c405edf..bf461d8 100644
--- a/lib/mu-config.hh
+++ b/lib/mu-config.hh
@@ -41,8 +41,9 @@ struct Property {
enum struct Id {
BatchSize, /**< Xapian batch-size */
Contacts, /**< Cache of contact information */
- Created, /**< Time of creation */
+ Created, /**< Time of creation */
IgnoredAddresses, /**< Email addresses ignored for the contacts-cache */
+ Labels, /**< Serialized label information. */
LastChange, /**< Time of last change */
LastIndex, /**< Time of last index */
MaxMessageSize, /**< Maximum message size (in bytes) */
@@ -130,6 +131,16 @@ public:
"E-mail addresses ignored for the contacts-cache, "
"literal or /regexp/"
},
+
+ {
+ Id::Labels,
+ Type::String,
+ Flags::Internal,
+ "labels",
+ {},
+ "Serialized labels information"
+ },
+
{
Id::LastChange,
Type::Timestamp,
diff --git a/lib/mu-labels-cache.hh b/lib/mu-labels-cache.hh
new file mode 100644
index 0000000..eb4cfe6
--- /dev/null
+++ b/lib/mu-labels-cache.hh
@@ -0,0 +1,156 @@
+/*
+** Copyright (C) 2025 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_LABELS_CACHE_HH
+#define MU_LABELS_CACHE_HH
+
+#include <set>
+#include <string>
+#include <unordered_map>
+
+#include "utils/mu-utils.hh"
+#include "message/mu-labels.hh"
+
+namespace Mu {
+
+/**
+ * The cache keeps track of what labels are being used. This can be used
+ * for completion etc. and `mu label list`
+ */
+class LabelsCache {
+public:
+ // maps a label to a number of occurrences
+ using Map = std::unordered_map<std::string, size_t>;
+ /**
+ * CTOR
+ *
+ * Deserialize the map from a string
+ *
+ * @param serialized serialization string
+ */
+ LabelsCache(const std::string serialized = {}): label_map_{deserialize(serialized)} {
+ }
+
+ /**
+ * Construct a new ContactsCache object
+ *
+ * @param config db configuration database object
+ */
+ LabelsCache(Config& config) {
+
+
+ }
+
+
+ /**
+ * Add a label occurrence to the cache
+ *
+ * @param label
+ */
+ void add(const std::string& label) {
+ if (auto it = label_map_.find(label); it == label_map_.end())
+ label_map_.insert({label, 1});
+ else
+ ++it->second;
+ }
+ /**
+ * Remove label occurrence from the cache
+ *
+ * @param label
+ */
+ void remove(const std::string& label) {
+ if (auto it = label_map_.find(label); it != label_map_.end()) {
+ if (it->second == 1)
+ label_map_.erase(it);
+ else
+ --it->second;
+ }
+ }
+
+ /**
+ * Update the cache with the the label changes
+ *
+ * @param updates a vector of delta-labels
+ */
+ void update(const Labels::DeltaLabelVec& updates) {
+ for(const auto& [delta, label]: updates) {
+ switch(delta) {
+ case Labels::Delta::Add:
+ add(label);
+ break;
+ case Labels::Delta::Remove:
+ remove(label);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Return a copy of the label-map
+ *
+ * @return the label-map
+ */
+ Map label_map() const { return label_map_; }
+
+
+ // serialization/deserialization could be optimized, but is not super
+ // time-critical
+
+ /**
+ * Serialize the cache into a string.
+ *
+ * @return serialized cache
+ */
+ std::string serialize() const {
+ std::string s;
+ for (const auto&[label, n]: label_map_)
+ s += mu_format("{}{}{}\n", label, SepaChar2, n);
+
+ return s;
+ }
+
+
+ /**
+ * Deserialize the cache into a Map
+ *
+ * @return serialized cache
+ */
+ Map deserialize(const std::string& serialized) const {
+
+ Map map;
+ std::stringstream ss{serialized, std::ios_base::in};
+ std::string line;
+
+ while (std::getline(ss, line)) {
+ if (const auto parts = Mu::split(line, SepaChar2); parts.size() != 2)
+ mu_warning("error: '{}'", line);
+ else
+ map.emplace(std::move(parts[0]),
+ static_cast<std::size_t>(g_ascii_strtoll(parts[1].c_str(),{}, 10)));
+ }
+ return map;
+ }
+
+private:
+ Map label_map_;
+};
+
+} // namespace Mux
+#endif /*MU_LABELS_CACHE_HH*/
diff --git a/lib/mu-store.cc b/lib/mu-store.cc
index 0db198a..f81d4ad 100644
--- a/lib/mu-store.cc
+++ b/lib/mu-store.cc
@@ -1,5 +1,5 @@
/*
-** Copyright (C) 2021-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+** Copyright (C) 2021-2025 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
@@ -38,6 +38,7 @@
#include "utils/mu-error.hh"
+
#include "utils/mu-utils.hh"
#include <utils/mu-utils-file.hh>
@@ -65,6 +66,7 @@ struct Store::Private {
: XapianDb::Flavor::Open)},
config_{xapian_db_},
contacts_cache_{config_},
+ labels_cache_{config_.get<Config::Id::Labels>()},
root_maildir_{remove_slash(config_.get<Config::Id::RootMaildir>())},
message_opts_{make_message_options(config_)}
{}
@@ -74,6 +76,7 @@ struct Store::Private {
xapian_db_{XapianDb(path, XapianDb::Flavor::CreateOverwrite)},
config_{make_config(xapian_db_, root_maildir, conf)},
contacts_cache_{config_},
+ labels_cache_{config_.get<Config::Id::Labels>()},
root_maildir_{remove_slash(config_.get<Config::Id::RootMaildir>())},
message_opts_{make_message_options(config_)} {
// so tell xapian-db to update its internal cacheed values from
@@ -83,8 +86,10 @@ struct Store::Private {
~Private() try {
mu_debug("closing store @ {}", xapian_db_.path());
- if (!xapian_db_.read_only())
+ if (!xapian_db_.read_only()) {
contacts_cache_.serialize();
+ config_.set<Config::Id::Labels>(labels_cache_.serialize());
+ }
} catch (...) {
mu_critical("caught exception in store dtor");
}
@@ -131,6 +136,7 @@ struct Store::Private {
XapianDb xapian_db_;
Config config_;
ContactsCache contacts_cache_;
+ LabelsCache labels_cache_;
std::unique_ptr<Indexer> indexer_;
const std::string root_maildir_;
@@ -589,6 +595,56 @@ Store::contains_message(const std::string& path) const
return xapian_db().term_exists(field_from_id(Field::Id::Path).xapian_term(path));
}
+
+Result<Labels::DeltaLabelVec>
+Store::update_labels(Message& message, const Labels::DeltaLabelVec& labels_delta)
+{
+ std::unique_lock lock{priv_->lock_};
+ // i.e. the set of effective labels. and the set up updates, the "diff"
+ auto updates{updated_labels(message.labels(), labels_delta)};
+
+ if (updates.second.empty())
+ return Ok(std::move(updates.second)); // nothing to do
+
+
+ message.set_labels(updates.first);
+ auto res{priv_->update_message_unlocked(message, message.docid())};
+ if (!res)
+ return Err(res.error());
+
+ priv_->labels_cache_.update(updates.second);
+
+ return Ok(std::move(updates.second));
+}
+
+Result<void>
+Store::clear_labels(Message& message)
+{
+ std::unique_lock lock{priv_->lock_};
+
+ const auto labels{message.labels()};
+ if (labels.empty())
+ return Ok(); // nothing to do
+
+ message.set_labels({}); // clear all
+ auto res{priv_->update_message_unlocked(message, message.docid())};
+ if (!res)
+ return Err(res.error());
+
+ for (auto label: labels)
+ priv_->labels_cache_.remove(label);
+
+ return Ok();
+}
+
+LabelsCache::Map
+Store::label_map() const
+{
+ std::unique_lock lock{priv_->lock_};
+
+ return priv_->labels_cache_.label_map();
+}
+
std::size_t
Store::for_each_message_path(Store::ForEachMessageFunc msg_func) const
{
diff --git a/lib/mu-store.hh b/lib/mu-store.hh
index cc7946a..afb1d75 100644
--- a/lib/mu-store.hh
+++ b/lib/mu-store.hh
@@ -35,6 +35,7 @@
#include <utils/mu-utils.hh>
#include <utils/mu-utils.hh>
#include <utils/mu-option.hh>
+#include "mu-labels-cache.hh"
#include <message/mu-message.hh>
@@ -339,6 +340,36 @@ public:
static IdVec id_vec(const IdPathVec& ips);
/**
+ * Update the labels for a message with the labels-delta
+ *
+ * Update the message in the store, and update the labels-cache
+ *
+ * @param message some message
+ * @param labels_delta the set of changes
+ *
+ * @return the effective changes for this message
+ */
+ Result<Labels::DeltaLabelVec> update_labels(Message& message, const Labels::DeltaLabelVec& labels_delta);
+
+ /**
+ * Clear all labels from message
+ *
+ * @param message some message
+ *
+ * @retgurn Ok or some error
+ */
+ Result<void> clear_labels(Message& message);
+
+ /**
+ * Get a copy of the map of labels in use.
+ *
+ * The map maps label-names to their count
+ *
+ * @return map
+ */
+ LabelsCache::Map label_map() const;
+
+ /**
* Prototype for the ForEachMessageFunc
*
* @param id :t store Id for the message