/*
 * Copyright (C) 2008, Nokia <ivan.frade@nokia.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301, USA.
 */

#include "config.h"

#include <string.h>

#include <gio/gio.h>
#include <gio/gunixmounts.h>

#include "storage.h"
#include "marshal.h"

/**
 * SECTION:storage
 * @short_description: Removable storage and mount point convenience API
 * @include: libmediaart/mediaart.h
 *
 * This API is a convenience to to be able to keep track of volumes
 * which are mounted and also the type of removable media available.
 * The API is built upon the top of GIO's #GMount, #GDrive and #GVolume API.
 **/

#define STORAGE_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), TYPE_STORAGE, StoragePrivate))

typedef struct {
	GVolumeMonitor *volume_monitor;

	GNode *mounts;
	GHashTable *mounts_by_uuid;
	GHashTable *unmount_watchdogs;
} StoragePrivate;

typedef struct {
	gchar *mount_point;
	gchar *uuid;
	guint unmount_timer_id;
	guint removable : 1;
	guint optical : 1;
} MountInfo;

typedef struct {
	const gchar *path;
	GNode *node;
} TraverseData;

typedef struct {
	GSList *roots;
	StorageType type;
	gboolean exact_match;
} GetRoots;

typedef struct {
	Storage *storage;
	GMount *mount;
} UnmountCheckData;

static void     storage_finalize         (GObject        *object);
static gboolean mount_info_free          (GNode          *node,
                                          gpointer        user_data);
static void     mount_node_free          (GNode          *node);
static gboolean mounts_setup             (Storage *storage);
static void     mount_added_cb           (GVolumeMonitor *monitor,
                                          GMount         *mount,
                                          gpointer        user_data);
static void     mount_removed_cb         (GVolumeMonitor *monitor,
                                          GMount         *mount,
                                          gpointer        user_data);
static void     mount_pre_removed_cb     (GVolumeMonitor *monitor,
                                          GMount         *mount,
                                          gpointer        user_data);

