rockbox/apps/plugins/disktidy.c
Richard Burke 1e7b93a9b2 Fixed disktidy bug and added a couple of new features to disktidy.
The following updates were made to disktidy:
- Fixed bug FS#12825. disktidy now checks subdirectories again for files to delete.
- Use iterative rather than recursive method to traverse file system.
- Once disktidy finishes a run it now returns to it's main menu rather than exiting.
- Added "Last Run Stats" view to disktidy. This shows how many files and directories
  were deleted in the last run as well as the total size of those files, the length of
  time the run took and when the run took place (for players with RTC).
- Added "Playback Control" option to disktidy main menu.

Change-Id: I9b7d6d5d08aef2b5f85fb63fcd2ec60f1c1ec2e0
Reviewed-on: http://gerrit.rockbox.org/808
Reviewed-by: Franklin Wei <frankhwei536@gmail.com>
Tested: Franklin Wei <frankhwei536@gmail.com>
Reviewed-by: Marcin Bukat <marcin.bukat@gmail.com>
2014-10-09 09:43:40 +02:00

749 lines
21 KiB
C

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2005 David Dent
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* 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
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];
int pre;
int post;
bool directory;
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;
static void dir_stack_init(struct dir_stack *dstack)
{
dstack->size = 0;
}
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)
{
struct tidy_type *entry = &tidy_types[index];
rb->strcpy(entry->filestring, name);
if (name[rb->strlen(name)-1] == '/')
{
entry->directory = true;
entry->filestring[rb->strlen(name)-1] = '\0';
}
else
entry->directory = false;
char *a = rb->strchr(entry->filestring, '*');
if (a)
{
entry->pre = a - entry->filestring;
entry->post = rb->strlen(a+1);
}
else
{
entry->pre = -1;
entry->post = -1;
}
}
static int find_file_string(const char *file, char *last_group)
{
char temp[MAX_PATH];
int idx_last_group = -1;
bool folder = false;
rb->strcpy(temp, file);
if (temp[rb->strlen(temp)-1] == '/')
{
folder = true;
temp[rb->strlen(temp)-1] = '\0';
}
for (unsigned i = 0; i < tidy_type_count; i++)
if (!rb->strcmp(tidy_types[i].filestring, temp) && folder == tidy_types[i].directory)
return i;
else if (!rb->strcmp(tidy_types[i].filestring, last_group))
idx_last_group = i;
if (file[0] == '<' || idx_last_group == -1)
return tidy_type_count;
/* not found, so insert it into its group */
for (unsigned i=idx_last_group; i<tidy_type_count; i++)
if (tidy_types[i].filestring[0] == '<')
{
idx_last_group = i;
break;
}
/* shift items up one */
for (int i=tidy_type_count;i>idx_last_group;i--)
rb->memcpy(&tidy_types[i], &tidy_types[i-1], sizeof(struct tidy_type));
tidy_type_count++;
add_item(file, idx_last_group+1);
return idx_last_group+1;
}
static void tidy_load_file(const char* file)
{
int fd = rb->open(file, O_RDONLY);
char buf[MAX_PATH], *str, *remove;
char last_group[MAX_PATH] = "";
if (fd < 0)
return;
while ((tidy_type_count < sizeof(tidy_types) / sizeof(tidy_types[0])) && rb->read_line(fd, buf, MAX_PATH))
{
if (!rb->settings_parseline(buf, &str, &remove))
continue;
if (*str == '\\') /* escape first character ? */
str++;
unsigned i = find_file_string(str, last_group);
tidy_types[i].remove = rb->strcmp(remove, "yes");
if (i >= tidy_type_count)
{
i = tidy_type_count;
add_item(str, i);
tidy_type_count++;
}
if (str[0] == '<')
rb->strcpy(last_group, str);
}
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;
if (tidy_type->pre < 0) /* no '*', just compare. */
return !rb->strcmp(pattern, string);
/* pattern is too long for the string. avoid 'ab*bc' matching 'abc'. */
if (len < tidy_type->pre + tidy_type->post)
return false;
/* pattern has '*', compare former part of '*' to the begining of
the string and compare next part of '*' to the end of string. */
return !rb->strncmp(pattern, string, tidy_type->pre) &&
!rb->strcmp(pattern + tidy_type->pre + 1, string + len - tidy_type->post);
}
static bool tidy_remove_item(const char *item, int attr)
{
for (struct tidy_type *t = &tidy_types[0]; t < &tidy_types[tidy_type_count]; t++)
if (match(t, item, rb->strlen(item)))
return t->remove && ((!!(attr&ATTR_DIRECTORY)) == t->directory);
return false;
}
static void tidy_lcd_status(const char *name)
{
/* display status text */
rb->lcd_clear_display();
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",
run_stats.files_removed + run_stats.dirs_removed);
#endif
rb->lcd_update();
}
static int tidy_path_append_entry(char *path, struct dirent *entry, int *path_length)
{
int name_len = rb->strlen(entry->d_name);
/* for the special case of path="/" this is one bigger but it's not a problem */
int new_length = *path_length + name_len + 1;
/* check overflow (keep space for trailing zero) */
if(new_length >= MAX_PATH)
return 0;
/* special case for path <> "/" */
if(rb->strcmp(path, "/") != 0)
{
rb->strcat(path + *path_length, "/");
(*path_length)++;
}
/* strcat is unsafe but the previous check normally avoid any problem */
/* use path_length to optimise */
rb->strcat(path + *path_length, entry->d_name);
*path_length += name_len;
return 1;
}
static void tidy_path_remove_entry(char *path, int old_path_length, int *path_length)
{
path[old_path_length] = '\0';
*path_length = old_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;
rb->closedir(dir);
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;
}
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);
#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();
if (user_abort)
{
rb->splash(HZ, "User aborted");
rb->lcd_clear_display();
}
rb->lcd_update();
rb->splashf(HZ*2, "Cleaned up %d items",
run_stats.files_removed + run_stats.dirs_removed);
}
return status;
}
static enum themable_icons get_icon(int item, void * data)
{
(void)data;
if (tidy_types[item].filestring[0] == '<') /* special type */
return Icon_Folder;
else if (tidy_types[item].remove)
return Icon_Cursor;
else
return Icon_NOICON;
}
static const char* get_name(int selected_item, void * data,
char * buffer, size_t buffer_len)
{
(void)data;
if (tidy_types[selected_item].directory)
{
rb->snprintf(buffer, buffer_len, "%s/",
tidy_types[selected_item].filestring);
return buffer;
}
return tidy_types[selected_item].filestring;
}
static int list_action_callback(int action, struct gui_synclist *lists)
{
if (action != ACTION_STD_OK)
return action;
unsigned selection = rb->gui_synclist_get_sel_pos(lists);
if (tidy_types[selection].filestring[0] == '<')
{
bool all = !rb->strcmp(tidy_types[selection].filestring, "< ALL >");
bool none= !rb->strcmp(tidy_types[selection].filestring, "< NONE >");
if (all || none)
{
for (unsigned i=0; i<tidy_type_count; i++)
if (tidy_types[i].filestring[0] != '<')
tidy_types[i].remove = all;
}
else /* toggle all untill the next <> */
while (++selection < tidy_type_count && tidy_types[selection].filestring[0] != '<')
tidy_types[selection].remove = !tidy_types[selection].remove;
}
else
tidy_types[selection].remove = !tidy_types[selection].remove;
tidy_loaded_and_changed = true;
return ACTION_REDRAW;
}
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", disktidy_menu_cb,
"Start Cleaning", "Files to Clean", "Last Run Stats",
"Playback Control", "Quit");
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");
}
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
delete and what to keep to it.
*/
static void save_config(void)
{
int fd = rb->creat(CUSTOM_FILES, 0666);
if (fd < 0)
return;
for (unsigned i=0; i<tidy_type_count; i++)
rb->fdprintf(fd, "%s%s%s: %s\n",
tidy_types[i].filestring[0] == '#' ? "\\" : "",
tidy_types[i].filestring,
tidy_types[i].directory ? "/" : "",
tidy_types[i].remove ? "yes" : "no");
rb->close(fd);
}
/* this is the plugin entry point */
enum plugin_status plugin_start(const void* parameter)
{
(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;
}
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;
}