summaryrefslogtreecommitdiffstats
path: root/src/doveadm/doveadm-fs.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/doveadm/doveadm-fs.c')
-rw-r--r--src/doveadm/doveadm-fs.c608
1 files changed, 608 insertions, 0 deletions
diff --git a/src/doveadm/doveadm-fs.c b/src/doveadm/doveadm-fs.c
new file mode 100644
index 0000000..17f6129
--- /dev/null
+++ b/src/doveadm/doveadm-fs.c
@@ -0,0 +1,608 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "md5.h"
+#include "sha2.h"
+#include "hash-method.h"
+#include "hex-binary.h"
+#include "istream.h"
+#include "ostream.h"
+#include "iostream-ssl.h"
+#include "fs-api.h"
+#include "doveadm.h"
+#include "doveadm-print.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+static void fs_cmd_help(struct doveadm_cmd_context *cctx) ATTR_NORETURN;
+static void cmd_fs_delete(struct doveadm_cmd_context *cctx);
+
+static struct fs *
+cmd_fs_init(struct doveadm_cmd_context *cctx)
+{
+ struct ssl_iostream_settings ssl_set;
+ struct fs_settings fs_set;
+ struct fs *fs;
+ const char *fs_driver, *fs_args, *error;
+
+ if (!doveadm_cmd_param_str(cctx, "fs-driver", &fs_driver) ||
+ !doveadm_cmd_param_str(cctx, "fs-args", &fs_args))
+ fs_cmd_help(cctx);
+
+ doveadm_get_ssl_settings(&ssl_set, pool_datastack_create());
+ ssl_set.verbose = doveadm_debug;
+ i_zero(&fs_set);
+ fs_set.ssl_client_set = &ssl_set;
+ fs_set.temp_dir = doveadm_settings->mail_temp_dir;
+ fs_set.base_dir = doveadm_settings->base_dir;
+ fs_set.debug = doveadm_debug;
+
+ if (fs_init(fs_driver, fs_args, &fs_set, &fs, &error) < 0)
+ i_fatal("fs_init() failed: %s", error);
+ return fs;
+}
+
+static void cmd_fs_get(struct doveadm_cmd_context *cctx)
+{
+ struct fs *fs;
+ struct fs_file *file;
+ struct istream *input;
+ const char *path;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_PAGER);
+ doveadm_print_header("content", "content", DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "path", &path))
+ fs_cmd_help(cctx);
+
+ file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+ input = fs_read_stream(file, IO_BLOCK_SIZE);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ doveadm_print_stream(data, size);
+ i_stream_skip(input, size);
+ }
+ doveadm_print_stream("", 0);
+ i_assert(ret == -1);
+ if (input->stream_errno == ENOENT) {
+ i_error("%s doesn't exist: %s", fs_file_path(file),
+ i_stream_get_error(input));
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else if (input->stream_errno != 0) {
+ i_error("read(%s) failed: %s", fs_file_path(file),
+ i_stream_get_error(input));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ i_stream_unref(&input);
+ fs_file_deinit(&file);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_put(struct doveadm_cmd_context *cctx)
+{
+ struct fs *fs;
+ enum fs_properties props;
+ const char *hash_str, *src_path, *dest_path;
+ struct fs_file *file;
+ struct istream *input;
+ struct ostream *output;
+ buffer_t *hash = NULL;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "input-path", &src_path) ||
+ !doveadm_cmd_param_str(cctx, "path", &dest_path))
+ fs_cmd_help(cctx);
+ if (doveadm_cmd_param_str(cctx, "hash", &hash_str)) {
+ hash = t_buffer_create(32);
+ if (hex_to_binary(optarg, hash) < 0)
+ i_fatal("Invalid -h parameter: Hash not in hex");
+ }
+
+ file = fs_file_init(fs, dest_path, FS_OPEN_MODE_REPLACE);
+ props = fs_get_properties(fs);
+ if (hash == NULL)
+ ;
+ else if (hash->used == hash_method_md5.digest_size) {
+ if ((props & FS_PROPERTY_WRITE_HASH_MD5) == 0)
+ i_fatal("fs backend doesn't support MD5 hashes");
+ fs_write_set_hash(file,
+ hash_method_lookup(hash_method_md5.name), hash->data);
+ } else if (hash->used == hash_method_sha256.digest_size) {
+ if ((props & FS_PROPERTY_WRITE_HASH_SHA256) == 0)
+ i_fatal("fs backend doesn't support SHA256 hashes");
+ fs_write_set_hash(file,
+ hash_method_lookup(hash_method_sha256.name), hash->data);
+ }
+
+ output = fs_write_stream(file);
+ input = i_stream_create_file(src_path, IO_BLOCK_SIZE);
+ o_stream_nsend_istream(output, input);
+ i_stream_destroy(&input);
+ if (fs_write_stream_finish(file, &output) < 0) {
+ i_error("fs_write_stream_finish() failed: %s",
+ fs_file_last_error(file));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_file_deinit(&file);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_copy(struct doveadm_cmd_context *cctx)
+{
+ struct fs *fs;
+ struct fs_file *src_file, *dest_file;
+ const char *src_path, *dest_path;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "source-path", &src_path) ||
+ !doveadm_cmd_param_str(cctx, "destination-path", &dest_path))
+ fs_cmd_help(cctx);
+
+ src_file = fs_file_init(fs, src_path, FS_OPEN_MODE_READONLY);
+ dest_file = fs_file_init(fs, dest_path, FS_OPEN_MODE_REPLACE);
+ if (fs_copy(src_file, dest_file) == 0) ;
+ else if (errno == ENOENT) {
+ i_error("%s doesn't exist: %s", src_path,
+ fs_file_last_error(dest_file));
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else {
+ i_error("fs_copy(%s, %s) failed: %s",
+ src_path, dest_path, fs_file_last_error(dest_file));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_file_deinit(&src_file);
+ fs_file_deinit(&dest_file);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_stat(struct doveadm_cmd_context *cctx)
+{
+ struct fs *fs;
+ struct fs_file *file;
+ struct stat st;
+ const char *path;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "path", &path))
+ fs_cmd_help(cctx);
+
+ file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+ doveadm_print_formatted_set_format("%{path} size=%{size}");
+ doveadm_print_header_simple("path");
+ doveadm_print_header("size", "size", DOVEADM_PRINT_HEADER_FLAG_NUMBER);
+
+ if (fs_stat(file, &st) == 0) {
+ doveadm_print(fs_file_path(file));
+ doveadm_print(dec2str(st.st_size));
+ } else if (errno == ENOENT) {
+ i_error("%s doesn't exist: %s", fs_file_path(file),
+ fs_file_last_error(file));
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else {
+ i_error("fs_stat(%s) failed: %s",
+ fs_file_path(file), fs_file_last_error(file));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_file_deinit(&file);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_metadata(struct doveadm_cmd_context *cctx)
+{
+ struct fs *fs;
+ struct fs_file *file;
+ const struct fs_metadata *m;
+ const ARRAY_TYPE(fs_metadata) *metadata;
+ const char *path;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "path", &path))
+ fs_cmd_help(cctx);
+
+ file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+ doveadm_print_formatted_set_format("%{key}=%{value}\n");
+ doveadm_print_header_simple("key");
+ doveadm_print_header_simple("value");
+
+ if (fs_get_metadata(file, &metadata) == 0) {
+ array_foreach(metadata, m) {
+ doveadm_print(m->key);
+ doveadm_print(m->value);
+ }
+ } else if (errno == ENOENT) {
+ i_error("%s doesn't exist: %s", fs_file_path(file),
+ fs_file_last_error(file));
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ } else {
+ i_error("fs_stat(%s) failed: %s",
+ fs_file_path(file), fs_file_last_error(file));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_file_deinit(&file);
+ fs_deinit(&fs);
+}
+
+struct fs_delete_ctx {
+ struct fs *fs;
+ const char *path_prefix;
+
+ unsigned int files_count;
+ struct fs_file **files;
+};
+
+static int cmd_fs_delete_ctx_run(struct fs_delete_ctx *ctx)
+{
+ unsigned int i;
+ int ret = 0;
+
+ for (i = 0; i < ctx->files_count; i++) {
+ if (ctx->files[i] == NULL)
+ ;
+ else if (fs_delete(ctx->files[i]) == 0)
+ fs_file_deinit(&ctx->files[i]);
+ else if (errno == EAGAIN) {
+ if (ret == 0)
+ ret = 1;
+ } else if (errno == ENOENT) {
+ i_error("%s doesn't exist: %s", fs_file_path(ctx->files[i]),
+ fs_file_last_error(ctx->files[i]));
+ doveadm_exit_code = DOVEADM_EX_NOTFOUND;
+ ret = -1;
+ } else {
+ i_error("fs_delete(%s) failed: %s",
+ fs_file_path(ctx->files[i]),
+ fs_file_last_error(ctx->files[i]));
+ doveadm_exit_code = EX_TEMPFAIL;
+ ret = -1;
+ }
+ }
+ return ret;
+}
+
+static int doveadm_fs_delete_async_fname(struct fs_delete_ctx *ctx,
+ const char *fname)
+{
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < ctx->files_count; i++) {
+ if (ctx->files[i] != NULL)
+ continue;
+
+ ctx->files[i] = fs_file_init(ctx->fs,
+ t_strdup_printf("%s%s", ctx->path_prefix, fname),
+ FS_OPEN_MODE_READONLY | FS_OPEN_FLAG_ASYNC |
+ FS_OPEN_FLAG_ASYNC_NOQUEUE);
+ fname = NULL;
+ break;
+ }
+ if ((ret = cmd_fs_delete_ctx_run(ctx)) < 0)
+ return -1;
+ if (fname != NULL) {
+ if (ret > 0)
+ fs_wait_async(ctx->fs);
+ return doveadm_fs_delete_async_fname(ctx, fname);
+ }
+ return 0;
+}
+
+static void doveadm_fs_delete_async_finish(struct fs_delete_ctx *ctx)
+{
+ unsigned int i;
+
+ while (doveadm_exit_code == 0 && cmd_fs_delete_ctx_run(ctx) > 0) {
+ fs_wait_async(ctx->fs);
+ }
+ for (i = 0; i < ctx->files_count; i++) {
+ fs_file_deinit(&ctx->files[i]);
+ }
+}
+
+static void
+cmd_fs_delete_dir_recursive(struct fs *fs, unsigned int async_count,
+ const char *path_prefix)
+{
+ struct fs_iter *iter;
+ ARRAY_TYPE(const_string) fnames;
+ struct fs_delete_ctx ctx;
+ const char *fname, *error;
+ int ret;
+
+ i_zero(&ctx);
+ ctx.fs = fs;
+ ctx.path_prefix = path_prefix;
+ ctx.files_count = I_MAX(async_count, 1);
+ ctx.files = t_new(struct fs_file *, ctx.files_count);
+
+ /* delete subdirs first. all fs backends can't handle recursive
+ lookups, so save the list first. */
+ t_array_init(&fnames, 8);
+ iter = fs_iter_init(fs, path_prefix, FS_ITER_FLAG_DIRS);
+ while ((fname = fs_iter_next(iter)) != NULL) {
+ /* append "/" so that if FS_PROPERTY_DIRECTORIES is set,
+ we'll include the "/" suffix in the filename when deleting
+ it. */
+ fname = t_strconcat(fname, "/", NULL);
+ array_push_back(&fnames, &fname);
+ }
+ if (fs_iter_deinit(&iter, &error) < 0) {
+ i_error("fs_iter_deinit(%s) failed: %s",
+ path_prefix, error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ array_foreach_elem(&fnames, fname) T_BEGIN {
+ cmd_fs_delete_dir_recursive(fs, async_count,
+ t_strdup_printf("%s%s", path_prefix, fname));
+ } T_END;
+
+ /* delete files. again because we're doing this asynchronously finish
+ the iteration first. */
+ if ((fs_get_properties(fs) & FS_PROPERTY_DIRECTORIES) != 0) {
+ /* we need to explicitly delete also the directories */
+ } else {
+ array_clear(&fnames);
+ }
+ iter = fs_iter_init(fs, path_prefix, 0);
+ while ((fname = fs_iter_next(iter)) != NULL) {
+ fname = t_strdup(fname);
+ array_push_back(&fnames, &fname);
+ }
+ if (fs_iter_deinit(&iter, &error) < 0) {
+ i_error("fs_iter_deinit(%s) failed: %s",
+ path_prefix, error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+
+ array_foreach_elem(&fnames, fname) {
+ T_BEGIN {
+ ret = doveadm_fs_delete_async_fname(&ctx, fname);
+ } T_END;
+ if (ret < 0)
+ break;
+ }
+ doveadm_fs_delete_async_finish(&ctx);
+}
+
+static void cmd_fs_delete_recursive_path(struct fs *fs, const char *path,
+ unsigned int async_count)
+{
+ struct fs_file *file;
+ size_t path_len;
+
+ path_len = strlen(path);
+ if (path_len > 0 && path[path_len-1] != '/')
+ path = t_strconcat(path, "/", NULL);
+
+ cmd_fs_delete_dir_recursive(fs, async_count, path);
+ if ((fs_get_properties(fs) & FS_PROPERTY_DIRECTORIES) != 0) {
+ /* delete the root itself */
+ file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
+ if (fs_delete(file) < 0) {
+ i_error("fs_delete(%s) failed: %s",
+ fs_file_path(file), fs_file_last_error(file));
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_file_deinit(&file);
+ }
+}
+
+static void
+cmd_fs_delete_recursive(struct doveadm_cmd_context *cctx,
+ unsigned int async_count)
+{
+ struct fs *fs;
+ const char *const *paths;
+ unsigned int i;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_array(cctx, "path", &paths))
+ fs_cmd_help(cctx);
+
+ for (i = 0; paths[i] != NULL; i++)
+ cmd_fs_delete_recursive_path(fs, paths[i], async_count);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_delete_paths(struct doveadm_cmd_context *cctx,
+ unsigned int async_count)
+{
+ struct fs *fs;
+ struct fs_delete_ctx ctx;
+ const char *const *paths;
+ unsigned int i;
+ int ret;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_array(cctx, "path", &paths))
+ fs_cmd_help(cctx);
+
+ i_zero(&ctx);
+ ctx.fs = fs;
+ ctx.path_prefix = "";
+ ctx.files_count = I_MAX(async_count, 1);
+ ctx.files = t_new(struct fs_file *, ctx.files_count);
+
+ for (i = 0; paths[i] != NULL; i++) {
+ T_BEGIN {
+ ret = doveadm_fs_delete_async_fname(&ctx, paths[i]);
+ } T_END;
+ if (ret < 0)
+ break;
+ }
+ doveadm_fs_delete_async_finish(&ctx);
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_delete(struct doveadm_cmd_context *cctx)
+{
+ bool recursive = FALSE;
+ int64_t async_count = 0;
+
+ (void)doveadm_cmd_param_bool(cctx, "recursive", &recursive);
+ (void)doveadm_cmd_param_int64(cctx, "max-parallel", &async_count);
+
+ if (recursive)
+ cmd_fs_delete_recursive(cctx, async_count);
+ else
+ cmd_fs_delete_paths(cctx, async_count);
+}
+
+static void cmd_fs_iter_full(struct doveadm_cmd_context *cctx,
+ enum fs_iter_flags flags)
+{
+ struct fs *fs;
+ struct fs_iter *iter;
+ const char *path, *fname, *error;
+ bool b;
+
+ if (doveadm_cmd_param_bool(cctx, "no-cache", &b) && b)
+ flags |= FS_ITER_FLAG_NOCACHE;
+ if (doveadm_cmd_param_bool(cctx, "object-ids", &b) && b)
+ flags |= FS_ITER_FLAG_OBJECTIDS;
+
+ fs = cmd_fs_init(cctx);
+ if (!doveadm_cmd_param_str(cctx, "path", &path))
+ fs_cmd_help(cctx);
+
+ doveadm_print_init(DOVEADM_PRINT_TYPE_FORMATTED);
+ doveadm_print_formatted_set_format("%{path}\n");
+ doveadm_print_header_simple("path");
+
+ iter = fs_iter_init(fs, path, flags);
+ while ((fname = fs_iter_next(iter)) != NULL) {
+ doveadm_print(fname);
+ }
+ if (fs_iter_deinit(&iter, &error) < 0) {
+ i_error("fs_iter_deinit(%s) failed: %s", path, error);
+ doveadm_exit_code = EX_TEMPFAIL;
+ }
+ fs_deinit(&fs);
+}
+
+static void cmd_fs_iter(struct doveadm_cmd_context *cctx)
+{
+ cmd_fs_iter_full(cctx, 0);
+}
+
+static void cmd_fs_iter_dirs(struct doveadm_cmd_context *cctx)
+{
+ cmd_fs_iter_full(cctx, FS_ITER_FLAG_DIRS);
+}
+
+struct doveadm_cmd_ver2 doveadm_cmd_fs[] = {
+{
+ .name = "fs get",
+ .cmd = cmd_fs_get,
+ .usage = "<fs-driver> <fs-args> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs put",
+ .cmd = cmd_fs_put,
+ .usage = "[-h <hash>] <fs-driver> <fs-args> <input path> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('h', "hash", CMD_PARAM_STR, 0)
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "input-path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs copy",
+ .cmd = cmd_fs_copy,
+ .usage = "<fs-driver> <fs-args> <source path> <dest path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "source-path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "destination-path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs stat",
+ .cmd = cmd_fs_stat,
+ .usage = "<fs-driver> <fs-args> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs metadata",
+ .cmd = cmd_fs_metadata,
+ .usage = "<fs-driver> <fs-args> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs delete",
+ .cmd = cmd_fs_delete,
+ .usage = "[-R] [-n <count>] <fs-driver> <fs-args> <path> [<path> ...]",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('R', "recursive", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('n', "max-parallel", CMD_PARAM_INT64, 0)
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_ARRAY, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs iter",
+ .cmd = cmd_fs_iter,
+ .usage = "[--no-cache] [--object-ids] <fs-driver> <fs-args> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('C', "no-cache", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('O', "object-ids", CMD_PARAM_BOOL, 0)
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+},
+{
+ .name = "fs iter-dirs",
+ .cmd = cmd_fs_iter_dirs,
+ .usage = "<fs-driver> <fs-args> <path>",
+DOVEADM_CMD_PARAMS_START
+DOVEADM_CMD_PARAM('\0', "fs-driver", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "fs-args", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAM('\0', "path", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
+DOVEADM_CMD_PARAMS_END
+}
+};
+
+static void fs_cmd_help(struct doveadm_cmd_context *cctx)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_fs); i++) {
+ if (doveadm_cmd_fs[i].cmd == cctx->cmd->cmd)
+ help_ver2(&doveadm_cmd_fs[i]);
+ }
+ i_unreached();
+}
+
+void doveadm_register_fs_commands(void)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(doveadm_cmd_fs); i++)
+ doveadm_cmd_register_ver2(&doveadm_cmd_fs[i]);
+}