1 /*
2  * Copyright (C) 2010, 2011 Igalia S.L.
3  * Copyright (C) 2011 Intel Corporation.
4  *
5  * Contact: Iago Toral Quiroga <itoral@igalia.com>
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public License
9  * as published by the Free Software Foundation; version 2.1 of
10  * the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
20  * 02110-1301 USA
21  *
22  */
23 
24 #ifdef HAVE_CONFIG_H
25 #include "config.h"
26 #endif
27 
28 #include <grilo.h>
29 #include <gio/gio.h>
30 #include <glib/gi18n-lib.h>
31 #include <string.h>
32 #include <stdlib.h>
33 #include <pls/grl-pls.h>
34 
35 #include "grl-filesystem.h"
36 
37 /* --------- Logging  -------- */
38 
39 #define GRL_LOG_DOMAIN_DEFAULT filesystem_log_domain
40 GRL_LOG_DOMAIN_STATIC(filesystem_log_domain);
41 
42 /* -------- File info ------- */
43 
44 #define FILE_ATTRIBUTES_FAST                    \
45   G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN
46 
47 /* ---- Emission chunks ----- */
48 
49 #define BROWSE_IDLE_CHUNK_SIZE 5
50 
51 /* ---- Default root ---- */
52 
53 #define DEFAULT_ROOT "file:///"
54 
55 /* --- Plugin information --- */
56 
57 #define SOURCE_ID   "grl-filesystem"
58 #define SOURCE_NAME _("Filesystem")
59 #define SOURCE_DESC _("A source for browsing the filesystem")
60 
61 /* --- Grilo Filesystem Private --- */
62 
63 struct _GrlFilesystemSourcePrivate {
64   GList *chosen_uris;
65   guint max_search_depth;
66   gboolean handle_pls;
67   /* a mapping operation_id -> GCancellable to cancel this operation */
68   GHashTable *cancellables;
69   /* URI -> GFileMonitor */
70   GHashTable *monitors;
71   GCancellable *cancellable_monitors;
72 };
73 
74 /* --- Data types --- */
75 
76 typedef struct _RecursiveOperation RecursiveOperation;
77 
78 typedef gboolean (*RecursiveOperationCb) (GFileInfo *file_info,
79                                           RecursiveOperation *operation);
80 
81 typedef struct {
82   GrlSourceBrowseSpec *spec;
83   GList *entries;
84   GList *current;
85   const gchar *uri;
86   guint remaining;
87   GCancellable *cancellable;
88   guint id;
89 }  BrowseIdleData;
90 
91 struct _RecursiveOperation {
92   RecursiveOperationCb on_cancel;
93   RecursiveOperationCb on_finish;
94   RecursiveOperationCb on_dir;
95   RecursiveOperationCb on_file;
96   gpointer on_dir_data;
97   gpointer on_file_data;
98   GCancellable *cancellable;
99   GQueue *directories;
100   guint max_depth;
101 };
102 
103 typedef struct {
104   guint depth;
105   GFile *directory;
106   gboolean handle_pls;
107 } RecursiveEntry;
108 
109 
110 static GrlFilesystemSource *grl_filesystem_source_new (void);
111 
112 static void grl_filesystem_source_finalize (GObject *object);
113 
114 gboolean grl_filesystem_plugin_init (GrlRegistry *registry,
115                                      GrlPlugin *plugin,
116                                      GList *configs);
117 
118 static const GList *grl_filesystem_source_supported_keys (GrlSource *source);
119 
120 static GrlCaps *grl_filesystem_source_get_caps (GrlSource *source,
121                                                 GrlSupportedOps operation);
122 static void grl_filesystem_source_resolve (GrlSource *source,
123                                            GrlSourceResolveSpec *rs);
124 
125 static void grl_filesystem_source_browse (GrlSource *source,
126                                           GrlSourceBrowseSpec *bs);
127 
128 static void grl_filesystem_source_search (GrlSource *source,
129                                           GrlSourceSearchSpec *ss);
130 
131 static gboolean grl_filesystem_test_media_from_uri (GrlSource *source,
132                                                     const gchar *uri);
133 
134 static void grl_filesystem_get_media_from_uri (GrlSource *source,
135                                                GrlSourceMediaFromUriSpec *mfus);
136 
137 static void grl_filesystem_source_cancel (GrlSource *source,
138                                           guint operation_id);
139 
140 static gboolean grl_filesystem_source_notify_change_start (GrlSource *source,
141                                                            GError **error);
142 
143 static gboolean grl_filesystem_source_notify_change_stop (GrlSource *source,
144                                                           GError **error);
145 
146 /* =================== Filesystem Plugin  =============== */
147 
148 gboolean
grl_filesystem_plugin_init(GrlRegistry * registry,GrlPlugin * plugin,GList * configs)149 grl_filesystem_plugin_init (GrlRegistry *registry,
150                             GrlPlugin *plugin,
151                             GList *configs)
152 {
153   GrlFilesystemSource *source;
154   GList *chosen_uris = NULL;
155   guint max_search_depth = GRILO_CONF_MAX_SEARCH_DEPTH_DEFAULT;
156   gboolean handle_pls = FALSE;
157 
158   GRL_LOG_DOMAIN_INIT (filesystem_log_domain, "filesystem");
159 
160   GRL_DEBUG ("filesystem_plugin_init");
161 
162   /* Initialize i18n */
163   bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
164   bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
165 
166   source = grl_filesystem_source_new ();
167 
168   for (; configs; configs = g_list_next (configs)) {
169     GrlConfig *config = configs->data;
170     gchar *uri;
171 
172     uri = grl_config_get_string (config, GRILO_CONF_CHOSEN_URI);
173     if (uri) {
174       chosen_uris = g_list_prepend (chosen_uris, uri);
175     }
176     if (grl_config_has_param (config, GRILO_CONF_MAX_SEARCH_DEPTH)) {
177       max_search_depth = (guint)grl_config_get_int (config, GRILO_CONF_MAX_SEARCH_DEPTH);
178     }
179     if (grl_config_has_param (config, GRILO_CONF_HANDLE_PLS)) {
180       handle_pls = grl_config_get_boolean (config, GRILO_CONF_HANDLE_PLS);
181     }
182   }
183   source->priv->chosen_uris = g_list_reverse (chosen_uris);
184   source->priv->max_search_depth = max_search_depth;
185   source->priv->handle_pls = handle_pls;
186 
187   grl_registry_register_source (registry,
188                                 plugin,
189                                 GRL_SOURCE (source),
190                                 NULL);
191 
192   return TRUE;
193 }
194 
195 GRL_PLUGIN_DEFINE (GRL_MAJOR,
196                    GRL_MINOR,
197                    FILESYSTEM_PLUGIN_ID,
198                    "Filesystem",
199                    "A plugin for browsing the filesystem",
200                    "Igalia S.L.",
201                    VERSION,
202                    "LGPL-2.1-or-later",
203                    "http://www.igalia.com",
204                    grl_filesystem_plugin_init,
205                    NULL,
206                    NULL);
207 
208 /* ================== Filesystem GObject ================ */
209 
210 
G_DEFINE_TYPE_WITH_PRIVATE(GrlFilesystemSource,grl_filesystem_source,GRL_TYPE_SOURCE)211 G_DEFINE_TYPE_WITH_PRIVATE (GrlFilesystemSource, grl_filesystem_source, GRL_TYPE_SOURCE)
212 
213 static GrlFilesystemSource *
214 grl_filesystem_source_new (void)
215 {
216   GRL_DEBUG ("grl_filesystem_source_new");
217   return g_object_new (GRL_FILESYSTEM_SOURCE_TYPE,
218 		       "source-id", SOURCE_ID,
219 		       "source-name", SOURCE_NAME,
220 		       "source-desc", SOURCE_DESC,
221 		       NULL);
222 }
223 
224 static void
grl_filesystem_source_class_init(GrlFilesystemSourceClass * klass)225 grl_filesystem_source_class_init (GrlFilesystemSourceClass * klass)
226 {
227   GObjectClass *g_class = G_OBJECT_CLASS (klass);
228   GrlSourceClass *source_class = GRL_SOURCE_CLASS (klass);
229 
230   g_class->finalize = grl_filesystem_source_finalize;
231 
232   source_class->supported_keys = grl_filesystem_source_supported_keys;
233   source_class->cancel = grl_filesystem_source_cancel;
234   source_class->get_caps = grl_filesystem_source_get_caps;
235   source_class->browse = grl_filesystem_source_browse;
236   source_class->search = grl_filesystem_source_search;
237   source_class->notify_change_start = grl_filesystem_source_notify_change_start;
238   source_class->notify_change_stop = grl_filesystem_source_notify_change_stop;
239   source_class->resolve = grl_filesystem_source_resolve;
240   source_class->test_media_from_uri = grl_filesystem_test_media_from_uri;
241   source_class->media_from_uri = grl_filesystem_get_media_from_uri;
242 }
243 
244 static void
grl_filesystem_source_init(GrlFilesystemSource * source)245 grl_filesystem_source_init (GrlFilesystemSource *source)
246 {
247   source->priv = grl_filesystem_source_get_instance_private (source);
248   source->priv->cancellables = g_hash_table_new (NULL, NULL);
249   source->priv->monitors = g_hash_table_new_full (g_str_hash, g_str_equal,
250                                                   g_free, g_object_unref);
251 }
252 
253 static void
grl_filesystem_source_finalize(GObject * object)254 grl_filesystem_source_finalize (GObject *object)
255 {
256   GrlFilesystemSource *filesystem_source = GRL_FILESYSTEM_SOURCE (object);
257   g_list_free_full (filesystem_source->priv->chosen_uris, g_free);
258   g_hash_table_unref (filesystem_source->priv->cancellables);
259   g_hash_table_unref (filesystem_source->priv->monitors);
260   G_OBJECT_CLASS (grl_filesystem_source_parent_class)->finalize (object);
261 }
262 
263 /* ======================= Utilities ==================== */
264 
265 static void recursive_operation_next_entry (RecursiveOperation *operation);
266 static void add_monitor (GrlFilesystemSource *fs_source, GFile *dir);
267 static void cancel_monitors (GrlFilesystemSource *fs_source);
268 
269 static gboolean
mime_is_video(const gchar * mime)270 mime_is_video (const gchar *mime)
271 {
272   return g_str_has_prefix (mime, "video/");
273 }
274 
275 static gboolean
mime_is_audio(const gchar * mime)276 mime_is_audio (const gchar *mime)
277 {
278   return g_str_has_prefix (mime, "audio/");
279 }
280 
281 static gboolean
mime_is_image(const gchar * mime)282 mime_is_image (const gchar *mime)
283 {
284   return g_str_has_prefix (mime, "image/");
285 }
286 
287 static gboolean
mime_is_media(const gchar * mime,GrlTypeFilter filter)288 mime_is_media (const gchar *mime, GrlTypeFilter filter)
289 {
290   if (!mime)
291     return FALSE;
292   if (!strcmp (mime, "inode/directory"))
293     return TRUE;
294   if (filter & GRL_TYPE_FILTER_AUDIO &&
295       mime_is_audio (mime))
296     return TRUE;
297   if (filter & GRL_TYPE_FILTER_VIDEO &&
298       mime_is_video (mime))
299     return TRUE;
300   if (filter & GRL_TYPE_FILTER_IMAGE &&
301       mime_is_image (mime))
302     return TRUE;
303   return FALSE;
304 }
305 
306 static gboolean
file_is_valid_content(GFileInfo * info,gboolean fast,GrlOperationOptions * options)307 file_is_valid_content (GFileInfo *info, gboolean fast, GrlOperationOptions *options)
308 {
309   const gchar *mime;
310   const gchar *mime_filter = NULL;
311   GValue *mime_filter_value = NULL;
312   GValue *min_date_value = NULL;
313   GValue *max_date_value = NULL;
314   GDateTime *min_date = NULL;
315   GDateTime *max_date = NULL;
316   GDateTime *file_date = NULL;
317   GrlTypeFilter type_filter;
318   gboolean is_media = TRUE;
319   GFileType type;
320 
321   /* Ignore hidden files */
322   if (g_file_info_get_is_hidden (info)) {
323       is_media = FALSE;
324       goto end;
325   }
326 
327   type = g_file_info_get_file_type (info);
328 
329   /* Directories are always accepted */
330   if (type == G_FILE_TYPE_DIRECTORY) {
331     goto end;
332   }
333 
334   type_filter = options? grl_operation_options_get_type_filter (options): GRL_TYPE_FILTER_ALL;
335 
336   /* In fast mode we do not check mime-types, any non-hidden file is accepted */
337   if (fast) {
338     if (type_filter == GRL_TYPE_FILTER_NONE) {
339       is_media = FALSE;
340     }
341     goto end;
342   }
343 
344   /* Filter by type */
345   mime = g_file_info_get_content_type (info);
346   if (!mime_is_media (mime, type_filter)) {
347     is_media = FALSE;
348     goto end;
349   }
350 
351   /* Filter by mime */
352   mime_filter_value =
353     options? grl_operation_options_get_key_filter (options,
354                                                    GRL_METADATA_KEY_MIME): NULL;
355   if (mime_filter_value) {
356     mime_filter = g_value_get_string (mime_filter_value);
357   }
358 
359   if (mime_filter && g_strcmp0 (mime, mime_filter) != 0) {
360     is_media = FALSE;
361     goto end;
362   }
363 
364   /* Filter by date */
365   if (options) {
366     grl_operation_options_get_key_range_filter (options,
367                                                 GRL_METADATA_KEY_MODIFICATION_DATE,
368                                                 &min_date_value,
369                                                 &max_date_value);
370   }
371 
372   if (min_date_value) {
373     min_date = g_date_time_ref (g_value_get_boxed (min_date_value));
374   }
375   if (max_date_value) {
376     max_date = g_date_time_ref (g_value_get_boxed (max_date_value));
377   }
378 
379   if (min_date || max_date) {
380     GTimeVal time = {0,};
381 
382     g_file_info_get_modification_time (info, &time);
383     file_date = g_date_time_new_from_timeval_utc (&time);
384   }
385 
386   if (min_date && file_date && g_date_time_compare (min_date, file_date) > 0) {
387     is_media = FALSE;
388     goto end;
389   }
390 
391   if (max_date && file_date && g_date_time_compare (max_date, file_date) < 0) {
392     is_media = FALSE;
393     goto end;
394   }
395 
396  end:
397   g_clear_pointer (&file_date, g_date_time_unref);
398   g_clear_pointer (&min_date, g_date_time_unref);
399   g_clear_pointer (&max_date, g_date_time_unref);
400 
401   return is_media;
402 }
403 
404 static gboolean
browse_emit_idle(gpointer user_data)405 browse_emit_idle (gpointer user_data)
406 {
407   BrowseIdleData *idle_data;
408   guint count;
409   GrlFilesystemSource *fs_source;
410 
411   GRL_DEBUG ("browse_emit_idle");
412 
413   idle_data = (BrowseIdleData *) user_data;
414   fs_source = GRL_FILESYSTEM_SOURCE (idle_data->spec->source);
415 
416   if (g_cancellable_is_cancelled (idle_data->cancellable)) {
417     GRL_DEBUG ("Browse operation %d (\"%s\") has been cancelled",
418                idle_data->id, idle_data->uri);
419     idle_data->spec->callback(idle_data->spec->source,
420                               idle_data->id, NULL, 0,
421                               idle_data->spec->user_data, NULL);
422     goto finish;
423   }
424 
425   count = 0;
426   do {
427     gchar *uri;
428     GrlMedia *content;
429     GFile *file;
430     GrlOperationOptions *options = idle_data->spec->options;
431 
432     uri = (gchar *) idle_data->current->data;
433     file = g_file_new_for_uri (uri);
434 
435     content = grl_pls_file_to_media (NULL,
436                                      file,
437                                      NULL,
438                                      fs_source->priv->handle_pls,
439                                      options);
440     g_object_unref (file);
441 
442     idle_data->spec->callback (idle_data->spec->source,
443 			       idle_data->spec->operation_id,
444 			       content,
445 			       idle_data->remaining--,
446 			       idle_data->spec->user_data,
447 			       NULL);
448 
449     idle_data->current = g_list_next (idle_data->current);
450     count++;
451   } while (count < BROWSE_IDLE_CHUNK_SIZE && idle_data->current);
452 
453   if (!idle_data->current)
454     goto finish;
455 
456   return TRUE;
457 
458 finish:
459     g_list_free_full (idle_data->entries, g_free);
460     g_hash_table_remove (fs_source->priv->cancellables,
461                          GUINT_TO_POINTER (idle_data->id));
462     g_object_unref (idle_data->cancellable);
463     g_slice_free (BrowseIdleData, idle_data);
464     return FALSE;
465 }
466 
467 static void
produce_from_uri(GrlSourceBrowseSpec * bs,const gchar * uri,GrlOperationOptions * options)468 produce_from_uri (GrlSourceBrowseSpec *bs, const gchar *uri, GrlOperationOptions *options)
469 {
470   GFile *file;
471   GFileEnumerator *e;
472   GFileInfo *info;
473   GError *error = NULL;
474   guint skip, count;
475   GList *entries = NULL;
476   GList *iter;
477 
478   /* Open directory */
479   GRL_DEBUG ("Opening directory '%s'", uri);
480   file = g_file_new_for_uri (uri);
481   e = g_file_enumerate_children (file,
482                                  grl_pls_get_file_attributes (),
483                                  G_FILE_QUERY_INFO_NONE,
484                                  NULL,
485                                  &error);
486 
487   if (!e) {
488     GRL_DEBUG ("Failed to open directory '%s': %s", uri, error->message);
489     bs->callback (bs->source, bs->operation_id, NULL, 0, bs->user_data, error);
490     g_error_free (error);
491     g_object_unref (file);
492     return;
493   }
494 
495   /* Filter out media and directories */
496   while ((info = g_file_enumerator_next_file (e, NULL, NULL)) != NULL) {
497     if (file_is_valid_content (info, FALSE, options)) {
498       GFile *entry;
499       entry = g_file_get_child (file, g_file_info_get_name (info));
500       entries = g_list_prepend (entries, g_file_get_uri (entry));
501       g_object_unref (entry);
502     }
503     g_object_unref (info);
504   }
505 
506   g_object_unref (e);
507   g_object_unref (file);
508 
509   /* Apply skip and count */
510   skip = grl_operation_options_get_skip (bs->options);
511   count = grl_operation_options_get_count (bs->options);
512   iter = entries;
513   while (iter) {
514     gboolean remove;
515     GList *tmp;
516     if (skip > 0)  {
517       skip--;
518       remove = TRUE;
519     } else if (count > 0) {
520       count--;
521       remove = FALSE;
522     } else {
523       remove = TRUE;
524     }
525     if (remove) {
526       tmp = iter;
527       iter = g_list_next (iter);
528       g_free (tmp->data);
529       entries = g_list_delete_link (entries, tmp);
530     } else {
531       iter = g_list_next (iter);
532     }
533   }
534 
535   /* Emit results */
536   if (entries) {
537     guint id;
538 
539     /* Use the idle loop to avoid blocking for too long */
540     BrowseIdleData *idle_data = g_slice_new (BrowseIdleData);
541     gint global_count = grl_operation_options_get_count (bs->options);
542     idle_data->spec = bs;
543     idle_data->remaining = global_count - count - 1;
544     idle_data->uri = uri;
545     idle_data->entries = entries;
546     idle_data->current = entries;
547     idle_data->cancellable = g_cancellable_new ();
548     idle_data->id = bs->operation_id;
549     g_hash_table_insert (GRL_FILESYSTEM_SOURCE (bs->source)->priv->cancellables,
550                          GUINT_TO_POINTER (bs->operation_id),
551                          idle_data->cancellable);
552 
553     id = g_idle_add (browse_emit_idle, idle_data);
554     g_source_set_name_by_id (id, "[filesystem] browse_emit_idle");
555   } else {
556     /* No results */
557     bs->callback (bs->source,
558 		  bs->operation_id,
559 		  NULL,
560 		  0,
561 		  bs->user_data,
562 		  NULL);
563   }
564 }
565 
566 static RecursiveEntry *
recursive_entry_new(guint depth,GFile * directory)567 recursive_entry_new (guint depth, GFile *directory)
568 {
569   RecursiveEntry *entry;
570 
571   entry = g_slice_new(RecursiveEntry);
572   entry->depth = depth;
573   entry->directory = g_object_ref (directory);
574 
575   return entry;
576 }
577 
578 static void
recursive_entry_free(RecursiveEntry * entry)579 recursive_entry_free (RecursiveEntry *entry)
580 {
581   g_object_unref (entry->directory);
582   g_slice_free (RecursiveEntry, entry);
583 }
584 
585 static RecursiveOperation *
recursive_operation_new(void)586 recursive_operation_new (void)
587 {
588   RecursiveOperation *operation;
589 
590   operation = g_slice_new0 (RecursiveOperation);
591   operation->directories = g_queue_new ();
592   operation->cancellable = g_cancellable_new ();
593 
594   return operation;
595 }
596 
597 static void
recursive_operation_free(RecursiveOperation * operation)598 recursive_operation_free (RecursiveOperation *operation)
599 {
600   g_queue_foreach (operation->directories, (GFunc) recursive_entry_free, NULL);
601   g_queue_free (operation->directories);
602   g_object_unref (operation->cancellable);
603   g_slice_free (RecursiveOperation, operation);
604 }
605 
606 static void
recursive_operation_got_file(GFileEnumerator * enumerator,GAsyncResult * res,RecursiveOperation * operation)607 recursive_operation_got_file (GFileEnumerator *enumerator, GAsyncResult *res, RecursiveOperation *operation)
608 {
609   GList *files;
610   GError *error = NULL;
611   gboolean continue_operation = TRUE;
612 
613   GRL_DEBUG (__func__);
614 
615   files = g_file_enumerator_next_files_finish (enumerator, res, &error);
616   if (error) {
617     if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
618       GRL_WARNING ("Got error: %s", error->message);
619     g_error_free (error);
620     goto finished;
621   }
622 
623   if (files) {
624     GFileInfo *file_info;
625     RecursiveEntry *entry;
626 
627     /* we assume there is only one GFileInfo in the list since that's what we ask
628      * for when calling g_file_enumerator_next_files_async() */
629     file_info = (GFileInfo *)files->data;
630     g_list_free (files);
631     /* Get the entry we are running now */
632     entry = g_queue_peek_head (operation->directories);
633     switch (g_file_info_get_file_type (file_info)) {
634     case G_FILE_TYPE_SYMBOLIC_LINK:
635       /* we're too afraid of infinite recursion to touch this for now */
636       break;
637     case G_FILE_TYPE_DIRECTORY:
638         {
639           if (entry->depth < operation->max_depth) {
640             GFile *subdir;
641             RecursiveEntry *subentry;
642 
643             if (operation->on_dir) {
644               continue_operation = operation->on_dir(file_info, operation);
645             }
646 
647             if (continue_operation) {
648               subdir = g_file_get_child (entry->directory,
649                                          g_file_info_get_name (file_info));
650               subentry = recursive_entry_new (entry->depth + 1, subdir);
651               g_queue_push_tail (operation->directories, subentry);
652               g_object_unref (subdir);
653             } else {
654               g_object_unref (file_info);
655               goto finished;
656             }
657           }
658         }
659       break;
660     case G_FILE_TYPE_REGULAR:
661       if (operation->on_file) {
662         continue_operation = operation->on_file(file_info, operation);
663         if (!continue_operation) {
664           g_object_unref (file_info);
665           goto finished;
666         }
667       }
668       break;
669     default:
670       /* this file is a weirdo, we ignore it */
671       break;
672     }
673     g_object_unref (file_info);
674   } else {    /* end of enumerator */
675     goto finished;
676   }
677 
678   g_file_enumerator_next_files_async (enumerator, 1, G_PRIORITY_DEFAULT,
679                                       operation->cancellable,
680                                       (GAsyncReadyCallback)recursive_operation_got_file,
681                                       operation);
682 
683   return;
684 
685 finished:
686   /* we're done with this dir/enumerator, let's treat the next one */
687   g_object_unref (enumerator);
688   recursive_entry_free (g_queue_pop_head (operation->directories));
689   if (continue_operation) {
690     recursive_operation_next_entry (operation);
691   } else {
692     recursive_operation_free (operation);
693   }
694 }
695 
696 static void
recursive_operation_got_entry(GFile * directory,GAsyncResult * res,RecursiveOperation * operation)697 recursive_operation_got_entry (GFile *directory, GAsyncResult *res, RecursiveOperation *operation)
698 {
699   GError *error = NULL;
700   GFileEnumerator *enumerator;
701 
702   GRL_DEBUG (__func__);
703 
704   enumerator = g_file_enumerate_children_finish (directory, res, &error);
705   if (error) {
706     if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
707       g_error_free (error);
708       return;
709     }
710     GRL_WARNING ("Got error: %s", error->message);
711     g_error_free (error);
712     /* we couldn't get the children of this directory, but we probably have
713      * other directories to try */
714     recursive_entry_free (g_queue_pop_head (operation->directories));
715     recursive_operation_next_entry (operation);
716     return;
717   }
718 
719   g_file_enumerator_next_files_async (enumerator, 1, G_PRIORITY_DEFAULT,
720                                       operation->cancellable,
721                                       (GAsyncReadyCallback)recursive_operation_got_file,
722                                       operation);
723 }
724 
725 static void
recursive_operation_next_entry(RecursiveOperation * operation)726 recursive_operation_next_entry (RecursiveOperation *operation)
727 {
728   RecursiveEntry *entry;
729 
730   GRL_DEBUG (__func__);
731 
732   if (g_cancellable_is_cancelled (operation->cancellable)) {
733     /* We've been cancelled! */
734     GRL_DEBUG ("Operation has been cancelled");
735     if (operation->on_cancel) {
736       operation->on_cancel(NULL, operation);
737     }
738     goto finished;
739   }
740 
741   entry = g_queue_peek_head (operation->directories);
742   if (!entry) { /* We've crawled everything, before reaching count */
743     if (operation->on_finish) {
744       operation->on_finish (NULL, operation);
745     }
746     goto finished;
747   }
748 
749   g_file_enumerate_children_async (entry->directory, G_FILE_ATTRIBUTE_STANDARD_TYPE ","
750                                    G_FILE_ATTRIBUTE_STANDARD_NAME ","
751                                    G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
752                                    G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
753                                    G_PRIORITY_DEFAULT,
754                                    operation->cancellable,
755                                    (GAsyncReadyCallback)recursive_operation_got_entry,
756                                    operation);
757 
758   return;
759 
760 finished:
761   recursive_operation_free (operation);
762 }
763 
764 static void
recursive_operation_initialize(RecursiveOperation * operation,GrlFilesystemSource * source)765 recursive_operation_initialize (RecursiveOperation *operation, GrlFilesystemSource *source)
766 {
767   GList *chosen_uris, *uri;
768 
769   chosen_uris = source->priv->chosen_uris;
770   if (chosen_uris) {
771     for (uri = chosen_uris; uri; uri = g_list_next (uri)) {
772       GFile *directory = g_file_new_for_uri (uri->data);
773       g_queue_push_tail (operation->directories,
774                          recursive_entry_new (0, directory));
775       add_monitor (source, directory);
776       g_object_unref (directory);
777     }
778   } else {
779     const gchar *home;
780     GFile *directory;
781 
782     /* This is necessary for GLIB < 2.36 */
783     home = g_getenv ("HOME");
784     if (!home)
785       home = g_get_home_dir ();
786     directory = g_file_new_for_path (home);
787     g_queue_push_tail (operation->directories,
788                        recursive_entry_new (0, directory));
789     add_monitor (source, directory);
790     g_object_unref (directory);
791   }
792 }
793 
794 static gboolean
cancel_cb(GFileInfo * file_info,RecursiveOperation * operation)795 cancel_cb (GFileInfo *file_info, RecursiveOperation *operation)
796 {
797   GrlFilesystemSource *fs_source;
798 
799   if (operation->on_file_data) {
800     GrlSourceSearchSpec *ss =
801       (GrlSourceSearchSpec *) operation->on_file_data;
802     fs_source = GRL_FILESYSTEM_SOURCE (ss->source);
803     g_hash_table_remove (fs_source->priv->cancellables,
804                          GUINT_TO_POINTER (ss->operation_id));
805     ss->callback (ss->source, ss->operation_id, NULL, 0, ss->user_data, NULL);
806   }
807 
808   if (operation->on_dir_data) {
809     /* Remove all monitors */
810     fs_source = GRL_FILESYSTEM_SOURCE (operation->on_dir_data);
811     cancel_monitors (fs_source);
812   }
813   return FALSE;
814 }
815 
816 static gboolean
finish_cb(GFileInfo * file_info,RecursiveOperation * operation)817 finish_cb (GFileInfo *file_info, RecursiveOperation *operation)
818 {
819   if (operation->on_file_data) {
820     GrlSourceSearchSpec *ss =
821       (GrlSourceSearchSpec *) operation->on_file_data;
822     g_hash_table_remove (GRL_FILESYSTEM_SOURCE (ss->source)->priv->cancellables,
823                          GUINT_TO_POINTER (ss->operation_id));
824     ss->callback (ss->source, ss->operation_id, NULL, 0, ss->user_data, NULL);
825   }
826 
827   if (operation->on_dir_data) {
828     GRL_FILESYSTEM_SOURCE (operation->on_dir_data)->priv->cancellable_monitors = NULL;
829   }
830 
831   return FALSE;
832 }
833 
834 /* return TRUE if more files need to be returned, FALSE if we sent the count */
835 static gboolean
file_cb(GFileInfo * file_info,RecursiveOperation * operation)836 file_cb (GFileInfo *file_info, RecursiveOperation *operation)
837 {
838   gchar *needle = NULL;
839   gchar *haystack = NULL;
840   gchar *normalized_needle = NULL;
841   gchar *normalized_haystack = NULL;
842   GrlSourceSearchSpec *ss = operation->on_file_data;
843   gint remaining = -1;
844 
845   GRL_DEBUG (__func__);
846 
847   if (ss == NULL)
848     return FALSE;
849 
850   if (ss->text) {
851     haystack = g_utf8_casefold (g_file_info_get_display_name (file_info), -1);
852     normalized_haystack = g_utf8_normalize (haystack, -1, G_NORMALIZE_ALL);
853 
854     needle = g_utf8_casefold (ss->text, -1);
855     normalized_needle = g_utf8_normalize (needle, -1, G_NORMALIZE_ALL);
856   }
857 
858   if (!ss->text ||
859       strstr (normalized_haystack, normalized_needle)) {
860     GrlMedia *media = NULL;
861     RecursiveEntry *entry;
862     GFile *file;
863     GFileInfo *info;
864 
865     entry = g_queue_peek_head (operation->directories);
866     file = g_file_get_child (entry->directory,
867                              g_file_info_get_name (file_info));
868 
869     /* FIXME This is likely to block */
870     info = g_file_query_info (file, grl_pls_get_file_attributes (), G_FILE_QUERY_INFO_NONE, NULL, NULL);
871 
872     if (file_is_valid_content (info, FALSE, ss->options)) {
873       guint skip = grl_operation_options_get_skip (ss->options);
874       if (skip) {
875         grl_operation_options_set_skip (ss->options, skip - 1);
876       } else {
877         gboolean handle_pls;
878         handle_pls = GRL_FILESYSTEM_SOURCE(ss->source)->priv->handle_pls;
879         media = grl_pls_file_to_media (NULL, file, info, handle_pls, ss->options);
880       }
881     }
882 
883     g_object_unref (info);
884     g_object_unref (file);
885 
886     if (media) {
887       gint count = grl_operation_options_get_count (ss->options);
888       count--;
889       grl_operation_options_set_count (ss->options, count);
890       if (count == 0) {
891         remaining = 0;
892       }
893       ss->callback (ss->source, ss->operation_id, media, remaining, ss->user_data, NULL);
894     }
895   }
896 
897   g_free (haystack);
898   g_free (normalized_haystack);
899   g_free (needle);
900   g_free (normalized_needle);
901   return remaining == -1;
902 }
903 
904 static void
notify_change(GrlSource * source,GFile * file,GrlSourceChangeType change)905 notify_change (GrlSource *source, GFile *file, GrlSourceChangeType change)
906 {
907   GrlMedia *media;
908   GrlOperationOptions *options;
909   GrlFilesystemSource *fs_source;
910 
911   fs_source = GRL_FILESYSTEM_SOURCE (source);
912   options = grl_operation_options_new (NULL);
913   grl_operation_options_set_resolution_flags (options, GRL_RESOLVE_FAST_ONLY);
914 
915   media = grl_pls_file_to_media (NULL, file, NULL, fs_source->priv->handle_pls, options);
916   grl_source_notify_change (source, media, change, FALSE);
917   g_object_unref (media);
918   g_object_unref (options);
919 }
920 
921 static void
directory_changed(GFileMonitor * monitor,GFile * file,GFile * other_file,GFileMonitorEvent event,gpointer data)922 directory_changed (GFileMonitor *monitor,
923                    GFile *file,
924                    GFile *other_file,
925                    GFileMonitorEvent event,
926                    gpointer data)
927 {
928   GrlSource *source = GRL_SOURCE (data);
929   GrlFilesystemSource *fs_source = GRL_FILESYSTEM_SOURCE (data);
930   GFileInfo *info = NULL;
931 
932   /* Keep only signals we are interested in */
933   if (event != G_FILE_MONITOR_EVENT_CREATED &&
934       event != G_FILE_MONITOR_EVENT_CHANGED &&
935       event != G_FILE_MONITOR_EVENT_MOVED &&
936       event != G_FILE_MONITOR_EVENT_DELETED)
937     return;
938 
939   /* File DELETED */
940   if (event == G_FILE_MONITOR_EVENT_DELETED) {
941     gchar *uri;
942 
943     /* Avoid duplicated notification when a directory being monitored is
944      * deleted. The signal will be emitted by the monitor tracking its parent.
945      */
946     uri = g_file_get_uri (file);
947     if (g_hash_table_lookup (fs_source->priv->monitors, uri) != monitor)
948       notify_change (source, file, GRL_CONTENT_REMOVED);
949     g_free (uri);
950 
951     goto out;
952   }
953 
954   /* Query the file and leave if we are not interested in it */
955   info = g_file_query_info (file,
956                             grl_pls_get_file_attributes (),
957                             G_FILE_QUERY_INFO_NONE,
958                             NULL, NULL);
959   if (!info || !file_is_valid_content (info, TRUE, NULL))
960     goto out;
961 
962   /* File CHANGED */
963   if (event == G_FILE_MONITOR_EVENT_CHANGED) {
964     notify_change (source, file, GRL_CONTENT_CHANGED);
965     goto out;
966   }
967 
968   /* File CREATED */
969   if (event == G_FILE_MONITOR_EVENT_CREATED) {
970     notify_change (source, file, GRL_CONTENT_ADDED);
971     if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
972       add_monitor (GRL_FILESYSTEM_SOURCE (source), file);
973     goto out;
974   }
975 
976   /* File MOVED */
977   if (event == G_FILE_MONITOR_EVENT_MOVED) {
978       notify_change (source, file, GRL_CONTENT_REMOVED);
979       notify_change (source, other_file, GRL_CONTENT_ADDED);
980     goto out;
981   }
982 
983 out:
984   g_clear_object (&info);
985 }
986 
987 static void
cancel_monitors(GrlFilesystemSource * fs_source)988 cancel_monitors (GrlFilesystemSource *fs_source)
989 {
990   /* That table holds the only ref to our GFileMonitor, and dispose will
991    * cancel them. */
992   g_hash_table_remove_all (fs_source->priv->monitors);
993 }
994 
995 static void
add_monitor(GrlFilesystemSource * fs_source,GFile * dir)996 add_monitor (GrlFilesystemSource *fs_source, GFile *dir)
997 {
998   GFileMonitor *monitor;
999   gchar *uri;
1000 
1001   uri = g_file_get_uri (dir);
1002   if (g_hash_table_contains (fs_source->priv->monitors, uri))
1003     goto out;
1004 
1005   monitor = g_file_monitor_directory (dir, G_FILE_MONITOR_SEND_MOVED, NULL, NULL);
1006   if (!monitor) {
1007     GRL_DEBUG ("Unable to set up monitor in %s\n", uri);
1008     goto out;
1009   }
1010 
1011   /* transfer ownership of uri and monitor */
1012   g_hash_table_insert (fs_source->priv->monitors, uri, monitor);
1013   g_signal_connect (monitor,
1014                     "changed",
1015                     G_CALLBACK (directory_changed),
1016                     fs_source);
1017   uri = NULL;
1018 
1019 out:
1020   g_free (uri);
1021 }
1022 
1023 static gboolean
directory_cb(GFileInfo * dir_info,RecursiveOperation * operation)1024 directory_cb (GFileInfo *dir_info, RecursiveOperation *operation)
1025 {
1026   RecursiveEntry *entry;
1027   GFile *dir;
1028   GrlFilesystemSource *fs_source;
1029 
1030   fs_source = GRL_FILESYSTEM_SOURCE (operation->on_dir_data);
1031   entry = g_queue_peek_head (operation->directories);
1032   dir = g_file_get_child (entry->directory,
1033                           g_file_info_get_name (dir_info));
1034 
1035   add_monitor (fs_source, dir);
1036   g_object_unref (dir);
1037 
1038   return TRUE;
1039 }
1040 
1041 /* ================== API Implementation ================ */
1042 
1043 static const GList *
grl_filesystem_source_supported_keys(GrlSource * source)1044 grl_filesystem_source_supported_keys (GrlSource *source)
1045 {
1046   static GList *keys = NULL;
1047   if (!keys) {
1048     keys = grl_metadata_key_list_new (GRL_METADATA_KEY_ID,
1049                                       GRL_METADATA_KEY_TITLE,
1050                                       GRL_METADATA_KEY_URL,
1051                                       GRL_METADATA_KEY_MIME,
1052                                       GRL_METADATA_KEY_MODIFICATION_DATE,
1053                                       GRL_METADATA_KEY_CHILDCOUNT,
1054                                       NULL);
1055   }
1056   return keys;
1057 }
1058 
1059 static void
grl_filesystem_source_browse(GrlSource * source,GrlSourceBrowseSpec * bs)1060 grl_filesystem_source_browse (GrlSource *source,
1061                               GrlSourceBrowseSpec *bs)
1062 {
1063   const gchar *id;
1064   GList *chosen_uris;
1065 
1066   GRL_DEBUG (__FUNCTION__);
1067 
1068   if (grl_pls_media_is_playlist (bs->container)) {
1069     grl_pls_browse_by_spec (source, NULL, bs);
1070     return;
1071   }
1072 
1073   id = grl_media_get_id (bs->container);
1074   chosen_uris = GRL_FILESYSTEM_SOURCE(source)->priv->chosen_uris;
1075   if (!id && chosen_uris) {
1076     guint remaining = g_list_length (chosen_uris);
1077 
1078     if (remaining == 1) {
1079       produce_from_uri (bs, chosen_uris->data, bs->options);
1080     } else {
1081       for (; chosen_uris; chosen_uris = g_list_next (chosen_uris)) {
1082         GrlMedia *content;
1083         GFile *file;
1084 
1085         file = g_file_new_for_uri ((gchar *) chosen_uris->data);
1086         content = grl_pls_file_to_media (NULL,
1087                                          file,
1088                                          NULL,
1089                                          GRL_FILESYSTEM_SOURCE(source)->priv->handle_pls,
1090                                          bs->options);
1091         g_object_unref (file);
1092 
1093         if (content) {
1094           bs->callback (source,
1095                         bs->operation_id,
1096                         content,
1097                         --remaining,
1098                         bs->user_data,
1099                         NULL);
1100         }
1101       }
1102     }
1103   } else {
1104     produce_from_uri (bs, id ? id : DEFAULT_ROOT, bs->options);
1105   }
1106 }
1107 
grl_filesystem_source_search(GrlSource * source,GrlSourceSearchSpec * ss)1108 static void grl_filesystem_source_search (GrlSource *source,
1109                                           GrlSourceSearchSpec *ss)
1110 {
1111   RecursiveOperation *operation;
1112   GrlFilesystemSource *fs_source;
1113 
1114   GRL_DEBUG (__FUNCTION__);
1115 
1116   fs_source = GRL_FILESYSTEM_SOURCE (source);
1117 
1118   operation = recursive_operation_new ();
1119   operation->on_cancel = cancel_cb;
1120   operation->on_finish = finish_cb;
1121   operation->on_file = file_cb;
1122   operation->on_file_data = ss;
1123   operation->max_depth = fs_source->priv->max_search_depth;
1124   g_hash_table_insert (GRL_FILESYSTEM_SOURCE (source)->priv->cancellables,
1125                        GUINT_TO_POINTER (ss->operation_id),
1126                        operation->cancellable);
1127 
1128   recursive_operation_initialize (operation, fs_source);
1129   recursive_operation_next_entry (operation);
1130 }
1131 
1132 static void
grl_filesystem_source_resolve(GrlSource * source,GrlSourceResolveSpec * rs)1133 grl_filesystem_source_resolve (GrlSource *source,
1134                                GrlSourceResolveSpec *rs)
1135 {
1136   GFile *file;
1137   const gchar *id;
1138   GFileInfo *info;
1139   GList *chosen_uris;
1140   GError *error = NULL;
1141 
1142   GRL_DEBUG (__FUNCTION__);
1143 
1144   id = grl_media_get_id (rs->media);
1145   chosen_uris = GRL_FILESYSTEM_SOURCE(source)->priv->chosen_uris;
1146 
1147   if (!id && chosen_uris) {
1148     guint len;
1149 
1150     len = g_list_length (chosen_uris);
1151     if (len == 1) {
1152       file = g_file_new_for_uri (chosen_uris->data);
1153     } else {
1154       grl_media_set_title (rs->media, SOURCE_NAME);
1155       grl_media_set_childcount (rs->media, len);
1156       rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, NULL);
1157       return;
1158     }
1159   } else {
1160     file = g_file_new_for_uri (id ? id : DEFAULT_ROOT);
1161   }
1162 
1163   info = g_file_query_info (file, "", G_FILE_QUERY_INFO_NONE, NULL, &error);
1164   if (info != NULL) {
1165     grl_pls_file_to_media (rs->media, file, NULL, GRL_FILESYSTEM_SOURCE(source)->priv->handle_pls, rs->options);
1166     rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, NULL);
1167     g_object_unref (info);
1168   } else {
1169     GError *error_new = g_error_new (error->domain,
1170                                      error->code,
1171                                      _("File %s does not exist"),
1172                                      id);
1173     rs->callback (rs->source, rs->operation_id, rs->media, rs->user_data, error_new);
1174     g_error_free (error_new);
1175     g_error_free (error);
1176   }
1177 
1178   g_object_unref (file);
1179 }
1180 
1181 static gboolean
is_supported_scheme(const char * scheme)1182 is_supported_scheme (const char *scheme)
1183 {
1184   GVfs *vfs;
1185   const gchar * const * schemes;
1186   guint i;
1187 
1188   if (g_strcmp0(scheme, "file") == 0)
1189     return TRUE;
1190 
1191   vfs = g_vfs_get_default ();
1192   schemes = g_vfs_get_supported_uri_schemes (vfs);
1193   for (i = 0; schemes[i] != NULL; i++) {
1194     if (strcmp (schemes[i], scheme) == 0)
1195       return TRUE;
1196   }
1197 
1198   return FALSE;
1199 }
1200 
1201 static gboolean
grl_filesystem_test_media_from_uri(GrlSource * source,const gchar * uri)1202 grl_filesystem_test_media_from_uri (GrlSource *source,
1203                                     const gchar *uri)
1204 {
1205   GFile *file;
1206   GFileInfo *info;
1207   gchar *scheme;
1208   gboolean ret = FALSE;
1209 
1210   GRL_DEBUG (__FUNCTION__);
1211 
1212   scheme = g_uri_parse_scheme (uri);
1213   ret = is_supported_scheme (scheme);
1214   g_free (scheme);
1215   if (!ret)
1216     return ret;
1217 
1218   file = g_file_new_for_uri (uri);
1219   info = g_file_query_info (file, FILE_ATTRIBUTES_FAST, G_FILE_QUERY_INFO_NONE, NULL, NULL);
1220   g_object_unref (file);
1221 
1222   if (!info)
1223     return FALSE;
1224 
1225   ret = file_is_valid_content (info, TRUE, NULL);
1226 
1227   g_object_unref (info);
1228 
1229   return ret;
1230 }
1231 
grl_filesystem_get_media_from_uri(GrlSource * source,GrlSourceMediaFromUriSpec * mfus)1232 static void grl_filesystem_get_media_from_uri (GrlSource *source,
1233                                                GrlSourceMediaFromUriSpec *mfus)
1234 {
1235   gchar *scheme;
1236   GError *error = NULL;
1237   gboolean ret = FALSE;
1238   GrlMedia *media;
1239   GFile *file;
1240 
1241   GRL_DEBUG (__FUNCTION__);
1242 
1243   scheme = g_uri_parse_scheme (mfus->uri);
1244   ret = is_supported_scheme (scheme);
1245   g_free (scheme);
1246   if (!ret) {
1247     error = g_error_new (GRL_CORE_ERROR,
1248                          GRL_CORE_ERROR_MEDIA_FROM_URI_FAILED,
1249                          _("Cannot get media from %s"), mfus->uri);
1250     mfus->callback (source, mfus->operation_id, NULL, mfus->user_data, error);
1251     g_clear_error (&error);
1252     return;
1253   }
1254 
1255   /* FIXME: this is a blocking call, not sure we want that in here */
1256   /* Note: we assume grl_pls_file_to_media() never returns NULL, which seems to be true */
1257   file = g_file_new_for_uri (mfus->uri);
1258   media = grl_pls_file_to_media (NULL, file, NULL, GRL_FILESYSTEM_SOURCE(source)->priv->handle_pls, mfus->options);
1259   g_object_unref (file);
1260   mfus->callback (source, mfus->operation_id, media, mfus->user_data, NULL);
1261 }
1262 
1263 static void
grl_filesystem_source_cancel(GrlSource * source,guint operation_id)1264 grl_filesystem_source_cancel (GrlSource *source, guint operation_id)
1265 {
1266   GCancellable *cancellable;
1267   GrlFilesystemSourcePrivate *priv;
1268 
1269   priv = GRL_FILESYSTEM_SOURCE (source)->priv;
1270 
1271   cancellable =
1272       G_CANCELLABLE (g_hash_table_lookup (priv->cancellables,
1273                                           GUINT_TO_POINTER (operation_id)));
1274   if (cancellable)
1275     g_cancellable_cancel (cancellable);
1276 }
1277 
1278 static gboolean
grl_filesystem_source_notify_change_start(GrlSource * source,GError ** error)1279 grl_filesystem_source_notify_change_start (GrlSource *source,
1280                                            GError **error)
1281 {
1282   GrlFilesystemSource *fs_source;
1283   RecursiveOperation *operation;
1284 
1285   GRL_DEBUG (__func__);
1286 
1287   fs_source = GRL_FILESYSTEM_SOURCE (source);
1288   operation = recursive_operation_new ();
1289   operation->on_cancel = cancel_cb;
1290   operation->on_finish = finish_cb;
1291   operation->on_dir = directory_cb;
1292   operation->on_dir_data = fs_source;
1293   operation->max_depth = fs_source->priv->max_search_depth;
1294 
1295   fs_source->priv->cancellable_monitors = operation->cancellable;
1296 
1297   recursive_operation_initialize (operation, fs_source);
1298   recursive_operation_next_entry (operation);
1299 
1300   return TRUE;
1301 }
1302 
1303 static gboolean
grl_filesystem_source_notify_change_stop(GrlSource * source,GError ** error)1304 grl_filesystem_source_notify_change_stop (GrlSource *source,
1305                                           GError **error)
1306 {
1307   GrlFilesystemSource *fs_source = GRL_FILESYSTEM_SOURCE (source);
1308 
1309   /* Check if notifying is being initialized */
1310   if (fs_source->priv->cancellable_monitors) {
1311     g_cancellable_cancel (fs_source->priv->cancellable_monitors);
1312     fs_source->priv->cancellable_monitors = NULL;
1313   } else {
1314     /* Cancel and remove all monitors */
1315     cancel_monitors (fs_source);
1316   }
1317 
1318   return TRUE;
1319 }
1320 
1321 static GrlCaps *
grl_filesystem_source_get_caps(GrlSource * source,GrlSupportedOps operation)1322 grl_filesystem_source_get_caps (GrlSource *source,
1323                                 GrlSupportedOps operation)
1324 {
1325   GList *keys;
1326   static GrlCaps *caps = NULL;
1327 
1328   if (caps == NULL) {
1329    caps = grl_caps_new ();
1330    grl_caps_set_type_filter (caps, GRL_TYPE_FILTER_ALL);
1331    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_MIME,
1332                                      NULL);
1333    grl_caps_set_key_filter (caps, keys);
1334    g_list_free (keys);
1335    keys = grl_metadata_key_list_new (GRL_METADATA_KEY_MODIFICATION_DATE,
1336                                      NULL);
1337    grl_caps_set_key_range_filter (caps, keys);
1338    g_list_free (keys);
1339   }
1340 
1341   return caps;
1342 }
1343