gvfs/daemon/trashlib/trashdir.c

427 lines
9.7 KiB
C

/*
* 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 <sys/stat.h>
#include <string.h>
#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)
trash_root_add_item (dir->root, file, dir->topdir, dir->is_homedir);
else if (event_type == G_FILE_MONITOR_EVENT_DELETED)
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);
}