Line data Source code
1 : /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
2 : /* egg-file-tracker.c - Watch for changes in a directory
3 :
4 : Copyright (C) 2008 Stefan Walter
5 :
6 : The Gnome Keyring Library is free software; you can redistribute it and/or
7 : modify it under the terms of the GNU Library General Public License as
8 : published by the Free Software Foundation; either version 2 of the
9 : License, or (at your option) any later version.
10 :
11 : The Gnome Keyring Library is distributed in the hope that it will be useful,
12 : but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 : Library General Public License for more details.
15 :
16 : You should have received a copy of the GNU Library General Public
17 : License along with the Gnome Library; see the file COPYING.LIB. If not,
18 : <http://www.gnu.org/licenses/>.
19 :
20 : Author: Stef Walter <stef@memberwebs.com>
21 : */
22 :
23 : #include "config.h"
24 :
25 : #include "egg-file-tracker.h"
26 :
27 : #include "egg/egg-error.h"
28 :
29 : #include <glib.h>
30 : #include <glib/gstdio.h>
31 :
32 : #include <sys/stat.h>
33 : #include <errno.h>
34 : #include <unistd.h>
35 :
36 : typedef struct _UpdateDescendants {
37 : EggFileTracker *tracker;
38 : GHashTable *checks;
39 : } UpdateDescendants;
40 :
41 : struct _EggFileTracker {
42 : GObject parent;
43 :
44 : /* Specification */
45 : GPatternSpec *include;
46 : GPatternSpec *exclude;
47 : gchar *directory_path;
48 : time_t directory_mtime;
49 :
50 : /* Matched files */
51 : GHashTable *files;
52 : };
53 :
54 : enum {
55 : FILE_ADDED,
56 : FILE_REMOVED,
57 : FILE_CHANGED,
58 : LAST_SIGNAL
59 : };
60 :
61 : static guint signals[LAST_SIGNAL] = { 0 };
62 :
63 1088 : G_DEFINE_TYPE (EggFileTracker, egg_file_tracker, G_TYPE_OBJECT);
64 :
65 : /* -----------------------------------------------------------------------------
66 : * HELPERS
67 : */
68 :
69 : static void
70 192 : copy_key_string (gpointer key, gpointer value, gpointer data)
71 : {
72 192 : GHashTable *dest = (GHashTable*)data;
73 192 : g_hash_table_replace (dest, g_strdup (key), value);
74 192 : }
75 :
76 : static void
77 4 : remove_files (gpointer key, gpointer value, gpointer data)
78 : {
79 4 : EggFileTracker *self = EGG_FILE_TRACKER (data);
80 :
81 4 : g_hash_table_remove (self->files, key);
82 4 : g_signal_emit (self, signals[FILE_REMOVED], 0, key);
83 4 : }
84 :
85 : static gboolean
86 192 : update_file (EggFileTracker *self, gboolean force_all, const gchar *path)
87 : {
88 : time_t old_mtime;
89 : struct stat sb;
90 :
91 192 : if (stat (path, &sb) < 0) {
92 4 : if (errno != ENOENT && errno != ENOTDIR && errno != EPERM)
93 0 : g_warning ("couldn't stat file: %s: %s", path, g_strerror (errno));
94 4 : return FALSE;
95 : }
96 :
97 188 : old_mtime = GPOINTER_TO_UINT (g_hash_table_lookup (self->files, path));
98 188 : g_assert (old_mtime);
99 :
100 : /* See if it has actually changed */
101 188 : if (force_all || old_mtime != sb.st_mtime) {
102 4 : g_assert (g_hash_table_lookup (self->files, path));
103 8 : g_hash_table_insert (self->files, g_strdup (path), GUINT_TO_POINTER (sb.st_mtime));
104 4 : g_signal_emit (self, signals[FILE_CHANGED], 0, path);
105 : }
106 :
107 188 : return TRUE;
108 : }
109 :
110 : static void
111 190 : update_each_file (gpointer key, gpointer unused, gpointer data)
112 : {
113 190 : UpdateDescendants *ctx = (UpdateDescendants*)data;
114 190 : if (update_file (ctx->tracker, FALSE, key))
115 186 : g_hash_table_remove (ctx->checks, key);
116 190 : }
117 :
118 : static void
119 244 : update_directory (EggFileTracker *self, gboolean force_all, GHashTable *checks)
120 : {
121 : UpdateDescendants uctx;
122 : struct stat sb;
123 244 : GError *err = NULL;
124 : const char *filename;
125 : gchar *file;
126 : GDir *dir;
127 : int ret, lasterr;
128 :
129 244 : g_assert (checks);
130 244 : g_assert (EGG_IS_FILE_TRACKER (self));
131 :
132 244 : if (!self->directory_path)
133 179 : return;
134 :
135 244 : if (stat (self->directory_path, &sb) < 0) {
136 1 : if (errno != ENOENT && errno != ENOTDIR && errno != EPERM)
137 0 : g_message ("couldn't stat directory: %s: %s",
138 : self->directory_path, g_strerror (errno));
139 1 : return;
140 : }
141 :
142 : /* See if it was updated since last seen or not */
143 243 : if (!force_all && self->directory_mtime == sb.st_mtime) {
144 :
145 178 : uctx.checks = checks;
146 178 : uctx.tracker = self;
147 :
148 : /* Still need to check for individual file updates */
149 178 : g_hash_table_foreach (self->files, update_each_file, &uctx);
150 178 : return;
151 : }
152 :
153 65 : self->directory_mtime = sb.st_mtime;
154 :
155 : /* Actually list the directory */
156 65 : dir = g_dir_open (self->directory_path, 0, &err);
157 65 : if (dir == NULL) {
158 0 : if (errno != ENOENT && errno != ENOTDIR && errno != EPERM)
159 0 : g_message ("couldn't list keyrings at: %s: %s", self->directory_path,
160 : egg_error_message (err));
161 0 : g_error_free (err);
162 0 : return;
163 : }
164 :
165 203 : while ((filename = g_dir_read_name (dir)) != NULL) {
166 138 : if (filename[0] == '.')
167 0 : continue;
168 138 : if (self->include && !g_pattern_match_string (self->include, filename))
169 24 : continue;
170 114 : if (self->exclude && g_pattern_match_string (self->exclude, filename))
171 0 : continue;
172 :
173 114 : file = g_build_filename (self->directory_path, filename, NULL);
174 :
175 : /* If we hadn't yet seen this, then add it */
176 114 : if (!g_hash_table_remove (checks, file)) {
177 :
178 : /* Get the last modified time for this one */
179 112 : ret = g_stat (file, &sb);
180 112 : lasterr = errno;
181 :
182 : /* Couldn't access the file */
183 112 : if (ret < 0) {
184 0 : g_message ("couldn't stat file: %s: %s", file, g_strerror (lasterr));
185 :
186 : } else {
187 :
188 : /* We don't do directories */
189 112 : if (!(sb.st_mode & S_IFDIR)) {
190 224 : g_hash_table_replace (self->files, g_strdup (file), GINT_TO_POINTER (sb.st_mtime));
191 112 : g_signal_emit (self, signals[FILE_ADDED], 0, file);
192 : }
193 : }
194 :
195 : /* Otherwise we already had it, see if it needs updating */
196 : } else {
197 2 : update_file (self, force_all, file);
198 : }
199 :
200 114 : g_free (file);
201 : }
202 :
203 65 : g_dir_close (dir);
204 : }
205 :
206 : /* -----------------------------------------------------------------------------
207 : * OBJECT
208 : */
209 :
210 : static void
211 203 : egg_file_tracker_init (EggFileTracker *self)
212 : {
213 203 : self->files = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
214 203 : }
215 :
216 : static void
217 203 : egg_file_tracker_finalize (GObject *obj)
218 : {
219 203 : EggFileTracker *self = EGG_FILE_TRACKER (obj);
220 :
221 203 : if (self->include)
222 203 : g_pattern_spec_free (self->include);
223 203 : if (self->exclude)
224 0 : g_pattern_spec_free (self->exclude);
225 203 : g_free (self->directory_path);
226 :
227 203 : g_hash_table_destroy (self->files);
228 :
229 203 : G_OBJECT_CLASS (egg_file_tracker_parent_class)->finalize (obj);
230 203 : }
231 :
232 : static void
233 38 : egg_file_tracker_class_init (EggFileTrackerClass *klass)
234 : {
235 : GObjectClass *gobject_class;
236 38 : gobject_class = (GObjectClass*) klass;
237 :
238 38 : egg_file_tracker_parent_class = g_type_class_peek_parent (klass);
239 38 : gobject_class->finalize = egg_file_tracker_finalize;
240 :
241 38 : signals[FILE_ADDED] = g_signal_new ("file-added", EGG_TYPE_FILE_TRACKER,
242 : G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EggFileTrackerClass, file_added),
243 : NULL, NULL, g_cclosure_marshal_VOID__STRING,
244 : G_TYPE_NONE, 1, G_TYPE_STRING);
245 :
246 38 : signals[FILE_CHANGED] = g_signal_new ("file-changed", EGG_TYPE_FILE_TRACKER,
247 : G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EggFileTrackerClass, file_changed),
248 : NULL, NULL, g_cclosure_marshal_VOID__STRING,
249 : G_TYPE_NONE, 1, G_TYPE_STRING);
250 :
251 38 : signals[FILE_REMOVED] = g_signal_new ("file-removed", EGG_TYPE_FILE_TRACKER,
252 : G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (EggFileTrackerClass, file_removed),
253 : NULL, NULL, g_cclosure_marshal_VOID__STRING,
254 : G_TYPE_NONE, 1, G_TYPE_STRING);
255 38 : }
256 :
257 : EggFileTracker*
258 203 : egg_file_tracker_new (const gchar *directory, const gchar *include, const gchar *exclude)
259 : {
260 : EggFileTracker *self;
261 : const gchar *homedir;
262 :
263 203 : g_return_val_if_fail (directory, NULL);
264 :
265 203 : self = g_object_new (EGG_TYPE_FILE_TRACKER, NULL);
266 :
267 : /* TODO: Use properties */
268 :
269 203 : if (directory[0] == '~' && directory[1] == '/') {
270 5 : homedir = g_getenv ("HOME");
271 5 : if (!homedir)
272 0 : homedir = g_get_home_dir ();
273 5 : self->directory_path = g_build_filename (homedir, directory + 2, NULL);
274 :
275 : /* A relative or absolute path */
276 : } else {
277 198 : self->directory_path = g_strdup (directory);
278 : }
279 :
280 203 : self->include = include ? g_pattern_spec_new (include) : NULL;
281 203 : self->exclude = exclude ? g_pattern_spec_new (exclude) : NULL;
282 :
283 203 : return self;
284 : }
285 :
286 : void
287 244 : egg_file_tracker_refresh (EggFileTracker *self, gboolean force_all)
288 : {
289 : GHashTable *checks;
290 :
291 244 : g_return_if_fail (EGG_IS_FILE_TRACKER (self));
292 :
293 : /* Copy into our check set */
294 244 : checks = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
295 244 : g_hash_table_foreach (self->files, copy_key_string, checks);
296 :
297 : /* If only one volume, then just try and access it directly */
298 244 : update_directory (self, force_all, checks);
299 :
300 : /* Find any keyrings whose paths we didn't see */
301 244 : g_hash_table_foreach (checks, remove_files, self);
302 244 : g_hash_table_destroy (checks);
303 : }
|