enum {
	MOUNT_POINT_ADDED,
	MOUNT_POINT_REMOVED,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = {0};

G_DEFINE_TYPE (Storage, storage, G_TYPE_OBJECT);

static void
storage_class_init (StorageClass *klass)
{
	GObjectClass *object_class;

	object_class = G_OBJECT_CLASS (klass);

	object_class->finalize     = storage_finalize;

	signals[MOUNT_POINT_ADDED] =
		g_signal_new ("mount-point-added",
		              G_TYPE_FROM_CLASS (klass),
		              G_SIGNAL_RUN_LAST,
		              0,
		              NULL, NULL,
		              media_art_marshal_VOID__STRING_STRING_STRING_BOOLEAN_BOOLEAN,
		              G_TYPE_NONE,
		              5,
		              G_TYPE_STRING,
		              G_TYPE_STRING,
		              G_TYPE_STRING,
		              G_TYPE_BOOLEAN,
		              G_TYPE_BOOLEAN);

	signals[MOUNT_POINT_REMOVED] =
		g_signal_new ("mount-point-removed",
		              G_TYPE_FROM_CLASS (klass),
		              G_SIGNAL_RUN_LAST,
		              0,
		              NULL, NULL,
		              media_art_marshal_VOID__STRING_STRING,
		              G_TYPE_NONE,
		              2,
		              G_TYPE_STRING,
		              G_TYPE_STRING);

	g_type_class_add_private (object_class, sizeof (StoragePrivate));
}

static void
storage_init (Storage *storage)
{
	StoragePrivate *priv;

	priv = STORAGE_GET_PRIVATE (storage);

	priv->mounts = g_node_new (NULL);

	priv->mounts_by_uuid = g_hash_table_new_full (g_str_hash,
	                                              g_str_equal,
	                                              (GDestroyNotify) g_free,
	                                              NULL);
	priv->unmount_watchdogs = g_hash_table_new_full (NULL, NULL, NULL,
	                                                 (GDestroyNotify) g_source_remove);

	priv->volume_monitor = g_volume_monitor_get ();

	/* Volume and property notification callbacks */
	g_signal_connect_object (priv->volume_monitor, "mount-removed",
	                         G_CALLBACK (mount_removed_cb), storage, 0);
	g_signal_connect_object (priv->volume_monitor, "mount-pre-unmount",
	                         G_CALLBACK (mount_pre_removed_cb), storage, 0);
	g_signal_connect_object (priv->volume_monitor, "mount-added",
	                         G_CALLBACK (mount_added_cb), storage, 0);

	/* Get all mounts and set them up */
	if (!mounts_setup (storage)) {
		return;
	}
}

static void
storage_finalize (GObject *object)
{
	StoragePrivate *priv;

	priv = STORAGE_GET_PRIVATE (object);

	g_hash_table_destroy (priv->unmount_watchdogs);

	if (priv->mounts_by_uuid) {
		g_hash_table_unref (priv->mounts_by_uuid);
	}

	if (priv->mounts) {
		mount_node_free (priv->mounts);
	}

	if (priv->volume_monitor) {
		g_object_unref (priv->volume_monitor);
	}

	(G_OBJECT_CLASS (storage_parent_class)->finalize) (object);
}

static void
mount_node_free (GNode *node)
{
	g_node_traverse (node,
	                 G_POST_ORDER,
	                 G_TRAVERSE_ALL,
	                 -1,
	                 mount_info_free,
	                 NULL);

	g_node_destroy (node);
}

static gboolean
mount_node_traverse_func (GNode    *node,
                          gpointer  user_data)
{
	TraverseData *data;
	MountInfo *info;

	if (!node->data) {
		/* Root node */
		return FALSE;
	}

	data = user_data;
	info = node->data;

	if (g_str_has_prefix (data->path, info->mount_point)) {
		data->node = node;
		return TRUE;
	}

	return FALSE;
}

static GNode *
mount_node_find (GNode       *root,
                 const gchar *path)
{
	TraverseData data = { path, NULL };

	g_node_traverse (root,
	                 G_POST_ORDER,
	                 G_TRAVERSE_ALL,
	                 -1,
	                 mount_node_traverse_func,
	                 &data);

	return data.node;
}

static gboolean
mount_info_free (GNode    *node,
                 gpointer  user_data)
{
	MountInfo *info;

	info = node->data;

	if (info) {
		g_free (info->mount_point);
		g_free (info->uuid);

		g_slice_free (MountInfo, info);
	}

	return FALSE;
}

static MountInfo *
mount_info_find (GNode       *root,
                 const gchar *path)
{
	GNode *node;

	node = mount_node_find (root, path);
	return (node) ? node->data : NULL;
}

static StorageType
mount_info_get_type (MountInfo *info)
{
	StorageType mount_type = 0;

	if (info->removable) {
		mount_type |= STORAGE_REMOVABLE;
	}

	if (info->optical) {
		mount_type |= STORAGE_OPTICAL;
	}

	return mount_type;
}

static gchar *
mount_point_normalize (const gchar *mount_point)
{
	gchar *mp;

	/* Normalize all mount points to have a / at the end */
	if (g_str_has_suffix (mount_point, G_DIR_SEPARATOR_S)) {
		mp = g_strdup (mount_point);
	} else {
		mp = g_strconcat (mount_point, G_DIR_SEPARATOR_S, NULL);
	}

	return mp;
}

static GNode *
mount_add_hierarchy (GNode       *root,
                     const gchar *uuid,
                     const gchar *mount_point,
                     gboolean     removable,
                     gboolean     optical)
{
	MountInfo *info;
	GNode *node;
	gchar *mp;

	mp = mount_point_normalize (mount_point);
	node = mount_node_find (root, mp);

	if (!node) {
		node = root;
	}

	info = g_slice_new (MountInfo);
	info->mount_point = mp;
	info->uuid = g_strdup (uuid);
	info->removable = removable;
	info->optical = optical;

	return g_node_append_data (node, info);
}

static void
mount_add_new (Storage *storage,
               const gchar    *uuid,
               const gchar    *mount_point,
               const gchar    *mount_name,
               gboolean        removable_device,
               gboolean        optical_disc)
{
	StoragePrivate *priv;
	GNode *node;

	priv = STORAGE_GET_PRIVATE (storage);

	node = mount_add_hierarchy (priv->mounts, uuid, mount_point, removable_device, optical_disc);
	g_hash_table_insert (priv->mounts_by_uuid, g_strdup (uuid), node);

	g_signal_emit (storage,
	               signals[MOUNT_POINT_ADDED],
	               0,
	               uuid,
	               mount_point,
	               mount_name,
	               removable_device,
	               optical_disc,
	               NULL);
}

static gchar *
mount_guess_content_type (GMount   *mount,
                          gboolean *is_optical,
                          gboolean *is_multimedia,
                          gboolean *is_blank)
{
	gchar *content_type = NULL;
	gchar **guess_type;

	*is_optical = FALSE;
	*is_multimedia = FALSE;
	*is_blank = FALSE;

	/* This function has 2 purposes:
	 *
	 * 1. Detect if we are using optical media
	 * 2. Detect if we are video or music, we can't index those types
	 *
	 * We try to determine the content type because we don't want
	 * to store Volume information in  about DVDs and media
	 * which has no real data for us to mine.
	 *
	 * Generally, if is_multimedia is TRUE then we end up ignoring
	 * the media.
	 */
	guess_type = g_mount_guess_content_type_sync (mount, TRUE, NULL, NULL);

	if (guess_type) {
		gint i = 0;

		while (!content_type && guess_type[i]) {
			if (!g_strcmp0 (guess_type[i], "x-content/image-picturecd")) {
				/* Images */
				content_type = g_strdup (guess_type[i]);
			} else if (!g_strcmp0 (guess_type[i], "x-content/video-bluray") ||
			           !g_strcmp0 (guess_type[i], "x-content/video-dvd") ||
			           !g_strcmp0 (guess_type[i], "x-content/video-hddvd") ||
			           !g_strcmp0 (guess_type[i], "x-content/video-svcd") ||
			           !g_strcmp0 (guess_type[i], "x-content/video-vcd")) {
				/* Videos */
				*is_multimedia = TRUE;
				content_type = g_strdup (guess_type[i]);
			} else if (!g_strcmp0 (guess_type[i], "x-content/audio-cdda") ||
			           !g_strcmp0 (guess_type[i], "x-content/audio-dvd") ||
			           !g_strcmp0 (guess_type[i], "x-content/audio-player")) {
				/* Audios */
				*is_multimedia = TRUE;
				content_type = g_strdup (guess_type[i]);
			} else if (!g_strcmp0 (guess_type[i], "x-content/blank-bd") ||
			           !g_strcmp0 (guess_type[i], "x-content/blank-cd") ||
			           !g_strcmp0 (guess_type[i], "x-content/blank-dvd") ||
			           !g_strcmp0 (guess_type[i], "x-content/blank-hddvd")) {
				/* Blank */
				*is_blank = TRUE;
				content_type = g_strdup (guess_type[i]);
			} else if (!g_strcmp0 (guess_type[i], "x-content/software") ||
			           !g_strcmp0 (guess_type[i], "x-content/unix-software") ||
			           !g_strcmp0 (guess_type[i], "x-content/win32-software")) {
				/* NOTE: This one is a guess, can we
				 * have this content type on
				 * none-optical mount points?
				 */
				content_type = g_strdup (guess_type[i]);
			} else {
				/* else, keep on with the next guess, if any */
				i++;
			}
		}

		/* If we didn't have an exact match on possible guessed content types,
		 *  then use the first one returned (best guess always first) if any */
		if (!content_type && guess_type[0]) {
			content_type = g_strdup (guess_type[0]);
		}

		g_strfreev (guess_type);
	}

	if (content_type) {
		if (strstr (content_type, "vcd") ||
		    strstr (content_type, "cdda") ||
		    strstr (content_type, "dvd") ||
		    strstr (content_type, "bluray")) {
			*is_optical = TRUE;
		}
	} else {
		GUnixMountEntry *entry;
		gchar *mount_path;
		GFile *mount_root;

		/* No content type was guessed, try to find out
		 * at least whether it's an optical media or not
		 */
		mount_root = g_mount_get_root (mount);
		mount_path = g_file_get_path (mount_root);

		/* FIXME: Try to assume we have a unix mount :(
		 * EEK, once in a while, I have to write crack, oh well
		 */
		if (mount_path &&
		    (entry = g_unix_mount_at (mount_path, NULL)) != NULL) {
			const gchar *filesystem_type;
			gchar *device_path = NULL;
			GVolume *volume;

			volume = g_mount_get_volume (mount);
			filesystem_type = g_unix_mount_get_fs_type (entry);
			g_debug ("  Using filesystem type:'%s'",
			         filesystem_type);

			/* Volume may be NULL */
			if (volume) {
				device_path = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE);
				g_debug ("  Using device path:'%s'",
				         device_path);
				g_object_unref (volume);
			}

			/* NOTE: This code was taken from guess_mount_type()
			 * in GIO's gunixmounts.c and adapted purely for
			 * guessing optical media. We don't use the guessing
			 * code for other types such as MEMSTICKS, ZIPs,
			 * IPODs, etc.
			 *
			 * This code may need updating over time since it is
			 * very situational depending on how distributions
			 * mount their devices and how devices are named in
			 * /dev.
			 */
			if (strcmp (filesystem_type, "udf") == 0 ||
			    strcmp (filesystem_type, "iso9660") == 0 ||
			    strcmp (filesystem_type, "cd9660") == 0 ||
			    (device_path &&
			     (g_str_has_prefix (device_path, "/dev/cdrom") ||
			      g_str_has_prefix (device_path, "/dev/acd") ||
			      g_str_has_prefix (device_path, "/dev/cd")))) {
				*is_optical = TRUE;
			} else if (device_path &&
			           g_str_has_prefix (device_path, "/vol/")) {
				const gchar *name;

				name = mount_path + strlen ("/");

				if (g_str_has_prefix (name, "cdrom")) {
					*is_optical = TRUE;
				}
			} else {
				gchar *basename = g_path_get_basename (mount_path);

				if (g_str_has_prefix (basename, "cdr") ||
				    g_str_has_prefix (basename, "cdwriter") ||
				    g_str_has_prefix (basename, "burn") ||
				    g_str_has_prefix (basename, "dvdr")) {
					*is_optical = TRUE;
				}

				g_free (basename);
			}

			g_free (device_path);
			g_free (mount_path);
			g_unix_mount_free (entry);
		} else {
			g_debug ("  No GUnixMountEntry found, needed for detecting if optical media... :(");
			g_free (mount_path);
		}

		g_object_unref (mount_root);
	}

