mu-init: automatic export labels with --reinit

When re-initializing the store, automatically write the labels to a file in mu's
cache, so user can later import them.
This commit is contained in:
Dirk-Jan C. Binnema
2025-08-16 15:48:08 +03:00
parent a6b1f47a30
commit 8c706a77db
6 changed files with 98 additions and 33 deletions

View File

@ -24,9 +24,9 @@
using namespace Mu; using namespace Mu;
namespace { namespace {
constexpr std::string_view path_key = "path:"; constexpr std::string_view path_key = "path:";
constexpr std::string_view message_id_key = "message-id:"; constexpr std::string_view message_id_key = "message-id:";
constexpr std::string_view labels_key = "labels:"; constexpr std::string_view labels_key = "labels:";
} }
using OutputPair = std::pair<std::ofstream, std::string>; using OutputPair = std::pair<std::ofstream, std::string>;
@ -36,14 +36,26 @@ export_output(Option<std::string> path)
{ {
const auto now_t{::time({})}; const auto now_t{::time({})};
const auto now_tm{::localtime(&now_t)}; const auto now_tm{::localtime(&now_t)};
const auto now{mu_format("{:%F-%T}", *now_tm)}; const auto now{mu_format("{:%F-%T}", *now_tm)};
auto fname = path.value_or(mu_format("mu-export-{}.txt", now));
// if path is not specified, use a generated file name (in pwd)
// if path is specified but ends in '/', use the generated file in that
// directory (must exist)
// otherwise, use the path.
auto fname = [&]() {
const auto default_fname{mu_format("mu-export-{}.txt", now)};
if (!path || path->empty())
return default_fname;
else if (path->at(path->length() - 1) == '/')
return *path + default_fname;
else
return *path;
}();
auto output{std::ofstream{fname, std::ios::out}}; auto output{std::ofstream{fname, std::ios::out}};
if (!output.good()) if (!output.good())
return Err(Error{Error::Code::File, return Err(Error{Error::Code::File,
"failed pen '{}' for writing", fname}); "failed to open '{}' for writing", fname});
mu_println(output, ";; version:0 @ {}\n", now); mu_println(output, ";; version:0 @ {}\n", now);

View File

@ -129,11 +129,13 @@ public:
std::string line; std::string line;
while (std::getline(ss, line)) { while (std::getline(ss, line)) {
if (const auto parts = Mu::split(line, SepaChar2); parts.size() != 2) if (const auto parts =
Mu::split(line, SepaChar2); parts.size() != 2)
mu_warning("error: '{}'", line); mu_warning("error: '{}'", line);
else else
map.emplace(std::move(parts[0]), map.emplace(std::move(parts[0]),
static_cast<std::size_t>(g_ascii_strtoll(parts[1].c_str(),{}, 10))); static_cast<std::size_t>(
g_ascii_strtoll(parts[1].c_str(),{}, 10)));
} }
return map; return map;
} }
@ -148,14 +150,17 @@ class Store;
* Export labels to a file * Export labels to a file
* *
* If path is not specified, use a file in the current directory * If path is not specified, use a file in the current directory
* If path ends in '/', write file in the path-directory
* *
* @param store a store object * @param store a store object
* @param query for the message whose labels to export * @param query for the message whose labels to export (empty for "all")
* @param path the path or nothing * @param path the path or nothing
* *
* @return either the output filename or some error * @return either the output filename or some error
*/ */
Result<std::string> export_labels(const Store& store, const std::string& query="", Option<std::string> path); Result<std::string> export_labels(const Store& store,
const std::string& query="",
Option<std::string> path={});
/** /**
* Import labels from a file * Import labels from a file
@ -170,7 +175,8 @@ Result<std::string> export_labels(const Store& store, const std::string& query="
* *
* @return Ok or some error * @return Ok or some error
*/ */
Result<void> import_labels(Store&, const std::string& path, bool dry_run, bool quiet, bool verbose); 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

