mu-move: add new move sub command

Add sub-command to move messages; add tests and docs.

Fixes #157
This commit is contained in:
Dirk-Jan C. Binnema 2023-09-13 23:54:45 +03:00
parent 1a3dc46866
commit 2d20074b99
14 changed files with 597 additions and 90 deletions

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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;
};

View File

@ -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

View File

@ -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();

View File

@ -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));

115
man/mu-move.1.org Normal file
View File

@ -0,0 +1,115 @@
#+TITLE: MU MOVE
#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
* NAME
*mu move* - move a message file or change its flags
* SYNOPSIS
*mu [common-options] move [options] <src> [--flags=<flags>] [<target>]*
* DESCRIPTION
*mu move* is the command for moving messages in a Maildir or changing their flags.
For any change, both the message file in the file system as well as its
representation in the database are updated accordingly.
The source message file and target-maildir must reside under the root-maildir
for mu's database (see *mu info store*).
* MOVE OPTIONS
** --flags=<flags>
specify the new message flags. See *FLAGS* for details.
** --change-name
change the basename of the message file when moving; this can be useful when
using some external tools such as *mbsync(1)* which otherwise get confused
** --update-dups
update the flags of duplicate messages too, where "duplicate messages" are
defined as all message that share the same message-id. Note that the
Draft/Flagged/Trashed flags are deliberately _not_ changed if you change those on
the source message.
** --dry-run,-n
print the target filename(s), but don't change anything.
Note that with the ~--change-name~, the target name is not constant, so you cannot
use a dry-run to predict the exact name when doing a 'real' run.
#+include: "common-options.inc" :minlevel 1
* FLAGS
(Note: if you are not familiar with Maildirs, please refer to the *maildir(5)*
man-page, or see http://cr.yp.to/proto/maildir.html)
The message flags specify the Maildir-metadata for a message and are represented
by uppercase letters at the end of the message file name for all 'non-new'
messages, i.e. messages that live in the ~cur/~ sub-directory of a Maildir.
| Flag | Meaning |
|------+------------------------------------|
| D | Draft message |
| F | Flagged message |
| P | Passed message (i.e., 'forwarded') |
| R | Replied message |
| S | Seen message |
| T | Trashed; to be deleted later |
New messages (in the ~new/~ sub-directory) do not have flags encoded in their
file-name; but we *mu* uses 'N' in the ~--flags~ to represent that:
| Flag | Meaning |
|------+---------|
| N | New |
Thus, changing flags means changing the letters at the end of the message
file-name, except when setting or removing the 'N' (new) flag. Setting or
un-setting the New flag causes the message is to be moved from ~cur/~ to ~new/~ or
vice-versa, respectively. When marking a message as New, it looses the other
flags.
* ABSOLUTE AND RELATIVE FLAGS
You can specify the flags with the ~--flags~ parameter, and do either with either
*absolute* or *relative* flags.
Absolute flags just specify the new flags by their letters; e.g. to specify a
/Trashed/, /Seen/, /Replied/ message, you'd use ~--flags STR~.
#+end_example
Relative flags are relative to the current flags for some message, and each of
the flags is prefixed with either ~+~ ("add this flag") or ~-~ ("remove this flag").
So to add the /Seen/ flag and remove the /Draft/ flag from whatever the message
already has, ~--flags +S-D~.
You cannot combine relative and relative flags.
* EXAMPLES
** change some flags
#+begin_example
$ mu move /home/user/Maildir/inbox/cur/1695559560.a73985881f4611ac2.hostname!2,S --flags +F
/home/user/Maildir/inbox/cur/1695559560.a73985881f4611ac2.hostname!2,FS
#+end_example
** move to a different maildir
#+begin_example
$ mu move /home/user/Maildir/project1/cur/1695559560.a73985881f4611ac2.hostname!2,S /project2
/home/user/Maildir/project2/cur/1695559560.a73985881f4611ac2.hostname!2,S
#+end_example
#+include: "prefooter.inc" :minlevel 1
* SEE ALSO
*maildir(5)*

View File

@ -26,6 +26,7 @@ mu = executable(
'mu-cmd-init.cc',
'mu-cmd-index.cc',
'mu-cmd-mkdir.cc',
'mu-cmd-move.cc',
'mu-cmd-remove.cc',
'mu-cmd-script.cc',
'mu-cmd-server.cc',
@ -76,6 +77,13 @@ test('test-cmd-mkdir',
cpp_args: ['-DBUILD_TESTS'],
dependencies: [glib_dep, lib_mu_dep]))
test('test-cmd-move',
executable('test-cmd-move',
'mu-cmd-move.cc',
install: false,
cpp_args: ['-DBUILD_TESTS'],
dependencies: [glib_dep, lib_mu_dep]))
test('test-cmd-remove',
executable('test-cmd-remove',
'mu-cmd-remove.cc',

276
mu/mu-cmd-move.cc Normal file
View File

@ -0,0 +1,276 @@
/*
** Copyright (C) 2023 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 "config.h"
#include "mu-cmd.hh"
#include "mu-store.hh"
#include "mu-maildir.hh"
#include "message/mu-message-file.hh"
#include <unistd.h>
using namespace Mu;
Result<void>
Mu::mu_cmd_move(Mu::Store& store, const Options& opts)
{
const auto& src{opts.move.src};
if (::access(src.c_str(), R_OK) != 0 || determine_dtype(src) != DT_REG)
return Err(Error::Code::InvalidArgument,
"Source is not a readable file");
auto id{store.find_message_id(src)};
if (!id)
return Err(Error{Error::Code::InvalidArgument,
"Source file is not present in database"}
.add_hint("Perhaps run mu index?"));
std::string dest{opts.move.dest};
Option<const std::string&> dest_path;
if (dest.empty() && opts.move.flags.empty())
return Err(Error::Code::InvalidArgument,
"Must have at least one of destination and flags");
else if (!dest.empty()) {
const auto mdirs{store.maildirs()};
mu_printerrln("XXXX");
for (auto&& m:mdirs)
mu_printerrln("m:'{}'", m);
if (!seq_some(mdirs, [&](auto &&d){ return d == dest;}))
return Err(Error{Error::Code::InvalidArgument,
"No maildir '{}' in store", dest}
.add_hint("Try 'mu mkdir'"));
else
dest_path = dest;
}
auto old_flags{flags_from_path(src)};
if (!old_flags)
return Err(Error::Code::InvalidArgument, "failed to determine old flags");
Flags new_flags;
if (!opts.move.flags.empty()) {
if (auto&& nflags{flags_from_expr(to_string_view(opts.move.flags),
*old_flags)}; !nflags)
return Err(Error::Code::InvalidArgument, "Invalid flags");
else
new_flags = flags_maildir_file(*nflags);
if (any_of(new_flags & Flags::New) && new_flags != Flags::New)
return Err(Error{Error::Code::File,
"the New flag cannot be combined with others"}
.add_hint("See the mu-move manpage"));
}
Store::MoveOptions move_opts{};
if (opts.move.change_name)
move_opts |= Store::MoveOptions::ChangeName;
if (opts.move.update_dups)
move_opts |= Store::MoveOptions::DupFlags;
if (opts.move.dry_run)
move_opts |= Store::MoveOptions::DryRun;
auto id_paths = store.move_message(*id, dest_path, new_flags, move_opts);
if (!id_paths)
return Err(std::move(id_paths.error()));
for (const auto&[_id, path]: *id_paths)
mu_println("{}", path);
return Ok();
}
#ifdef BUILD_TESTS
/*
* Tests.
*
*/
#include "utils/mu-test-utils.hh"
static void
test_move_dry_run()
{
allow_warnings();
TempDir tdir;
const auto dbpath{runtime_path(RuntimePath::XapianDb, tdir.path())};
auto res = run_command0({CP_PROGRAM, "-r", MU_TESTMAILDIR, tdir.path()});
assert_valid_command(res);
const auto testpath{join_paths(tdir.path(), "testdir")};
const auto src{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,S")};
{
auto store = Store::make_new(dbpath, testpath, {});
assert_valid_result(store);
g_assert_true(store->indexer().start({}, true/*block*/));
}
// make a message 'New'
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "N", "--dry-run"});
assert_valid_command(res);
auto dst{join_paths(testpath, "new", "1220863042.12663_1.mindcrime")};
assert_equal(res->standard_out, dst + '\n');
g_assert_true(::access(dst.c_str(), F_OK) != 0);
g_assert_true(::access(src.c_str(), F_OK) == 0);
}
// change some flags
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "FP", "--dry-run"});
assert_valid_command(res);
auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,FP")};
assert_equal(res->standard_out, dst + '\n');
}
// change some relative flag
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "+F", "--dry-run"});
assert_valid_command(res);
auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,FS")};
assert_equal(res->standard_out, dst + '\n');
}
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "-S+P+T", "--dry-run"});
assert_valid_command(res);
auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,PT")};
assert_equal(res->standard_out, dst + '\n');
}
// change maildir
for (auto& o : {"o1", "o2"})
assert_valid_result(maildir_mkdir(join_paths(tdir.path(), "testdir", o)));
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"/o1", "--flags", "-S+F", "--dry-run"});
assert_valid_command(res);
assert_equal(res->standard_out,
join_paths(testpath,
"o1/cur", "1220863042.12663_1.mindcrime!2,F") + "\n");
}
// change-dups; first create some dups and index them.
assert_valid_result(run_command0({CP_PROGRAM, src, join_paths(testpath, "o1/cur")}));
assert_valid_result(run_command0({CP_PROGRAM, src, join_paths(testpath, "o2/cur")}));
{
auto store = Store::make(dbpath, Store::Options::Writable);
assert_valid_result(store);
g_assert_true(store->indexer().start({}, true/*block*/));
}
// change some flags + update dups
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "-S+S+T+R", "--update-dups", "--dry-run"});
assert_valid_command(res);
auto p{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,RST")};
auto p1{join_paths(testpath, "o1", "cur", "1220863042.12663_1.mindcrime!2,RS")};
auto p2{join_paths(testpath, "o2", "cur", "1220863042.12663_1.mindcrime!2,RS")};
assert_equal(res->standard_out, mu_format("{}\n{}\n{}\n", p, p1, p2));
}
}
static void
test_move_real()
{
allow_warnings();
TempDir tdir;
const auto dbpath{runtime_path(RuntimePath::XapianDb, tdir.path())};
auto res = run_command0({CP_PROGRAM, "-r", MU_TESTMAILDIR, tdir.path()});
assert_valid_command(res);
const auto testpath{join_paths(tdir.path(), "testdir")};
const auto src{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,S")};
{
auto store = Store::make_new(dbpath, testpath, {});
assert_valid_result(res);
g_assert_true(store->indexer().start({}, true/*block*/));
}
{
auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
"--flags", "N"});
assert_valid_command(res);
auto dst{join_paths(testpath, "new", "1220863042.12663_1.mindcrime")};
g_assert_true(::access(dst.c_str(), F_OK) == 0);
g_assert_true(::access(src.c_str(), F_OK) != 0);
}
// change flags, maildir, update-dups
// change-dups; first create some dups and index them.
const auto src2{join_paths(testpath, "cur", "1305664394.2171_402.cthulhu!2,")};
for (auto& o : {"o1", "o2", "o3"})
assert_valid_result(maildir_mkdir(join_paths(tdir.path(), "testdir", o)));
assert_valid_result(run_command0({CP_PROGRAM, src2, join_paths(testpath, "o1/cur")}));
assert_valid_result(run_command0({CP_PROGRAM, src2, join_paths(testpath, "o2/new")}));
{
auto store = Store::make(dbpath, Store::Options::Writable);
assert_valid_result(store);
g_assert_true(store->indexer().start({}, true/*block*/));
}
auto res2 = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src2, "/o3",
"--flags", "-S+S+T+R", "--update-dups", "--change-name"});
assert_valid_command(res2);
auto store = Store::make(dbpath, Store::Options::Writable);
assert_valid_result(store);
g_assert_true(store->indexer().start({}, true/*block*/));
for (auto&& f: split(res2->standard_out, "\n")) {
//mu_println(">> {}", f);
if (f.length() > 2)
g_assert_true(::access(f.c_str(), F_OK) == 0);
}
}
int
main(int argc, char* argv[])
{
mu_test_init(&argc, &argv);
g_test_add_func("/cmd/move/dry-run", test_move_dry_run);
g_test_add_func("/cmd/move/real", test_move_real);
return g_test_run();
}
#endif /*BUILD_TESTS*/