	return content_type;
}

static void
mount_add (Storage *storage,
           GMount         *mount)
{
	StoragePrivate *priv;
	GFile *root;
	GVolume *volume;
	gchar *mount_name, *mount_path, *uuid;
	gboolean is_optical = FALSE;
	gboolean is_removable = FALSE;

	/* Get mount name */
	mount_name = g_mount_get_name (mount);

	/* Get root path of the mount */
	root = g_mount_get_root (mount);
	mount_path = g_file_get_path (root);

	g_debug ("Found '%s' mounted on path '%s'",
	         mount_name,
	         mount_path);

	/* Do not process shadowed mounts! */
	if (g_mount_is_shadowed (mount)) {
		g_debug ("  Skipping shadowed mount '%s'", mount_name);
		g_object_unref (root);
		g_free (mount_path);
		g_free (mount_name);
		return;
	}

	priv = STORAGE_GET_PRIVATE (storage);

	/* fstab partitions may not have corresponding
	 * GVolumes, so volume may be NULL */
	volume = g_mount_get_volume (mount);
	if (volume) {
		/* GMount with GVolume */

		/* Try to get UUID from the Volume.
		 * Note that g_volume_get_uuid() is NOT equivalent */
		uuid = g_volume_get_identifier (volume,
		                                G_VOLUME_IDENTIFIER_KIND_UUID);
		if (!uuid) {
			gchar *content_type;
			gboolean is_multimedia;
			gboolean is_blank;

			/* Optical discs usually won't have UUID in the GVolume */
			content_type = mount_guess_content_type (mount, &is_optical, &is_multimedia, &is_blank);
			is_removable = TRUE;

			/* We don't index content which is video, music or blank */
			if (!is_multimedia && !is_blank) {
				uuid = g_compute_checksum_for_string (G_CHECKSUM_MD5,
				                                      mount_name,
				                                      -1);
				g_debug ("  No UUID, generated:'%s' (based on mount name)", uuid);
				g_debug ("  Assuming GVolume has removable media, if wrong report a bug! "
				         "content type is '%s'",
				         content_type);
			} else {
				g_debug ("  Being ignored because mount with volume is music/video/blank "
				         "(content type:%s, optical:%s, multimedia:%s, blank:%s)",
				         content_type,
				         is_optical ? "yes" : "no",
				         is_multimedia ? "yes" : "no",
				         is_blank ? "yes" : "no");
			}

			g_free (content_type);
		} else {
			/* Any other removable media will have UUID in the
			 * GVolume. Note that this also may include some
			 * partitions in the machine which have GVolumes
			 * associated to the GMounts. We also check a drive
			 * exists to be sure the device is local. */
			GDrive *drive;

			drive = g_volume_get_drive (volume);

			if (drive) {
				/* We can't mount/unmount system volumes, so tag
				 * them as non removable. */
				is_removable = g_volume_can_mount (volume);
				g_debug ("  Found mount with volume and drive which %s be mounted: "
				         "Assuming it's %s removable, if wrong report a bug!",
				         is_removable ? "can" : "cannot",
				         is_removable ? "" : "not");
				g_object_unref (drive);
			} else {
				/* Note: not sure when this can happen... */
				g_debug ("  Mount with volume but no drive, "
				         "assuming not a removable device, "
				         "if wrong report a bug!");
				is_removable = FALSE;
			}
		}

		g_object_unref (volume);
	} else {
		/* GMount without GVolume.
		 * Note: Never found a case where this g_mount_get_uuid() returns
		 * non-NULL... :-) */
		uuid = g_mount_get_uuid (mount);
		if (!uuid) {
			if (mount_path) {
				gchar *content_type;
				gboolean is_multimedia;
				gboolean is_blank;

				content_type = mount_guess_content_type (mount, &is_optical, &is_multimedia, &is_blank);

				/* Note: for GMounts without GVolume, is_blank should NOT be considered,
				 * as it may give unwanted results... */
				if (!is_multimedia) {
					uuid = g_compute_checksum_for_string (G_CHECKSUM_MD5,
					                                      mount_path,
					                                      -1);
					g_debug ("  No UUID, generated:'%s' (based on mount path)", uuid);
				} else {
					g_debug ("  Being ignored because mount is music/video "
					         "(content type:%s, optical:%s, multimedia:%s)",
					         content_type,
					         is_optical ? "yes" : "no",
					         is_multimedia ? "yes" : "no");
				}

				g_free (content_type);
			} else {
				g_debug ("  Being ignored because mount has no GVolume (i.e. not user mountable) "
				         "and has no mount root path available");
			}
		}
	}

	/* If we got something to be used as UUID, then add the mount
	 * to the Storage */
	if (uuid && mount_path && !g_hash_table_lookup (priv->mounts_by_uuid, uuid)) {
		g_debug ("  Adding mount point with UUID: '%s', removable: %s, optical: %s, path: '%s'",
		         uuid,
		         is_removable ? "yes" : "no",
		         is_optical ? "yes" : "no",
		         mount_path);
		mount_add_new (storage, uuid, mount_path, mount_name, is_removable, is_optical);
	} else {
		g_debug ("  Skipping mount point with UUID: '%s', path: '%s', already managed: '%s'",
		         uuid ? uuid : "none",
		         mount_path ? mount_path : "none",
		         (uuid && g_hash_table_lookup (priv->mounts_by_uuid, uuid)) ? "yes" : "no");
	}

	g_free (mount_name);
	g_free (mount_path);
	g_free (uuid);
	g_object_unref (root);
}

