From a6b1f47a30c7aea84697553da4848b626c9faaca Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Sat, 16 Aug 2025 11:40:15 +0300 Subject: [PATCH] labels: refactor import/export to mu-store-labels Move the import/export code to 'lib'. --- lib/meson.build | 1 + lib/mu-store-labels.cc | 198 ++++++++++++++++++ ...{mu-labels-cache.hh => mu-store-labels.hh} | 42 +++- lib/mu-store.cc | 2 +- lib/mu-store.hh | 2 +- mu/mu-cmd-label.cc | 149 +------------ 6 files changed, 238 insertions(+), 156 deletions(-) create mode 100644 lib/mu-store-labels.cc rename lib/{mu-labels-cache.hh => mu-store-labels.hh} (79%) diff --git a/lib/meson.build b/lib/meson.build index c3a798db..35b61bed 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -25,6 +25,7 @@ lib_mu=static_library( 'mu-config.cc', 'mu-contacts-cache.cc', 'mu-store.cc', + 'mu-store-labels.cc', 'mu-xapian-db.cc', # querying 'mu-query-macros.cc', diff --git a/lib/mu-store-labels.cc b/lib/mu-store-labels.cc new file mode 100644 index 00000000..5aa6c946 --- /dev/null +++ b/lib/mu-store-labels.cc @@ -0,0 +1,198 @@ +/* +** 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-store-labels.hh" +#include "mu-store.hh" +#include "message/mu-labels.hh" + +using namespace Mu; + +namespace { +constexpr std::string_view path_key = "path:"; +constexpr std::string_view message_id_key = "message-id:"; +constexpr std::string_view labels_key = "labels:"; +} + +using OutputPair = std::pair; + +static Result +export_output(Option path) +{ + const auto now_t{::time({})}; + const auto now_tm{::localtime(&now_t)}; + + const auto now{mu_format("{:%F-%T}", *now_tm)}; + auto fname = path.value_or(mu_format("mu-export-{}.txt", now)); + + auto output{std::ofstream{fname, std::ios::out}}; + if (!output.good()) + return Err(Error{Error::Code::File, + "failed pen '{}' for writing", fname}); + + mu_println(output, ";; version:0 @ {}\n", now); + + return Ok(OutputPair{std::move(output), std::move(fname)}); +} + +Result +Mu::export_labels(const Store& store, const std::string& query, Option path) +{ + const auto results{store.run_query(query)}; + if (!results) + return Err(Error{Error::Code::Query, + "failed to run query '{}': {}", + query, *results.error().what()}); + + auto output_res = export_output(path); + if (!output_res) + return Err(std::move(output_res.error())); + + auto&[output, output_path] = *output_res; + + for (auto&& result : *results) { + if (auto &&msg{result.message()}; msg) { + if (const auto labels{msg->labels()}; !labels.empty()) { + mu_print(output, + "{}{}\n" + "{}{}\n" + "{}{}\n\n", + path_key, msg->path(), + message_id_key, msg->message_id(), + labels_key, join(labels,',')); + } + } + } + + return Ok(std::move(output_path)); +} + +static void +log_import(bool quiet, bool verbose, const std::string& msg, bool is_err=false) +{ + if (is_err) + mu_debug("{}", msg); + else + mu_warning("{}", msg); + + if (is_err && !quiet) + mu_printerrln("{}", msg); + else if (verbose) + mu_println("{}", msg); +} + +static void +log_import_err(bool quiet, bool verbose, const std::string& msg) +{ + log_import(quiet, verbose, msg, true); +} + + +static Result +log_import_get_matching(Mu::Store& store, const std::string& query, int max=1) +{ + if (auto qres = store.run_query(query, {}, {}, max); !qres) + return Err(std::move(qres.error())); + else if (qres->empty()) + return Err(Error{Error::Code::Query, + "no matching messages for {}", query}); + else + return Ok(std::move(*qres)); +} + + +static void +import_labels_for_message(Mu::Store& store, + bool dry_run, bool quiet, bool verbose, + const std::string& path, const std::string& msgid, + const std::vector labels) +{ + using namespace Labels; + + Labels::DeltaLabelVec delta_labels{}; + std::transform(labels.begin(), labels.end(), + std::back_inserter(delta_labels), + [](const auto& label) { + return DeltaLabel{Delta::Add, label}; }); + + const auto qres = [&]()->Result{ + // plan A: match by path + if (auto qres_a{log_import_get_matching(store, "path:" + path)}; !qres_a) { + log_import_err(quiet, verbose, mu_format("failed to find by path: {}; try with message-id", + qres_a.error().what())); + // plan B: try the message-id + return log_import_get_matching(store, "msgid:" + msgid, -1/*all matching*/); + } else + return qres_a; + }(); + + // neither plan a or b worked? we have to give up... + if (!qres) { + log_import_err(quiet, verbose, qres.error().what()); + return; + } + + // we have match(es)! + for (auto&& item: *qres) { + auto msg{*item.message()}; + if (dry_run ) + mu_println("labels: would apply label '{}' to {}", join(labels, ","), path); + else if (const auto res = store.update_labels(msg, delta_labels); !res) + log_import_err(quiet, verbose, + mu_format("failed to update labels for {}: {}", + msg.path(), res.error().what())); + else + log_import(quiet, verbose, + mu_format("applied labels {} to {}", join(labels, ","), path)); + } +} + +Result +Mu::import_labels(Mu::Store& store, const std::string& path, bool dry_run, bool quiet, bool verbose) +{ + auto input{std::ifstream{path, std::ios::in}}; + if (!input.good()) + return Err(Error{Error::Code::File, + "failed to open '{}' for reading", + path}); + + std::string line; + std::string current_path, current_msgid; + std::vector current_labels; + + while (std::getline(input, line)) { + + if (line.find(path_key) == 0) + current_path = line.substr(path_key.length()); + else if (line.find(message_id_key) == 0) + current_msgid = line.substr(message_id_key.length()); + else if (line.find(labels_key) == 0) { + current_labels = split(line.substr(labels_key.length()), ','); + if (!current_labels.empty()) + import_labels_for_message(store, dry_run, quiet, verbose, + current_path, current_msgid, + current_labels); + current_path.clear(); + current_msgid.clear(); + current_labels.clear(); + } + // ignore anything else. + } + + return Ok(); +} diff --git a/lib/mu-labels-cache.hh b/lib/mu-store-labels.hh similarity index 79% rename from lib/mu-labels-cache.hh rename to lib/mu-store-labels.hh index eb4cfe62..55b33f47 100644 --- a/lib/mu-labels-cache.hh +++ b/lib/mu-store-labels.hh @@ -26,6 +26,7 @@ #include #include "utils/mu-utils.hh" +#include "utils/mu-option.hh" #include "message/mu-labels.hh" namespace Mu { @@ -48,17 +49,6 @@ public: 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 * @@ -152,5 +142,35 @@ private: Map label_map_; }; +class Store; + +/** + * Export labels to a file + * + * If path is not specified, use a file in the current directory + * + * @param store a store object + * @param query for the message whose labels to export + * @param path the path or nothing + * + * @return either the output filename or some error + */ +Result export_labels(const Store& store, const std::string& query="", Option path); + +/** + * Import labels from a file + * + * If path is not specified, use a file in the current directory + * + * @param store a store object + * @param path the path to the file + * @param dry_run only show what would be imported + * @param quiet suppress output + * @param verbose give verbose output + * + * @return Ok or some error + */ +Result import_labels(Store&, const std::string& path, bool dry_run, bool quiet, bool verbose); + } // namespace Mux #endif /*MU_LABELS_CACHE_HH*/ diff --git a/lib/mu-store.cc b/lib/mu-store.cc index f81d4ada..f0a36ed3 100644 --- a/lib/mu-store.cc +++ b/lib/mu-store.cc @@ -79,7 +79,7 @@ struct Store::Private { labels_cache_{config_.get()}, root_maildir_{remove_slash(config_.get())}, message_opts_{make_message_options(config_)} { - // so tell xapian-db to update its internal cacheed values from + // so tell xapian-db to update its internal cached values from // config. In practice: batch-size. xapian_db_.reinit(); } diff --git a/lib/mu-store.hh b/lib/mu-store.hh index afb1d754..341da2a7 100644 --- a/lib/mu-store.hh +++ b/lib/mu-store.hh @@ -35,7 +35,7 @@ #include #include #include -#include "mu-labels-cache.hh" +#include "mu-store-labels.hh" #include diff --git a/mu/mu-cmd-label.cc b/mu/mu-cmd-label.cc index 30396fe5..b7814df1 100644 --- a/mu/mu-cmd-label.cc +++ b/mu/mu-cmd-label.cc @@ -125,128 +125,19 @@ label_list(const Mu::Store& store, const Options& opts) return Ok(); } -constexpr std::string_view path_key = "path:"; -constexpr std::string_view message_id_key = "message-id:"; -constexpr std::string_view labels_key = "labels:"; - static Result label_export(const Mu::Store& store, const Options& opts) { - const auto now_t{::time({})}; - const auto now_tm{::localtime(&now_t)}; - - const auto now{mu_format("{:%F-%T}", *now_tm)}; - const auto fname = opts.label.file.value_or( - mu_format("mu-export-{}.txt", now)); - auto output{std::ofstream{fname, std::ios::out}}; - if (!output.good()) - return Err(Error{Error::Code::File, - "failed to open '{}' for writing", fname}); - - const auto query{opts.label.query.value_or("")}; - auto results{store.run_query(query)}; - if (!results) - return Err(Error{Error::Code::Query, - "failed to run query '{}': {}", - query, *results.error().what()}); - - mu_println(output, ";; version:0 @ {}\n", now); - - for (auto&& result : *results) { - if (auto &&msg{result.message()}; msg) { - if (const auto labels{msg->labels()}; !labels.empty()) { - mu_print(output, - "{}{}\n" - "{}{}\n" - "{}{}\n\n", - path_key, msg->path(), - message_id_key, msg->message_id(), - labels_key, join(labels,',')); - } - } - } + const auto res = export_labels(store, "", opts.label.file); + if (!res) + return Err(res.error()); if (!opts.quiet) - mu_println("written {}", fname); + mu_println("written {}", *res); return Ok(); } -static void -log_import(const Options& opts, const std::string& msg, bool is_err=false) -{ - if (is_err) - mu_debug("{}", msg); - else - mu_warning("{}", msg); - - if (is_err && !opts.quiet) - mu_printerrln("{}", msg); - else if (opts.verbose) - mu_println("{}", msg); -} - -static void -log_import_err(const Options& opts, const std::string& msg) -{ - log_import(opts, msg, true); -} - - -static Result -log_import_get_matching(Mu::Store& store, const std::string& query, int max=1) -{ - if (auto qres = store.run_query(query, {}, {}, max); !qres) - return Err(std::move(qres.error())); - else if (qres->empty()) - return Err(Error{Error::Code::Query, - "no matching messages for {}", query}); - else - return Ok(std::move(*qres)); -} - - -static void -import_labels_for_message(Mu::Store& store, const Options& opts, - const std::string& path, const std::string& msgid, - const std::vector labels) -{ - Labels::DeltaLabelVec delta_labels{}; - std::transform(labels.begin(), labels.end(), std::back_inserter(delta_labels), - [](const auto& label) { - return DeltaLabel{Delta::Add, label}; }); - - const auto qres = [&]() - ->Result{ - // plan A: match by path - if (auto qres_a{log_import_get_matching(store, "path:" + path)}; !qres_a) { - log_import_err(opts, mu_format("failed to find by path: {}; try with message-id", - qres_a.error().what())); - // plan B: try the message-id - return log_import_get_matching(store, "msgid:" + msgid, -1/*all matching*/); - } else - return qres_a; - }(); - - // neither plan a or b worked? we have to give up... - if (!qres) { - log_import_err(opts, qres.error().what()); - return; - } - - // we have match(es)! - for (auto&& item: *qres) { - auto msg{*item.message()}; - if (opts.label.dry_run ) - mu_println("labels: would apply label '{}' to {}", join(labels, ","), path); - else if (const auto res = store.update_labels(msg, delta_labels); !res) - log_import_err(opts, mu_format("failed to update labels for {}: {}", - msg.path(), res.error().what())); - else - log_import(opts, mu_format("applied labels {} to {}", join(labels, ","), path)); - } -} - static Result label_import(Mu::Store& store, const Options& opts) { @@ -255,36 +146,8 @@ label_import(Mu::Store& store, const Options& opts) return Err(Error{Error::Code::InvalidArgument, "missing input file"}); - auto input{std::ifstream{*opts.label.file, std::ios::in}}; - if (!input.good()) - return Err(Error{Error::Code::File, - "failed to open '{}' for reading", - *opts.label.file}); - - std::string line; - std::string current_path, current_msgid; - std::vector current_labels; - - while (std::getline(input, line)) { - - if (line.find(path_key) == 0) - current_path = line.substr(path_key.length()); - else if (line.find(message_id_key) == 0) - current_msgid = line.substr(message_id_key.length()); - else if (line.find(labels_key) == 0) { - current_labels = split(line.substr(labels_key.length()), ','); - if (!current_labels.empty()) - import_labels_for_message(store, opts, - current_path, current_msgid, - current_labels); - current_path.clear(); - current_msgid.clear(); - current_labels.clear(); - } - // ignore anything else. - } - - return Ok(); + return Mu::import_labels(store, *opts.label.file, + opts.label.dry_run, opts.quiet, opts.verbose); } Result