diff --git a/lib/mu-store-labels.cc b/lib/mu-store-labels.cc index 5aa6c946..61666aa1 100644 --- a/lib/mu-store-labels.cc +++ b/lib/mu-store-labels.cc @@ -24,9 +24,9 @@ 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:"; +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; @@ -36,14 +36,26 @@ 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)); + + // 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}}; if (!output.good()) return Err(Error{Error::Code::File, - "failed pen '{}' for writing", fname}); + "failed to open '{}' for writing", fname}); mu_println(output, ";; version:0 @ {}\n", now); diff --git a/lib/mu-store-labels.hh b/lib/mu-store-labels.hh index 55b33f47..f9daae30 100644 --- a/lib/mu-store-labels.hh +++ b/lib/mu-store-labels.hh @@ -129,11 +129,13 @@ public: std::string 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); else map.emplace(std::move(parts[0]), - static_cast(g_ascii_strtoll(parts[1].c_str(),{}, 10))); + static_cast( + g_ascii_strtoll(parts[1].c_str(),{}, 10))); } return map; } @@ -148,14 +150,17 @@ class Store; * Export labels to a file * * 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 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 * * @return either the output filename or some error */ -Result export_labels(const Store& store, const std::string& query="", Option path); +Result export_labels(const Store& store, + const std::string& query="", + Option path={}); /** * Import labels from a file @@ -170,7 +175,8 @@ Result export_labels(const Store& store, const std::string& query=" * * @return Ok or some error */ -Result import_labels(Store&, const std::string& path, bool dry_run, bool quiet, bool verbose); +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 f0a36ed3..b8526e98 100644 --- a/lib/mu-store.cc +++ b/lib/mu-store.cc @@ -127,7 +127,6 @@ struct Store::Private { Result update_message_unlocked(Message& msg, Store::Id docid); Result update_message_unlocked(Message& msg, const std::string& old_path); - using PathMessage = std::pair; Result move_message_unlocked(Message&& msg, Option 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) - : priv_{std::make_unique(path, none_of(opts & Store::Options::Writable))} + : priv_{std::make_unique( + path, none_of(opts & Store::Options::Writable))} { if (none_of(opts & Store::Options::Writable) && 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()}; 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) - throw Mu::Error(Error::Code::CannotReinit, - "old schema ({}) is too old to re-initialize from", - s_version).add_hint("Invoke 'mu init' without '--reinit'; " - "see mu-init(1) for details"); + throw Mu::Error( + Error::Code::CannotReinit, + "old schema ({}) is too old to re-initialize from", + s_version).add_hint("Invoke 'mu init' without '--reinit'; " + "see mu-init(1) for details"); const auto old_root_maildir{root_maildir()}; MemDb mem_db; diff --git a/lib/utils/mu-utils-file.cc b/lib/utils/mu-utils-file.cc index bd581026..e9bccb03 100644 --- a/lib/utils/mu-utils-file.cc +++ b/lib/utils/mu-utils-file.cc @@ -131,9 +131,6 @@ Mu::remove_directory(const std::string& path) return Ok(); } - - - std::string Mu::runtime_path(Mu::RuntimePath path, const std::string& muhome) { diff --git a/man/mu-init.1.org b/man/mu-init.1.org index 09681df3..a451eeaa 100644 --- a/man/mu-init.1.org +++ b/man/mu-init.1.org @@ -13,26 +13,26 @@ mu-init - initialize the *mu* message database * DESCRIPTION *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 ** -m, --maildir _maildir_ Use _maildir_ as the root-maildir. -By default, *mu* uses the *MAILDIR* environment; if it is not set, it uses _~/Maildir_ -if it is an existing directory. If neither of those can be used, the *--maildir* -option is required; it must be an absolute path (but ~~/~ expansion is -performed). +By default, *mu* uses the *MAILDIR* environment to find the root-maildir. If it is +not set, it uses _~/Maildir_ if it is an existing directory. If neither of those +can be used, the *--maildir* option is required; it must be an absolute path (but +~~/~ expansion is performed). ** --personal-address _email-address-or-regex_ ** --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. -Any message in which at least one of the contact fields contains such an address -is considered a `personal' message; this can then be used for filtering in -{{{man-link(mu-find,1)}}}, {{{man-link(mu-cfind,1)}}} and *mu4e*, e.g. to +Any message in which at least one of the contact fields matches a personal +address, is considered a `personal' message; this can then be used for filtering +in {{{man-link(mu-find,1)}}}, {{{man-link(mu-cfind,1)}}} and *mu4e*, e.g. to filter-out mailing list messages. _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* 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 * NGRAM SUPPORT @@ -93,6 +95,22 @@ variables such as *XAPIAN_CJK_NGRAM* are ignored. #+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 #+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-find,1)}}}, +{{{man-link(mu-label,1)}}}, {{{man-link(mu-cfind,1)}}}, {{{man-link(pcre,3)}}} diff --git a/man/mu-label.1.org b/man/mu-label.1.org index aab82b94..1bdddf78 100644 --- a/man/mu-label.1.org +++ b/man/mu-label.1.org @@ -71,12 +71,17 @@ The *list* command lists all the labels that are currently in use in the store. * EXPORT OPTIONS 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. -If no file is specified, *mu* creates one for you, in the current directory. - * IMPORT OPTIONS The *import* command is for restoring the labels from a file created through