static gboolean
mounts_setup (Storage *storage)
{
	StoragePrivate *priv;
	GList *mounts, *lm;

	priv = STORAGE_GET_PRIVATE (storage);

	mounts = g_volume_monitor_get_mounts (priv->volume_monitor);

	if (!mounts) {
		g_debug ("No mounts found to iterate");
		return TRUE;
	}

	/* Iterate over all available mounts and add them.
	 * Note that GVolumeMonitor shows only those mounts which are
	 * actually mounted. */
	for (lm = mounts; lm; lm = g_list_next (lm)) {
		mount_add (storage, lm->data);
		g_object_unref (lm->data);
	}

	g_list_free (mounts);

	return TRUE;
}

static void
mount_added_cb (GVolumeMonitor *monitor,
                GMount         *mount,
                gpointer        user_data)
{
	mount_add (user_data, mount);
}

static void
mount_remove (Storage *storage,
              GMount         *mount)
{
	StoragePrivate *priv;
	MountInfo *info;
	GNode *node;
	GFile *file;
	gchar *name;
	gchar *mount_point;
	gchar *mp;

	priv = STORAGE_GET_PRIVATE (storage);

	file = g_mount_get_root (mount);
	mount_point = g_file_get_path (file);
	name = g_mount_get_name (mount);

	mp = mount_point_normalize (mount_point);
	node = mount_node_find (priv->mounts, mp);
	g_free (mp);

	if (node) {
		info = node->data;

		g_debug ("Mount:'%s' with UUID:'%s' now unmounted from:'%s'",
		         name,
		         info->uuid,
		         mount_point);

		g_signal_emit (storage, signals[MOUNT_POINT_REMOVED], 0, info->uuid, mount_point, NULL);

		g_hash_table_remove (priv->mounts_by_uuid, info->uuid);
		mount_node_free (node);
	} else {
		g_debug ("Mount:'%s' now unmounted from:'%s' (was not tracked)",
		         name,
		         mount_point);
	}

	g_free (name);
	g_free (mount_point);
	g_object_unref (file);
}

