/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
* Copyright (C) 1999, 2000, 2001 Eazel, Inc.
* Copyright (C) 2011 Red Hat, Inc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*
* Author: Cosimo Cecchi
*
* The code for crawling the directory hierarchy is based on
* nautilus/libnautilus-private/nautilus-directory-async.c, with
* the following copyright and author:
*
* Copyright (C) 1999, 2000, 2001 Eazel, Inc.
* Author: Darin Adler
*
*/
#include "shell-mime-sniffer.h"
#include "hotplug-mimetypes.h"
#include
#include
#define LOADER_ATTRS \
G_FILE_ATTRIBUTE_STANDARD_TYPE "," \
G_FILE_ATTRIBUTE_STANDARD_NAME "," \
G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","
#define WATCHDOG_TIMEOUT 1500
#define DIRECTORY_LOAD_ITEMS_PER_CALLBACK 100
#define HIGH_SCORE_RATIO 0.10
enum {
PROP_FILE = 1,
NUM_PROPERTIES
};
static GHashTable *image_type_table = NULL;
static GHashTable *audio_type_table = NULL;
static GHashTable *video_type_table = NULL;
static GHashTable *docs_type_table = NULL;
static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
typedef struct {
ShellMimeSniffer *self;
GFile *file;
GFileEnumerator *enumerator;
GList *deep_count_subdirectories;
gint audio_count;
gint image_count;
gint document_count;
gint video_count;
gint total_items;
} DeepCountState;
typedef struct _ShellMimeSnifferPrivate ShellMimeSnifferPrivate;
struct _ShellMimeSniffer
{
GObject parent_instance;
ShellMimeSnifferPrivate *priv;
};
struct _ShellMimeSnifferPrivate {
GFile *file;
GCancellable *cancellable;
guint watchdog_id;
GTask *task;
};
G_DEFINE_TYPE_WITH_PRIVATE (ShellMimeSniffer, shell_mime_sniffer, G_TYPE_OBJECT);
static void deep_count_load (DeepCountState *state,
GFile *file);
static void
init_mimetypes (void)
{
static gsize once_init = 0;
if (g_once_init_enter (&once_init))
{
GSList *formats, *l;
GdkPixbufFormat *format;
gchar **types;
gint idx;
image_type_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
video_type_table = g_hash_table_new (g_str_hash, g_str_equal);
audio_type_table = g_hash_table_new (g_str_hash, g_str_equal);
docs_type_table = g_hash_table_new (g_str_hash, g_str_equal);
formats = gdk_pixbuf_get_formats ();
for (l = formats; l != NULL; l = l->next)
{
format = l->data;
types = gdk_pixbuf_format_get_mime_types (format);
for (idx = 0; types[idx] != NULL; idx++)
g_hash_table_insert (image_type_table, g_strdup (types[idx]), GINT_TO_POINTER (1));
g_strfreev (types);
}
g_slist_free (formats);
for (idx = 0; audio_mimetypes[idx] != NULL; idx++)
g_hash_table_insert (audio_type_table, (gpointer) audio_mimetypes[idx], GINT_TO_POINTER (1));
for (idx = 0; video_mimetypes[idx] != NULL; idx++)
g_hash_table_insert (video_type_table, (gpointer) video_mimetypes[idx], GINT_TO_POINTER (1));
for (idx = 0; docs_mimetypes[idx] != NULL; idx++)
g_hash_table_insert (docs_type_table, (gpointer) docs_mimetypes[idx], GINT_TO_POINTER (1));
g_once_init_leave (&once_init, 1);
}
}
static void
add_content_type_to_cache (DeepCountState *state,
const gchar *content_type)
{
gboolean matched = TRUE;
if (g_hash_table_lookup (image_type_table, content_type))
state->image_count++;
else if (g_hash_table_lookup (video_type_table, content_type))
state->video_count++;
else if (g_hash_table_lookup (docs_type_table, content_type))
state->document_count++;
else if (g_hash_table_lookup (audio_type_table, content_type))
state->audio_count++;
else
matched = FALSE;
if (matched)
state->total_items++;
}
typedef struct {
const gchar *type;
gdouble ratio;
} SniffedResult;
static gint
results_cmp_func (gconstpointer a,
gconstpointer b)
{
const SniffedResult *sniffed_a = a;
const SniffedResult *sniffed_b = b;
if (sniffed_a->ratio < sniffed_b->ratio)
return 1;
if (sniffed_a->ratio > sniffed_b->ratio)
return -1;
return 0;
}
static void
prepare_async_result (DeepCountState *state)
{
ShellMimeSniffer *self = state->self;
GArray *results;
GPtrArray *sniffed_mime;
SniffedResult result;
char **mimes;
sniffed_mime = g_ptr_array_new ();
results = g_array_new (TRUE, TRUE, sizeof (SniffedResult));
if (state->total_items == 0)
goto out;
result.type = "x-content/video";
result.ratio = (gdouble) state->video_count / (gdouble) state->total_items;
g_array_append_val (results, result);
result.type = "x-content/audio";
result.ratio = (gdouble) state->audio_count / (gdouble) state->total_items;
g_array_append_val (results, result);
result.type = "x-content/pictures";
result.ratio = (gdouble) state->image_count / (gdouble) state->total_items;
g_array_append_val (results, result);
result.type = "x-content/documents";
result.ratio = (gdouble) state->document_count / (gdouble) state->total_items;
g_array_append_val (results, result);
g_array_sort (results, results_cmp_func);
result = g_array_index (results, SniffedResult, 0);
g_ptr_array_add (sniffed_mime, g_strdup (result.type));
/* if other types score high in ratio, add them, up to three */
result = g_array_index (results, SniffedResult, 1);
if (result.ratio < HIGH_SCORE_RATIO)
goto out;
g_ptr_array_add (sniffed_mime, g_strdup (result.type));
result = g_array_index (results, SniffedResult, 2);
if (result.ratio < HIGH_SCORE_RATIO)
goto out;
g_ptr_array_add (sniffed_mime, g_strdup (result.type));
out:
g_ptr_array_add (sniffed_mime, NULL);
mimes = (gchar **) g_ptr_array_free (sniffed_mime, FALSE);
g_array_free (results, TRUE);
g_task_return_pointer (self->priv->task, mimes, (GDestroyNotify)g_strfreev);
}
/* adapted from nautilus/libnautilus-private/nautilus-directory-async.c */
static void
deep_count_one (DeepCountState *state,
GFileInfo *info)
{
GFile *subdir;
const char *content_type;
if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
{
/* record the fact that we have to descend into this directory */
subdir = g_file_get_child (state->file, g_file_info_get_name (info));
state->deep_count_subdirectories =
g_list_append (state->deep_count_subdirectories, subdir);
}
else
{
content_type = g_file_info_get_content_type (info);
if (content_type)
add_content_type_to_cache (state, content_type);
}
}
static void
deep_count_finish (DeepCountState *state)
{
prepare_async_result (state);
if (state->enumerator)
{
if (!g_file_enumerator_is_closed (state->enumerator))
g_file_enumerator_close_async (state->enumerator,
0, NULL, NULL, NULL);
g_object_unref (state->enumerator);
}
g_cancellable_reset (state->self->priv->cancellable);
g_clear_object (&state->file);
g_list_free_full (state->deep_count_subdirectories, g_object_unref);
g_free (state);
}
static void
deep_count_next_dir (DeepCountState *state)
{
GFile *new_file;
g_clear_object (&state->file);
if (state->deep_count_subdirectories != NULL)
{
/* Work on a new directory. */
new_file = state->deep_count_subdirectories->data;
state->deep_count_subdirectories =
g_list_remove (state->deep_count_subdirectories, new_file);
deep_count_load (state, new_file);
g_object_unref (new_file);
}
else
{
deep_count_finish (state);
}
}
static void
deep_count_more_files_callback (GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
DeepCountState *state;
GList *files, *l;
GFileInfo *info;
state = user_data;
if (g_cancellable_is_cancelled (state->self->priv->cancellable))
{
deep_count_finish (state);
return;
}
files = g_file_enumerator_next_files_finish (state->enumerator,
res, NULL);
for (l = files; l != NULL; l = l->next)
{
info = l->data;
deep_count_one (state, info);
g_object_unref (info);
}
if (files == NULL)
{
g_file_enumerator_close_async (state->enumerator, 0, NULL, NULL, NULL);
g_object_unref (state->enumerator);
state->enumerator = NULL;
deep_count_next_dir (state);
}
else
{
g_file_enumerator_next_files_async (state->enumerator,
DIRECTORY_LOAD_ITEMS_PER_CALLBACK,
G_PRIORITY_LOW,
state->self->priv->cancellable,
deep_count_more_files_callback,
state);
}
g_list_free (files);
}
static void
deep_count_callback (GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
DeepCountState *state;
GFileEnumerator *enumerator;
state = user_data;
if (g_cancellable_is_cancelled (state->self->priv->cancellable))
{
deep_count_finish (state);
return;
}
enumerator = g_file_enumerate_children_finish (G_FILE (source_object),
res, NULL);
if (enumerator == NULL)
{
deep_count_next_dir (state);
}
else
{
state->enumerator = enumerator;
g_file_enumerator_next_files_async (state->enumerator,
DIRECTORY_LOAD_ITEMS_PER_CALLBACK,
G_PRIORITY_LOW,
state->self->priv->cancellable,
deep_count_more_files_callback,
state);
}
}
static void
deep_count_load (DeepCountState *state,
GFile *file)
{
state->file = g_object_ref (file);
g_file_enumerate_children_async (state->file,
LOADER_ATTRS,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, /* flags */
G_PRIORITY_LOW, /* prio */
state->self->priv->cancellable,
deep_count_callback,
state);
}
static void
deep_count_start (ShellMimeSniffer *self)
{
DeepCountState *state;
state = g_new0 (DeepCountState, 1);
state->self = self;
deep_count_load (state, self->priv->file);
}
static void
query_info_async_ready_cb (GObject *source,
GAsyncResult *res,
gpointer user_data)
{
GFileInfo *info;
GError *error = NULL;
ShellMimeSniffer *self = user_data;
info = g_file_query_info_finish (G_FILE (source),
res, &error);
if (error != NULL)
{
g_task_return_error (self->priv->task, error);
return;
}
if (g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY)
{
g_task_return_new_error (self->priv->task,
G_IO_ERROR,
G_IO_ERROR_NOT_DIRECTORY,
"Not a directory");
return;
}
deep_count_start (self);
}
static gboolean
watchdog_timeout_reached_cb (gpointer user_data)
{
ShellMimeSniffer *self = user_data;
self->priv->watchdog_id = 0;
g_cancellable_cancel (self->priv->cancellable);
return FALSE;
}
static void
start_loading_file (ShellMimeSniffer *self)
{
g_file_query_info_async (self->priv->file,
LOADER_ATTRS,
G_FILE_QUERY_INFO_NONE,
G_PRIORITY_DEFAULT,
self->priv->cancellable,
query_info_async_ready_cb,
self);
}
static void
shell_mime_sniffer_set_file (ShellMimeSniffer *self,
GFile *file)
{
g_clear_object (&self->priv->file);
self->priv->file = g_object_ref (file);
}
static void
shell_mime_sniffer_dispose (GObject *object)
{
ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object);
g_clear_object (&self->priv->file);
g_clear_object (&self->priv->cancellable);
g_clear_object (&self->priv->task);
g_clear_handle_id (&self->priv->watchdog_id, g_source_remove);
G_OBJECT_CLASS (shell_mime_sniffer_parent_class)->dispose (object);
}
static void
shell_mime_sniffer_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object);
switch (prop_id) {
case PROP_FILE:
g_value_set_object (value, self->priv->file);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
shell_mime_sniffer_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object);
switch (prop_id) {
case PROP_FILE:
shell_mime_sniffer_set_file (self, g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
shell_mime_sniffer_class_init (ShellMimeSnifferClass *klass)
{
GObjectClass *oclass;
oclass = G_OBJECT_CLASS (klass);
oclass->dispose = shell_mime_sniffer_dispose;
oclass->get_property = shell_mime_sniffer_get_property;
oclass->set_property = shell_mime_sniffer_set_property;
properties[PROP_FILE] =
g_param_spec_object ("file",
"File",
"The loaded file",
G_TYPE_FILE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (oclass, NUM_PROPERTIES, properties);
}
static void
shell_mime_sniffer_init (ShellMimeSniffer *self)
{
self->priv = shell_mime_sniffer_get_instance_private (self);
init_mimetypes ();
}
ShellMimeSniffer *
shell_mime_sniffer_new (GFile *file)
{
return g_object_new (SHELL_TYPE_MIME_SNIFFER,
"file", file,
NULL);
}
void
shell_mime_sniffer_sniff_async (ShellMimeSniffer *self,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_assert (self->priv->watchdog_id == 0);
g_assert (self->priv->task == NULL);
self->priv->cancellable = g_cancellable_new ();
self->priv->task = g_task_new (self, self->priv->cancellable,
callback, user_data);
self->priv->watchdog_id =
g_timeout_add (WATCHDOG_TIMEOUT,
watchdog_timeout_reached_cb, self);
g_source_set_name_by_id (self->priv->watchdog_id, "[gnome-shell] watchdog_timeout_reached_cb");
start_loading_file (self);
}
gchar **
shell_mime_sniffer_sniff_finish (ShellMimeSniffer *self,
GAsyncResult *res,
GError **error)
{
return g_task_propagate_pointer (self->priv->task, error);
}