View File

@ -140,6 +140,8 @@ Mu::mu_cmd_execute(const Options& opts) try {
return with_writable_store(mu_cmd_add, opts);
case Options::SubCommand::Remove:
return with_writable_store(mu_cmd_remove, opts);
case Options::SubCommand::Move:
return with_writable_store(mu_cmd_move, opts);
case Options::SubCommand::Index:
return with_writable_store(mu_cmd_index, opts);

View File

@ -127,6 +127,15 @@ Result<void> mu_cmd_init(const Options& opts);
*/
Result<void> mu_cmd_mkdir(const Options& opts);
/**
* execute the 'move' command
*
* @param opts configuration options
*
* @return Ok() or some error
*/
Result<void> mu_cmd_move(Store& store, const Options& opts);
/**
* execute the 'remove' command
*

View File

@ -197,6 +197,12 @@ static const std::function ExpandPath = [](std::string filepath)->std::string {
return filepath = std::move(res.value());
};
// Canonicalize path
static const std::function CanonicalizePath = [](std::string filepath)->std::string {
return filepath = canonicalize_filename(filepath);
};
/*
* common
*/
@ -481,6 +487,31 @@ sub_mkdir(CLI::App& sub, Options& opts)
->required();
}
static void
sub_move(CLI::App& sub, Options& opts)
{
sub.add_flag("--change-name", opts.move.change_name,
"Change name of target file");
sub.add_flag("--update-dups", opts.move.update_dups,
"Update duplicate messages too");
sub.add_flag("--dry-run,-n", opts.move.dry_run,
"Print target name, but do not change anything");
sub.add_option("--flags", opts.move.flags, "Target flags")
->type_name("<flags>");
sub.add_option("source", opts.move.src, "Message file to move")
->type_name("<message-path>")
->transform(ExpandPath, "expand path")
->transform(CanonicalizePath, "canonicalize path")
->required();
sub.add_option("destination", opts.move.dest,
"Destination maildir")
->type_name("<maildir>");
}
static void
sub_remove(CLI::App& sub, Options& opts)
{
@ -602,7 +633,7 @@ AssocPairs<SubCommand, CommandInfo, Options::SubCommandNum> SubCommandInfos= {{
},
{ SubCommand::Info,
{Category::NeedsReadOnlyStore,
"info", "Show information about the message store database", sub_info }
"info", "Show information", sub_info }
},
{ SubCommand::Init,
{Category::NeedsWritableStore,
@ -612,6 +643,10 @@ AssocPairs<SubCommand, CommandInfo, Options::SubCommandNum> SubCommandInfos= {{
{Category::None,
"mkdir", "Create a new Maildir", sub_mkdir }
},
{ SubCommand::Move,
{Category::NeedsWritableStore,
"move", "Move a message or change flags", sub_move }
},
{ SubCommand::Remove,
{Category::NeedsWritableStore,
"remove", "Remove message from file-system and database", sub_remove }
@ -718,7 +753,8 @@ add_global_options(CLI::App& cli, Options& opts)
cli.add_flag("-q,--quiet", opts.quiet, "Hide non-essential output");
cli.add_flag("-v,--verbose", opts.verbose, "Show verbose output");
cli.add_flag("--log-stderr", opts.log_stderr, "Log to stderr");
cli.add_flag("--log-stderr", opts.log_stderr, "Log to stderr")
->group(""/*always hide*/);
cli.add_flag("--nocolor", opts.nocolor, "Don't show ANSI colors")
->default_val(Options::default_no_color())
->default_str(Options::default_no_color() ? "<true>" : "<false>");
@ -780,7 +816,7 @@ There is NO WARRANTY, to the extent permitted by law.
->transform(ExpandPath, "expand path");
}
/* add scripts (if supported) as semi-subscommands as well */
/* add scripts (if supported) as semi-subcommands as well */
const auto scripts = add_scripts(app, opts);
try {
@ -842,6 +878,11 @@ Options::category(Options::SubCommand sub)
static constexpr bool
validate_subcommand_ids()
{
size_t val{};
for (auto& cmd: Options::SubCommands)
if (static_cast<size_t>(cmd) != val++)
return false;
for (auto u = 0U; u != SubCommandInfos.size(); ++u)
if (static_cast<size_t>(SubCommandInfos.at(u).first) != u)
return false;

View File

@ -8,7 +8,7 @@
**
** 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
** 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
@ -36,10 +36,10 @@
/* command-line options for Mu */
namespace Mu {
struct Options {
using OptSize = Option<std::size_t>;
using SizeVec = std::vector<std::size_t>;
using OptTStamp = Option<std::time_t>;
using OptFieldId = Option<Field::Id>;
using OptSize = Option<std::size_t>;
using SizeVec = std::vector<std::size_t>;
using OptTStamp = Option<std::time_t>;
using OptFieldId = Option<Field::Id>;
using StringVec = std::vector<std::string>;
/*
@ -62,10 +62,11 @@ struct Options {
enum struct SubCommand {
Add, Cfind, Extract, Fields, Find, Help, Index,Info, Init, Mkdir,
Remove, Script, Server, Verify, View/*must be last*/
Move, Remove, Script, Server, Verify, View,
// <private>
__count__
};
static constexpr std::size_t SubCommandNum =
1 + static_cast<std::size_t>(SubCommand::View);
static constexpr auto SubCommandNum = static_cast<size_t>(SubCommand::__count__);
static constexpr std::array<SubCommand, SubCommandNum> SubCommands = {{
SubCommand::Add,
SubCommand::Cfind,
@ -77,6 +78,7 @@ struct Options {
SubCommand::Info,
SubCommand::Init,
SubCommand::Mkdir,
SubCommand::Move,
SubCommand::Remove,
SubCommand::Script,
SubCommand::Server,
@ -84,7 +86,6 @@ struct Options {
SubCommand::View
}};
Option<SubCommand> sub_command; /**< The chosen sub-command, if any. */
/*
@ -117,16 +118,16 @@ struct Options {
* Extract
*/
struct Extract: public Crypto {
std::string message; /**< path to message file */
std::string message; /**< path to message file */
bool save_all; /**< extract all parts */
bool save_attachments; /**< extract all attachment parts */
SizeVec parts; /**< parts to save / open */
SizeVec parts; /**< parts to save / open */
std::string targetdir{}; /**< where to save attachments */
bool overwrite; /**< overwrite same-named files */
bool play; /**< try to 'play' attachment */
std::string filename_rx; /**< Filename rx to save */
bool uncooked{}; /**< Whether to avoid massaging
* output filename */
std::string filename_rx; /**< Filename rx to save */
bool uncooked{}; /**< Whether to avoid massaging
* the output filename */
} extract;
/*
@ -138,7 +139,7 @@ struct Options {
*/
struct Find {
std::string fields; /**< fields to show in output */
Field::Id sortfield; /**< field to sort by */
Field::Id sortfield; /**< field to sort by */
OptSize maxnum; /**< max # of entries to print */
bool reverse; /**< sort in revers order (z->a) */
bool threads; /**< show message threads */
@ -146,7 +147,7 @@ struct Options {
std::string linksdir; /**< directory for links */
OptSize summary_len; /**< max # of lines for summary */
std::string bookmark; /**< use bookmark */
bool analyze; /**< analyze query */
bool analyze; /**< analyze query */
enum struct Format { Plain, Links, Xml, Json, Sexp, Exec };
Format format; /**< Output format */
@ -158,7 +159,7 @@ struct Options {
bool auto_retrieve; /**< assume we're online */
bool decrypt; /**< try to decrypt the body */
StringVec query; /**< search query */
StringVec query; /**< search query */
} find;
struct Help {
@ -189,10 +190,10 @@ struct Options {
StringVec my_addresses; /**< personal e-mail addresses */
StringVec ignored_addresses; /**< addresses to be ignored for
* the contacts-cache */
OptSize max_msg_size; /**< max size for message files */
OptSize max_msg_size; /**< max size for message files */
OptSize batch_size; /**< db transaction batch size */
bool reinit; /**< re-initialize */
bool support_ngrams; /**< support CJK etc. ngrams */
bool support_ngrams; /**< support CJK etc. ngrams */
} init;
@ -204,6 +205,19 @@ struct Options {
mode_t mode; /**< Mode for the maildir */
} mkdir;
/*
* Move
*/
struct Move {
std::string src; /**< Source file */
std::string dest; /**< Destination dir */
std::string flags; /**< Flags for destination */
bool change_name; /**< Change basename for destination */
bool update_dups; /**< Update duplicate messages too */
bool dry_run; /**< Just print the result path,
but do not change anything */
} move;
/*
* Remove
*/
@ -215,7 +229,7 @@ struct Options {
* Scripts (i.e., finding scriot)
*/
struct Script {
std::string name; /**< name of script */
std::string name; /**< name of script */
StringVec params; /**< script params */
} script;
@ -225,7 +239,7 @@ struct Options {
struct Server {
bool commands; /**< dump docs for commands */
std::string eval; /**< command to evaluate */
bool allow_temp_file; /**< temp-file optimization allowed? */
bool allow_temp_file; /**< temp-file optimization allowed? */
} server;
/*