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

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