/* * Copyright © 2008 Ryan Lortie * * This program is free software: you can redistribute it and/or modify * it under the terms of version 3 of the GNU General Public License as * published by the Free Software Foundation. */ #include "trashdir.h" #include #include #include "dirwatch.h" struct OPAQUE_TYPE__TrashDir { TrashRoot *root; GSList *items; GFile *directory; GFile *topdir; gboolean is_homedir; GDateTime *mtime; DirWatch *watch; GFileMonitor *monitor; }; static gint compare_basename (gconstpointer a, gconstpointer b) { GFile *file_a, *file_b; char *name_a, *name_b; gint result; file_a = (GFile *) a; file_b = (GFile *) b; name_a = g_file_get_basename (file_a); name_b = g_file_get_basename (file_b); result = strcmp (name_a, name_b); g_free (name_a); g_free (name_b); return result; } static GDateTime * trash_dir_query_mtime (TrashDir *dir) { GFileInfo *info; GDateTime *datetime = NULL; info = g_file_query_info (dir->directory, G_FILE_ATTRIBUTE_TIME_MODIFIED "," G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, G_FILE_QUERY_INFO_NONE, NULL, NULL); if (info) { datetime = g_file_info_get_modification_date_time (info); g_object_unref (info); } return datetime; } static void trash_dir_set_files (TrashDir *dir, GSList *items) { GSList **old, *new; items = g_slist_sort (items, (GCompareFunc) compare_basename); old = &dir->items; new = items; while (new || *old) { int result; if ((result = (new == NULL) - (*old == NULL)) == 0) result = compare_basename (new->data, (*old)->data); if (result < 0) { /* new entry. add it. */ *old = g_slist_prepend (*old, new->data); /* take reference */ old = &(*old)->next; trash_root_add_item (dir->root, new->data, dir->topdir, dir->is_homedir); new = new->next; } else if (result > 0) { /* old entry. remove it. */ trash_root_remove_item (dir->root, (*old)->data, dir->is_homedir); g_object_unref ((*old)->data); *old = g_slist_delete_link (*old, *old); } else { /* match. no change. */ old = &(*old)->next; g_object_unref (new->data); new = new->next; } } g_slist_free (items); trash_root_thaw (dir->root); } static void trash_dir_empty (TrashDir *dir) { trash_dir_set_files (dir, NULL); } static void trash_dir_enumerate (TrashDir *dir) { GFileEnumerator *enumerator; GSList *files = NULL; g_clear_pointer (&dir->mtime, g_date_time_unref); dir->mtime = trash_dir_query_mtime (dir); enumerator = g_file_enumerate_children (dir->directory, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL); if (enumerator) { GFileInfo *info; while ((info = g_file_enumerator_next_file (enumerator, NULL, NULL))) { GFile *file; file = g_file_get_child (dir->directory, g_file_info_get_name (info)); files = g_slist_prepend (files, file); g_object_unref (info); } g_object_unref (enumerator); } trash_dir_set_files (dir, files); /* consumes files */ } static void trash_dir_changed (GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { TrashDir *dir = user_data; if (event_type == G_FILE_MONITOR_EVENT_CREATED) { dir->items = g_slist_insert_sorted (dir->items, g_object_ref (file), (GCompareFunc) compare_basename); trash_root_add_item (dir->root, file, dir->topdir, dir->is_homedir); } else if (event_type == G_FILE_MONITOR_EVENT_DELETED) { GSList *node; node = g_slist_find_custom (dir->items, file, (GCompareFunc) compare_basename); if (node) { g_object_unref (node->data); dir->items = g_slist_delete_link (dir->items, node); } trash_root_remove_item (dir->root, file, dir->is_homedir); } else if (event_type == G_FILE_MONITOR_EVENT_PRE_UNMOUNT || event_type == G_FILE_MONITOR_EVENT_UNMOUNTED || event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) ; else { static gboolean already_did_warning; char *dirname; char *name; g_warning ("*** Unsupported operation detected on trash directory"); if (!already_did_warning) { g_warning (" A trash files/ directory should only have files " "linked or unlinked (via moves or deletes). Some other " "operation has been detected on a file in the directory " "(eg: a file has been modified). Likely, the data " "reported by the trash backend will now be " "inconsistent."); already_did_warning = TRUE; } name = file ? g_file_get_basename (file) : NULL; dirname = g_file_get_path (dir->directory); g_warning (" dir: %s, file: %s, type: %d\n\n", dirname, name, event_type); g_free (dirname); g_free (name); } trash_root_thaw (dir->root); } static void trash_dir_created (gpointer user_data) { TrashDir *dir = user_data; g_assert (dir->monitor == NULL); dir->monitor = g_file_monitor_directory (dir->directory, 0, NULL, NULL); g_signal_connect (dir->monitor, "changed", G_CALLBACK (trash_dir_changed), dir); trash_dir_enumerate (dir); } static void trash_dir_check (gpointer user_data) { TrashDir *dir = user_data; trash_dir_enumerate (dir); } static void trash_dir_destroyed (gpointer user_data) { TrashDir *dir = user_data; g_assert (dir->monitor != NULL); g_object_unref (dir->monitor); dir->monitor = NULL; g_clear_pointer (&dir->mtime, g_date_time_unref); trash_dir_empty (dir); } void trash_dir_watch (TrashDir *dir) { g_assert (dir->monitor == NULL); g_assert (dir->watch == NULL); /* start monitoring after a period of not monitoring. * * there are two possible cases here: * 1) the directory now exists * - we have to rescan the directory to ensure that we notice * any changes that have occured since we last looked * * 2) the directory does not exist * - if it existed last time we looked then we may have stale * toplevel items that need to be removed. * * in case 1, trash_dir_created() will be called from * dir_watch_new(). it calls trash_enumerate() itself. * * in case 2, no other function will be called and we must manually * call trash_dir_empty(). * * we can tell if case 1 happened because trash_dir_created() also * sets the dir->monitor. */ dir->watch = dir_watch_new (dir->directory, dir->topdir, trash_dir_created, trash_dir_check, trash_dir_destroyed, dir); if (dir->monitor == NULL) /* case 2 */ trash_dir_empty (dir); } void trash_dir_unwatch (TrashDir *dir) { g_assert (dir->watch != NULL); /* stop monitoring. * * in all cases, we just fall silent. */ if (dir->monitor != NULL) { g_object_unref (dir->monitor); dir->monitor = NULL; } dir_watch_free (dir->watch); dir->watch = NULL; } static gboolean dir_exists (GFile *directory, GFile *top_dir) { gboolean result = FALSE; GFile *parent; if (g_file_equal (directory, top_dir)) return TRUE; parent = g_file_get_parent (directory); if (dir_exists (parent, top_dir)) { struct stat buf; gchar *path; path = g_file_get_path (directory); result = !lstat (path, &buf) && S_ISDIR (buf.st_mode); g_free (path); } g_object_unref (parent); return result; } static gboolean trash_dir_is_dirty (TrashDir *dir) { GDateTime *mtime; gboolean retval = TRUE; if (dir->mtime == NULL) return TRUE; mtime = trash_dir_query_mtime (dir); if (mtime == NULL) return TRUE; if (g_date_time_equal (mtime, dir->mtime)) retval = FALSE; g_date_time_unref (mtime); return retval; } void trash_dir_rescan (TrashDir *dir) { if (!trash_dir_is_dirty (dir)) return; if (dir->watch) dir_watch_check (dir->watch); else if (dir_exists (dir->directory, dir->topdir)) trash_dir_enumerate (dir); else trash_dir_empty (dir); } static trash_dir_ui_hook ui_hook; TrashDir * trash_dir_new (TrashRoot *root, gboolean watching, gboolean is_homedir, const char *mount_point, const char *format, ...) { TrashDir *dir; va_list ap; char *rel; va_start (ap, format); rel = g_strdup_vprintf (format, ap); va_end (ap); dir = g_slice_new (TrashDir); dir->root = root; dir->items = NULL; dir->topdir = g_file_new_for_path (mount_point); dir->directory = g_file_get_child (dir->topdir, rel); dir->monitor = NULL; dir->is_homedir = is_homedir; dir->mtime = NULL; if (watching) dir->watch = dir_watch_new (dir->directory, dir->topdir, trash_dir_created, trash_dir_check, trash_dir_destroyed, dir); else dir->watch = NULL; if (ui_hook) ui_hook (dir, dir->directory); g_free (rel); return dir; } void trash_dir_set_ui_hook (trash_dir_ui_hook _ui_hook) { ui_hook = _ui_hook; } void trash_dir_free (TrashDir *dir) { if (dir->watch) dir_watch_free (dir->watch); if (dir->monitor) g_object_unref (dir->monitor); trash_dir_set_files (dir, NULL); g_object_unref (dir->directory); g_object_unref (dir->topdir); g_slice_free (TrashDir, dir); }