static void
mount_removed_cb (GVolumeMonitor *monitor,
                  GMount         *mount,
                  gpointer        user_data)
{
	Storage *storage;
	StoragePrivate *priv;

	storage = user_data;
	priv = STORAGE_GET_PRIVATE (storage);

	mount_remove (storage, mount);

	/* Unmount suceeded, remove the pending check */
	g_hash_table_remove (priv->unmount_watchdogs, mount);
}

static gboolean
unmount_failed_cb (gpointer user_data)
{
	UnmountCheckData *data = user_data;
	StoragePrivate *priv;

	/* If this timeout gets to be executed, this is due
	 * to a pre-unmount signal with no corresponding
	 * unmount in a timely fashion, we assume this is
	 * due to an error, and add back the still mounted
	 * path.
	 */
	priv = STORAGE_GET_PRIVATE (data->storage);

	g_warning ("Unmount operation failed, adding back mount point...");

	mount_add (data->storage, data->mount);

	g_hash_table_remove (priv->unmount_watchdogs, data->mount);
	return FALSE;
}

static void
mount_pre_removed_cb (GVolumeMonitor *monitor,
                      GMount         *mount,
                      gpointer        user_data)
{
	Storage *storage;
	StoragePrivate *priv;
	UnmountCheckData *data;
	guint id;

	storage = user_data;
	priv = STORAGE_GET_PRIVATE (storage);

	mount_remove (storage, mount);

	/* Add check for failed unmounts */
	data = g_new (UnmountCheckData, 1);
	data->storage = storage;
	data->mount = mount;

	id = g_timeout_add_seconds_full (G_PRIORITY_DEFAULT_IDLE + 10, 3,
	                                 unmount_failed_cb,
	                                 data, (GDestroyNotify) g_free);
	g_hash_table_insert (priv->unmount_watchdogs, data->mount,
	                     GUINT_TO_POINTER (id));
}

