gvfs/daemon/trashlib/dirwatch.c

318 lines
8.6 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 <sys/stat.h>
#include "dirwatch.h"
/* DirWatch
*
* a directory watcher utility for use by the trash:/ backend.
*
* A DirWatch monitors a given directory for existence under a very
* specific set of circumstances. When the directory comes into
* existence, the create() callback is invoked. When the directory
* stops existing the destroy() callback is invoked. If the directory
* initially exists, then create() is invoked before the call to
* dir_watch_new() returns.
*
* The directory to watch is considered to exist only if it is a
* directory (and not a symlink) and its parent directory also exists.
* A topdir must be given, which is always assumed to "exist".
*
* For example, if '/mnt/disk/.Trash/1000/files/' is monitored with
* '/mnt/disk/' as a topdir then the following conditions must be true
* in order for the directory to be reported as existing:
*
* /mnt/disk/ is blindly assumed to exist
* /mnt/disk/.Trash must be a directory (not a symlink)
* /mnt/disk/.Trash/1000 must be a directory (not a symlink)
* /mnt/disk/.Trash/1000/files must be a directory (not a symlink)
*
* If any of these ceases to be true (even momentarily), the directory
* will be reported as having been destroyed. create() and destroy()
* callbacks are never issued spuriously (ie: two calls to one
* callback will never occur in a row). Events where the directory
* exists momentarily might be missed, but events where the directory
* stops existing momentarily will (hopefully) always be reported.
* The first call (if it happens) will always be to create().
*
* check() is only ever called in response to a call to
* dir_watch_check() in which case it will be called only if the
* watched directory was marked as having existed before the check and
* is found to still exist. This facilitates the checking that has to
* occur in that case (ie: check the contents of the directory to make
* sure that they are also unchanged).
*
* This implementation is currently tweaked a bit for how GFileMonitor
* currently works with inotify. If GFileMonitor's implementation is
* changed it might be a good idea to take another look at this code.
*/
struct OPAQUE_TYPE__DirWatch
{
GFile *directory;
GFile *topdir;
DirWatchFunc create;
DirWatchFunc check;
DirWatchFunc destroy;
gpointer user_data;
gboolean state;
DirWatch *parent;
GFileMonitor *parent_monitor;
};
#ifdef DIR_WATCH_DEBUG
# define dir_watch_created(watch) \
G_STMT_START { \
char *path = g_file_get_path ((watch)->directory); \
g_print (">> created '%s'\n", path); \
g_free (path); \
(watch)->create ((watch)->user_data); \
} G_STMT_END
# define dir_watch_destroyed(watch) \
G_STMT_START { \
char *path = g_file_get_path ((watch)->directory); \
g_print (">> destroyed '%s'\n", path); \
g_free (path); \
(watch)->destroy ((watch)->user_data); \
} G_STMT_END
#else
# define dir_watch_created(watch) (watch)->create ((watch)->user_data)
# define dir_watch_destroyed(watch) (watch)->destroy ((watch)->user_data)
#endif
#ifdef DIR_WATCH_DEBUG
#include <errno.h>
#endif
static gboolean
dir_exists (GFile *file)
{
gboolean result;
struct stat buf;
char *path;
path = g_file_get_path (file);
#ifdef DIR_WATCH_DEBUG
errno = 0;
#endif
result = !lstat (path, &buf) && S_ISDIR (buf.st_mode);
#ifdef DIR_WATCH_DEBUG
g_print (" lstat ('%s') -> is%s a directory (%s)\n",
path, result ? "" : " not", g_strerror (errno));
#endif
g_free (path);
return result;
}
static void
dir_watch_parent_changed (GFileMonitor *monitor,
GFile *file,
GFile *other_file,
GFileMonitorEvent event_type,
gpointer user_data)
{
DirWatch *watch = user_data;
g_assert (watch->parent_monitor == monitor);
if (!g_file_equal (file, watch->directory))
return;
if (event_type == G_FILE_MONITOR_EVENT_CREATED)
{
if (watch->state)
return;
/* we were just created. ensure that it's a directory. */
if (dir_exists (file))
{
/* we're official now. report it. */
watch->state = TRUE;
dir_watch_created (watch);
}
}
else if (event_type == G_FILE_MONITOR_EVENT_DELETED)
{
if (!watch->state)
return;
watch->state = FALSE;
dir_watch_destroyed (watch);
}
}
static void
dir_watch_recursive_create (gpointer user_data)
{
DirWatch *watch = user_data;
GFile *parent;
g_assert (watch->parent_monitor == NULL);
parent = g_file_get_parent (watch->directory);
watch->parent_monitor = g_file_monitor_directory (parent, 0,
NULL, NULL);
g_object_unref (parent);
g_signal_connect (watch->parent_monitor, "changed",
G_CALLBACK (dir_watch_parent_changed), watch);
/* check if directory was created before we started to monitor */
if (dir_exists (watch->directory))
{
watch->state = TRUE;
dir_watch_created (watch);
}
}
static void
dir_watch_recursive_check (gpointer user_data)
{
DirWatch *watch = user_data;
gboolean exists;
exists = dir_exists (watch->directory);
if (watch->state && exists)
watch->check (watch->user_data);
else if (!watch->state && exists)
{
watch->state = TRUE;
dir_watch_created (watch);
}
else if (watch->state && !exists)
{
watch->state = FALSE;
dir_watch_destroyed (watch);
}
}
static void
dir_watch_recursive_destroy (gpointer user_data)
{
DirWatch *watch = user_data;
/* exactly one monitor should be active */
g_assert (watch->parent_monitor != NULL);
/* if we were monitoring the directory... */
if (watch->state)
{
dir_watch_destroyed (watch);
watch->state = FALSE;
}
g_file_monitor_cancel (watch->parent_monitor);
g_object_unref (watch->parent_monitor);
watch->parent_monitor = NULL;
}
DirWatch *
dir_watch_new (GFile *directory,
GFile *topdir,
DirWatchFunc create,
DirWatchFunc check,
DirWatchFunc destroy,
gpointer user_data)
{
DirWatch *watch;
watch = g_slice_new0 (DirWatch);
watch->create = create;
watch->check = check;
watch->destroy = destroy;
watch->user_data = user_data;
watch->directory = g_object_ref (directory);
watch->topdir = g_object_ref (topdir);
/* the top directory always exists */
if (g_file_equal (directory, topdir))
{
dir_watch_created (watch);
watch->state = TRUE;
}
else
{
GFile *parent;
parent = g_file_get_parent (directory);
g_assert (parent != NULL);
watch->parent = dir_watch_new (parent, topdir,
dir_watch_recursive_create,
dir_watch_recursive_check,
dir_watch_recursive_destroy,
watch);
g_object_unref (parent);
}
return watch;
}
void
dir_watch_free (DirWatch *watch)
{
if (watch != NULL)
{
if (watch->parent_monitor)
{
g_file_monitor_cancel (watch->parent_monitor);
g_object_unref (watch->parent_monitor);
}
g_object_unref (watch->directory);
g_object_unref (watch->topdir);
dir_watch_free (watch->parent);
g_slice_free (DirWatch, watch);
}
}
/**
* dir_watch_check:
* @watch: a #DirWatch
*
* Emit missed events.
*
* This function is called on a DirWatch that might have missed events
* (because it is watching on an NFS mount, for example).
*
* This function will manually check if any directories have come into
* or gone out of existence and will emit created or destroyed callbacks
* as appropriate.
*
* Additionally, if a directory is found to still exist, the checked
* callback will be emitted.
**/
void
dir_watch_check (DirWatch *watch)
{
if (watch->parent == NULL)
{
g_assert (watch->state);
watch->check (watch->user_data);
return;
}
dir_watch_check (watch->parent);
}