From da69dd6815d75e3bc046ac74f5dc04f2babb5bd3 Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Tue, 2 Sep 2025 23:19:54 +0300 Subject: mu: refactor labels code Split labels-cache and store-labels. --- lib/meson.build | 3 +- lib/mu-contacts-cache.cc | 1 - lib/mu-labels-cache.cc | 79 +++++++++++++++++++++++ lib/mu-labels-cache.hh | 148 +++++++++++++++++++++++++++++++++++++++++++ lib/mu-store-labels.cc | 24 ++----- lib/mu-store-labels.hh | 160 +++-------------------------------------------- lib/mu-store.cc | 17 +++-- lib/mu-store.hh | 3 +- lib/mu-xapian-db.hh | 2 +- mu/mu-cmd-label.cc | 5 +- mu/mu-options.cc | 2 +- 11 files changed, 256 insertions(+), 188 deletions(-) create mode 100644 lib/mu-labels-cache.cc create mode 100644 lib/mu-labels-cache.hh diff --git a/lib/meson.build b/lib/meson.build index 35b61be..eb772ef 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -1,4 +1,4 @@ -## Copyright (C) 2021-2023 Dirk-Jan C. Binnema +## Copyright (C) 2021-2025 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 @@ -24,6 +24,7 @@ lib_mu=static_library( # db 'mu-config.cc', 'mu-contacts-cache.cc', + 'mu-labels-cache.cc', 'mu-store.cc', 'mu-store-labels.cc', 'mu-xapian-db.cc', diff --git a/lib/mu-contacts-cache.cc b/lib/mu-contacts-cache.cc index b422ed9..66cb4d7 100644 --- a/lib/mu-contacts-cache.cc +++ b/lib/mu-contacts-cache.cc @@ -273,7 +273,6 @@ ContactsCache::size() const return priv_->contacts_.size(); } - /** * Wrapper to allow for Contact sorting * (needed due to reference_wrapper) diff --git a/lib/mu-labels-cache.cc b/lib/mu-labels-cache.cc new file mode 100644 index 0000000..0e4fa83 --- /dev/null +++ b/lib/mu-labels-cache.cc @@ -0,0 +1,79 @@ +/* +** Copyright (C) 2025 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 "mu-labels-cache.hh" +#include "mu-store.hh" +#include "message/mu-labels.hh" + +using namespace Mu; + +void +Mu::LabelsCache::serialize() const +{ + std::string s; + for (const auto&[label, n]: label_map_) + s += mu_format("{}{}{}\n", label, SepaChar2, n); + + config_.set(s); + mu_debug("labels: serialized {} change(s)", dirty_); + dirty_ = 0; +} + +Mu::LabelsCache::Map +Mu::LabelsCache::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( + g_ascii_strtoll(parts[1].c_str(),{}, 10))); + } + return map; +} + +Result +Mu::LabelsCache::restore(const Store& store) +{ + const auto res{store.run_query("")}; + if (!res) + return Err(Error{Error::Code::Query, + "failed to run query: {}", + *res.error().what()}); + label_map_.clear(); + ++dirty_; + + for (auto&& item: *res) { + if (auto &&msg{item.message()}; msg) { + for (const auto& label: msg->labels()) + increase(label); + } + } + + serialize(); + + return Ok(); +} diff --git a/lib/mu-labels-cache.hh b/lib/mu-labels-cache.hh new file mode 100644 index 0000000..9f95d09 --- /dev/null +++ b/lib/mu-labels-cache.hh @@ -0,0 +1,148 @@ +/* +** Copyright (C) 2025 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. +** +*/ + +#ifndef MU_LABELS_CACHE_HH +#define MU_LABELS_CACHE_HH + +#include +#include +#include + +#include "utils/mu-utils.hh" +#include "utils/mu-option.hh" +#include "message/mu-labels.hh" + +#include "mu-config.hh" + +namespace Mu { + +class Store; // fwd declaration + +/** + * 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; + + /** + * CTOR + * + * Deserialize the map from the configuration. + * + * @param serialized serialization string + */ + LabelsCache(Config& config): config_{config}, + label_map_{deserialize(config_.get())}, + dirty_{false}{} + + ~LabelsCache() { + if (dirty_) + serialize(); + }; + + /** + * Add a label occurrence to the cache + * + * @param label some label + */ + void increase(const std::string& label) { + if (auto it = label_map_.find(label); it == label_map_.end()) + label_map_.insert({label, 1}); + else + ++it->second; + ++dirty_; + } + /** + * Remove a label occurrence from the cache + * + * Removes the label completely if this was the _last_ occurence. + * + * @param label some label + */ + void decrease(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; + ++dirty_; + } + } + + /** + * Update the cache with 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: + increase(label); + break; + case Labels::Delta::Remove: + decrease(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 Config if there are changes. + * + */ + void serialize() const; + + /** + * Restore the labels-cache from the labels seen in the store. + * + * @param store a store + * + * @return Ok() or some error + */ + Result restore(const Store& store); + +private: + /** + * Deserialize the cache into a Map + * + * @return serialized cache + */ + Map deserialize(const std::string& serialized) const; + + Config& config_; + Map label_map_; + mutable size_t dirty_{}; +}; + +} // namespace Mux +#endif /*MU_LABELS_CACHE_HH*/ diff --git a/lib/mu-store-labels.cc b/lib/mu-store-labels.cc index 8653c84..a08f3b4 100644 --- a/lib/mu-store-labels.cc +++ b/lib/mu-store-labels.cc @@ -18,30 +18,14 @@ */ #include "mu-store-labels.hh" + +#include +#include + #include "mu-store.hh" -#include "message/mu-labels.hh" using namespace Mu; -Result -Mu::LabelsCache::restore(const Store& store) -{ - const auto res{store.run_query("")}; - if (!res) - return Err(Error{Error::Code::Query, - "failed to run query: {}", - *res.error().what()}); - label_map_.clear(); - - for (auto&& item: *res) { - if (auto &&msg{item.message()}; msg) { - for (const auto& label: msg->labels()) - increase(label); - } - } - return Ok(); -} - namespace { constexpr std::string_view path_key = "path:"; constexpr std::string_view message_id_key = "message-id:"; diff --git a/lib/mu-store-labels.hh b/lib/mu-store-labels.hh index 6104973..919e9de 100644 --- a/lib/mu-store-labels.hh +++ b/lib/mu-store-labels.hh @@ -17,160 +17,16 @@ ** */ -#ifndef MU_LABELS_CACHE_HH -#define MU_LABELS_CACHE_HH - -#include #include -#include +#include "mu-option.hh" +#include "mu-result.hh" -#include "utils/mu-utils.hh" -#include "utils/mu-option.hh" -#include "message/mu-labels.hh" +#ifndef MU_STORE_LABELS_HH +#define MU_STORE_LABELS_HH namespace Mu { -class Store; - -/** - * 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; - /** - * CTOR - * - * Deserialize the map from a string - * - * @param serialized serialization string - */ - LabelsCache(const std::string serialized = {}): label_map_{deserialize(serialized)} { - } - - /** - * Add a label occurrence to the cache - * - * @param label some label - */ - void increase(const std::string& label) { - if (auto it = label_map_.find(label); it == label_map_.end()) - label_map_.insert({label, 1}); - else - ++it->second; - dirty_ = true; - } - /** - * Remove a label occurrence from the cache - * - * Removes the label completely if this was the _last_ occurence. - * - * @param label some label - */ - void decrease(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; - dirty_ = true; - } - } - - /** - * Update the cache with 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: - increase(label); - break; - case Labels::Delta::Remove: - decrease(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. - * - * Note: this also marks the cache a _non_ dirty; - * - * @return serialized cache - */ - [[nodiscard]] std::string serialize() const { - std::string s; - for (const auto&[label, n]: label_map_) - s += mu_format("{}{}{}\n", label, SepaChar2, n); - dirty_ = false; - 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( - g_ascii_strtoll(parts[1].c_str(),{}, 10))); - } - return map; - } - - - /** - * Restore the labels-cache from the labels seen in the store. - * - * @param store a store - * - * @return Ok() or some error - */ - Result restore(const Store& store); - - - /** - * Is the cache "dirty"? - * - * I.e. have there been changes since "serialize()" was called? - * - * @return true or false - */ - bool dirty() const { - return dirty_; - } - -private: - Map label_map_; - mutable bool dirty_{}; -}; +class Store; // fwd declaration /** * Export labels to a file @@ -187,7 +43,6 @@ private: Result export_labels(const Store& store, const std::string& query="", Option path={}); - /** * Import labels from a file * @@ -203,6 +58,7 @@ Result export_labels(const Store& store, */ Result import_labels(Store&, const std::string& path, bool dry_run, bool quiet, bool verbose); +} // Mu + -} // namespace Mux -#endif /*MU_LABELS_CACHE_HH*/ +#endif /*MU_STORE_LABELS_HH*/ diff --git a/lib/mu-store.cc b/lib/mu-store.cc index de148af..e5ba902 100644 --- a/lib/mu-store.cc +++ b/lib/mu-store.cc @@ -35,10 +35,10 @@ #include "mu-query.hh" #include "mu-xapian-db.hh" #include "mu-scanner.hh" +#include "mu-store-labels.hh" #include "utils/mu-error.hh" - #include "utils/mu-utils.hh" #include @@ -66,7 +66,7 @@ struct Store::Private { : XapianDb::Flavor::Open)}, config_{xapian_db_}, contacts_cache_{config_}, - labels_cache_{config_.get()}, + labels_cache_{config_}, root_maildir_{remove_slash(config_.get())}, message_opts_{make_message_options(config_)} {} @@ -76,7 +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()}, + labels_cache_{config_}, root_maildir_{remove_slash(config_.get())}, message_opts_{make_message_options(config_)} { // so tell xapian-db to update its internal cached values from @@ -85,10 +85,6 @@ struct Store::Private { } ~Private() try { - if (!xapian_db_.read_only()) { - if (const auto res = serialize(); !res) - mu_critical("failed to serialize: {}", res.error().what()); - } mu_debug("closing store @ {}", xapian_db_.path()); } catch (...) { mu_critical("caught exception in store dtor"); @@ -156,9 +152,7 @@ Store::Private::serialize() mu_debug("serialize data into store @ {}", xapian_db_.path()); contacts_cache_.serialize(); // does the 'dirty' check internally. - - if (labels_cache_.dirty()) - config_.set(labels_cache_.serialize()); + labels_cache_.serialize(); return Ok(); } @@ -697,6 +691,9 @@ Store::clear_labels(Message& message) Result Store::restore_label_map() { + if (xapian_db().read_only()) + throw Error{Error::Code::Store, "store is read-only"}; + std::unique_lock lock{priv_->lock_}; return priv_->labels_cache_.restore(*this); diff --git a/lib/mu-store.hh b/lib/mu-store.hh index 9aff22c..8a7f13c 100644 --- a/lib/mu-store.hh +++ b/lib/mu-store.hh @@ -27,6 +27,8 @@ #include #include "mu-contacts-cache.hh" +#include "mu-labels-cache.hh" + #include "mu-xapian-db.hh" #include "mu-config.hh" #include "mu-indexer.hh" @@ -35,7 +37,6 @@ #include #include #include -#include "mu-store-labels.hh" #include diff --git a/lib/mu-xapian-db.hh b/lib/mu-xapian-db.hh index e6f6557..1f727a3 100644 --- a/lib/mu-xapian-db.hh +++ b/lib/mu-xapian-db.hh @@ -480,7 +480,7 @@ private: if ((++changes_ < batch_size_) && !force) return; xapian_try([&]{ - mu_debug("committing {} changes; transaction={}; " + mu_debug("committing {} change(s); transaction={}; " "forced={}", changes_, in_transaction() ? "yes" : "no", force ? "yes" : "no"); diff --git a/mu/mu-cmd-label.cc b/mu/mu-cmd-label.cc index d2be417..72c9798 100644 --- a/mu/mu-cmd-label.cc +++ b/mu/mu-cmd-label.cc @@ -23,6 +23,8 @@ #include #include "mu-store.hh" +#include "mu-store-labels.hh" + #include "message/mu-message.hh" #include "message/mu-labels.hh" @@ -118,8 +120,9 @@ label_list(Mu::Store& store, const Options& opts) if (opts.label.restore) { if (!opts.quiet) mu_println("labels: restoring list from store..."); - if (const auto res = store.restore_label_map(); !res) + if (const auto res = store.restore_label_map(); !res) { return res; + } } for (const auto& [label, n]: store.label_map()) diff --git a/mu/mu-options.cc b/mu/mu-options.cc index bacaf40..3d9f0da 100644 --- a/mu/mu-options.cc +++ b/mu/mu-options.cc @@ -556,7 +556,7 @@ sub_label(CLI::App& sub, Options& opts) sub.final_callback([&](){ if (sub.got_subcommand("list")) { opts.label.sub = Options::Label::Sub::List; - opts.label.read_only = true; + opts.label.read_only = opts.label.restore ? false : true; } else if (sub.got_subcommand("clear")) { opts.label.sub = Options::Label::Sub::Clear; opts.label.read_only = opts.label.dry_run; -- cgit v1.0