/**
 * storage_new:
 *
 * Creates a new instance of #Storage.
 *
 * Returns: The newly created #Storage.
 *
 * Since: 0.2.0
 **/
Storage *
storage_new (void)
{
	return g_object_new (TYPE_STORAGE, NULL);
}

static void
get_mount_point_by_uuid_foreach (gpointer key,
                                 gpointer value,
                                 gpointer user_data)
{
	GetRoots *gr;
	GNode *node;
	MountInfo *info;
	StorageType mount_type;

	gr = user_data;
	node = value;
	info = node->data;
	mount_type = mount_info_get_type (info);

	/* is mount of the type we're looking for? */
	if ((gr->exact_match && mount_type == gr->type) ||
	    (!gr->exact_match && (mount_type & gr->type))) {
		gchar *normalized_mount_point;
		gint len;

		normalized_mount_point = g_strdup (info->mount_point);
		len = strlen (normalized_mount_point);

		/* Don't include trailing slashes */
		if (len > 2 && normalized_mount_point[len - 1] == G_DIR_SEPARATOR) {
			normalized_mount_point[len - 1] = '\0';
		}

		gr->roots = g_slist_prepend (gr->roots, normalized_mount_point);
	}
}

/**
 * storage_get_device_roots:
 * @storage: A #Storage
 * @type: A #StorageType
 * @exact_match: if all devices should exactly match the types
 *
 * Returns: (transfer full) (element-type utf8): a #GSList of strings
 * containing the root directories for devices with @type based on
 * @exact_match. Each element must be freed using g_free() and the
 * list itself through g_slist_free().
 *
 * Since: 0.2.0
 **/
