1 /* dzl-directory-reaper.c
2 *
3 * Copyright (C) 2017 Christian Hergert <chergert@redhat.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #define G_LOG_DOMAIN "dzl-directory-reaper"
20
21 #include "config.h"
22
23 #include "files/dzl-directory-reaper.h"
24 #include "util/dzl-macros.h"
25
26 typedef enum
27 {
28 PATTERN_FILE,
29 PATTERN_GLOB,
30 } PatternType;
31
32 typedef struct
33 {
34 PatternType type;
35 GTimeSpan min_age;
36 union {
37 struct {
38 GFile *directory;
39 gchar *glob;
40 } glob;
41 struct {
42 GFile *file;
43 } file;
44 };
45 } Pattern;
46
47 struct _DzlDirectoryReaper
48 {
49 GObject parent_instance;
50 GArray *patterns;
51 };
52
53 G_DEFINE_TYPE (DzlDirectoryReaper, dzl_directory_reaper, G_TYPE_OBJECT)
54
55 enum {
56 REMOVE_FILE,
57 N_SIGNALS
58 };
59
60 static guint signals [N_SIGNALS];
61
62 static gboolean
emit_remove_file_from_main_cb(gpointer data)63 emit_remove_file_from_main_cb (gpointer data)
64 {
65 gpointer *pair = data;
66
67 g_signal_emit (pair[0], signals [REMOVE_FILE], 0, pair[1]);
68 g_object_unref (pair[0]);
69 g_object_unref (pair[1]);
70 g_slice_free1 (sizeof (gpointer) * 2, pair);
71
72 return G_SOURCE_REMOVE;
73 }
74
75 static gboolean
file_delete(DzlDirectoryReaper * self,GFile * file,GCancellable * cancellable,GError ** error)76 file_delete (DzlDirectoryReaper *self,
77 GFile *file,
78 GCancellable *cancellable,
79 GError **error)
80 {
81 gpointer *data = g_slice_alloc (sizeof (gpointer) * 2);
82
83 data[0] = g_object_ref (self);
84 data[1] = g_object_ref (file);
85
86 /* XXX:
87 *
88 * It would be awesome if we didn't round-trip to the main
89 * thread for every one of these files. At least group some
90 * together occasionally.
91 */
92
93 g_idle_add_full (G_PRIORITY_LOW + 1000,
94 emit_remove_file_from_main_cb,
95 data, NULL);
96
97 return g_file_delete (file, cancellable, error);
98 }
99
100 static void
clear_pattern(gpointer data)101 clear_pattern (gpointer data)
102 {
103 Pattern *p = data;
104
105 switch (p->type)
106 {
107 case PATTERN_GLOB:
108 g_clear_object (&p->glob.directory);
109 g_clear_pointer (&p->glob.glob, g_free);
110 break;
111
112 case PATTERN_FILE:
113 g_clear_object (&p->file.file);
114 break;
115
116 default:
117 g_assert_not_reached ();
118 }
119 }
120
121 static void
dzl_directory_reaper_finalize(GObject * object)122 dzl_directory_reaper_finalize (GObject *object)
123 {
124 DzlDirectoryReaper *self = (DzlDirectoryReaper *)object;
125
126 g_clear_pointer (&self->patterns, g_array_unref);
127
128 G_OBJECT_CLASS (dzl_directory_reaper_parent_class)->finalize (object);
129 }
130
131 static void
dzl_directory_reaper_class_init(DzlDirectoryReaperClass * klass)132 dzl_directory_reaper_class_init (DzlDirectoryReaperClass *klass)
133 {
134 GObjectClass *object_class = G_OBJECT_CLASS (klass);
135
136 object_class->finalize = dzl_directory_reaper_finalize;
137
138 /**
139 * DzlDirectoryReaper::remove-file:
140 * @self: a #DzlDirectoryReaper
141 * @file: a #GFile
142 *
143 * The "remove-file" signal is emitted for each file that is removed by the
144 * #DzlDirectoryReaper instance. This may be useful if you want to show the
145 * user what was processed by the reaper.
146 *
147 * Since: 3.32
148 */
149 signals [REMOVE_FILE] =
150 g_signal_new_class_handler ("remove-file",
151 G_TYPE_FROM_CLASS (klass),
152 G_SIGNAL_RUN_LAST,
153 NULL,
154 NULL, NULL,
155 g_cclosure_marshal_VOID__OBJECT,
156 G_TYPE_NONE, 1, G_TYPE_FILE);
157 g_signal_set_va_marshaller (signals [REMOVE_FILE],
158 G_TYPE_FROM_CLASS (klass),
159 g_cclosure_marshal_VOID__OBJECTv);
160 }
161
162 static void
dzl_directory_reaper_init(DzlDirectoryReaper * self)163 dzl_directory_reaper_init (DzlDirectoryReaper *self)
164 {
165 self->patterns = g_array_new (FALSE, FALSE, sizeof (Pattern));
166 g_array_set_clear_func (self->patterns, clear_pattern);
167 }
168
169 void
dzl_directory_reaper_add_directory(DzlDirectoryReaper * self,GFile * directory,GTimeSpan min_age)170 dzl_directory_reaper_add_directory (DzlDirectoryReaper *self,
171 GFile *directory,
172 GTimeSpan min_age)
173 {
174 g_return_if_fail (DZL_IS_DIRECTORY_REAPER (self));
175 g_return_if_fail (G_IS_FILE (directory));
176
177 dzl_directory_reaper_add_glob (self, directory, NULL, min_age);
178 }
179
180 void
dzl_directory_reaper_add_glob(DzlDirectoryReaper * self,GFile * directory,const gchar * glob,GTimeSpan min_age)181 dzl_directory_reaper_add_glob (DzlDirectoryReaper *self,
182 GFile *directory,
183 const gchar *glob,
184 GTimeSpan min_age)
185 {
186 Pattern p = { 0 };
187
188 g_return_if_fail (DZL_IS_DIRECTORY_REAPER (self));
189 g_return_if_fail (G_IS_FILE (directory));
190
191 if (glob == NULL)
192 glob = "*";
193
194 p.type = PATTERN_GLOB;
195 p.min_age = ABS (min_age);
196 p.glob.directory = g_object_ref (directory);
197 p.glob.glob = g_strdup (glob);
198
199 g_array_append_val (self->patterns, p);
200 }
201
202 void
dzl_directory_reaper_add_file(DzlDirectoryReaper * self,GFile * file,GTimeSpan min_age)203 dzl_directory_reaper_add_file (DzlDirectoryReaper *self,
204 GFile *file,
205 GTimeSpan min_age)
206 {
207 Pattern p = { 0 };
208
209 g_return_if_fail (DZL_IS_DIRECTORY_REAPER (self));
210 g_return_if_fail (G_IS_FILE (file));
211
212 p.type = PATTERN_FILE;
213 p.min_age = ABS (min_age);
214 p.file.file = g_object_ref (file);
215
216 g_array_append_val (self->patterns, p);
217 }
218
219 DzlDirectoryReaper *
dzl_directory_reaper_new(void)220 dzl_directory_reaper_new (void)
221 {
222 return g_object_new (DZL_TYPE_DIRECTORY_REAPER, NULL);
223 }
224
225 static gboolean
remove_directory_with_children(DzlDirectoryReaper * self,GFile * file,GCancellable * cancellable,GError ** error)226 remove_directory_with_children (DzlDirectoryReaper *self,
227 GFile *file,
228 GCancellable *cancellable,
229 GError **error)
230 {
231 g_autoptr(GFileEnumerator) enumerator = NULL;
232 g_autoptr(GError) enum_error = NULL;
233 g_autofree gchar *uri = NULL;
234 gpointer infoptr;
235
236 g_assert (G_IS_FILE (file));
237 g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
238
239 uri = g_file_get_uri (file);
240 g_debug ("Removing uri recursively \"%s\"", uri);
241
242 enumerator = g_file_enumerate_children (file,
243 G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK","
244 G_FILE_ATTRIBUTE_STANDARD_NAME","
245 G_FILE_ATTRIBUTE_STANDARD_TYPE","
246 G_FILE_ATTRIBUTE_TIME_MODIFIED,
247 G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
248 cancellable,
249 &enum_error);
250
251
252 if (enumerator == NULL)
253 {
254 /* If the directory does not exist, nothing to do */
255 if (g_error_matches (enum_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
256 return TRUE;
257 g_propagate_error (error, g_steal_pointer (&enum_error));
258 return FALSE;
259 }
260
261 g_assert (enum_error == NULL);
262
263 while (NULL != (infoptr = g_file_enumerator_next_file (enumerator, cancellable, &enum_error)))
264 {
265 g_autoptr(GFileInfo) info = infoptr;
266 g_autoptr(GFile) child = g_file_enumerator_get_child (enumerator, info);
267 GFileType file_type = g_file_info_get_file_type (info);
268
269 if (!g_file_info_get_is_symlink (info) && file_type == G_FILE_TYPE_DIRECTORY)
270 {
271 if (!remove_directory_with_children (self, child, cancellable, error))
272 return FALSE;
273 }
274
275 if (!file_delete (self, child, cancellable, error))
276 return FALSE;
277 }
278
279 if (enum_error != NULL)
280 {
281 g_propagate_error (error, g_steal_pointer (&enum_error));
282 return FALSE;
283 }
284
285 if (!g_file_enumerator_close (enumerator, cancellable, error))
286 return FALSE;
287
288 return TRUE;
289 }
290
291 static void
dzl_directory_reaper_execute_worker(GTask * task,gpointer source_object,gpointer task_data,GCancellable * cancellable)292 dzl_directory_reaper_execute_worker (GTask *task,
293 gpointer source_object,
294 gpointer task_data,
295 GCancellable *cancellable)
296 {
297 DzlDirectoryReaper *self;
298 GArray *patterns = task_data;
299 gint64 now = g_get_real_time ();
300
301 g_assert (G_IS_TASK (task));
302 g_assert (DZL_IS_DIRECTORY_REAPER (source_object));
303 g_assert (patterns != NULL);
304 g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
305
306 self = g_task_get_source_object (task);
307
308 for (guint i = 0; i < patterns->len; i++)
309 {
310 const Pattern *p = &g_array_index (patterns, Pattern, i);
311 g_autoptr(GFileInfo) info = NULL;
312 g_autoptr(GFileInfo) dir_info = NULL;
313 g_autoptr(GPatternSpec) spec = NULL;
314 g_autoptr(GFileEnumerator) enumerator = NULL;
315 g_autoptr(GError) error = NULL;
316 guint64 v64;
317
318 switch (p->type)
319 {
320 case PATTERN_FILE:
321
322 info = g_file_query_info (p->file.file,
323 G_FILE_ATTRIBUTE_TIME_MODIFIED,
324 G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
325 cancellable,
326 &error);
327
328 if (info == NULL)
329 {
330 if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
331 g_warning ("%s", error->message);
332 break;
333 }
334
335 v64 = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
336
337 /* mtime is in seconds */
338 v64 *= G_USEC_PER_SEC;
339
340 if (v64 < now - p->min_age)
341 {
342 if (!file_delete (self, p->file.file, cancellable, &error))
343 g_warning ("%s", error->message);
344 }
345
346 break;
347
348 case PATTERN_GLOB:
349
350 spec = g_pattern_spec_new (p->glob.glob);
351
352 if (spec == NULL)
353 {
354 g_warning ("Invalid pattern spec \"%s\"", p->glob.glob);
355 break;
356 }
357
358 dir_info = g_file_query_info (p->glob.directory,
359 G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK","
360 G_FILE_ATTRIBUTE_STANDARD_TYPE",",
361 G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
362 cancellable,
363 &error);
364
365 if (dir_info == NULL)
366 {
367 if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
368 g_warning ("%s", error->message);
369 break;
370 }
371
372 /* Do not follow through symlinks. */
373 if (g_file_info_get_is_symlink (dir_info) ||
374 g_file_info_get_file_type (dir_info) != G_FILE_TYPE_DIRECTORY)
375 break;
376
377 enumerator = g_file_enumerate_children (p->glob.directory,
378 G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK","
379 G_FILE_ATTRIBUTE_STANDARD_NAME","
380 G_FILE_ATTRIBUTE_STANDARD_TYPE","
381 G_FILE_ATTRIBUTE_TIME_MODIFIED,
382 G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
383 cancellable,
384 &error);
385
386 if (enumerator == NULL)
387 {
388 if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
389 g_warning ("%s", error->message);
390 break;
391 }
392
393 while (NULL != (info = g_file_enumerator_next_file (enumerator, cancellable, NULL)))
394 {
395 v64 = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
396
397 /* mtime is in seconds */
398 v64 *= G_USEC_PER_SEC;
399
400 if (v64 < now - p->min_age)
401 {
402 g_autoptr(GFile) file = g_file_enumerator_get_child (enumerator, info);
403 GFileType file_type = g_file_info_get_file_type (info);
404
405 if (g_file_info_get_is_symlink (info) || file_type != G_FILE_TYPE_DIRECTORY)
406 {
407 if (!file_delete (self, file, cancellable, &error))
408 {
409 g_warning ("%s", error->message);
410 g_clear_error (&error);
411 }
412 }
413 else
414 {
415 g_assert (file_type == G_FILE_TYPE_DIRECTORY);
416
417 if (!remove_directory_with_children (self, file, cancellable, &error) ||
418 !file_delete (self, file, cancellable, &error))
419 {
420 g_warning ("%s", error->message);
421 g_clear_error (&error);
422 }
423 }
424 }
425
426 g_clear_object (&info);
427 }
428
429 break;
430
431 default:
432 g_assert_not_reached ();
433 }
434 }
435
436 g_task_return_boolean (task, TRUE);
437 }
438
439 static GArray *
dzl_directory_reaper_copy_state(DzlDirectoryReaper * self)440 dzl_directory_reaper_copy_state (DzlDirectoryReaper *self)
441 {
442 g_autoptr(GArray) copy = NULL;
443
444 g_assert (DZL_IS_DIRECTORY_REAPER (self));
445 g_assert (self->patterns != NULL);
446
447 copy = g_array_new (FALSE, FALSE, sizeof (Pattern));
448 g_array_set_clear_func (copy, clear_pattern);
449
450 for (guint i = 0; i < self->patterns->len; i++)
451 {
452 Pattern p = g_array_index (self->patterns, Pattern, i);
453
454 switch (p.type)
455 {
456 case PATTERN_GLOB:
457 p.glob.directory = g_object_ref (p.glob.directory);
458 p.glob.glob = g_strdup (p.glob.glob);
459 break;
460
461 case PATTERN_FILE:
462 p.file.file = g_object_ref (p.file.file);
463 break;
464
465 default:
466 g_assert_not_reached ();
467 }
468
469 g_array_append_val (copy, p);
470 }
471
472 return g_steal_pointer (©);
473 }
474
475 void
dzl_directory_reaper_execute_async(DzlDirectoryReaper * self,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)476 dzl_directory_reaper_execute_async (DzlDirectoryReaper *self,
477 GCancellable *cancellable,
478 GAsyncReadyCallback callback,
479 gpointer user_data)
480 {
481 g_autoptr(GTask) task = NULL;
482 g_autoptr(GArray) copy = NULL;
483
484 g_return_if_fail (DZL_IS_DIRECTORY_REAPER (self));
485 g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
486
487 copy = dzl_directory_reaper_copy_state (self);
488
489 task = g_task_new (self, cancellable, callback, user_data);
490 g_task_set_source_tag (task, dzl_directory_reaper_execute_async);
491 g_task_set_task_data (task, g_steal_pointer (©), (GDestroyNotify)g_array_unref);
492 g_task_set_priority (task, G_PRIORITY_LOW + 1000);
493 g_task_run_in_thread (task, dzl_directory_reaper_execute_worker);
494 }
495
496 gboolean
dzl_directory_reaper_execute_finish(DzlDirectoryReaper * self,GAsyncResult * result,GError ** error)497 dzl_directory_reaper_execute_finish (DzlDirectoryReaper *self,
498 GAsyncResult *result,
499 GError **error)
500 {
501 g_return_val_if_fail (DZL_IS_DIRECTORY_REAPER (self), FALSE);
502 g_return_val_if_fail (G_IS_TASK (result), FALSE);
503
504 return g_task_propagate_boolean (G_TASK (result), error);
505 }
506
507 gboolean
dzl_directory_reaper_execute(DzlDirectoryReaper * self,GCancellable * cancellable,GError ** error)508 dzl_directory_reaper_execute (DzlDirectoryReaper *self,
509 GCancellable *cancellable,
510 GError **error)
511 {
512 g_autoptr(GTask) task = NULL;
513 g_autoptr(GArray) copy = NULL;
514
515 g_return_val_if_fail (DZL_IS_DIRECTORY_REAPER (self), FALSE);
516 g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
517
518 copy = dzl_directory_reaper_copy_state (self);
519
520 task = g_task_new (self, cancellable, NULL, NULL);
521 g_task_set_source_tag (task, dzl_directory_reaper_execute);
522 g_task_set_task_data (task, g_steal_pointer (©), (GDestroyNotify)g_array_unref);
523 g_task_run_in_thread_sync (task, dzl_directory_reaper_execute_worker);
524
525 return g_task_propagate_boolean (task, error);
526 }
527