diff --git a/apps/plugins/disktidy.c b/apps/plugins/disktidy.c index 621238acfb..6f131e37a6 100644 --- a/apps/plugins/disktidy.c +++ b/apps/plugins/disktidy.c @@ -18,12 +18,38 @@ * KIND, either express or implied. * ****************************************************************************/ + #include "plugin.h" #include "errno.h" +#include "lib/playback_control.h" +#include "lib/display_text.h" +#define DEFAULT_FILES PLUGIN_APPS_DATA_DIR "/disktidy.config" +#define CUSTOM_FILES PLUGIN_APPS_DATA_DIR "/disktidy_custom.config" +#define LAST_RUN_STATS_FILE PLUGIN_APPS_DATA_DIR "/disktidy.stats" +#define DIR_STACK_SIZE 25 -static int removed = 0; /* number of items removed */ -static bool user_abort; +struct dir_info { + DIR *dir; + int path_length; + long size; +}; + +/* Store directory info when traversing file system */ +struct dir_stack { + struct dir_info dirs[DIR_STACK_SIZE]; + int size; +}; + +struct run_statistics { + int files_removed; /* Number of files removed */ + int dirs_removed; /* Number of directories removed */ + int run_duration; /* Duration of last run in seconds */ + double removed_size; /* Size of items removed */ +#if CONFIG_RTC + struct tm last_run_time; /* Last time disktidy was run */ +#endif +}; struct tidy_type { char filestring[64]; @@ -33,12 +59,41 @@ struct tidy_type { bool remove; } tidy_types[64]; +static struct run_statistics run_stats; static size_t tidy_type_count; +static bool user_abort; +static bool tidy_loaded_and_changed = false; +static bool stats_file_exists = false; -bool tidy_loaded_and_changed = false; +static void dir_stack_init(struct dir_stack *dstack) +{ + dstack->size = 0; +} -#define DEFAULT_FILES PLUGIN_APPS_DATA_DIR "/disktidy.config" -#define CUSTOM_FILES PLUGIN_APPS_DATA_DIR "/disktidy_custom.config" +static inline int dir_stack_size(struct dir_stack *dstack) +{ + return dstack->size; +} + +static inline bool dir_stack_push(struct dir_stack *dstack, struct dir_info dinfo) +{ + if (dstack->size == DIR_STACK_SIZE) { + return false; + } + + dstack->dirs[dstack->size++] = dinfo; + return true; +} + +static inline bool dir_stack_pop(struct dir_stack *dstack, struct dir_info *dinfo) +{ + if (dstack->size == 0) { + return false; + } + + *dinfo = dstack->dirs[--dstack->size]; + return true; +} static void add_item(const char* name, int index) { @@ -135,6 +190,127 @@ static void tidy_load_file(const char* file) rb->close(fd); } +static bool save_run_stats(void) +{ + int fd = rb->open(LAST_RUN_STATS_FILE, O_WRONLY|O_CREAT, 0666); + + if (fd < 0) { + return false; + } + + bool save_success = rb->write(fd, &run_stats, + sizeof(struct run_statistics)) > 0; + + rb->close(fd); + + return save_success; +} + +static bool load_run_stats(void) +{ + int fd = rb->open(LAST_RUN_STATS_FILE, O_RDONLY); + + if (fd < 0) { + return false; + } + + bool load_success = rb->read(fd, &run_stats, + sizeof(struct run_statistics)) == sizeof(struct run_statistics); + + rb->close(fd); + + return load_success; +} + +static enum plugin_status display_run_stats(void) +{ + if (!load_run_stats()) { + rb->splash(HZ * 2, "Unable to load last run stats"); + return PLUGIN_OK; + } + +#if CONFIG_RTC + static const char *months[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; +#endif + static const char *size_units[] = { + "B", "KB", "MB", "GB", "TB", "PB" + }; + + int magnitude = 0; + double rm_size = run_stats.removed_size; + + while (rm_size >= 1000) { + rm_size /= 1024; + magnitude++; + } + + char total_removed[8]; + rb->snprintf(total_removed, sizeof(total_removed), "%d", + run_stats.files_removed + run_stats.dirs_removed); + + char files_removed[8]; + rb->snprintf(files_removed, sizeof(files_removed), "%d", + run_stats.files_removed); + + char dirs_removed[8]; + rb->snprintf(dirs_removed, sizeof(dirs_removed), "%d", + run_stats.dirs_removed); + + char removed_size[9]; + rb->snprintf(removed_size, sizeof(removed_size), "%d.%d%s", + (int)rm_size, (int)((rm_size - (int)rm_size) * 100), + size_units[magnitude]); + + char run_time[9]; + rb->snprintf(run_time, sizeof(run_time), "%02d:%02d:%02d", + run_stats.run_duration / 3600, run_stats.run_duration / 60, + run_stats.run_duration % 60); + +#if CONFIG_RTC + char last_run[18]; + rb->snprintf(last_run, sizeof(last_run), "%02d:%02d %d/%s/%d", + run_stats.last_run_time.tm_hour, + run_stats.last_run_time.tm_min, run_stats.last_run_time.tm_mday, + months[run_stats.last_run_time.tm_mon], + 2000 + (run_stats.last_run_time.tm_year % 100)); +#endif + + char* last_run_text[] = { + "Last Run Stats" , "" , "", + "Total Removed: ", total_removed, "", + "Files Removed: ", files_removed, "", + "Dirs Removed: " , dirs_removed , "", + "Removed Size: " , removed_size , "", + "Run Time: " , run_time , "", +#if CONFIG_RTC + "Run: " , last_run +#endif + }; + + static struct style_text display_style[] = { + { 0, C_ORANGE | TEXT_CENTER }, + { 3, C_BLUE }, + { 6, C_BLUE }, + { 9, C_BLUE }, + { 12, C_BLUE }, + { 15, C_BLUE }, +#if CONFIG_RTC + { 18, C_BLUE }, +#endif + LAST_STYLE_ITEM + }; + + if (display_text(ARRAYLEN(last_run_text), last_run_text, + display_style, NULL, true)) { + return PLUGIN_USB_CONNECTED; + } + + return PLUGIN_OK; +} + static bool match(struct tidy_type *tidy_type, const char *string, int len) { char *pattern = tidy_type->filestring; @@ -168,7 +344,8 @@ static void tidy_lcd_status(const char *name) rb->lcd_puts(0, 0, "Working ..."); rb->lcd_puts(0, 1, name); #ifdef HAVE_LCD_BITMAP - rb->lcd_putsf(0, 2, "Cleaned up %d items", removed); + rb->lcd_putsf(0, 2, "Cleaned up %d items", + run_stats.files_removed + run_stats.dirs_removed); #endif rb->lcd_update(); } @@ -204,65 +381,158 @@ static void tidy_path_remove_entry(char *path, int old_path_length, int *path_le *path_length = old_path_length; } -/* path is assumed to be array of size MAX_PATH. */ -static enum plugin_status tidy_clean(char *path, int *path_length, bool rmdir) -{ - int old_path_length = *path_length; +/* Cleanup when user abort or USB event during tidy_clean */ +static void tidy_clean_cleanup(struct dir_stack *dstack, DIR *dir) { + struct dir_info dinfo; - tidy_lcd_status(path); - - DIR *dir = rb->opendir(path); - if (!dir) - return PLUGIN_ERROR; - - struct dirent *entry; - while ((entry = rb->readdir(dir))) - { - /* check for user input and usb connect */ - int button = rb->get_action(CONTEXT_STD, TIMEOUT_NOBLOCK); - if (button == ACTION_STD_CANCEL) - { - rb->closedir(dir); - user_abort = true; - return PLUGIN_OK; - } - if (rb->default_event_handler(button) == SYS_USB_CONNECTED) - { - rb->closedir(dir); - return PLUGIN_USB_CONNECTED; - } - - rb->yield(); - - struct dirinfo info = rb->dir_get_info(dir, entry); - if (!rmdir && !tidy_remove_item(entry->d_name, info.attribute)) - continue; - - /* get absolute path, returns an error if path is too long */ - if(!tidy_path_append_entry(path, entry, path_length)) - continue; /* silent error */ - - if (info.attribute & ATTR_DIRECTORY) - { - /* dir ignore "." and ".." */ - if (rb->strcmp(entry->d_name, ".") && rb->strcmp(entry->d_name, "..")) - tidy_clean(path, path_length, true); - } - else - { - removed++; - rb->remove(path); - } - - /* restore path */ - tidy_path_remove_entry(path, old_path_length, path_length); - } rb->closedir(dir); - if (rmdir) - { - removed++; - rb->rmdir(path); + while (dir_stack_pop(dstack, &dinfo)) { + rb->closedir(dinfo.dir); + } +} + +/* Perform iterative depth-first search for files to clean */ +static enum plugin_status tidy_clean(char *path, int *path_length) { + struct dir_stack dstack; + struct dir_info dinfo; + struct dirent *entry; + struct dirinfo info; + DIR *dir, *dir_test; + /* Set to true when directory and its contents are to be deleted */ + bool rm_all = false; + /* Used to mark where rm_all starts and ends */ + int rm_all_start_depth = 0; + int button; + bool remove; + int old_path_length; + + dir_stack_init(&dstack); + dir = rb->opendir(path); + + if (!dir) { + /* If can't open / then immediately stop */ + return PLUGIN_ERROR; + } + + dinfo.dir = dir; + dinfo.path_length = *path_length; + /* Size only used when deleting directory so value here doesn't matter */ + dinfo.size = 0; + + dir_stack_push(&dstack, dinfo); + + while (dir_stack_pop(&dstack, &dinfo)) { + /* Restore path to poped dir */ + tidy_path_remove_entry(path, dinfo.path_length, path_length); + dir = dinfo.dir; + tidy_lcd_status(path); + + while ((entry = rb->readdir(dir))) { + /* Check for user input and usb connect */ + button = rb->get_action(CONTEXT_STD, TIMEOUT_NOBLOCK); + + if (button == ACTION_STD_CANCEL) { + tidy_clean_cleanup(&dstack, dir); + user_abort = true; + return PLUGIN_OK; + } + if (rb->default_event_handler(button) == SYS_USB_CONNECTED) { + tidy_clean_cleanup(&dstack, dir); + return PLUGIN_USB_CONNECTED; + } + + rb->yield(); + + old_path_length = *path_length; + info = rb->dir_get_info(dir, entry); + + remove = rm_all || tidy_remove_item(entry->d_name, info.attribute); + + if (info.attribute & ATTR_DIRECTORY) { + if (rb->strcmp(entry->d_name, ".") == 0 || + rb->strcmp(entry->d_name, "..") == 0) { + continue; + } + + if (!remove) { + /* Get absolute path, returns an error if path is too long */ + if (!tidy_path_append_entry(path, entry, path_length)) { + continue; /* Silent error */ + } + + dinfo.dir = dir; + dinfo.path_length = old_path_length; + dinfo.size = 0; + + /* This directory doesn't need to be deleted, so try to add + the current directory we're in to the stack and search + this one for files/directories to delete. If we can't + add the current directory to the stack or open the new + directory to search then continue on in the current + directory. */ + if (dir_stack_push(&dstack, dinfo)) { + dir_test = rb->opendir(path); + + if (dir_test) { + dir = dir_test; + tidy_lcd_status(path); + } + } + } + } + + if (!remove) { + continue; + } + + /* Get absolute path, returns an error if path is too long */ + if (!tidy_path_append_entry(path, entry, path_length)) { + continue; /* Silent error */ + } + + if (info.attribute & ATTR_DIRECTORY) { + /* Remove this directory and all files/directories it contains */ + dinfo.dir = dir; + dinfo.path_length = old_path_length; + dinfo.size = info.size; + + if (dir_stack_push(&dstack, dinfo)) { + dir_test = rb->opendir(path); + + if (dir_test) { + dir = dir_test; + + if (!rm_all) { + rm_all = true; + rm_all_start_depth = dir_stack_size(&dstack); + } + + tidy_lcd_status(path); + } + } + } else { + run_stats.files_removed++; + run_stats.removed_size += info.size; + rb->remove(path); + + /* Restore path */ + tidy_path_remove_entry(path, old_path_length, path_length); + } + } + + rb->closedir(dir); + + if (rm_all) { + /* Check if returned to the directory we started rm_all from */ + if (rm_all_start_depth == dir_stack_size(&dstack)) { + rm_all = false; + } + + rb->rmdir(path); + run_stats.dirs_removed++; + run_stats.removed_size += dinfo.size; + } } return PLUGIN_OK; @@ -273,18 +543,30 @@ static enum plugin_status tidy_do(void) /* clean disk and display num of items removed */ char path[MAX_PATH]; + run_stats.files_removed = 0; + run_stats.dirs_removed = 0; + run_stats.removed_size = 0; + long start_time = *rb->current_tick; + +#if CONFIG_RTC + run_stats.last_run_time = *rb->get_time(); +#endif + #ifdef HAVE_ADJUSTABLE_CPU_FREQ rb->cpu_boost(true); #endif rb->strcpy(path, "/"); int path_length = rb->strlen(path); - enum plugin_status status = tidy_clean(path, &path_length, false); + enum plugin_status status = tidy_clean(path, &path_length); #ifdef HAVE_ADJUSTABLE_CPU_FREQ rb->cpu_boost(false); #endif + run_stats.run_duration = (*rb->current_tick - start_time) / HZ; + stats_file_exists = save_run_stats(); + if (status == PLUGIN_OK) { rb->lcd_clear_display(); @@ -293,8 +575,11 @@ static enum plugin_status tidy_do(void) rb->splash(HZ, "User aborted"); rb->lcd_clear_display(); } - rb->splashf(HZ*2, "Cleaned up %d items", removed); + rb->lcd_update(); + rb->splashf(HZ*2, "Cleaned up %d items", + run_stats.files_removed + run_stats.dirs_removed); } + return status; } @@ -349,30 +634,75 @@ static int list_action_callback(int action, struct gui_synclist *lists) return ACTION_REDRAW; } -static void tidy_lcd_menu(void) +static bool tidy_types_selected(void) { + for (unsigned int i = 0; i < tidy_type_count; i++) { + if (tidy_types[i].filestring[0] != '<' && tidy_types[i].remove) { + return true; + } + } + + return false; +} + +static int disktidy_menu_cb(int action, const struct menu_item_ex *this_item) +{ + int item = ((intptr_t)this_item); + + if (action == ACTION_REQUEST_MENUITEM && + !stats_file_exists && item == 2) { + + return ACTION_EXIT_MENUITEM; + } + + return action; +} + +static enum plugin_status tidy_lcd_menu(void) +{ + enum plugin_status disktidy_status = PLUGIN_OK; + bool exit = false; int selection = 0; struct simplelist_info list; - MENUITEM_STRINGLIST(menu, "Disktidy Menu", NULL, "Start Cleaning", - "Files to Clean", "Quit"); + MENUITEM_STRINGLIST(menu, "Disktidy Menu", disktidy_menu_cb, + "Start Cleaning", "Files to Clean", "Last Run Stats", + "Playback Control", "Quit"); - for(;;) - switch(rb->do_menu(&menu, &selection, NULL, false)) - { - default: - user_abort = true; - case 0: - return; /* start cleaning */ + while (!exit && disktidy_status == PLUGIN_OK) { + switch(rb->do_menu(&menu, &selection, NULL, false)) { + case 0: + if (tidy_types_selected()) { + disktidy_status = tidy_do(); + } else { + rb->splash(HZ * 2, "Select at least one file type to clean"); + } - case 1: - rb->simplelist_info_init(&list, "Files to Clean", tidy_type_count, NULL); - list.get_icon = get_icon; - list.get_name = get_name; - list.action_callback = list_action_callback; - rb->simplelist_show_list(&list); - break; + break; + case 1: + rb->simplelist_info_init(&list, "Files to Clean", + tidy_type_count, NULL); + list.get_icon = get_icon; + list.get_name = get_name; + list.action_callback = list_action_callback; + rb->simplelist_show_list(&list); + break; + case 2: + disktidy_status = display_run_stats(); + break; + case 3: + if (playback_control(NULL)) { + disktidy_status = PLUGIN_USB_CONNECTED; + } + + break; + default: + exit = true; + break; } + } + + return disktidy_status; } /* Creates a file and writes information about what files to @@ -400,14 +730,20 @@ enum plugin_status plugin_start(const void* parameter) tidy_load_file(DEFAULT_FILES); tidy_load_file(CUSTOM_FILES); + if (tidy_type_count == 0) { rb->splash(3*HZ, "Missing disktidy.config file"); return PLUGIN_ERROR; } - tidy_lcd_menu(); - if (tidy_loaded_and_changed) - save_config(); - return user_abort ? PLUGIN_OK : tidy_do(); + stats_file_exists = rb->file_exists(LAST_RUN_STATS_FILE); + + enum plugin_status disktidy_status = tidy_lcd_menu(); + + if (tidy_loaded_and_changed) { + save_config(); + } + + return disktidy_status; }