From 047b0681c1fbf025b0de1c47e0959273a8ac8fc7 Mon Sep 17 00:00:00 2001 From: Simon Halvorsen Date: Wed, 18 Feb 2026 15:17:11 +0100 Subject: [PATCH] Added getdir command to cf-net, recursively copies directory Ticket: CFE-2986 Changelog: Title Signed-off-by: Simon Halvorsen --- cf-net/cf-net.c | 285 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) diff --git a/cf-net/cf-net.c b/cf-net/cf-net.c index 1573847e15..e87d199e3f 100644 --- a/cf-net/cf-net.c +++ b/cf-net/cf-net.c @@ -44,6 +44,7 @@ #include #include #include +#include #define ARG_UNUSED __attribute__((unused)) @@ -100,6 +101,8 @@ static const Description COMMANDS[] = "\t\t\t(%d can be used in both the remote and output file paths when '-j' is used)"}, {"opendir", "List files and folders in a directory", "cf-net opendir masterfiles"}, + {"getdir", "Recursively downloads files and folders in a directory", + "cf-net getdir masterfiles/ -o /tmp/ [-lRECURSIONLIMIT]"}, {NULL, NULL, NULL} }; @@ -144,6 +147,7 @@ static const char *const HINTS[] = generator_macro(STAT) \ generator_macro(GET) \ generator_macro(OPENDIR) \ + generator_macro(GETDIR) \ generator_macro(MULTI) \ generator_macro(MULTITLS) \ generator_macro(HELP) \ @@ -197,6 +201,7 @@ static int CFNetGet(CFNetOptions *opts, const char *hostname, char **args); static int CFNetOpenDir(CFNetOptions *opts, const char *hostname, char **args); static int CFNetMulti(const char *server); static int CFNetMultiTLS(const char *server, const char *use_protocol_version); +static int CFNetGetDir(CFNetOptions *opts, const char *hostname, char **args); //******************************************************************* @@ -411,6 +416,8 @@ static int CFNetCommandSwitch(CFNetOptions *opts, const char *hostname, return CFNetGet(opts, hostname, args); case CFNET_CMD_OPENDIR: return CFNetOpenDir(opts, hostname, args); + case CFNET_CMD_GETDIR: + return CFNetGetDir(opts, hostname, args); case CFNET_CMD_MULTI: return CFNetMulti(hostname); case CFNET_CMD_MULTITLS: @@ -591,6 +598,16 @@ static int CFNetHelpTopic(const char *topic) "\nbasename in current working directory (cwd). Override this" "\nusing the -o filename option (-o - for stdout).\n"); } + else if (strcmp("getdir", topic) == 0) + { + printf("\ncf-net getdir recursively downloads a directory from a remote host." + "\nIt uses OPENDIR to list contents, STAT to check file types, and GET" + "\nto download files. By default the directory is saved with its basename" + "\nin the current working directory (cwd). Override the destination using" + "\nthe -o path option. Use --limit N to control recursion depth (default: 10)." + "\n\nUsage: cf-net getdir [-o output_path] [--limit N] " + "\n\nExample: cf-net getdir -o /tmp/backup --limit 5 masterfiles/\n"); + } else { if (found == false) @@ -976,6 +993,274 @@ static int CFNetOpenDir(ARG_UNUSED CFNetOptions *opts, const char *hostname, cha return 0; } +// Helper: Get a single file with permissions +static bool CFNetGetWithPerms(AgentConnection *conn, const char *remote_path, + const char *local_path, bool print_stats) +{ + assert(conn != NULL && remote_path != NULL && local_path != NULL); + + struct stat perms; + if (!ProtocolStat(conn, remote_path, &perms)) + { + Log(LOG_LEVEL_ERR, "Failed to stat remote file: %s:%s", + conn->this_server, remote_path); + return false; + } + + if (!ProtocolGet(conn, remote_path, local_path, perms.st_size, perms.st_mode, print_stats)) + { + Log(LOG_LEVEL_ERR, "Failed to get remote file: %s:%s", + conn->this_server, remote_path); + return false; + } + + return true; +} + +// Helper: Create local directory path +static bool create_local_dir(const char *local_base, const char *subdir, + bool has_output_path, mode_t perms) +{ + char path[PATH_MAX]; + int written; + + if (has_output_path) + { + written = snprintf(path, sizeof(path), "%s/%s/", local_base, subdir); + } + else + { + char cwd[PATH_MAX]; + if (!getcwd(cwd, sizeof(cwd))) + { + Log(LOG_LEVEL_ERR, "Failed to get current working directory"); + return false; + } + written = snprintf(path, sizeof(path), "%s/%s/%s/", cwd, local_base, subdir); + } + + if (written < 0 || written >= sizeof(path)) + { + Log(LOG_LEVEL_ERR, "Path too long for new directory: %s", subdir); + return false; + } + + bool* created = NULL; + bool force = false; + MakeParentDirectoryPerms(path, force, created, perms); + return true; +} + +// Helper: Recursively process directory entries +static int process_dir_recursive(AgentConnection *conn, + const char *remote_path, + const char *local_path, + bool has_output_path, + bool print_stats, + int limit) +{ + if (0 > limit - 1) + { + Log(LOG_LEVEL_ERR, "Recursion limit reached"); + return -2; + } + + int written; + Seq *items = ProtocolOpenDir(conn, remote_path); + if (!items) + { + return -1; + } + + for (size_t i = 0; i < SeqLength(items); i++) + { + char *item = SeqAt(items, i); + + if (strcmp(".", item) == 0 || strcmp("..", item) == 0) + { + continue; + } + + char remote_full[PATH_MAX]; + written = snprintf(remote_full, sizeof(remote_full), "%s/%s", remote_path, item); + if (written < 0 || written >= sizeof(remote_full)) + { + Log(LOG_LEVEL_ERR, + "Path too long for building full remote path: %s and %s", + remote_path, item); + continue; + } + + char local_full[PATH_MAX]; + written = snprintf(local_full, sizeof(local_full), "%s/%s", local_path, item); + if (written < 0 || written >= sizeof(local_full)) + { + Log(LOG_LEVEL_ERR, + "Path too long for building full local path: %s and %s", + local_path, item); + return -1; + } + + struct stat sb; + if (!ProtocolStat(conn, remote_full, &sb)) + { + Log(LOG_LEVEL_ERR, "Could not stat: %s", remote_full); + return -1; + } + + if (S_ISDIR(sb.st_mode)) // Is directory + { + if (!create_local_dir(local_path, item, has_output_path, sb.st_mode)) + { + // Error already logged + return -1; + } + process_dir_recursive(conn, remote_full, local_full, has_output_path, print_stats, (limit - 1)); + } + else + { + CFNetGetWithPerms(conn, remote_full, local_full, print_stats); + } + } + + SeqDestroy(items); + return 0; +} + +static int CFNetGetDir(CFNetOptions *opts, const char *hostname, char **args) +{ + assert(opts != NULL); + assert(hostname != NULL); + assert(args != NULL); + char *local_dir = NULL; + int limit = 10; + + int argc = 0; + while (args[argc] != NULL) + { + ++argc; + } + + static struct option longopts[] = { + { "output", required_argument, NULL, 'o' }, + { "limit", required_argument, NULL, 'l' }, + { NULL, 0, NULL, 0 } + }; + if (argc <= 1) + { + return invalid_command("getdir"); + } + extern int optind; + optind = 0; + extern char *optarg; + int c = 0; + const char *optstr = "o:l:"; + bool specified_path = false; + while ((c = getopt_long(argc, args, optstr, longopts, NULL)) + != -1) + { + switch (c) + { + case 'o': + { + if (local_dir != NULL) + { + Log(LOG_LEVEL_INFO, + "Warning: multiple occurrences of -o in command, "\ + "only last one will be used."); + free(local_dir); + } + local_dir = xstrdup(optarg); + specified_path = true; + break; + } + case 'l': + { + char *lim; + long val = strtol(optarg, &lim, 10); + if (*lim != '\0' || val < 0) + { + Log(LOG_LEVEL_ERR, "Invalid limit value: %s", optarg); + return invalid_command("getdir"); + } + limit = (int)val; + break; + } + case ':': + case '?': + { + return invalid_command("getdir"); + } + default: + { + printf("Default optarg = '%s', c = '%c' = %i\n", + optarg, c, (int)c); + break; + } + } + } + args = &(args[optind]); + argc -= optind; + char *remote_dir = args[0]; + char *base = basename(remote_dir); + if (specified_path) + { + char temp[PATH_MAX]; + + int written = snprintf(temp, sizeof(temp), "%s/%s", local_dir, base); + if (written < 0 || written >= sizeof(temp)) + { + Log(LOG_LEVEL_ERR, "Path too long for local path: %s/%s", local_dir, base); + free(local_dir); + return -1; + } + free(local_dir); + local_dir = xstrdup(temp); + } + + if (local_dir == NULL) + { + local_dir = xstrdup(base); + } + + AgentConnection *conn = CFNetOpenConnection(hostname, opts->use_protocol_version); + if (conn == NULL) + { + free(local_dir); + return -1; + } + struct stat sb; + int ret = (int) ProtocolStat(conn, remote_dir, &sb); + if (ret != 1) + { + printf("Could not stat: '%s'\n", remote_dir); + free(local_dir); + CFNetDisconnect(conn); + return -1; + } + if (!S_ISDIR(sb.st_mode)) + { + printf("'%s' is not a directory, use 'get' for single file download\n", remote_dir); + free(local_dir); + CFNetDisconnect(conn); + return -1; + } + + ret = process_dir_recursive(conn, remote_dir, local_dir, specified_path, opts->print_stats, limit); + if (ret == -2) + { + Log(LOG_LEVEL_INFO, "Recursion limit(%d) reached", limit); + } + else if (ret == -1) + { + Log(LOG_LEVEL_INFO, "Failed to copy all contents of %s", remote_dir); + } + + free(local_dir); + CFNetDisconnect(conn); + return (ret == 0) ? 0 : -1; +} + static int CFNetMulti(const char *server) { time_t start;