/* SPDX-License-Identifier: Apache-2.0 */ /* Copyright 2025 Thorsten Töpper * * dir_monitor - print specified stat() information from entries of a given * directory to stdout. Call it via watch for repeated output to a terminal. * * vim:ts=4:sw=4:expandtab */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOGERR(...) {fprintf(stderr, "[%s:%d] %s: ", __FILE__, __LINE__, __func__); fprintf(stderr, __VA_ARGS__);} #ifdef DEBUGBUILD #define DBGTRC(...) LOGERR(__VA_ARGS__) #else #define DBGTRC(...) #endif /* === DEFINITIONS === */ #define PATH_SEP '/' enum esort_type { SORT_BY_SIZE, SORT_BY_TIME }; struct list_node { struct list_node *next; size_t fsize; char fname[256]; time_t ftime; /* can be creation, access or modification */ }; struct list_head { struct list_node *first; }; struct list_node *create_node(char *fname, size_t fsize, time_t ftime); struct list_head *create_list_sort_reversed(struct list_head *list); void destroy_list(struct list_head *list); int fputc_all_cols(char c, FILE *fdout); int fputc_width_x(char c, size_t x, FILE *fdout); struct list_head *get_data_from_directory(char *path); int insert_sorted_by_size(struct list_head *list, struct list_node *node); int insert_sorted_by_time(struct list_head *list, struct list_node *node); int parse_arguments(int argc, char **argv); void print_list(struct list_head *list); void set_option(const char *option_name, char *option_argument); void usage(char *executable); /* === GLOBAL VARIABLES === */ struct option long_options[] = { { "reverse-sort", no_argument, 0, 0 }, { "show-hidden-entries", no_argument, 0, 0 }, { "sort-by", required_argument, 0, 0 }, { "long-timestamp", no_argument, 0, 0 }, { 0, 0, 0, 0 } }; bool option_sort_reverse_order = false; enum esort_type option_sort_type = SORT_BY_SIZE; bool option_show_hidden_entries = false; bool option_timestamp_long = false; /* === IMPLEMENTATION === */ inline struct list_node *create_node(char *fname, size_t fsize, time_t ftime) { struct list_node *node = NULL; if (fname == NULL || fname[0] == '\0') { LOGERR("ERROR: No valid filename given\n"); return NULL; } if ((node = calloc(1, sizeof(struct list_node))) == NULL) { LOGERR("ERROR: Failed allocate memory for node: %s\n", strerror(errno)); return NULL; } node->next = NULL; node->fsize = fsize; node->ftime = ftime; strncpy(node->fname, fname, 256); return node; } inline void destroy_list(struct list_head *list) { struct list_node *ptr = NULL; if (list == NULL) return; while (list->first != NULL) { ptr = list->first->next; if (ptr == NULL) { free(list->first); break; } list->first->next = ptr->next; free(ptr); } free(list); } inline int fputc_all_cols(char c, FILE *fdout) { struct winsize terminal; ioctl(STDOUT_FILENO, TIOCGWINSZ, &terminal); return fputc_width_x(c, ((terminal.ws_col == 0) ? 72 : terminal.ws_col), fdout); } /* Not the most performant way, but during output the time critical tasks are already done. */ inline int fputc_width_x(char c, size_t x, FILE *fdout) { size_t i=0; if (fdout == NULL) { LOGERR("ERROR: No output stream given.\n"); return EOF; } if ( ! isprint(c) ) { c = '?'; } for (i=0; i_fileno); return EOF; } } return 1; } /* dirty... */ #define INSERT_BY_NUMERIC_FIELD(list, node, field) \ { struct list_node *ptr = NULL; \ if (node->field > list->first->field) { \ node->next = list->first; list->first = node; return 0; } \ ptr = list->first; \ while (ptr != NULL) { \ if (ptr->next == NULL) { ptr->next = node; return 0; } \ if (node->field > ptr->next->field) { \ node->next = ptr->next; ptr->next = node; \ return 0; } \ ptr = ptr->next; \ } \ } /* TODO: later when, other parameters are actively used move the action to a template */ inline int insert_sorted_by_size(struct list_head *list, struct list_node *node) { if (list == NULL) { LOGERR("ERROR: No list given.\n"); return -1; } if (node == NULL) { LOGERR("ERROR: No node given.\n"); return -2; } if (list->first == NULL) { list->first = node; return 0; } INSERT_BY_NUMERIC_FIELD(list, node, fsize); return -3; } inline int insert_sorted_by_time(struct list_head *list, struct list_node *node) { if (list == NULL) { LOGERR("ERROR: No list given.\n"); return -1; } if (node == NULL) { LOGERR("ERROR: No node given.\n"); return -2; } if (list->first == NULL) { list->first = node; return 0; } INSERT_BY_NUMERIC_FIELD(list, node, ftime); return -3; } #undef INSERT_BY_NUMERIC_FIELD /* The most simple default output, formatted output is planned */ void print_list(struct list_head *list) { struct list_head *lh = list; struct list_node *ptr; struct tm *tm = NULL; char timestamp[128]; size_t total_size = 0; if (list == NULL) return; if (option_sort_reverse_order) { lh = create_list_sort_reversed(list); /* be tolerant */ if (lh == NULL) { lh = list; } } ptr = lh->first; while (ptr != NULL) { printf(" %8lu %2s ", ((ptr->fsize>=1024) ? (ptr->fsize/1024) : ptr->fsize), ((ptr->fsize >= 1024) ? "kB" : "")); tm = localtime(&ptr->ftime); if (option_timestamp_long) { strftime(timestamp, sizeof(timestamp), "%Y%m%d %H:%M:%S %Z", tm); printf(" %20s ", timestamp); } else { strftime(timestamp, sizeof(timestamp), "%H:%M:%S", tm); printf(" %8s ", timestamp); } printf(" %s\n", ptr->fname); total_size += ptr->fsize; ptr = ptr->next; } fputc_all_cols('=', stdout); printf("\nTotal size: %lu %s\n", ((total_size>1024) ? total_size/1024 : total_size), ((total_size >= 1024) ? "kB" : "")); if (lh != list) destroy_list(lh); } struct list_head *create_list_sort_reversed(struct list_head *list) { struct list_head *lh = NULL; struct list_node *cpynode = NULL, *ptr = NULL; if (list == NULL) { LOGERR("ERROR: No list given\n"); return NULL; } if ((lh = calloc(1, sizeof(struct list_head))) == NULL) { LOGERR("ERROR: Failed to allocate memory for a struct list_head\n"); return NULL; } lh->first = NULL; ptr = list->first; while (ptr != NULL) { cpynode = create_node(ptr->fname, ptr->fsize, ptr->ftime); if (cpynode == NULL) { destroy_list(lh); return NULL; } cpynode->next = lh->first; lh->first = cpynode; ptr = ptr->next; } return lh; } /* This function contains the only time critical code. The loop over * the directory content. * * Note for future: * I myself use the dir_monitor in combination with watch on a directory * containing temporary data, it may happen that a run into an error occurs, * but it's not tragic. To reduce the chance of the error the first step * would be to optimize the loop with putting the data into a list in stack * form and moving the nodes into a sorted list afterwards. */ struct list_head *get_data_from_directory(char *path) { char *fullpath = NULL, *fname_in_path = NULL; DIR *dir = NULL; size_t l; struct dirent *de = NULL; struct list_node *tmp = NULL; struct list_head *list = NULL; struct stat stat_res; if (path == NULL) { LOGERR("ERROR: No path given.\n"); return NULL; } l = strlen(path); /* 258 filename and possibly required PATH_SEP and of course string termination */ if ((fullpath = calloc(l+258, sizeof(char))) == NULL) { LOGERR("ERROR: Failed to allocate memory for list head.\n"); return NULL; } sprintf(fullpath, "%s%c", path, ((path[l-1] == PATH_SEP) ? '\0' : PATH_SEP)); fname_in_path = (fullpath[l] == PATH_SEP) ? &(fullpath[l+1]) : &(fullpath[l]) ; if ((dir = opendir(path)) == NULL) { LOGERR("ERROR: Failed to open directory '%s': %s (errno %d)\n", path, strerror(errno), errno); return NULL; } /* TBH: In case those few bytes are not available, the process will be killed anyways */ if ((list = calloc(1, sizeof(struct list_head))) == NULL) { LOGERR("ERROR: Failed to allocate memory for list head.\n"); closedir(dir); return NULL; } errno = 0; /* Currently the code is specific UNIXoid, needs to be adjusted in case someone ports it. */ while ((de = readdir(dir)) != NULL) { if (de->d_name[0] == '.' && option_show_hidden_entries == false) continue; sprintf(fname_in_path, "%s", de->d_name); DBGTRC("DEBUG: fullpath: '%s'\n", fullpath); if (lstat(fullpath, &stat_res) != 0) { LOGERR("ERROR: lstat call on '%s' failed: %s (errno %d)\n", fullpath, strerror(errno), errno); continue; } if ((tmp = create_node(de->d_name, stat_res.st_size, stat_res.st_mtime)) == NULL) { LOGERR("ERROR: Skipping entry %s\n", de->d_name); continue; } /* SORT_BY_SIZE is the default value, always check it first. */ switch (option_sort_type) { case SORT_BY_SIZE: insert_sorted_by_size(list, tmp); break; case SORT_BY_TIME: insert_sorted_by_time(list, tmp); break; }; } free(fullpath); closedir(dir); return list; } void set_option(const char *option_name, char *option_argument) { DBGTRC("DEBUG: called with option_name '%s' and option_argument '%s'\n", option_name, option_argument); if (option_name == NULL) return; /* options WITHOUT arguments */ if (strcmp(option_name, "show-hidden-entries") == 0) { option_show_hidden_entries = true; return; } if (strcmp(option_name, "long-timestamp") == 0) { option_timestamp_long = true; return; } if (strcmp("reverse-sort", option_name) == 0) { option_sort_reverse_order = true; return; } /* options WITH arguments */ if (option_argument == NULL || option_argument[0] == '\0') { LOGERR("ERROR: option_name %s with missing option_argument\n", option_name); return; } if (strcmp(option_name, "sort-by") == 0) { if (strncmp("size", option_argument, 4) == 0) { option_sort_type = SORT_BY_SIZE; } else if (strncmp("time", option_argument, 4) == 0) { option_sort_type = SORT_BY_TIME; } else { LOGERR("WARNING: '%s' is an invalig argument for %s\n", option_argument, option_name); } return; } LOGERR("ERROR: Option '%s' not recognized\n.", option_name); } int parse_arguments(int argc, char **argv) { int c = 0, index; while(1) { index = 0; c = getopt_long(argc, argv, "tv", long_options, &index); if (c == -1) { break; } switch (c) { case 0: set_option(long_options[index].name, optarg); break; case 't': option_timestamp_long = true; break; case 'v': option_show_hidden_entries = true; break; case '?': break; default: LOGERR("ERROR: unrecognized option 0x%02X '%c'\n", c, c); break; } } return optind; } void usage(char *executable) { fprintf(stderr, "Call: %s OPTIONS path_to_open\n", executable); fprintf(stderr, "\nOPTIONS are\n"); /* long name, short name, optional argument, explanation */ fprintf(stderr, " %-25s %2s %10s %s\n", "--long-timestamp", "-t", "", "Print timestamp in long form yyyymmdd HH:MM:SS ZONE"); fprintf(stderr, " %-25s %2s %10s %s\n", "--reverse-sort", "", "", "Sort reversed"); fprintf(stderr, " %-25s %2s %10s %s\n", "--show-hidden-entries", "-v", "", "Show hidden entries in the directory"); fprintf(stderr, " %-25s %2s %10s %s\n", "--sort-by", "", "size|time", "Sort either by time or size"); } int main(int argc, char **argv) { struct list_head *list = NULL; int path_index = 1; /* TODO: Handle options */ if (argc < 2) { usage(argv[0]); return EXIT_FAILURE; } path_index = parse_arguments(argc, argv); if (path_index == argc) { usage(argv[0]); return EXIT_FAILURE; } list = get_data_from_directory(argv[path_index]); print_list(list); destroy_list(list); return EXIT_SUCCESS; }