GSList *
storage_get_device_roots (Storage     *storage,
                          StorageType  type,
                          gboolean     exact_match)
{
	StoragePrivate *priv;
	GetRoots gr;

	g_return_val_if_fail (IS_STORAGE (storage), NULL);

	priv = STORAGE_GET_PRIVATE (storage);

	gr.roots = NULL;
	gr.type = type;
	gr.exact_match = exact_match;

	g_hash_table_foreach (priv->mounts_by_uuid,
	                      get_mount_point_by_uuid_foreach,
	                      &gr);

	return gr.roots;
}

/**
 * storage_get_device_uuids:
 * @storage: A #Storage
 * @type: A #StorageType
 * @exact_match: if all devices should exactly match the types
 *
 * Returns: (transfer full) (element-type utf8): a #GSList of
 * strings containing the UUID for devices with @type based
 * on @exact_match. Each element must be freed using g_free()
 * and the list itself through g_slist_free().
 *
 * Since: 0.2.0
 **/
GSList *
storage_get_device_uuids (Storage     *storage,
                          StorageType  type,
                          gboolean     exact_match)
{
	StoragePrivate *priv;
	GHashTableIter iter;
	gpointer key, value;
	GSList *uuids;

	g_return_val_if_fail (IS_STORAGE (storage), NULL);

	priv = STORAGE_GET_PRIVATE (storage);

	uuids = NULL;

	g_hash_table_iter_init (&iter, priv->mounts_by_uuid);

	while (g_hash_table_iter_next (&iter, &key, &value)) {
		const gchar *uuid;
		GNode *node;
		MountInfo *info;
		StorageType mount_type;

		uuid = key;
		node = value;
		info = node->data;

		mount_type = mount_info_get_type (info);

		/* is mount of the type we're looking for? */
		if ((exact_match && mount_type == type) ||
		    (!exact_match && (mount_type & type))) {
			uuids = g_slist_prepend (uuids, g_strdup (uuid));
		}
	}

	return uuids;
}

