labels: refactor import/export to mu-store-labels

Move the import/export code to 'lib'.
This commit is contained in:
Dirk-Jan C. Binnema
2025-08-16 11:40:15 +03:00
parent f504289a02
commit a6b1f47a30
6 changed files with 238 additions and 156 deletions

View File

@ -25,6 +25,7 @@ lib_mu=static_library(
'mu-config.cc', 'mu-config.cc',
'mu-contacts-cache.cc', 'mu-contacts-cache.cc',
'mu-store.cc', 'mu-store.cc',
'mu-store-labels.cc',
'mu-xapian-db.cc', 'mu-xapian-db.cc',
# querying # querying
'mu-query-macros.cc', 'mu-query-macros.cc',

198
lib/mu-store-labels.cc Normal file
View File

@ -0,0 +1,198 @@
/*
** 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.
**
*/
#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<std::ofstream, std::string>;
static Result<OutputPair>
export_output(Option<std::string> 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<std::string>
Mu::export_labels(const Store& store, const std::string& query, Option<std::string> 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<QueryResults>
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<std::string> 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<QueryResults>{
// 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<void>
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<std::string> 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();
}

View File

@ -26,6 +26,7 @@
#include <unordered_map> #include <unordered_map>
#include "utils/mu-utils.hh" #include "utils/mu-utils.hh"
#include "utils/mu-option.hh"
#include "message/mu-labels.hh" #include "message/mu-labels.hh"
namespace Mu { namespace Mu {
@ -48,17 +49,6 @@ public:
LabelsCache(const std::string serialized = {}): label_map_{deserialize(serialized)} { 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 * Add a label occurrence to the cache
* *
@ -152,5 +142,35 @@ private:
Map label_map_; 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<std::string> export_labels(const Store& store, const std::string& query="", Option<std::string> 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<void> import_labels(Store&, const std::string& path, bool dry_run, bool quiet, bool verbose);
} // namespace Mux } // namespace Mux
#endif /*MU_LABELS_CACHE_HH*/ #endif /*MU_LABELS_CACHE_HH*/

View File

@ -79,7 +79,7 @@ struct Store::Private {
labels_cache_{config_.get<Config::Id::Labels>()}, labels_cache_{config_.get<Config::Id::Labels>()},
root_maildir_{remove_slash(config_.get<Config::Id::RootMaildir>())}, root_maildir_{remove_slash(config_.get<Config::Id::RootMaildir>())},
message_opts_{make_message_options(config_)} { 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. // config. In practice: batch-size.
xapian_db_.reinit(); xapian_db_.reinit();
} }

View File

@ -35,7 +35,7 @@
#include <utils/mu-utils.hh> #include <utils/mu-utils.hh>
#include <utils/mu-utils.hh> #include <utils/mu-utils.hh>
#include <utils/mu-option.hh> #include <utils/mu-option.hh>
#include "mu-labels-cache.hh" #include "mu-store-labels.hh"
#include <message/mu-message.hh> #include <message/mu-message.hh>

View File

@ -125,128 +125,19 @@ label_list(const Mu::Store& store, const Options& opts)
return Ok(); 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<void> static Result<void>
label_export(const Mu::Store& store, const Options& opts) label_export(const Mu::Store& store, const Options& opts)
{ {
const auto now_t{::time({})}; const auto res = export_labels(store, "", opts.label.file);
const auto now_tm{::localtime(&now_t)}; if (!res)
return Err(res.error());
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,','));
}
}
}
if (!opts.quiet) if (!opts.quiet)
mu_println("written {}", fname); mu_println("written {}", *res);
return Ok(); 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<QueryResults>
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<std::string> 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<QueryResults>{
// 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<void> static Result<void>
label_import(Mu::Store& store, const Options& opts) 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, return Err(Error{Error::Code::InvalidArgument,
"missing input file"}); "missing input file"});
auto input{std::ifstream{*opts.label.file, std::ios::in}}; return Mu::import_labels(store, *opts.label.file,
if (!input.good()) opts.label.dry_run, opts.quiet, opts.verbose);
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<std::string> 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();
} }
Result<void> Result<void>