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 (&copy);
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 (&copy), (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 (&copy), (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