@ -127,7 +127,6 @@ struct Store::Private {
Result<Store::Id> update_message_unlocked(Message& msg, Store::Id docid); Result<Store::Id> update_message_unlocked(Message& msg, Store::Id docid);
Result<Store::Id> update_message_unlocked(Message& msg, const std::string& old_path); Result<Store::Id> update_message_unlocked(Message& msg, const std::string& old_path);
using PathMessage = std::pair<std::string, Message>; using PathMessage = std::pair<std::string, Message>;
Result<PathMessage> move_message_unlocked(Message&& msg, Result<PathMessage> move_message_unlocked(Message&& msg,
Option<const std::string&> target_mdir, Option<const std::string&> target_mdir,
@ -211,9 +210,29 @@ Store::Private::find_duplicates_unlocked(const Store& store,
} }
} }
static void
reinit_export_labels(const Store& store)
{
// slightly hacky way to get the cache-path...
const auto cache_path{canonicalize_filename(
join_paths(store.path(), "..")) + "/"};
if (const auto res =
Mu::export_labels(store, "", cache_path); !res)
throw Mu::Error(Error::Code::CannotReinit,
"cannot re-init; "
"failed to export labels: {}",
res.error().what())
.add_hint("see mu-init(1) for details");
else {
mu_info("exported labels to: {}", *res);
mu_println("exported labels to: {}", *res);
}
}
Store::Store(const std::string& path, Store::Options opts) Store::Store(const std::string& path, Store::Options opts)
: priv_{std::make_unique<Private>(path, none_of(opts & Store::Options::Writable))} : priv_{std::make_unique<Private>(
path, none_of(opts & Store::Options::Writable))}
{ {
if (none_of(opts & Store::Options::Writable) && if (none_of(opts & Store::Options::Writable) &&
any_of(opts & Store::Options::ReInit)) any_of(opts & Store::Options::ReInit))
@ -222,12 +241,19 @@ Store::Store(const std::string& path, Store::Options opts)
const auto s_version{config().get<Config::Id::SchemaVersion>()}; const auto s_version{config().get<Config::Id::SchemaVersion>()};
if (any_of(opts & Store::Options::ReInit)) { if (any_of(opts & Store::Options::ReInit)) {
/* don't try to recover from version with an incompatible scheme */
/* export labels, if necessary (throws if there's an error) */
if (!label_map().empty())
reinit_export_labels(*this);
/* don't try to recover from version with an incompatible
* scheme */
if (s_version < 500) if (s_version < 500)
throw Mu::Error(Error::Code::CannotReinit, throw Mu::Error(
"old schema ({}) is too old to re-initialize from", Error::Code::CannotReinit,
s_version).add_hint("Invoke 'mu init' without '--reinit'; " "old schema ({}) is too old to re-initialize from",
"see mu-init(1) for details"); s_version).add_hint("Invoke 'mu init' without '--reinit'; "
"see mu-init(1) for details");
const auto old_root_maildir{root_maildir()}; const auto old_root_maildir{root_maildir()};
MemDb mem_db; MemDb mem_db;

View File

@ -131,9 +131,6 @@ Mu::remove_directory(const std::string& path)
return Ok(); return Ok();
} }
std::string std::string
Mu::runtime_path(Mu::RuntimePath path, const std::string& muhome) Mu::runtime_path(Mu::RuntimePath path, const std::string& muhome)
{ {

View File

@ -13,26 +13,26 @@ mu-init - initialize the *mu* message database
* DESCRIPTION * DESCRIPTION
*mu init* is the subcommand for setting up the *mu* message database. After *mu init* *mu init* is the subcommand for setting up the *mu* message database. After *mu init*
has completed, you can run *mu index* has completed, you can run *mu index*.
* INIT OPTIONS * INIT OPTIONS
** -m, --maildir _maildir_ ** -m, --maildir _maildir_
Use _maildir_ as the root-maildir. Use _maildir_ as the root-maildir.
By default, *mu* uses the *MAILDIR* environment; if it is not set, it uses _~/Maildir_ By default, *mu* uses the *MAILDIR* environment to find the root-maildir. If it is
if it is an existing directory. If neither of those can be used, the *--maildir* not set, it uses _~/Maildir_ if it is an existing directory. If neither of those
option is required; it must be an absolute path (but ~~/~ expansion is can be used, the *--maildir* option is required; it must be an absolute path (but
performed). ~~/~ expansion is performed).
** --personal-address _email-address-or-regex_ ** --personal-address _email-address-or-regex_
** --my-address _email-address-or-regex_ (alias) ** --my-address _email-address-or-regex_ (alias)
Specifies that some e-mail address is a personal address. The option can be used Specifies that some e-mail address is a /personal/ address. The option can be used
multiple times, to specify all your addresses. multiple times, to specify all your addresses.
Any message in which at least one of the contact fields contains such an address Any message in which at least one of the contact fields matches a personal
is considered a `personal' message; this can then be used for filtering in address, is considered a `personal' message; this can then be used for filtering
{{{man-link(mu-find,1)}}}, {{{man-link(mu-cfind,1)}}} and *mu4e*, e.g. to in {{{man-link(mu-find,1)}}}, {{{man-link(mu-cfind,1)}}} and *mu4e*, e.g. to
filter-out mailing list messages. filter-out mailing list messages.
_email-address-or-regex_ can be either a plain e-mail address (such as _email-address-or-regex_ can be either a plain e-mail address (such as
@ -78,6 +78,8 @@ Reinitialize the database from an earlier version; that is, create a new empty
database with the existing settings. This cannot be combined with the other *init* database with the existing settings. This cannot be combined with the other *init*
options. options.
When you have _labels_ defined for messages in your database, *mu* automatically exports those to file for importing later. See *RESTORING LABELS* below.
#+include: "muhome.inc" :minlevel 2 #+include: "muhome.inc" :minlevel 2
* NGRAM SUPPORT * NGRAM SUPPORT
@ -93,6 +95,22 @@ variables such as *XAPIAN_CJK_NGRAM* are ignored.
#+include: "exit-code.inc" :minlevel 1 #+include: "exit-code.inc" :minlevel 1
* RESTORING LABELS
When you have any _labels_ defined for your database, *mu* automatically exports
those to a file in the *mu* cache directory; you see this in the ~--init~ output.
#+begin_example
$ mu init --reinit
exported labels to: /home/user/.cache/mu/mu-export-2025-08-16-13:43:27.txt
#+end_example
You can restore those labels _after_ re-indexing, e.g.,
#+begin_example
$ mu label import /home/user/.cache/mu/mu-export-2025-08-16-13:43:27.txt
#+end_example
Please see {{{man-link(mu-label,1)}}} for further details.
* EXAMPLE * EXAMPLE
#+begin_example #+begin_example
@ -105,5 +123,6 @@ $ mu init --maildir=~/Maildir --my-address=alice@example.com --my-address=bob@ex
{{{man-link(mu-index,1)}}}, {{{man-link(mu-index,1)}}},
{{{man-link(mu-find,1)}}}, {{{man-link(mu-find,1)}}},
{{{man-link(mu-label,1)}}},
{{{man-link(mu-cfind,1)}}}, {{{man-link(mu-cfind,1)}}},
{{{man-link(pcre,3)}}} {{{man-link(pcre,3)}}}

View File

@ -71,12 +71,17 @@ The *list* command lists all the labels that are currently in use in the store.
* EXPORT OPTIONS * EXPORT OPTIONS
The *export* command outputs /all/ labels in the store to a file, so you can *import* The *export* command outputs /all/ labels in the store to a file, so you can *import*
it later. The command takes a path to a file as its argument. it later. The command takes a path to a file or a directory (ending in '/') as
its argument.
If a file is specified, *mu* writes the export to it.
If a directory is specified, *mu* writes to a file in that directory. The directory must already exist.
When neither is specified, *mu* writes to a file in the current directory.
See *EXPORT FORMAT* below for details about the format. See *EXPORT FORMAT* below for details about the format.
If no file is specified, *mu* creates one for you, in the current directory.
* IMPORT OPTIONS * IMPORT OPTIONS
The *import* command is for restoring the labels from a file created through The *import* command is for restoring the labels from a file created through