mu-move: add new move sub command
Add sub-command to move messages; add tests and docs. Fixes #157
This commit is contained in:
@ -329,8 +329,7 @@ flags_from_delta_expr(std::string_view expr, Flags flags,
|
||||
* @return either messages flags or Nothing in case of error.
|
||||
*/
|
||||
constexpr Option<Flags>
|
||||
flags_from_expr(std::string_view expr,
|
||||
Option<Flags> flags = Nothing)
|
||||
flags_from_expr(std::string_view expr, Option<Flags> flags = Nothing)
|
||||
{
|
||||
if (expr.empty())
|
||||
return Nothing;
|
||||
|
||||
@ -64,7 +64,6 @@ Result<void> maildir_mkdir(const std::string& path, mode_t mode=0700,
|
||||
*/
|
||||
Result<void> maildir_link(const std::string& src, const std::string& targetpath,
|
||||
bool unique_names=true);
|
||||
|
||||
/**
|
||||
* Recursively delete all the symbolic links in a directory tree
|
||||
*
|
||||
@ -111,10 +110,10 @@ Result<void> maildir_move_message(const std::string& oldpath,
|
||||
*/
|
||||
Result<std::string>
|
||||
maildir_determine_target(const std::string& old_path,
|
||||
const std::string& root_maildir_path,
|
||||
const std::string& target_maildir,
|
||||
Flags newflags,
|
||||
bool new_name);
|
||||
const std::string& root_maildir_path,
|
||||
const std::string& target_maildir,
|
||||
Flags newflags,
|
||||
bool new_name);
|
||||
|
||||
} // namespace Mu
|
||||
|
||||
|
||||
@ -925,9 +925,8 @@ Server::Private::perform_move(Store::Id docid,
|
||||
|
||||
/* note: we get back _all_ the messages that changed; the first is the
|
||||
* primary mover; the rest (if present) are any dups affected */
|
||||
const auto ids{unwrap(store().move_message(docid, maildir, flags, move_opts))};
|
||||
|
||||
for (auto&& id: ids) {
|
||||
const auto id_paths{unwrap(store().move_message(docid, maildir, flags, move_opts))};
|
||||
for (auto& [id,path]: id_paths) {
|
||||
auto idmsg{store().find_message(id)};
|
||||
if (!idmsg)
|
||||
mu_warning("failed to find message for id {}", id);
|
||||
@ -1113,15 +1112,14 @@ Server::Private::view_mark_as_read(Store::Id docid, Message&& msg, bool rename)
|
||||
}
|
||||
|
||||
// move message + dups, present results.
|
||||
|
||||
Store::MoveOptions move_opts{Store::MoveOptions::DupFlags};
|
||||
if (rename)
|
||||
move_opts |= Store::MoveOptions::ChangeName;
|
||||
auto&& ids = unwrap(store().move_message(docid, {}, nflags, move_opts));
|
||||
for (auto&& [id, moved_msg]: store().find_messages(ids)) {
|
||||
|
||||
const auto ids{Store::id_vec(unwrap(store().move_message(docid, {}, nflags, move_opts)))};
|
||||
for (auto&& [id, moved_msg]: store().find_messages(ids))
|
||||
output(mu_format("({} {})", id == docid ? ":view" : ":update",
|
||||
msg_sexp_str(moved_msg, id, {})));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@ -145,10 +145,12 @@ struct Store::Private {
|
||||
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<Message> move_message_unlocked(Message&& msg,
|
||||
Option<const std::string&> target_mdir,
|
||||
Option<Flags> new_flags,
|
||||
MoveOptions opts);
|
||||
|
||||
using PathMessage = std::pair<std::string, Message>;
|
||||
Result<PathMessage> move_message_unlocked(Message&& msg,
|
||||
Option<const std::string&> target_mdir,
|
||||
Option<Flags> new_flags,
|
||||
MoveOptions opts);
|
||||
XapianDb xapian_db_;
|
||||
Config config_;
|
||||
ContactsCache contacts_cache_;
|
||||
@ -341,8 +343,7 @@ Store::indexer()
|
||||
Result<Store::Id>
|
||||
Store::add_message(Message& msg, bool use_transaction, bool is_new)
|
||||
{
|
||||
const auto mdir{maildir_from_path(msg.path(),
|
||||
root_maildir())};
|
||||
const auto mdir{maildir_from_path(msg.path(), root_maildir())};
|
||||
if (!mdir)
|
||||
return Err(mdir.error());
|
||||
if (auto&& res = msg.set_maildir(mdir.value()); !res)
|
||||
@ -428,6 +429,23 @@ Store::find_message(Store::Id docid) const
|
||||
return priv_->find_message_unlocked(docid);
|
||||
}
|
||||
|
||||
Option<Store::Id>
|
||||
Store::find_message_id(const std::string& path) const
|
||||
{
|
||||
constexpr auto path_field{field_from_id(Field::Id::Path)};
|
||||
|
||||
std::lock_guard guard{priv_->lock_};
|
||||
|
||||
auto enq{xapian_db().enquire()};
|
||||
enq.set_query(Xapian::Query{path_field.xapian_term(path)});
|
||||
|
||||
if (auto mset{enq.get_mset(0, 1)}; mset.empty())
|
||||
return Nothing; // message not found
|
||||
else
|
||||
return Some(*mset.begin());
|
||||
}
|
||||
|
||||
|
||||
Store::IdMessageVec
|
||||
Store::find_messages(IdVec ids) const
|
||||
{
|
||||
@ -443,7 +461,7 @@ Store::find_messages(IdVec ids) const
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a message in store and filesystem.
|
||||
* Move a message in store and filesystem; with DryRun, only calculate the target name.
|
||||
*
|
||||
* Lock is assumed taken already
|
||||
*
|
||||
@ -454,7 +472,7 @@ Store::find_messages(IdVec ids) const
|
||||
*
|
||||
* @return the Message after the moving, or an Error
|
||||
*/
|
||||
Result<Message>
|
||||
Result<Store::Private::PathMessage>
|
||||
Store::Private::move_message_unlocked(Message&& msg,
|
||||
Option<const std::string&> target_mdir,
|
||||
Option<Flags> new_flags,
|
||||
@ -472,21 +490,25 @@ Store::Private::move_message_unlocked(Message&& msg,
|
||||
if (!target_path)
|
||||
return Err(target_path.error());
|
||||
|
||||
/* 2. let's move it */
|
||||
if (const auto res = maildir_move_message(msg.path(), target_path.value()); !res)
|
||||
return Err(res.error());
|
||||
// in dry-run mode, we only determine the target-path
|
||||
if (none_of(opts & MoveOptions::DryRun)) {
|
||||
|
||||
/* 3. file move worked, now update the message with the new info.*/
|
||||
if (auto&& res = msg.update_after_move(
|
||||
target_path.value(), target_maildir, target_flags); !res)
|
||||
return Err(res.error());
|
||||
/* 2. let's move it */
|
||||
if (const auto res = maildir_move_message(msg.path(), target_path.value()); !res)
|
||||
return Err(res.error());
|
||||
|
||||
/* 4. update message worked; re-store it */
|
||||
if (auto&& res = update_message_unlocked(msg, old_path); !res)
|
||||
return Err(res.error());
|
||||
/* 3. file move worked, now update the message with the new info.*/
|
||||
if (auto&& res = msg.update_after_move(
|
||||
target_path.value(), target_maildir, target_flags); !res)
|
||||
return Err(res.error());
|
||||
|
||||
/* 4. update message worked; re-store it */
|
||||
if (auto&& res = update_message_unlocked(msg, old_path); !res)
|
||||
return Err(res.error());
|
||||
}
|
||||
|
||||
/* 6. Profit! */
|
||||
return Ok(std::move(msg));
|
||||
return Ok(PathMessage{std::move(*target_path), std::move(msg)});
|
||||
}
|
||||
|
||||
Store::IdVec
|
||||
@ -498,8 +520,7 @@ Store::find_duplicates(const std::string& message_id) const
|
||||
}
|
||||
|
||||
|
||||
|
||||
Result<Store::IdVec>
|
||||
Result<Store::IdPathVec>
|
||||
Store::move_message(Store::Id id,
|
||||
Option<const std::string&> target_mdir,
|
||||
Option<Flags> new_flags,
|
||||
@ -519,14 +540,13 @@ Store::move_message(Store::Id id,
|
||||
return Err(Error::Code::Store, "cannot find message <{}>", id);
|
||||
|
||||
const auto message_id{msg->message_id()};
|
||||
auto res{priv_->move_message_unlocked(std::move(*msg),
|
||||
target_mdir, new_flags, opts)};
|
||||
auto res{priv_->move_message_unlocked(std::move(*msg), target_mdir, new_flags, opts)};
|
||||
if (!res)
|
||||
return Err(res.error());
|
||||
|
||||
IdVec ids{id};
|
||||
IdPathVec id_paths{{id, res->first}};
|
||||
if (none_of(opts & Store::MoveOptions::DupFlags) || message_id.empty() || !new_flags)
|
||||
return Ok(std::move(ids));
|
||||
return Ok(std::move(id_paths));
|
||||
|
||||
/* handle the dupflags case; i.e. apply (a subset of) the flags to
|
||||
* all messages with the same message-id as well */
|
||||
@ -550,12 +570,23 @@ Store::move_message(Store::Id id,
|
||||
Store::MoveOptions::None); !dup_res)
|
||||
mu_warning("failed to move dup: {}", dup_res.error().what());
|
||||
else
|
||||
ids.emplace_back(dupid);
|
||||
id_paths.emplace_back(dupid, dup_res->first);
|
||||
}
|
||||
|
||||
return Ok(std::move(ids));
|
||||
return Ok(std::move(id_paths));
|
||||
}
|
||||
|
||||
Store::IdVec
|
||||
Store::id_vec(const IdPathVec& ips)
|
||||
{
|
||||
IdVec idv;
|
||||
for (auto&& ip: ips)
|
||||
idv.emplace_back(ip.first);
|
||||
|
||||
return idv;
|
||||
}
|
||||
|
||||
|
||||
time_t
|
||||
Store::dirstamp(const std::string& path) const
|
||||
{
|
||||
@ -660,9 +691,11 @@ std::vector<std::string>
|
||||
Store::maildirs() const
|
||||
{
|
||||
std::vector<std::string> mdirs;
|
||||
const auto prefix_size = root_maildir().size();
|
||||
const auto prefix_size{root_maildir().size()};
|
||||
|
||||
Scanner::Handler handler = [&](const std::string& path, auto&& _1, auto&& _2) {
|
||||
mdirs.emplace_back(path.substr(prefix_size));
|
||||
auto md{path.substr(prefix_size)};
|
||||
mdirs.emplace_back(std::move(md.empty() ? "/" : md));
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@ -47,6 +47,8 @@ public:
|
||||
using Id = Xapian::docid; /**< Id for a message in the store */
|
||||
static constexpr Id InvalidId = 0; /**< Invalid store id */
|
||||
using IdVec = std::vector<Id>; /**< Vector of document ids */
|
||||
using IdPathVec = std::vector<std::pair<Id, std::string>>;
|
||||
/**< vector of id, path pairs */
|
||||
|
||||
/**
|
||||
* Configuration options.
|
||||
@ -246,6 +248,15 @@ public:
|
||||
*/
|
||||
Option<Message> find_message(Id id) const;
|
||||
|
||||
/**
|
||||
* Find a message's docid based on its path
|
||||
*
|
||||
* @param path path to the message
|
||||
*
|
||||
* @return the docid or Nothing if not found
|
||||
*/
|
||||
Option<Id> find_message_id(const std::string& path) const;
|
||||
|
||||
/**
|
||||
* Find the messages for the given ids
|
||||
*
|
||||
@ -282,27 +293,35 @@ public:
|
||||
enum struct MoveOptions {
|
||||
None = 0, /**< Defaults */
|
||||
ChangeName = 1 << 0, /**< Change the name when moving */
|
||||
DupFlags = 1 << 1, /**< Update flags for duplicate messages too*/
|
||||
DupFlags = 1 << 1, /**< Update flags for duplicate messages too */
|
||||
DryRun = 1 << 2, /**< Don't really move, just determine target paths */
|
||||
};
|
||||
|
||||
/**
|
||||
* Move a message both in the filesystem and in the store. After a
|
||||
* successful move, the message is updated.
|
||||
* Move a message both in the filesystem and in the store. After a successful move, the
|
||||
* message is updated.
|
||||
*
|
||||
* @param id the id for some message
|
||||
* @param target_mdir the target maildir (if any)
|
||||
* @param new_flags new flags (if any)
|
||||
* @param change_name whether to change the name
|
||||
* @param opts move options
|
||||
*
|
||||
* @return Result, either an IdVec with ids for the moved
|
||||
* message(s) or some error. Note that in case of success at least one
|
||||
* message is returned, and only with MoveOptions::DupFlags can it be
|
||||
* more than one.
|
||||
* @return Result, either an IdPathVec with ids and paths for the moved message(s) or some
|
||||
* error. Note that in case of success at least one message is returned, and only with
|
||||
* MoveOptions::DupFlags can it be more than one.
|
||||
*/
|
||||
Result<IdVec> move_message(Store::Id id,
|
||||
Option<const std::string&> target_mdir = Nothing,
|
||||
Option<Flags> new_flags = Nothing,
|
||||
MoveOptions opts = MoveOptions::None);
|
||||
Result<IdPathVec> move_message(Store::Id id,
|
||||
Option<const std::string&> target_mdir = Nothing,
|
||||
Option<Flags> new_flags = Nothing,
|
||||
MoveOptions opts = MoveOptions::None);
|
||||
/**
|
||||
* Convert IdPathVec -> IdVec
|
||||
*
|
||||
* @param ips idpath vector
|
||||
*
|
||||
* @return vector of ids
|
||||
*/
|
||||
static IdVec id_vec(const IdPathVec& ips);
|
||||
|
||||
/**
|
||||
* Prototype for the ForEachMessageFunc
|
||||
|
||||
@ -71,13 +71,7 @@ make_test_store(const std::string& test_path, const TestMap& test_map,
|
||||
assert_valid_result(store);
|
||||
|
||||
/* index the messages */
|
||||
auto res = store->indexer().start({});
|
||||
g_assert_true(res);
|
||||
while(store->indexer().is_running()) {
|
||||
using namespace std::chrono_literals;
|
||||
std::this_thread::sleep_for(100ms);
|
||||
}
|
||||
|
||||
g_assert_true(store->indexer().start({},true/*block*/));
|
||||
if (test_map.size() > 0)
|
||||
g_assert_false(store->empty());
|
||||
|
||||
@ -575,7 +569,7 @@ Boo!
|
||||
assert_valid_result(moved_msgs);
|
||||
|
||||
g_assert_true(moved_msgs->size() == 1);
|
||||
auto&& moved_msg_opt = store.find_message(moved_msgs->at(0));
|
||||
auto&& moved_msg_opt = store.find_message(moved_msgs->at(0).first);
|
||||
g_assert_true(!!moved_msg_opt);
|
||||
const auto&moved_msg = std::move(*moved_msg_opt);
|
||||
const auto new_path = moved_msg.path();
|
||||
|
||||
@ -381,7 +381,7 @@ Yes, that would be excellent.
|
||||
const auto msgs3 = store->move_message(msg->docid(), {}, Flags::Seen);
|
||||
assert_valid_result(msgs3);
|
||||
g_assert_true(msgs3->size() == 1);
|
||||
auto&& msg3_opt{store->find_message(msgs3->at(0))};
|
||||
auto&& msg3_opt{store->find_message(msgs3->at(0).first/*id*/)};
|
||||
g_assert_true(!!msg3_opt);
|
||||
auto&& msg3{std::move(*msg3_opt)};
|
||||
|
||||
@ -442,11 +442,11 @@ Yes, that would be excellent.
|
||||
assert_valid_result(mres);
|
||||
mu_info("found {} matches", mres->size());
|
||||
for (auto&& m: *mres)
|
||||
mu_info("id: {}", m);
|
||||
mu_info("id: {}: {}", m.first, m.second);
|
||||
|
||||
// al three dups should have been updated
|
||||
g_assert_cmpuint(mres->size(), ==, 3);
|
||||
auto&& id_msgs{store->find_messages(*mres)};
|
||||
auto&& id_msgs{store->find_messages(Store::id_vec(*mres))};
|
||||
|
||||
// first should be the original
|
||||
g_assert_cmpuint(id_msgs.at(0).first, ==, ids.at(0));
|
||||
|
||||
Reference in New Issue
Block a user