summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorDirk-Jan C. Binnema <djcb@djcbsoftware.nl>2025-08-16 11:40:15 +0300
committerDirk-Jan C. Binnema <djcb@djcbsoftware.nl>2025-08-16 12:18:21 +0300
commita6b1f47a30c7aea84697553da4848b626c9faaca (patch)
tree5d4ae6435a2382d0541102ff50cf52bb75a8f9ab /lib
parentf504289a021b0296701428467c469cbdaa351923 (diff)
labels: refactor import/export to mu-store-labels
Move the import/export code to 'lib'.
Diffstat (limited to 'lib')
-rw-r--r--lib/meson.build1
-rw-r--r--lib/mu-store-labels.cc198
-rw-r--r--lib/mu-store-labels.hh (renamed from lib/mu-labels-cache.hh)42
-rw-r--r--lib/mu-store.cc2
-rw-r--r--lib/mu-store.hh2
5 files changed, 232 insertions, 13 deletions
diff --git a/lib/meson.build b/lib/meson.build
index c3a798d..35b61be 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 0000000..5aa6c94
--- /dev/null
+++ b/lib/mu-store-labels.cc
@@ -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();
+}
diff --git a/lib/mu-labels-cache.hh b/lib/mu-store-labels.hh
index eb4cfe6..55b33f4 100644
--- a/lib/mu-labels-cache.hh
+++ b/lib/mu-store-labels.hh
@@ -26,6 +26,7 @@
#include <unordered_map>
#include "utils/mu-utils.hh"
+#include "utils/mu-option.hh"
#include "message/mu-labels.hh"
namespace Mu {
@@ -49,17 +50,6 @@ public:
}
/**
- * Construct a new ContactsCache object
- *
- * @param config db configuration database object
- */
- LabelsCache(Config& config) {
-
-
- }
-
-
- /**
* Add a label occurrence to the cache
*
* @param label
@@ -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<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
#endif /*MU_LABELS_CACHE_HH*/
diff --git a/lib/mu-store.cc b/lib/mu-store.cc
index f81d4ad..f0a36ed 100644
--- a/lib/mu-store.cc
+++ b/lib/mu-store.cc
@@ -79,7 +79,7 @@ struct Store::Private {
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
+ // 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 afb1d75..341da2a 100644
--- a/lib/mu-store.hh
+++ b/lib/mu-store.hh
@@ -35,7 +35,7 @@
#include <utils/mu-utils.hh>
#include <utils/mu-utils.hh>
#include <utils/mu-option.hh>
-#include "mu-labels-cache.hh"
+#include "mu-store-labels.hh"
#include <message/mu-message.hh>