/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ /* egg-file-tracker.c - Watch for changes in a directory Copyright (C) 2008 Stefan Walter The Gnome Keyring Library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. The Gnome Keyring Library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with the Gnome Library; see the file COPYING.LIB. If not, . Author: Stef Walter */ #include "config.h" #include "egg-file-tracker.h" #include "egg/egg-error.h" #include #include #include #include #include typedef struct _UpdateDescendants { EggFileTracker *tracker; GHashTable *checks; } UpdateDescendants; struct _EggFileTracker { GObject parent; /* Specification */ GPatternSpec *include; GPatternSpec *exclude; gchar *directory_path; time_t directory_mtime; /* Matched files */ GHashTable *files; }; enum { FILE_ADDED, FILE_REMOVED, FILE_CHANGED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0 }; G_DEFINE_TYPE (EggFileTracker, egg_file_tracker, G_TYPE_OBJECT); /* ----------------------------------------------------------------------------- * HELPERS */ static void copy_key_string (gpointer key, gpointer value, gpointer data) { GHashTable *dest = (GHashTable*)data; g_hash_table_replace (dest, g_strdup (key), value); } static void remove_files (gpointer key, gpointer value, gpointer data) { EggFileTracker *self = EGG_FILE_TRACKER (data); g_hash_table_remove (self->files, key); g_signal_emit (self, signals[FILE_REMOVED], 0, key); } static gboolean update_file (EggFileTracker *self, gboolean force_all, const gchar *path) { time_t old_mtime; struct stat sb; if (stat (path, &sb) < 0) { if (errno != ENOENT && errno != ENOTDIR && errno != EPERM) g_warning ("couldn't stat file: %s: %s", path, g_strerror (errno)); return FALSE; } old_mtime = GPOINTER_TO_UINT (g_hash_table_lookup (self->files, path)); g_assert (old_mtime); /* See if it has actually changed */ if (force_all || old_mtime != sb.st_mtime) { g_assert (g_hash_table_lookup (self->files, path)); g_hash_table_insert (self->files, g_strdup (path), GUINT_TO_POINTER (sb.st_mtime)); g_signal_emit (self, signals[FILE_CHANGED], 0, path); } return TRUE; } static void update_each_file (gpointer key, gpointer unused, gpointer data) { UpdateDescendants *ctx = (UpdateDescendants*)data; if (update_file (ctx->tracker, FALSE, key)) g_hash_table_remove (ctx->checks, key); } static void update_directory (EggFileTracker *self, gboolean force_all, GHashTable *checks) { UpdateDescendants uctx; struct stat sb; GError *err = NULL; const char *filename; gchar *file; GDir *dir; int ret, lasterr; g_assert (checks); g_assert (EGG_IS_FILE_TRACKER (self)); if (!self->directory_path) return; if (stat (self->directory_path, &sb) < 0) { if (errno != ENOENT && errno != ENOTDIR && errno != EPERM) g_message ("couldn't stat directory: %s: %s", self->directory_path, g_strerror (errno)); return; } /* See if it was updated since last seen or not */ if (!force_all && self->directory_mtime == sb.st_mtime) { uctx.checks = checks; uctx.tracker = self; /* Still need to check for individual file updates */ g_hash_table_foreach (self->files, update_each_file, &uctx); return; } self->directory_mtime = sb.st_mtime; /* Actually list the directory */ dir = g_dir_open (self->directory_path, 0, &err); if (dir == NULL) { if (errno != ENOENT && errno != ENOTDIR && errno != EPERM) g_message ("couldn't list keyrings at: %s: %s", self->directory_path, egg_error_message (err)); g_error_free (err); return; } while ((filename = g_dir_read_name (dir)) != NULL) { if (filename[0] == '.') continue; if (self->include && !g_pattern_match_string (self->include, filename)) continue; if (self->exclude && g_pattern_match_string (self->exclude, filename)) continue; file = g_build_filename (self->directory_path, filename, NULL); /* If we hadn't yet seen this, then add it */ if (!g_hash_table_remove (checks, file)) { /* Get the last modified time for this one */ ret = g_stat (file, &sb); lasterr = errno; /* Couldn't access the file */ if (ret < 0) { g_message ("couldn't stat file: %s: %s", file, g_strerror (lasterr)); } else { /* We don't do directories */ if (!(sb.st_mode & S_IFDIR)) { g_hash_table_replace (self->files, g_strdup (file), GINT_TO_POINTER (sb.st_mtime)); g_signal_emit (self, signals[FILE_ADDED], 0, file); } } /* Otherwise we already had it, see if it needs updating */ } else { update_file (self, force_all, file); } g_free (file); } g_dir_close (dir); } /* ----------------------------------------------------------------------------- * OBJECT */ static void egg_file_tracker_init (EggFileTracker *self) { self->files = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); } static void egg_file_tracker_finalize (GObject *obj) { EggFileTracker *self = EGG_FILE_TRACKER (obj); if (self->include) g_pattern_spec_free (self->include); if (self->exclude) g_pattern_spec_free (self->exclude); g_free (self->directory_path); g_hash_table_destroy (self->files); G_OBJECT_CLASS (egg_file_tracker_parent_class)->finalize (obj); } static void egg_file_tracker_class_init (EggFileTrackerClass *klass) { GObjectClass *gobject_class; gobject_class = (GObjectClass*) klass; egg_file_tracker_parent_class = g_type_class_peek_parent (klass); gobject_class->finalize = egg_file_tracker_finalize; signals[FILE_ADDED] = g_signal_new ("file-added", EGG_TYPE_FILE_TRACKER, G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EggFileTrackerClass, file_added), NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); signals[FILE_CHANGED] = g_signal_new ("file-changed", EGG_TYPE_FILE_TRACKER, G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EggFileTrackerClass, file_changed), NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); signals[FILE_REMOVED] = g_signal_new ("file-removed", EGG_TYPE_FILE_TRACKER, G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EggFileTrackerClass, file_removed), NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); } EggFileTracker* egg_file_tracker_new (const gchar *directory, const gchar *include, const gchar *exclude) { EggFileTracker *self; const gchar *homedir; g_return_val_if_fail (directory, NULL); self = g_object_new (EGG_TYPE_FILE_TRACKER, NULL); /* TODO: Use properties */ if (directory[0] == '~' && directory[1] == '/') { homedir = g_getenv ("HOME"); if (!homedir) homedir = g_get_home_dir (); self->directory_path = g_build_filename (homedir, directory + 2, NULL); /* A relative or absolute path */ } else { self->directory_path = g_strdup (directory); } self->include = include ? g_pattern_spec_new (include) : NULL; self->exclude = exclude ? g_pattern_spec_new (exclude) : NULL; return self; } void egg_file_tracker_refresh (EggFileTracker *self, gboolean force_all) { GHashTable *checks; g_return_if_fail (EGG_IS_FILE_TRACKER (self)); /* Copy into our check set */ checks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); g_hash_table_foreach (self->files, copy_key_string, checks); /* If only one volume, then just try and access it directly */ update_directory (self, force_all, checks); /* Find any keyrings whose paths we didn't see */ g_hash_table_foreach (checks, remove_files, self); g_hash_table_destroy (checks); }