|
44 | 44 | #include <cleanup.h> |
45 | 45 | #include <protocol.h> |
46 | 46 | #include <sequence.h> |
| 47 | +#include <files_lib.h> |
47 | 48 |
|
48 | 49 | #define ARG_UNUSED __attribute__((unused)) |
49 | 50 |
|
@@ -100,6 +101,8 @@ static const Description COMMANDS[] = |
100 | 101 | "\t\t\t(%d can be used in both the remote and output file paths when '-j' is used)"}, |
101 | 102 | {"opendir", "List files and folders in a directory", |
102 | 103 | "cf-net opendir masterfiles"}, |
| 104 | + {"getdir", "Recursively downloads files and folders in a directory", |
| 105 | + "cf-net getdir masterfiles/ -o /tmp/ [-lRECURSIONLIMIT]"}, |
103 | 106 | {NULL, NULL, NULL} |
104 | 107 | }; |
105 | 108 |
|
@@ -144,6 +147,7 @@ static const char *const HINTS[] = |
144 | 147 | generator_macro(STAT) \ |
145 | 148 | generator_macro(GET) \ |
146 | 149 | generator_macro(OPENDIR) \ |
| 150 | + generator_macro(GETDIR) \ |
147 | 151 | generator_macro(MULTI) \ |
148 | 152 | generator_macro(MULTITLS) \ |
149 | 153 | generator_macro(HELP) \ |
@@ -197,6 +201,7 @@ static int CFNetGet(CFNetOptions *opts, const char *hostname, char **args); |
197 | 201 | static int CFNetOpenDir(CFNetOptions *opts, const char *hostname, char **args); |
198 | 202 | static int CFNetMulti(const char *server); |
199 | 203 | static int CFNetMultiTLS(const char *server, const char *use_protocol_version); |
| 204 | +static int CFNetGetDir(CFNetOptions *opts, const char *hostname, char **args); |
200 | 205 |
|
201 | 206 |
|
202 | 207 | //******************************************************************* |
@@ -411,6 +416,8 @@ static int CFNetCommandSwitch(CFNetOptions *opts, const char *hostname, |
411 | 416 | return CFNetGet(opts, hostname, args); |
412 | 417 | case CFNET_CMD_OPENDIR: |
413 | 418 | return CFNetOpenDir(opts, hostname, args); |
| 419 | + case CFNET_CMD_GETDIR: |
| 420 | + return CFNetGetDir(opts, hostname, args); |
414 | 421 | case CFNET_CMD_MULTI: |
415 | 422 | return CFNetMulti(hostname); |
416 | 423 | case CFNET_CMD_MULTITLS: |
@@ -591,6 +598,16 @@ static int CFNetHelpTopic(const char *topic) |
591 | 598 | "\nbasename in current working directory (cwd). Override this" |
592 | 599 | "\nusing the -o filename option (-o - for stdout).\n"); |
593 | 600 | } |
| 601 | + else if (strcmp("getdir", topic) == 0) |
| 602 | + { |
| 603 | + printf("\ncf-net getdir recursively downloads a directory from a remote host." |
| 604 | + "\nIt uses OPENDIR to list contents, STAT to check file types, and GET" |
| 605 | + "\nto download files. By default the directory is saved with its basename" |
| 606 | + "\nin the current working directory (cwd). Override the destination using" |
| 607 | + "\nthe -o path option. Use --limit N to control recursion depth (default: 10)." |
| 608 | + "\n\nUsage: cf-net getdir [-o output_path] [--limit N] <remote_directory>" |
| 609 | + "\n\nExample: cf-net getdir -o /tmp/backup --limit 5 masterfiles/\n"); |
| 610 | + } |
594 | 611 | else |
595 | 612 | { |
596 | 613 | if (found == false) |
@@ -976,6 +993,274 @@ static int CFNetOpenDir(ARG_UNUSED CFNetOptions *opts, const char *hostname, cha |
976 | 993 | return 0; |
977 | 994 | } |
978 | 995 |
|
| 996 | +// Helper: Get a single file with permissions |
| 997 | +static bool CFNetGetWithPerms(AgentConnection *conn, const char *remote_path, |
| 998 | + const char *local_path, bool print_stats) |
| 999 | +{ |
| 1000 | + assert(conn != NULL && remote_path != NULL && local_path != NULL); |
| 1001 | + |
| 1002 | + struct stat perms; |
| 1003 | + if (!ProtocolStat(conn, remote_path, &perms)) |
| 1004 | + { |
| 1005 | + Log(LOG_LEVEL_ERR, "Failed to stat remote file: %s:%s", |
| 1006 | + conn->this_server, remote_path); |
| 1007 | + return false; |
| 1008 | + } |
| 1009 | + |
| 1010 | + if (!ProtocolGet(conn, remote_path, local_path, perms.st_size, perms.st_mode, print_stats)) |
| 1011 | + { |
| 1012 | + Log(LOG_LEVEL_ERR, "Failed to get remote file: %s:%s", |
| 1013 | + conn->this_server, remote_path); |
| 1014 | + return false; |
| 1015 | + } |
| 1016 | + |
| 1017 | + return true; |
| 1018 | +} |
| 1019 | + |
| 1020 | +// Helper: Create local directory path |
| 1021 | +static bool create_local_dir(const char *local_base, const char *subdir, |
| 1022 | + bool has_output_path, mode_t perms) |
| 1023 | +{ |
| 1024 | + char path[PATH_MAX]; |
| 1025 | + int written; |
| 1026 | + |
| 1027 | + if (has_output_path) |
| 1028 | + { |
| 1029 | + written = snprintf(path, sizeof(path), "%s/%s/", local_base, subdir); |
| 1030 | + } |
| 1031 | + else |
| 1032 | + { |
| 1033 | + char cwd[PATH_MAX]; |
| 1034 | + if (!getcwd(cwd, sizeof(cwd))) |
| 1035 | + { |
| 1036 | + Log(LOG_LEVEL_ERR, "Failed to get current working directory"); |
| 1037 | + return false; |
| 1038 | + } |
| 1039 | + written = snprintf(path, sizeof(path), "%s/%s/%s/", cwd, local_base, subdir); |
| 1040 | + } |
| 1041 | + |
| 1042 | + if (written < 0 || written >= sizeof(path)) |
| 1043 | + { |
| 1044 | + Log(LOG_LEVEL_ERR, "Path too long for new directory: %s", subdir); |
| 1045 | + return false; |
| 1046 | + } |
| 1047 | + |
| 1048 | + bool* created = NULL; |
| 1049 | + bool force = false; |
| 1050 | + MakeParentDirectoryPerms(path, force, created, perms); |
| 1051 | + return true; |
| 1052 | +} |
| 1053 | + |
| 1054 | +// Helper: Recursively process directory entries |
| 1055 | +static int process_dir_recursive(AgentConnection *conn, |
| 1056 | + const char *remote_path, |
| 1057 | + const char *local_path, |
| 1058 | + bool has_output_path, |
| 1059 | + bool print_stats, |
| 1060 | + int limit) |
| 1061 | +{ |
| 1062 | + if (0 > limit - 1) |
| 1063 | + { |
| 1064 | + Log(LOG_LEVEL_ERR, "Recursion limit reached"); |
| 1065 | + return -2; |
| 1066 | + } |
| 1067 | + |
| 1068 | + int written; |
| 1069 | + Seq *items = ProtocolOpenDir(conn, remote_path); |
| 1070 | + if (!items) |
| 1071 | + { |
| 1072 | + return -1; |
| 1073 | + } |
| 1074 | + |
| 1075 | + for (size_t i = 0; i < SeqLength(items); i++) |
| 1076 | + { |
| 1077 | + char *item = SeqAt(items, i); |
| 1078 | + |
| 1079 | + if (strcmp(".", item) == 0 || strcmp("..", item) == 0) |
| 1080 | + { |
| 1081 | + continue; |
| 1082 | + } |
| 1083 | + |
| 1084 | + char remote_full[PATH_MAX]; |
| 1085 | + written = snprintf(remote_full, sizeof(remote_full), "%s/%s", remote_path, item); |
| 1086 | + if (written < 0 || written >= sizeof(remote_full)) |
| 1087 | + { |
| 1088 | + Log(LOG_LEVEL_ERR, |
| 1089 | + "Path too long for building full remote path: %s and %s", |
| 1090 | + remote_path, item); |
| 1091 | + continue; |
| 1092 | + } |
| 1093 | + |
| 1094 | + char local_full[PATH_MAX]; |
| 1095 | + written = snprintf(local_full, sizeof(local_full), "%s/%s", local_path, item); |
| 1096 | + if (written < 0 || written >= sizeof(local_full)) |
| 1097 | + { |
| 1098 | + Log(LOG_LEVEL_ERR, |
| 1099 | + "Path too long for building full local path: %s and %s", |
| 1100 | + local_path, item); |
| 1101 | + return -1; |
| 1102 | + } |
| 1103 | + |
| 1104 | + struct stat sb; |
| 1105 | + if (!ProtocolStat(conn, remote_full, &sb)) |
| 1106 | + { |
| 1107 | + Log(LOG_LEVEL_ERR, "Could not stat: %s", remote_full); |
| 1108 | + return -1; |
| 1109 | + } |
| 1110 | + |
| 1111 | + if (S_ISDIR(sb.st_mode)) // Is directory |
| 1112 | + { |
| 1113 | + if (!create_local_dir(local_path, item, has_output_path, sb.st_mode)) |
| 1114 | + { |
| 1115 | + // Error already logged |
| 1116 | + return -1; |
| 1117 | + } |
| 1118 | + process_dir_recursive(conn, remote_full, local_full, has_output_path, print_stats, (limit - 1)); |
| 1119 | + } |
| 1120 | + else |
| 1121 | + { |
| 1122 | + CFNetGetWithPerms(conn, remote_full, local_full, print_stats); |
| 1123 | + } |
| 1124 | + } |
| 1125 | + |
| 1126 | + SeqDestroy(items); |
| 1127 | + return 0; |
| 1128 | +} |
| 1129 | + |
| 1130 | +static int CFNetGetDir(CFNetOptions *opts, const char *hostname, char **args) |
| 1131 | +{ |
| 1132 | + assert(opts != NULL); |
| 1133 | + assert(hostname != NULL); |
| 1134 | + assert(args != NULL); |
| 1135 | + char *local_dir = NULL; |
| 1136 | + int limit = 10; |
| 1137 | + |
| 1138 | + int argc = 0; |
| 1139 | + while (args[argc] != NULL) |
| 1140 | + { |
| 1141 | + ++argc; |
| 1142 | + } |
| 1143 | + |
| 1144 | + static struct option longopts[] = { |
| 1145 | + { "output", required_argument, NULL, 'o' }, |
| 1146 | + { "limit", required_argument, NULL, 'l' }, |
| 1147 | + { NULL, 0, NULL, 0 } |
| 1148 | + }; |
| 1149 | + if (argc <= 1) |
| 1150 | + { |
| 1151 | + return invalid_command("getdir"); |
| 1152 | + } |
| 1153 | + extern int optind; |
| 1154 | + optind = 0; |
| 1155 | + extern char *optarg; |
| 1156 | + int c = 0; |
| 1157 | + const char *optstr = "o:l:"; |
| 1158 | + bool specified_path = false; |
| 1159 | + while ((c = getopt_long(argc, args, optstr, longopts, NULL)) |
| 1160 | + != -1) |
| 1161 | + { |
| 1162 | + switch (c) |
| 1163 | + { |
| 1164 | + case 'o': |
| 1165 | + { |
| 1166 | + if (local_dir != NULL) |
| 1167 | + { |
| 1168 | + Log(LOG_LEVEL_INFO, |
| 1169 | + "Warning: multiple occurrences of -o in command, "\ |
| 1170 | + "only last one will be used."); |
| 1171 | + free(local_dir); |
| 1172 | + } |
| 1173 | + local_dir = xstrdup(optarg); |
| 1174 | + specified_path = true; |
| 1175 | + break; |
| 1176 | + } |
| 1177 | + case 'l': |
| 1178 | + { |
| 1179 | + char *lim; |
| 1180 | + long val = strtol(optarg, &lim, 10); |
| 1181 | + if (*lim != '\0' || val < 0) |
| 1182 | + { |
| 1183 | + Log(LOG_LEVEL_ERR, "Invalid limit value: %s", optarg); |
| 1184 | + return invalid_command("getdir"); |
| 1185 | + } |
| 1186 | + limit = (int)val; |
| 1187 | + break; |
| 1188 | + } |
| 1189 | + case ':': |
| 1190 | + case '?': |
| 1191 | + { |
| 1192 | + return invalid_command("getdir"); |
| 1193 | + } |
| 1194 | + default: |
| 1195 | + { |
| 1196 | + printf("Default optarg = '%s', c = '%c' = %i\n", |
| 1197 | + optarg, c, (int)c); |
| 1198 | + break; |
| 1199 | + } |
| 1200 | + } |
| 1201 | + } |
| 1202 | + args = &(args[optind]); |
| 1203 | + argc -= optind; |
| 1204 | + char *remote_dir = args[0]; |
| 1205 | + char *base = basename(remote_dir); |
| 1206 | + if (specified_path) |
| 1207 | + { |
| 1208 | + char temp[PATH_MAX]; |
| 1209 | + |
| 1210 | + int written = snprintf(temp, sizeof(temp), "%s/%s", local_dir, base); |
| 1211 | + if (written < 0 || written >= sizeof(temp)) |
| 1212 | + { |
| 1213 | + Log(LOG_LEVEL_ERR, "Path too long for local path: %s/%s", local_dir, base); |
| 1214 | + free(local_dir); |
| 1215 | + return -1; |
| 1216 | + } |
| 1217 | + free(local_dir); |
| 1218 | + local_dir = xstrdup(temp); |
| 1219 | + } |
| 1220 | + |
| 1221 | + if (local_dir == NULL) |
| 1222 | + { |
| 1223 | + local_dir = xstrdup(base); |
| 1224 | + } |
| 1225 | + |
| 1226 | + AgentConnection *conn = CFNetOpenConnection(hostname, opts->use_protocol_version); |
| 1227 | + if (conn == NULL) |
| 1228 | + { |
| 1229 | + free(local_dir); |
| 1230 | + return -1; |
| 1231 | + } |
| 1232 | + struct stat sb; |
| 1233 | + int ret = (int) ProtocolStat(conn, remote_dir, &sb); |
| 1234 | + if (ret != 1) |
| 1235 | + { |
| 1236 | + printf("Could not stat: '%s'\n", remote_dir); |
| 1237 | + free(local_dir); |
| 1238 | + CFNetDisconnect(conn); |
| 1239 | + return -1; |
| 1240 | + } |
| 1241 | + if (!S_ISDIR(sb.st_mode)) |
| 1242 | + { |
| 1243 | + printf("'%s' is not a directory, use 'get' for single file download\n", remote_dir); |
| 1244 | + free(local_dir); |
| 1245 | + CFNetDisconnect(conn); |
| 1246 | + return -1; |
| 1247 | + } |
| 1248 | + |
| 1249 | + ret = process_dir_recursive(conn, remote_dir, local_dir, specified_path, opts->print_stats, limit); |
| 1250 | + if (ret == -2) |
| 1251 | + { |
| 1252 | + Log(LOG_LEVEL_INFO, "Recursion limit(%d) reached", limit); |
| 1253 | + } |
| 1254 | + else if (ret == -1) |
| 1255 | + { |
| 1256 | + Log(LOG_LEVEL_INFO, "Failed to copy all contents of %s", remote_dir); |
| 1257 | + } |
| 1258 | + |
| 1259 | + free(local_dir); |
| 1260 | + CFNetDisconnect(conn); |
| 1261 | + return (ret == 0) ? 0 : -1; |
| 1262 | +} |
| 1263 | + |
979 | 1264 | static int CFNetMulti(const char *server) |
980 | 1265 | { |
981 | 1266 | time_t start; |
|
0 commit comments