/**
 * storage_get_mount_point_for_uuid:
 * @storage: A #Storage
 * @uuid: A string pointer to the UUID for the %GVolume.
 *
 * Returns: The mount point for @uuid, this should not be freed.
 *
 * Since: 0.2.0
 **/
const gchar *
storage_get_mount_point_for_uuid (Storage     *storage,
                                  const gchar *uuid)
{
	StoragePrivate *priv;
	GNode *node;
	MountInfo *info;

	g_return_val_if_fail (IS_STORAGE (storage), NULL);
	g_return_val_if_fail (uuid != NULL, NULL);

	priv = STORAGE_GET_PRIVATE (storage);

	node = g_hash_table_lookup (priv->mounts_by_uuid, uuid);

	if (!node) {
		return NULL;
	}

	info = node->data;

	return info->mount_point;
}

/**
 * storage_get_type_for_uuid:
 * @storage: A #Storage
 * @uuid: A string pointer to the UUID for the %GVolume.
 *
 * Returns: The type flags for @uuid.
 *
 * Since: 0.2.0
 **/
StorageType
storage_get_type_for_uuid (Storage     *storage,
                           const gchar *uuid)
{
	StoragePrivate *priv;
	GNode *node;
	StorageType type = 0;

	g_return_val_if_fail (IS_STORAGE (storage), 0);
	g_return_val_if_fail (uuid != NULL, 0);

	priv = STORAGE_GET_PRIVATE (storage);

	node = g_hash_table_lookup (priv->mounts_by_uuid, uuid);

	if (node) {
		MountInfo *info;

		info = node->data;

		if (info->removable) {
			type |= STORAGE_REMOVABLE;
		}
		if (info->optical) {
			type |= STORAGE_OPTICAL;
		}
	}

	return type;
}

/**
 * storage_get_uuid_for_file:
 * @storage: A #Storage
 * @file: a file
 *
 * Returns the UUID of the removable device for @file
 *
 * Returns: Returns the UUID of the removable device for @file, this
 * should not be freed.
 *
 * Since: 0.2.0
 **/
const gchar *
storage_get_uuid_for_file (Storage *storage,
                           GFile   *file)
{
	StoragePrivate *priv;
	gchar *path;
	MountInfo *info;

	g_return_val_if_fail (IS_STORAGE (storage), FALSE);

	path = g_file_get_path (file);

	if (!path) {
		return NULL;
	}

	/* Normalize all paths to have a / at the end */
	if (!g_str_has_suffix (path, G_DIR_SEPARATOR_S)) {
		gchar *norm_path;

		norm_path = g_strconcat (path, G_DIR_SEPARATOR_S, NULL);
		g_free (path);
		path = norm_path;
	}

	priv = STORAGE_GET_PRIVATE (storage);

	info = mount_info_find (priv->mounts, path);

	if (!info) {
		g_free (path);
		return NULL;
	}

	/* g_debug ("Mount for path '%s' is '%s' (UUID:'%s')", */
	/*          path, info->mount_point, info->uuid); */

	g_free (path);

	return info->uuid;
}

