/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

/* e-book-backend-decsync.c - DecSync contact backend.
 * Based on the file contact backend.
 *
 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 * Copyright (C) 2012 Intel Corporation
 * Copyright (C) 2018 Aldo Gunsing
 *
 * 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.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Nat Friedman <nat@novell.com>
 *          Chris Toshok <toshok@ximian.com>
 *          Hans Petter Jansson <hpj@novell.com>
 *          Tristan Van Berkom <tristanvb@openismus.com>
 */

#include "evolution-decsync-config.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/time.h>

#include <glib/gstdio.h>
#include <glib/gi18n-lib.h>

#include <e-source/e-source-decsync.h>
#include <json-glib/json-glib.h>
#include <libdecsync.h>

#include "e-book-backend-decsync.h"

#define d(x)

#define E_BOOK_BACKEND_DECSYNC_VERSION_NAME "PAS-DB-VERSION"
#define E_BOOK_BACKEND_DECSYNC_VERSION "0.2"

#define E_BOOK_BACKEND_DECSYNC_REVISION_NAME "PAS-DB-REVISION"

#define PAS_ID_PREFIX "pas-id-"

/* We need to specify the folderid in order to properly
 * migrate data from the old EBookSqliteDB API.
 */
#define SQLITEDB_FOLDER_ID   "folder_id"
#define SQLITE_REVISION_KEY  "revision"

/* Forward Declarations */
static gboolean	book_backend_decsync_refresh_start (EBookBackendDecsync *bf);
static void	e_book_backend_decsync_initable_init
						(GInitableIface *iface);

struct _EBookBackendDecsyncPrivate {
	gchar     *base_directory;
	gchar     *photo_dirname;
	gchar     *revision;
	gchar     *locale;
	volatile gint rev_counter;
	gboolean   revision_guards;
	GRWLock    lock;
	GList     *cursors;

	EBookSqlite *sqlitedb;
	Decsync   decsync;
};

G_DEFINE_TYPE_WITH_CODE (
	EBookBackendDecsync,
	e_book_backend_decsync,
	E_TYPE_BOOK_BACKEND_SYNC,
	G_ADD_PRIVATE (EBookBackendDecsync)
	G_IMPLEMENT_INTERFACE (
		G_TYPE_INITABLE,
		e_book_backend_decsync_initable_init))

/****************************************************************
 *                   File Management helper APIs                *
 ****************************************************************/
typedef enum {
	GET_PATH_DB_DIR,
	GET_PATH_PHOTO_DIR
} GetPathType;

typedef enum {
	STATUS_NORMAL = 0,
	STATUS_MODIFIED,
	STATUS_ERROR
} PhotoModifiedStatus;

static gboolean
remove_file (const gchar *filename,
             GError **error)
{
	if (-1 == g_unlink (filename)) {
		if (errno == EACCES || errno == EPERM) {
			g_set_error_literal (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_PERMISSION_DENIED,
				e_client_error_to_string (
				E_CLIENT_ERROR_PERMISSION_DENIED));
		} else {
			g_set_error (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_OTHER_ERROR,
				_("Failed to remove file “%s”: %s"),
				filename, g_strerror (errno));
		}
		return FALSE;
	}

	return TRUE;
}

static gboolean
create_directory (const gchar *dirname,
                  GError **error)
{
	gint rv;

	rv = g_mkdir_with_parents (dirname, 0700);
	if (rv == -1 && errno != EEXIST) {
		g_warning ("failed to make directory %s: %s", dirname, g_strerror (errno));
		if (errno == EACCES || errno == EPERM)
			g_set_error_literal (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_PERMISSION_DENIED,
				e_client_error_to_string (
				E_CLIENT_ERROR_PERMISSION_DENIED));
		else
			g_set_error (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_OTHER_ERROR,
				_("Failed to make directory %s: %s"),
				dirname, g_strerror (errno));
		return FALSE;
	}
	return TRUE;
}

static gchar *
check_remove_uri_for_field (EContact *old_contact,
                            EContact *new_contact,
                            EContactField field)
{
	EContactPhoto *old_photo = NULL, *new_photo = NULL;
	gchar         *uri = NULL;

	old_photo = e_contact_get (old_contact, field);
	if (!old_photo)
		return NULL;

	if (new_contact) {

		new_photo = e_contact_get (new_contact, field);

		if (new_photo == NULL ||
		    g_ascii_strcasecmp (old_photo->data.uri, new_photo->data.uri))
			uri = g_strdup (old_photo->data.uri);
	} else {
		uri = g_strdup (old_photo->data.uri);
	}

	e_contact_photo_free (old_photo);
	e_contact_photo_free (new_photo);

	return uri;
}

static void
maybe_delete_uri (EBookBackendDecsync *bf,
                  const gchar *uri)
{
	GError *error = NULL;
	gchar  *filename;

	/* A uri that does not give us a filename is certainly not
	 * a uri that we created for a local file, just skip it */
	if ((filename = g_filename_from_uri (uri, NULL, NULL)) == NULL)
		return;

	/* If the file is in our path it belongs to us and we need to delete it.
	 */
	if (bf->priv->photo_dirname &&
	    !strncmp (bf->priv->photo_dirname, filename, strlen (bf->priv->photo_dirname))) {

		d (g_print ("Deleting uri file: %s\n", filename));

		/* Deleting uris should not cause the backend to fail to update
		 * a contact so the best we can do from here is log warnings
		 * when we fail to unlink a file from the disk.
		 */
		if (!remove_file (filename, &error)) {
			g_warning ("Unable to cleanup photo uri: %s", error->message);
			g_error_free (error);
		}
	}

	g_free (filename);
}

static void
maybe_delete_unused_uris (EBookBackendDecsync *bf,
                          EContact *old_contact,
                          EContact *new_contact)
{
	gchar             *uri_photo, *uri_logo;

	g_return_if_fail (old_contact != NULL);

	/* If there is no new contact, collect all the uris to delete from old_contact
	 *
	 * Otherwise, if any of the photo uri fields have changed in new_contact, then collect the
	 * old uris for those fields from old_contact to delete
	 */
	uri_photo = check_remove_uri_for_field (old_contact, new_contact, E_CONTACT_PHOTO);
	uri_logo = check_remove_uri_for_field (old_contact, new_contact, E_CONTACT_LOGO);

	if (uri_photo) {
		maybe_delete_uri (bf, uri_photo);
		g_free (uri_photo);
	}

	if (uri_logo) {
		maybe_delete_uri (bf, uri_logo);
		g_free (uri_logo);
	}
}

static gchar *
e_book_backend_decsync_extract_path_from_source (ESourceRegistry *registry,
                                              ESource *source,
                                              GetPathType path_type)
{
	ESource *builtin_source;
	const gchar *user_data_dir;
	const gchar *uid;
	gchar *filename = NULL;

	uid = e_source_get_uid (source);
	g_return_val_if_fail (uid != NULL, NULL);

	user_data_dir = e_get_user_data_dir ();

	builtin_source = e_source_registry_ref_builtin_address_book (registry);

	/* XXX Backward-compatibility hack:
	 *
	 * The special built-in "Personal" data source UIDs are now named
	 * "system-$COMPONENT" but since the data directories are already
	 * split out by component, we'll continue to use the old "system"
	 * directories for these particular data sources. */
	if (e_source_equal (source, builtin_source))
		uid = "system";

	switch (path_type) {
		case GET_PATH_DB_DIR:
			filename = g_build_filename (
				user_data_dir, "addressbook", uid, NULL);
			break;
		case GET_PATH_PHOTO_DIR:
			filename = g_build_filename (
				user_data_dir, "addressbook", uid, "photos", NULL);
			break;
		default:
			g_warn_if_reached ();
	}

	g_object_unref (builtin_source);

	return filename;
}

static gchar *
safe_name_for_photo (EBookBackendDecsync *bf,
                     EContact *contact,
                     EContactPhoto *photo,
                     EContactField field)
{
	gchar *fullname = NULL, *name, *str;
	gchar *suffix = NULL;
	gint i = 0;

	g_return_val_if_fail (photo->type == E_CONTACT_PHOTO_TYPE_INLINED, NULL);

	/* Get a suitable filename extension */
	if (photo->data.inlined.mime_type != NULL &&
	    photo->data.inlined.mime_type[0] != '\0' &&
	    g_ascii_strcasecmp (photo->data.inlined.mime_type, "image/X-EVOLUTION-UNKNOWN") != 0) {
		suffix = g_uri_escape_string (
			photo->data.inlined.mime_type,
			NULL, TRUE);
	} else {
		gchar *mime_type = NULL;
		gchar *content_type = NULL;

		content_type = g_content_type_guess (
			NULL,
			photo->data.inlined.data,
			photo->data.inlined.length,
			NULL);

		if (content_type)
			mime_type = g_content_type_get_mime_type (content_type);

		if (mime_type)
			suffix = g_uri_escape_string (mime_type, NULL, TRUE);
		else
			suffix = g_strdup ("data");

		g_free (mime_type);
		g_free (content_type);
	}

	/* Replace any percent in the text with a dash, to avoid escaping issues in URI-s */
	str = suffix;
	while (str = strchr (str, '%'), str) {
		*str = '-';
	}

	/* Create a filename based on the uid/field */
	name = g_strconcat (
		e_contact_get_const (contact, E_CONTACT_UID), "_",
		e_contact_field_name (field), NULL);
	name = g_strdelimit (name, NULL, '_');

	/* Replace any percent in the text with a dash, to avoid escaping issues in URI-s */
	str = name;
	while (str = strchr (str, '%'), str) {
		*str = '-';
	}

	do {
		g_free (fullname);

		str = e_filename_mkdir_encoded (bf->priv->photo_dirname, name, NULL, i);
		fullname = g_strdup_printf ("%s.%s", str, suffix);
		g_free (str);

		i++;
	} while (g_file_test (fullname, G_FILE_TEST_EXISTS));

	g_free (name);
	g_free (suffix);

	return fullname;
}

static gchar *
hard_link_photo (EBookBackendDecsync *bf,
                 EContact *contact,
                 EContactField field,
                 const gchar *src_filename,
                 GError **error)
{
	gchar *fullname = NULL, *name, *str;
	gint   i = 0, ret;
	const gchar *suffix;

	/* Copy over the file suffix */
	suffix = strrchr (src_filename, '.');
	if (suffix)
		suffix++;

	if (!suffix)
		suffix = "data";

	/* Create a filename based on uid/field */
	name = g_strconcat (
		e_contact_get_const (contact, E_CONTACT_UID), "_",
		e_contact_field_name (field), NULL);
	name = g_strdelimit (name, NULL, '_');

	do {
		g_free (fullname);

		str = e_filename_mkdir_encoded (bf->priv->photo_dirname, name, NULL, i);
		fullname = g_strdup_printf ("%s.%s", str, suffix);
		g_free (str);

		i++;

		#ifdef G_OS_WIN32
		{
		GFile *src_file = g_file_new_for_path (src_filename);
		GFile *dst_file = g_file_new_for_path (fullname);
		GError *copy_error = NULL;

		g_file_copy (src_file, dst_file, G_FILE_COPY_NONE,
		             NULL, NULL, NULL, &copy_error);

		if (copy_error != NULL) {
			g_error_free (copy_error);
			ret = -1;
		} else
			ret = 0;

		g_object_unref (src_file);
		g_object_unref (dst_file);
		}
		#else
		ret = link (src_filename, fullname);
		#endif

	} while (ret < 0 && errno == EEXIST);

	if (ret < 0) {
		if (errno == EACCES || errno == EPERM) {
			g_set_error_literal (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_PERMISSION_DENIED,
				e_client_error_to_string (
				E_CLIENT_ERROR_PERMISSION_DENIED));
		} else {
			g_set_error (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_OTHER_ERROR,
				_("Failed to create hardlink for resource “%s”: %s"),
				src_filename, g_strerror (errno));
		}
		g_free (fullname);
		fullname = NULL;
	}

	g_free (name);

	return fullname;
}

static gboolean
is_backend_owned_uri (EBookBackendDecsync *bf,
                      const gchar *uri)
{
	gchar     *filename;
	gchar     *dirname;
	gboolean   owned_uri;

	/* Errors converting from uri definitily indicate it was
	 * not our uri to begin with, so just disregard this error. */
	filename = g_filename_from_uri (uri, NULL, NULL);
	if (!filename)
		return FALSE;

	dirname = g_path_get_dirname (filename);

	owned_uri = bf->priv->photo_dirname && (strcmp (dirname, bf->priv->photo_dirname) == 0);

	g_free (filename);
	g_free (dirname);

	return owned_uri;
}

static PhotoModifiedStatus
maybe_transform_vcard_field_for_photo (EBookBackendDecsync *bf,
                                       EContact *old_contact,
                                       EContact *contact,
                                       EContactField field,
                                       GError **error)
{
	PhotoModifiedStatus  status = STATUS_NORMAL;
	EContactPhoto       *photo;

	if (field != E_CONTACT_PHOTO && field != E_CONTACT_LOGO)
		return status;

	photo = e_contact_get (contact, field);
	if (!photo)
		return status;

	if (photo->type == E_CONTACT_PHOTO_TYPE_INLINED) {
		EContactPhoto *new_photo;
		gchar         *new_photo_path;
		gchar         *uri;

		/* Create a unique filename with an extension (hopefully) based on the mime type */
		new_photo_path = safe_name_for_photo (bf, contact, photo, field);

		if ((uri =
		     g_filename_to_uri (new_photo_path, NULL, error)) == NULL) {

			status = STATUS_ERROR;
		} else if (!g_file_set_contents (new_photo_path,
						 (const gchar *) photo->data.inlined.data,
						 photo->data.inlined.length,
						 error)) {

			status = STATUS_ERROR;
		} else {
			new_photo = e_contact_photo_new ();
			new_photo->type = E_CONTACT_PHOTO_TYPE_URI;
			new_photo->data.uri = g_strdup (uri);

			e_contact_set (contact, field, new_photo);

			d (g_print ("Backend modified incomming binary blob to be %s:\n", uri));

			status = STATUS_MODIFIED;

			e_contact_photo_free (new_photo);
		}

		g_free (uri);
		g_free (new_photo_path);

	} else { /* E_CONTACT_PHOTO_TYPE_URI */
		const gchar       *uid;
		EContactPhoto     *old_photo = NULL, *new_photo;

		/* First determine that the new contact uri points to our 'photos' directory,
		 * if not then we do nothing
		 */
		if (!is_backend_owned_uri (bf, photo->data.uri))
			goto done;

		/* Now check if the uri is changed from the BDB copy
		 */
		uid = e_contact_get_const (contact, E_CONTACT_UID);
		if (uid == NULL) {
			g_set_error_literal (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_OTHER_ERROR,
				_("No UID in the contact"));
			status = STATUS_ERROR;
			goto done;
		}

		if (old_contact)
			old_photo = e_contact_get (old_contact, field);

		/* Unless we are receiving the same uri that we already have
		 * stored in the BDB... */
		if (!old_photo || old_photo->type == E_CONTACT_PHOTO_TYPE_INLINED ||
		    g_ascii_strcasecmp (old_photo->data.uri, photo->data.uri) != 0) {
			gchar *filename;
			gchar *new_filename;
			gchar *new_uri = NULL;

			/* ... Assume that the incomming uri belongs to another contact
			 * still in the BDB. Lets go ahead and create a hard link to the 
			 * photo file and create a new name for the incomming uri, and
			 * use that in the incomming contact to save in place.
			 *
			 * This piece of code is here to ensure there are no problems if
			 * the libebook user decides to cross-reference and start "sharing"
			 * uris that we've previously stored in the photo directory.
			 *
			 * We use the hard-link here to off-load the necessary ref-counting
			 * logic to the file-system.
			 */
			filename = g_filename_from_uri (photo->data.uri, NULL, NULL);
			g_return_val_if_fail (filename, STATUS_NORMAL); /* we already checked this with 'is_backend_owned_uri ()' */

			new_filename = hard_link_photo (bf, contact, field, filename, error);

			if (!new_filename)
				status = STATUS_ERROR;
			else if ((new_uri = g_filename_to_uri (new_filename, NULL, error)) == NULL) {
				/* If we fail here... we need to clean up the hardlink we just created */
				GError *local_err = NULL;
				if (!remove_file (new_filename, &local_err)) {
					g_warning ("Unable to cleanup photo uri: %s", local_err->message);
					g_error_free (local_err);
				}
				status = STATUS_ERROR;
			} else {

				new_photo = e_contact_photo_new ();
				new_photo->type = E_CONTACT_PHOTO_TYPE_URI;
				new_photo->data.uri = new_uri;

				e_contact_set (contact, field, new_photo);

				d (g_print ("Backend modified incomming shared uri to be %s:\n", new_uri));

				e_contact_photo_free (new_photo);
				status = STATUS_MODIFIED;
			}
			g_free (new_filename);
			g_free (filename);
		}

		if (old_photo)
			e_contact_photo_free (old_photo);

	}

 done:
	e_contact_photo_free (photo);

	return status;
}

/*
 * When a contact is added or modified we receive a vCard,
 * this function checks if we've received inline data
 * and replaces it with a uri notation.
 *
 * If this function modifies 'contact' then it will
 * return the 'modified' status and 'vcard_ret' (if specified)
 * will be set to a newly allocated vcard string.
 */
static PhotoModifiedStatus
maybe_transform_vcard_for_photo (EBookBackendDecsync *bf,
                                 EContact *old_contact,
                                 EContact *contact,
                                 GError **error)
{
	PhotoModifiedStatus status;
	gboolean            modified = FALSE;

	status = maybe_transform_vcard_field_for_photo (
		bf, old_contact, contact,
		E_CONTACT_PHOTO, error);
	modified = (status == STATUS_MODIFIED);

	if (status != STATUS_ERROR) {
		status = maybe_transform_vcard_field_for_photo (
			bf, old_contact, contact,
			E_CONTACT_LOGO, error);
		modified = modified || (status == STATUS_MODIFIED);
	}

	if (status != STATUS_ERROR && modified)
		status = STATUS_MODIFIED;

	return status;
}

/****************************************************************
 *                     Global Revisioning Tools                 *
 ****************************************************************/
static gchar *
e_book_backend_decsync_create_unique_id (void)
{
	/* use a 32 counter and the 32 bit timestamp to make an id.
	 * it's doubtful 2^32 id's will be created in a second, so we
	 * should be okay. */
	static guint c = 0;
	return g_strdup_printf (PAS_ID_PREFIX "%08lX%08X", (glong) time (NULL), c++);
}

static gchar *
e_book_backend_decsync_new_revision (EBookBackendDecsync *bf,
                                  gboolean with_counter)
{
	gchar time_string[100] = {0};
	const struct tm *tm = NULL;
	time_t t;

	t = time (NULL);
	tm = gmtime (&t);
	if (tm) {
		struct tm ttm = *tm;

		if (!with_counter && bf->priv->revision_guards) {
			gint value = g_atomic_int_add (&bf->priv->rev_counter, 1);

			/* Artificial time, which encodes the revision counter */
			ttm.tm_sec = value % 60;
			ttm.tm_min = (value / 60 ) % 60;
			ttm.tm_hour = (value / 3600) % 24;
		}

		strftime (time_string, 100, "%Y-%m-%dT%H:%M:%SZ", &ttm);
	}

	if (with_counter)
		return g_strdup_printf ("%s(%d)", time_string, g_atomic_int_add (&bf->priv->rev_counter, 1));

	return g_strdup (time_string);
}

/* For now just bump the revision and set it in the DB every
 * time the revision bumps, this is the safest approach and
 * its unclear so far if bumping the revision string for 
 * every DB modification is going to really be an overhead.
 */
static gboolean
e_book_backend_decsync_bump_revision (EBookBackendDecsync *bf,
                                   GError **error)
{
	GError *local_error = NULL;
	gchar *new_revision;
	gboolean success;

	new_revision = e_book_backend_decsync_new_revision (bf, TRUE);
	success = e_book_sqlite_set_key_value (
		bf->priv->sqlitedb,
		SQLITE_REVISION_KEY,
		new_revision,
		&local_error);

	if (success) {
		g_free (bf->priv->revision);
		bf->priv->revision = new_revision;

		e_book_backend_notify_property_changed (E_BOOK_BACKEND (bf),
							E_BOOK_BACKEND_PROPERTY_REVISION,
							bf->priv->revision);
	} else {
		g_free (new_revision);
		g_warning (
			G_STRLOC ": Error setting database revision: %s",
			local_error->message);
		g_propagate_error (error, local_error);
	}

	return success;
}

static void
e_book_backend_decsync_load_revision (EBookBackendDecsync *bf)
{
	GError *error = NULL;

	if (!e_book_sqlite_get_key_value (bf->priv->sqlitedb,
					  SQLITE_REVISION_KEY,
					  &bf->priv->revision,
					  &error)) {
		g_warning (
			G_STRLOC ": Error loading database revision: %s",
			error ? error->message : "Unknown error");
		g_clear_error (&error);
	} else if (bf->priv->revision == NULL) {
		e_book_backend_decsync_bump_revision (bf, NULL);
	}
}

static void
e_book_backend_decsync_load_locale (EBookBackendDecsync *bf)
{
	GError *error = NULL;

	if (!e_book_sqlite_get_locale (bf->priv->sqlitedb,
				       &bf->priv->locale,
				       &error)) {
		g_warning (
			G_STRLOC ": Error loading database locale setting: %s",
			error ? error->message : "Unknown error");
		g_clear_error (&error);
	}
}

static void
set_revision (EBookBackendDecsync *bf,
              EContact *contact)
{
	gchar *rev;

	rev = e_book_backend_decsync_new_revision (bf, FALSE);
	e_contact_set (contact, E_CONTACT_REV, rev);
	g_free (rev);
}

/****************************************************************
 *                   Dealing with cursor updates                *
 ****************************************************************/
static void
cursors_contact_added (EBookBackendDecsync *bf,
                       EContact *contact)
{
	GList *l;

	for (l = bf->priv->cursors; l; l = l->next) {
		EDataBookCursor *cursor = l->data;

		e_data_book_cursor_contact_added (cursor, contact);
	}
}

static void
cursors_contact_removed (EBookBackendDecsync *bf,
                         EContact *contact)
{
	GList *l;

	for (l = bf->priv->cursors; l; l = l->next) {
		EDataBookCursor *cursor = l->data;

		e_data_book_cursor_contact_removed (cursor, contact);
	}
}

/****************************************************************
 *                   Main Backend Implementation                *
 ****************************************************************/

/**
 * This method will return TRUE if all the contacts were properly created.
 * If at least one contact fails, the method will return FALSE, all
 * changes will be reverted (the @contacts list will stay empty) and
 * @perror will be set.
 */
static gboolean
do_create (EBookBackendDecsync *bf,
           const gchar * const *vcards,
           const gchar * const *uids,
           GSList **out_contacts,
           GCancellable *cancellable,
           GError **error,
           gboolean update_decsync)
{
	PhotoModifiedStatus status = STATUS_NORMAL;
	guint ii, length;
	GError *local_error = NULL;
	const gchar *path[2];
	JsonNode *key_node, *value_node;
	gchar *key_string, *value_string;

	length = g_strv_length ((gchar **) vcards);

	for (ii = 0; ii < length; ii++) {
		gchar           *id;
		const gchar     *rev;
		EContact        *contact;

		if (uids != NULL && uids[ii] != NULL)
			contact = e_contact_new_from_vcard_with_uid (vcards[ii], uids[ii]);
		else
			contact = e_contact_new_from_vcard (vcards[ii]);

		/* Preserve original UID, create a unique UID if needed */
		if (e_contact_get_const (contact, E_CONTACT_UID) == NULL) {
			id = e_book_backend_decsync_create_unique_id ();
			e_contact_set (contact, E_CONTACT_UID, id);
			g_free (id);
		}

		if (update_decsync) {
			id = e_contact_get (contact, E_CONTACT_UID);

			path[0] = "resources";
			path[1] = id;
			key_node = json_node_new (JSON_NODE_NULL);
			key_string = json_to_string (key_node, FALSE);
			value_node = json_node_new (JSON_NODE_VALUE);
			json_node_set_string (value_node, vcards[ii]);
			value_string = json_to_string (value_node, FALSE);
			decsync_set_entry(bf->priv->decsync, path, 2, key_string, value_string);
			json_node_free (key_node);
			g_free (key_string);
			json_node_free (value_node);
			g_free (value_string);

			g_free (id);
		}

		rev = e_contact_get_const (contact, E_CONTACT_REV);
		if (!(rev && *rev))
			set_revision (bf, contact);

		status = maybe_transform_vcard_for_photo (bf, NULL, contact, error);

		if (status != STATUS_ERROR) {

			/* Contact was added successfully. */
			*out_contacts = g_slist_prepend (*out_contacts, contact);
		} else {
			/* Contact could not be transformed */
			g_warning (
				G_STRLOC ": Error transforming vcard with image data %s",
				(error && *error) ? (*error)->message :
				"Unknown error transforming vcard");
			g_object_unref (contact);

			/* Abort as soon as an error occurs */
			break;
		}
	}

	if (status != STATUS_ERROR) {
		GSList *link;

		if (!e_book_sqlite_add_contacts (bf->priv->sqlitedb,
						 *out_contacts, NULL, FALSE,
						 cancellable,
						 &local_error)) {

			if (g_error_matches (local_error,
					     E_BOOK_SQLITE_ERROR,
					     E_BOOK_SQLITE_ERROR_CONSTRAINT)) {
				g_set_error (
					error, E_BOOK_CLIENT_ERROR,
					E_BOOK_CLIENT_ERROR_CONTACT_ID_ALREADY_EXISTS,
					_("Conflicting UIDs found in added contacts"));
				g_clear_error (&local_error);
			} else {
				g_warning ("Failed to add contacts: %s", local_error->message);
				g_propagate_error (error, local_error);
			}

			status = STATUS_ERROR;
		}

		/* After adding any contacts, notify any cursors that the new contacts are added */
		for (link = *out_contacts; link; link = g_slist_next (link)) {
			cursors_contact_added (bf, link->data);
		}
	}

	return (status != STATUS_ERROR);
}

typedef struct {
	EBookBackendDecsync *bf;
	GThread *thread;
	EFlag *running;
} DecsyncBackendSearchClosure;

static void
closure_destroy (DecsyncBackendSearchClosure *closure)
{
	d (printf ("destroying search closure\n"));
	e_flag_free (closure->running);
	if (closure->thread)
		g_thread_unref (closure->thread);
	g_free (closure);
}

static DecsyncBackendSearchClosure *
init_closure (EDataBookView *book_view,
              EBookBackendDecsync *bf)
{
	DecsyncBackendSearchClosure *closure = g_new (DecsyncBackendSearchClosure, 1);

	closure->bf = bf;
	closure->thread = NULL;
	closure->running = e_flag_new ();

	g_object_set_data_full (
		G_OBJECT (book_view),
		"EBookBackendDecsync.BookView::closure",
		closure, (GDestroyNotify) closure_destroy);

	return closure;
}

static DecsyncBackendSearchClosure *
get_closure (EDataBookView *book_view)
{
	return g_object_get_data (
		G_OBJECT (book_view),
		"EBookBackendDecsync.BookView::closure");
}

static void
notify_update_vcard (EDataBookView *book_view,
                     gboolean prefiltered,
                     const gchar *id,
                     const gchar *vcard)
{
	if (prefiltered)
		e_data_book_view_notify_update_prefiltered_vcard (book_view, id, vcard);
	else
		e_data_book_view_notify_update_vcard (book_view, id, vcard);
}

static gboolean
uid_rev_fields (GHashTable *fields_of_interest)
{
	GHashTableIter iter;
	gpointer key, value;

	if (!fields_of_interest || g_hash_table_size (fields_of_interest) > 2)
		return FALSE;

	g_hash_table_iter_init (&iter, fields_of_interest);
	while (g_hash_table_iter_next (&iter, &key, &value)) {
		const gchar *field_name = key;
		EContactField field = e_contact_field_id (field_name);

		if (field != E_CONTACT_UID &&
		    field != E_CONTACT_REV)
			return FALSE;
	}

	return TRUE;
}

static gpointer
book_view_thread (gpointer user_data)
{
	EDataBookView *book_view = user_data;
	DecsyncBackendSearchClosure *closure;
	EBookBackendDecsync *bf;
	EBookBackendSExp *sexp;
	const gchar *query;
	GSList *summary_list = NULL, *l;
	GHashTable *fields_of_interest;
	GError *local_error = NULL;
	gboolean meta_contact, success;

	g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (book_view), NULL);

	closure = get_closure (book_view);
	if (!closure) {
		g_warning (G_STRLOC ": NULL closure in book view thread");
		return NULL;
	}
	bf = closure->bf;

	d (printf ("starting initial population of book view\n"));

	/* ref the book view because it'll be removed and unrefed
	 * when/if it's stopped */
	g_object_ref (book_view);

	sexp = e_data_book_view_get_sexp (book_view);
	query = e_book_backend_sexp_text (sexp);

	fields_of_interest = e_data_book_view_get_fields_of_interest (book_view);
	meta_contact = uid_rev_fields (fields_of_interest);

	if (query && !strcmp (query, "(contains \"x-evolution-any-field\" \"\")")) {
		e_data_book_view_notify_progress (book_view, -1, _("Loading..."));
	} else {
		e_data_book_view_notify_progress (book_view, -1, _("Searching..."));
	}

	d (printf ("signalling parent thread\n"));
	e_flag_set (closure->running);

	g_rw_lock_reader_lock (&(bf->priv->lock));
	success = e_book_sqlite_search (
		bf->priv->sqlitedb,
		query,
		meta_contact,
		&summary_list,
		NULL, /* GCancellable */
		&local_error);
	g_rw_lock_reader_unlock (&(bf->priv->lock));

	if (!success) {
		g_warning (G_STRLOC ": Failed to query initial contacts: %s", local_error->message);
		g_error_free (local_error);
		e_data_book_view_notify_complete (
			book_view,
			g_error_new_literal (
				E_CLIENT_ERROR,
				E_CLIENT_ERROR_NOT_OPENED,
				e_client_error_to_string (
				E_CLIENT_ERROR_NOT_OPENED)));
		g_object_unref (book_view);
		return NULL;
	}

	for (l = summary_list; l; l = l->next) {
		EbSqlSearchData *data = l->data;
		gchar *vcard = NULL;

		vcard = data->vcard;
		data->vcard = NULL;

		notify_update_vcard (book_view, TRUE, data->uid, vcard);
		g_free (vcard);
	}

	g_slist_foreach (summary_list, (GFunc) e_book_sqlite_search_data_free, NULL);
	g_slist_free (summary_list);

	if (e_flag_is_set (closure->running))
		e_data_book_view_notify_complete (book_view, NULL /* Success */);

	g_object_unref (book_view);

	d (printf ("finished population of book view\n"));

	return NULL;
}

static void
book_backend_decsync_dispose (GObject *object)
{
	EBookBackendDecsync *bf;

	bf = E_BOOK_BACKEND_DECSYNC (object);

	g_rw_lock_writer_lock (&(bf->priv->lock));

	if (bf->priv->cursors) {
		g_list_free_full (bf->priv->cursors, g_object_unref);
		bf->priv->cursors = NULL;
	}

	g_clear_object (&bf->priv->sqlitedb);

	g_rw_lock_writer_unlock (&(bf->priv->lock));

	G_OBJECT_CLASS (e_book_backend_decsync_parent_class)->dispose (object);
}

static void
book_backend_decsync_finalize (GObject *object)
{
	EBookBackendDecsyncPrivate *priv;

	priv = E_BOOK_BACKEND_DECSYNC (object)->priv;

	g_free (priv->photo_dirname);
	g_free (priv->revision);
	g_free (priv->locale);
	g_free (priv->base_directory);
	g_rw_lock_clear (&(priv->lock));

	/* Chain up to parent's finalize() method. */
	G_OBJECT_CLASS (e_book_backend_decsync_parent_class)->finalize (object);
}

static gchar *
book_backend_decsync_get_backend_property (EBookBackend *backend,
                                        const gchar *prop_name)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);

	g_return_val_if_fail (prop_name != NULL, NULL);

	if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
		return g_strdup ("local,do-initial-query,bulk-adds,bulk-modifies,bulk-removes,contact-lists,refresh-supported");

	} else if (g_str_equal (prop_name, E_BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS)) {
		return g_strdup (e_contact_field_name (E_CONTACT_FILE_AS));

	} else if (g_str_equal (prop_name, E_BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS)) {
		GString *fields;
		gint ii;

		fields = g_string_sized_new (1024);

		/* XXX we need a way to say "we support everything", since the
		 * file backend does */
		for (ii = 1; ii < E_CONTACT_FIELD_LAST; ii++) {
			if (fields->len > 0)
				g_string_append_c (fields, ',');
			g_string_append (fields, e_contact_field_name (ii));
		}

		return g_string_free (fields, FALSE);

	} else if (g_str_equal (prop_name, E_BOOK_BACKEND_PROPERTY_REVISION)) {
		gchar *prop_value;

		g_rw_lock_reader_lock (&(bf->priv->lock));
		prop_value = g_strdup (bf->priv->revision);
		g_rw_lock_reader_unlock (&(bf->priv->lock));

		return prop_value;
	}

	/* Chain up to parent's method. */
	return E_BOOK_BACKEND_CLASS (e_book_backend_decsync_parent_class)->impl_get_backend_property (backend, prop_name);
}

static gboolean
book_backend_decsync_open_sync (EBookBackendSync *backend,
                             GCancellable *cancellable,
                             GError **error)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	ESource          *source;
	ESourceRevisionGuards *guards;

	source = e_backend_get_source (E_BACKEND (backend));

	/* Local source is always connected. */
	e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);

	g_type_ensure (E_TYPE_SOURCE_REVISION_GUARDS);
	guards = e_source_get_extension (source, E_SOURCE_EXTENSION_REVISION_GUARDS);

	bf->priv->revision_guards = e_source_revision_guards_get_enabled (guards);

	g_rw_lock_writer_lock (&(bf->priv->lock));
	if (!bf->priv->revision) {
		e_book_backend_decsync_load_revision (bf);
		e_book_backend_notify_property_changed (
			E_BOOK_BACKEND (backend),
			E_BOOK_BACKEND_PROPERTY_REVISION,
			bf->priv->revision);
	}
	g_rw_lock_writer_unlock (&(bf->priv->lock));

	e_backend_set_online (E_BACKEND (backend), TRUE);
	e_book_backend_set_writable (E_BOOK_BACKEND (backend), TRUE);

	g_idle_add ((GSourceFunc) book_backend_decsync_refresh_start, bf);

	return TRUE;
}

static gboolean
book_backend_decsync_create_contacts_sync_with_decsync (EBookBackendSync *backend,
                                                     const gchar * const *vcards,
                                                     const gchar * const *uids,
                                                     guint32 opflags,
                                                     GSList **out_contacts,
                                                     GCancellable *cancellable,
                                                     GError **error,
                                                     gboolean update_decsync)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	gboolean success = FALSE;

	g_return_val_if_fail (out_contacts != NULL, FALSE);

	*out_contacts = NULL;

	g_rw_lock_writer_lock (&(bf->priv->lock));
	if (!e_book_sqlite_lock (bf->priv->sqlitedb,
				 EBSQL_LOCK_WRITE,
				 cancellable, error)) {
		g_rw_lock_writer_unlock (&(bf->priv->lock));
		return FALSE;
	}

	success = do_create (bf, vcards, uids, out_contacts, cancellable, error, update_decsync);

	if (success) {
		*out_contacts = g_slist_reverse (*out_contacts);
	} else {
		g_slist_free_full (*out_contacts, g_object_unref);
		*out_contacts = NULL;
	}

	if (success)
		success = e_book_backend_decsync_bump_revision (bf, error);

	if (success)
		success = e_book_sqlite_unlock (
			bf->priv->sqlitedb,
			EBSQL_UNLOCK_COMMIT,
			error);
	else {
		GError *local_error = NULL;

		/* Rollback transaction */
		e_book_sqlite_unlock (
			bf->priv->sqlitedb,
			EBSQL_UNLOCK_ROLLBACK,
			&local_error);

		if (local_error != NULL) {
			g_warning (
				"Failed to rollback transaction "
				"after failing to add contacts: %s",
				local_error->message);
			g_clear_error (&local_error);
		}
	}

	g_rw_lock_writer_unlock (&(bf->priv->lock));

	return success;
}

static gboolean
book_backend_decsync_create_contacts_sync (EBookBackendSync *backend,
                                        const gchar * const *vcards,
                                        guint32 opflags,
                                        GSList **out_contacts,
                                        GCancellable *cancellable,
                                        GError **error)
{
	return book_backend_decsync_create_contacts_sync_with_decsync (backend, vcards, NULL, opflags, out_contacts, cancellable, error, TRUE);
}

static gboolean
book_backend_decsync_modify_contacts_sync_with_decsync (EBookBackendSync *backend,
                                                     const gchar * const *vcards,
                                                     const gchar * const *uids,
                                                     guint32 opflags,
                                                     GSList **out_contacts,
                                                     GCancellable *cancellable,
                                                     GError **error,
                                                     gboolean update_decsync)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	GSList           *ids = NULL;
	GError           *local_error = NULL;
	PhotoModifiedStatus status = STATUS_NORMAL;
	GSList *old_contacts = NULL;
	guint ii, length;
	const gchar *path[2];
	JsonNode *key_node, *value_node;
	gchar *key_string, *value_string;

	length = g_strv_length ((gchar **) vcards);

	g_rw_lock_writer_lock (&(bf->priv->lock));

	if (!e_book_sqlite_lock (bf->priv->sqlitedb, EBSQL_LOCK_WRITE, cancellable, error)) {
		g_rw_lock_writer_unlock (&(bf->priv->lock));
		return FALSE;
	}

	for (ii = 0; ii < length && status != STATUS_ERROR; ii++) {
		gchar *id;
		EContact *mod_contact, *old_contact = NULL;
		const gchar *mod_contact_rev, *old_contact_rev;

		if (uids != NULL && uids[ii] != NULL)
			mod_contact = e_contact_new_from_vcard_with_uid (vcards[ii], uids[ii]);
		else
			mod_contact = e_contact_new_from_vcard (vcards[ii]);
		id = e_contact_get (mod_contact, E_CONTACT_UID);

		if (id == NULL) {
			status = STATUS_ERROR;

			g_set_error_literal (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_OTHER_ERROR,
				_("No UID in the contact"));
			g_object_unref (mod_contact);
			break;
		}

		if (update_decsync) {
			path[0] = "resources";
			path[1] = id;
			key_node = json_node_new (JSON_NODE_NULL);
			key_string = json_to_string (key_node, FALSE);
			value_node = json_node_new (JSON_NODE_VALUE);
			json_node_set_string (value_node, vcards[ii]);
			value_string = json_to_string (value_node, FALSE);
			decsync_set_entry (bf->priv->decsync, path, 2, key_string, value_string);
			json_node_free (key_node);
			g_free (key_string);
			json_node_free (value_node);
			g_free (value_string);
		}

		if (!e_book_sqlite_get_contact (bf->priv->sqlitedb,
						id, FALSE, &old_contact,
						&local_error)) {
			g_warning (G_STRLOC ": Failed to load contact %s: %s", id, local_error->message);
			g_propagate_error (error, local_error);
			local_error = NULL;

			status = STATUS_ERROR;

			g_free (id);
			g_object_unref (mod_contact);
			break;
		}

		if (bf->priv->revision_guards) {
			mod_contact_rev = e_contact_get_const (mod_contact, E_CONTACT_REV);
			old_contact_rev = e_contact_get_const (old_contact, E_CONTACT_REV);

			if (!mod_contact_rev || !old_contact_rev ||
			    strcmp (mod_contact_rev, old_contact_rev) != 0) {
				g_set_error (
					error, E_CLIENT_ERROR,
					E_CLIENT_ERROR_OUT_OF_SYNC,
					_("Tried to modify contact “%s” with out of sync revision"),
					(gchar *) e_contact_get_const (mod_contact, E_CONTACT_UID));

				status = STATUS_ERROR;

				g_free (id);
				g_object_unref (mod_contact);
				g_object_unref (old_contact);
				break;
			}
		}

		/* Transform incomming photo blobs to uris before storing this to the DB */
		status = maybe_transform_vcard_for_photo (bf, old_contact, mod_contact, &local_error);
		if (status == STATUS_ERROR) {
			g_warning (G_STRLOC ": Error transforming contact %s: %s", id, local_error->message);
			g_propagate_error (error, local_error);
			local_error = NULL;

			g_free (id);
			g_object_unref (old_contact);
			g_object_unref (mod_contact);
			break;
		}

		/* update the revision (modified time of contact) */
		set_revision (bf, mod_contact);

		old_contacts = g_slist_prepend (old_contacts, old_contact);
		*out_contacts = g_slist_prepend (*out_contacts, mod_contact);

		ids = g_slist_prepend (ids, id);
	}

	if (status != STATUS_ERROR) {
		GSList *old_link, *mod_link;

		/* Delete old photo file uris if need be (this will compare the new contact
		 * with the current copy in the BDB to extract the uris to delete) */
		old_link = old_contacts;
		mod_link = *out_contacts;

		while (old_link != NULL && mod_link != NULL) {
			maybe_delete_unused_uris (
				bf,
				E_CONTACT (old_link->data),
				E_CONTACT (mod_link->data));
			old_link = g_slist_next (old_link);
			mod_link = g_slist_next (mod_link);
		}

		/* Update summary as well */
		e_book_sqlite_add_contacts (
			bf->priv->sqlitedb,
			*out_contacts, NULL, TRUE,
			cancellable, &local_error);

		if (local_error != NULL) {
			g_warning (
				"Failed to modify contacts: %s",
				local_error->message);
			g_propagate_error (error, local_error);
			local_error = NULL;

			status = STATUS_ERROR;
		}
	}

	/* Bump the revision atomically in the same transaction */
	if (status != STATUS_ERROR) {
		if (!e_book_backend_decsync_bump_revision (bf, error))
			status = STATUS_ERROR;
	}

	/* Commit or rollback transaction */
	if (status != STATUS_ERROR) {
		gboolean success;

		success = e_book_sqlite_unlock (
			bf->priv->sqlitedb,
			EBSQL_UNLOCK_COMMIT,
			error);
		if (!success)
			status = STATUS_ERROR;

	} else {
		/* Rollback transaction */
		e_book_sqlite_unlock (
			bf->priv->sqlitedb,
			EBSQL_UNLOCK_ROLLBACK,
			&local_error);

		if (local_error != NULL) {
			g_warning (
				"Failed to rollback transaction after "
				"failing to modify contacts: %s",
				local_error->message);
			g_clear_error (&local_error);
		}
	}

	if (status != STATUS_ERROR) {
		*out_contacts = g_slist_reverse (*out_contacts);
	} else {
		g_slist_free_full (*out_contacts, g_object_unref);
		*out_contacts = NULL;
	}

	/* Now that we've modified the contact(s),
	 * notify cursors of the changes. */
	if (status != STATUS_ERROR) {
		GSList *link;

		for (link = old_contacts; link; link = g_slist_next (link)) {
			cursors_contact_removed (bf, E_CONTACT (link->data));
		}

		for (link = *out_contacts; link; link = g_slist_next (link)) {
			cursors_contact_added (bf, E_CONTACT (link->data));
		}
	}

	g_rw_lock_writer_unlock (&(bf->priv->lock));

	g_slist_free_full (old_contacts, g_object_unref);
	g_slist_free_full (ids, g_free);

	return (status != STATUS_ERROR);
}

static gboolean
book_backend_decsync_modify_contacts_sync (EBookBackendSync *backend,
                                        const gchar * const *vcards,
                                        guint32 opflags,
                                        GSList **out_contacts,
                                        GCancellable *cancellable,
                                        GError **error)
{
	return book_backend_decsync_modify_contacts_sync_with_decsync (backend, vcards, NULL, opflags, out_contacts, cancellable, error, TRUE);
}

static gboolean
book_backend_decsync_remove_contacts_sync_with_decsync (EBookBackendSync *backend,
                                                     const gchar * const *uids,
                                                     guint32 opflags,
                                                     GSList **out_removed_uids,
                                                     GCancellable *cancellable,
                                                     GError **error,
                                                     gboolean update_decsync)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	GSList           *removed_ids = NULL, *removed_contacts = NULL;
	GError           *local_error = NULL;
	const GSList     *l;
	gboolean success = TRUE;
	guint ii, length;
	const gchar *path[2];
	JsonNode *key_node, *value_node;
	gchar *key_string, *value_string;

	g_return_val_if_fail (out_removed_uids != NULL, FALSE);

	length = g_strv_length ((gchar **) uids);

	g_rw_lock_writer_lock (&(bf->priv->lock));

	if (!e_book_sqlite_lock (bf->priv->sqlitedb,
				 EBSQL_LOCK_WRITE,
				 cancellable, error)) {
		g_rw_lock_writer_unlock (&(bf->priv->lock));
		return FALSE;
	}

	for (ii = 0; ii < length && success; ii++) {
		EContact *contact = NULL;

		if (update_decsync) {
			path[0] = "resources";
			path[1] = uids[ii];
			key_node = json_node_new (JSON_NODE_NULL);
			key_string = json_to_string (key_node, FALSE);
			value_node = json_node_new (JSON_NODE_NULL);
			value_string = json_to_string (value_node, FALSE);
			decsync_set_entry (bf->priv->decsync, path, 2, key_string, value_string);
			json_node_free (key_node);
			g_free (key_string);
			json_node_free (value_node);
			g_free (value_string);
		}

		/* First load the EContacts which need to be removed, we might delete some
		 * photos from disk because of this...
		 *
		 * Note: sqlite backend can probably make this faster by executing a
		 * single query to fetch a list of contacts for a list of ids, the
		 * current method makes a query for each UID.
		 */
		if (e_book_sqlite_get_contact (bf->priv->sqlitedb,
					       uids[ii], FALSE, &contact,
					       &local_error)) {
			removed_ids = g_slist_prepend (removed_ids, g_strdup (uids[ii]));
			removed_contacts = g_slist_prepend (removed_contacts, contact);
		} else {

			if (g_error_matches (local_error,
					     E_BOOK_SQLITE_ERROR,
					     E_BOOK_SQLITE_ERROR_CONTACT_NOT_FOUND)) {
				g_set_error (
					error, E_BOOK_CLIENT_ERROR,
					E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
					_("Contact “%s” not found"), uids[ii]);
				g_error_free (local_error);
			} else {
				g_warning ("Failed to fetch contact to be removed: %s", local_error->message);
				g_propagate_error (error, local_error);
				local_error = NULL;
			}
			/* Abort as soon as missing contact is to be deleted */
			success = FALSE;
			break;
		}
	}

	if (success) {

		/* Delete URI associated to those contacts */
		for (l = removed_contacts; l; l = l->next) {
			maybe_delete_unused_uris (bf, E_CONTACT (l->data), NULL);
		}

		/* Remove from summary as well */
		if (!e_book_sqlite_remove_contacts (bf->priv->sqlitedb, removed_ids,
						    cancellable, &local_error)) {
			if (local_error) {
				g_warning ("Failed to remove contacts: %s", local_error->message);
				g_propagate_error (error, local_error);
			}
		}

		e_book_backend_decsync_bump_revision (bf, NULL);
	}

	/* Commit or rollback transaction */
	if (success) {
		success = e_book_sqlite_unlock (bf->priv->sqlitedb, EBSQL_UNLOCK_COMMIT, error);
	} else {
		/* Rollback transaction */
		if (!e_book_sqlite_unlock (bf->priv->sqlitedb, EBSQL_UNLOCK_ROLLBACK, &local_error)) {
			g_warning (
				"Failed to rollback transaction after failing to modify contacts: %s",
				local_error->message);
			g_clear_error (&local_error);
		}
	}

	/* After removing any contacts, notify any cursors that the new contacts are added */
	if (success) {
		for (l = removed_contacts; l; l = l->next) {
			cursors_contact_removed (bf, E_CONTACT (l->data));
		}
	}

	*out_removed_uids = removed_ids;

	g_rw_lock_writer_unlock (&(bf->priv->lock));

	g_slist_free_full (removed_contacts, (GDestroyNotify) g_object_unref);

	return success;
}

static gboolean
book_backend_decsync_remove_contacts_sync (EBookBackendSync *backend,
                                        const gchar * const *uids,
                                        guint32 opflags,
                                        GSList **out_removed_uids,
                                        GCancellable *cancellable,
                                        GError **error)
{
	return book_backend_decsync_remove_contacts_sync_with_decsync (backend, uids, opflags, out_removed_uids, cancellable, error, TRUE);
}

static EContact *
book_backend_decsync_get_contact_sync (EBookBackendSync *backend,
                                    const gchar *uid,
                                    GCancellable *cancellable,
                                    GError **error)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	EContact *contact = NULL;
	gboolean success;
	GError *local_error = NULL;

	g_rw_lock_reader_lock (&(bf->priv->lock));
	success = e_book_sqlite_get_contact (
		bf->priv->sqlitedb,
		uid, FALSE, &contact,
		&local_error);
	g_rw_lock_reader_unlock (&(bf->priv->lock));

	if (!success) {
		if (g_error_matches (local_error,
				     E_BOOK_SQLITE_ERROR,
				     E_BOOK_SQLITE_ERROR_CONTACT_NOT_FOUND)) {
			g_set_error (
				error, E_BOOK_CLIENT_ERROR,
				E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
				_("Contact “%s” not found"), uid);
			g_error_free (local_error);
		} else
			g_propagate_error (error, local_error);
	}

	return contact;
}

static gboolean
book_backend_decsync_get_contact_list_sync (EBookBackendSync *backend,
                                         const gchar *query,
                                         GSList **out_contacts,
                                         GCancellable *cancellable,
                                         GError **error)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	GSList *summary_list = NULL;
	GSList *link;
	gboolean success = TRUE;
	GError *local_error = NULL;

	g_return_val_if_fail (out_contacts != NULL, FALSE);

	*out_contacts = NULL;

	d (printf ("book_backend_decsync_get_contact_list_sync (%s)\n", query));

	g_rw_lock_reader_lock (&(bf->priv->lock));

	success = e_book_sqlite_lock (
		bf->priv->sqlitedb,
		EBSQL_LOCK_READ,
		cancellable, error);
	if (!success) {
		g_rw_lock_writer_unlock (&(bf->priv->lock));
		return FALSE;
	}

	success = e_book_sqlite_search (
		bf->priv->sqlitedb,
		query,
		FALSE,
		&summary_list,
		cancellable,
		&local_error);

	e_book_sqlite_unlock (
		bf->priv->sqlitedb,
		EBSQL_UNLOCK_NONE,
		success ? &local_error : NULL);

	g_rw_lock_reader_unlock (&(bf->priv->lock));

	if (!success) {

		g_warn_if_fail (summary_list == NULL);

		if (g_error_matches (local_error,
				     E_BOOK_SQLITE_ERROR,
				     E_BOOK_SQLITE_ERROR_UNSUPPORTED_QUERY)) {
			g_set_error (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_NOT_SUPPORTED,
				_("Query “%s” not supported"), query);
			g_error_free (local_error);

		} else if (g_error_matches (local_error,
				     E_BOOK_SQLITE_ERROR,
				     E_BOOK_SQLITE_ERROR_INVALID_QUERY)) {
			g_set_error (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_INVALID_QUERY,
				_("Invalid Query “%s”"), query);
			g_error_free (local_error);

		} else {
			g_warning ("Failed to fetch contact ids: %s", local_error->message);
			g_propagate_error (error, local_error);
		}
	}

	for (link = summary_list; link != NULL; link = g_slist_next (link)) {
		EbSqlSearchData *data = link->data;
		EContact *contact;

		contact = e_contact_new_from_vcard (data->vcard);
		link->data = contact;

		e_book_sqlite_search_data_free (data);
	}

	*out_contacts = summary_list;

	return success;
}

static gboolean
book_backend_decsync_get_contact_list_uids_sync (EBookBackendSync *backend,
                                              const gchar *query,
                                              GSList **out_uids,
                                              GCancellable *cancellable,
                                              GError **error)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	gboolean success = TRUE;
	GError *local_error = NULL;

	g_return_val_if_fail (out_uids != NULL, FALSE);

	*out_uids = NULL;

	d (printf ("book_backend_decsync_get_contact_list_sync (%s)\n", query));

	g_rw_lock_reader_lock (&(bf->priv->lock));

	success = e_book_sqlite_lock (
		bf->priv->sqlitedb,
		EBSQL_LOCK_READ,
		cancellable, error);
	if (!success) {
		g_rw_lock_writer_unlock (&(bf->priv->lock));
		return FALSE;
	}

	success = e_book_sqlite_search_uids (
		bf->priv->sqlitedb,
		query,
		out_uids,
		cancellable,
		&local_error);
	e_book_sqlite_unlock (
		bf->priv->sqlitedb,
		EBSQL_UNLOCK_NONE,
		success ? &local_error : NULL);

	g_rw_lock_reader_unlock (&(bf->priv->lock));

	if (!success) {
		g_warn_if_fail (*out_uids == NULL);

		if (g_error_matches (local_error,
				     E_BOOK_SQLITE_ERROR,
				     E_BOOK_SQLITE_ERROR_UNSUPPORTED_QUERY)) {
			g_set_error (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_NOT_SUPPORTED,
				_("Query “%s” not supported"), query);
			g_error_free (local_error);

		} else if (g_error_matches (local_error,
					    E_BOOK_SQLITE_ERROR,
					    E_BOOK_SQLITE_ERROR_INVALID_QUERY)) {
			g_set_error (
				error, E_CLIENT_ERROR,
				E_CLIENT_ERROR_INVALID_QUERY,
				_("Invalid Query “%s”"), query);
			g_error_free (local_error);

		} else {
			g_warning (
				"Failed to fetch contact ids: %s",
				local_error->message);
			g_propagate_error (error, local_error);
		}
	}

	return success;
}

static gboolean
book_backend_decsync_gather_addresses_cb (gpointer ptr_name,
				       gpointer ptr_email,
				       gpointer user_data)
{
	GPtrArray *array = user_data;
	const gchar *email = ptr_email;

	if (email && *email)
		g_ptr_array_add (array, e_book_query_field_test (E_CONTACT_EMAIL, E_BOOK_QUERY_IS, email));

	return TRUE;
}

static gboolean
book_backend_decsync_contains_email_sync (EBookBackendSync *backend,
				       const gchar *email_address,
				       GCancellable *cancellable,
				       GError **error)
{
	EBookQuery *book_query = NULL;
	GPtrArray *array;
	gchar *sexp = NULL;
	gboolean success = FALSE;

	g_return_val_if_fail (email_address != NULL, FALSE);

	d (printf ("book_backend_decsync_contains_email_sync (%s)\n", email_address));

	array = g_ptr_array_new_full (1, (GDestroyNotify) e_book_query_unref);

	e_book_util_foreach_address (email_address, book_backend_decsync_gather_addresses_cb, array);

	if (array->len > 0)
		book_query = e_book_query_or (array->len, (EBookQuery **) array->pdata, FALSE);

	if (book_query)
		sexp = e_book_query_to_string (book_query);

	if (sexp) {
		GSList *uids = NULL;

		success = book_backend_decsync_get_contact_list_uids_sync (backend, sexp,
			&uids, cancellable, error);
		success = success && uids != NULL;

		g_slist_free_full (uids, g_free);
	}

	g_clear_pointer (&book_query, e_book_query_unref);
	g_ptr_array_unref (array);
	g_free (sexp);

	return success;
}

static void
book_backend_decsync_start_view (EBookBackend *backend,
                              EDataBookView *book_view)
{
	DecsyncBackendSearchClosure *closure;

	closure = init_closure (book_view, E_BOOK_BACKEND_DECSYNC (backend));

	d (printf ("starting book view thread\n"));
	closure->thread = g_thread_new (NULL, book_view_thread, book_view);

	e_flag_wait (closure->running);

	/* at this point we know the book view thread is actually running */
	d (printf ("returning from start_view\n"));
}

static void
book_backend_decsync_stop_view (EBookBackend *backend,
                             EDataBookView *book_view)
{
	DecsyncBackendSearchClosure *closure = get_closure (book_view);
	gboolean need_join;

	if (!closure)
		return;

	d (printf ("stopping query\n"));
	need_join = e_flag_is_set (closure->running);
	e_flag_clear (closure->running);

	if (need_join) {
		g_thread_join (closure->thread);
		closure->thread = NULL;
	}
}

static EDataBookDirect *
book_backend_decsync_get_direct_book (EBookBackend *backend)
{
	EDataBookDirect *direct;
	ESourceRegistry *registry;
	ESource *source;
	gchar *backend_path;
	gchar *dirname;
	const gchar *modules_env = NULL;

	modules_env = g_getenv (EDS_ADDRESS_BOOK_MODULES);

	source = e_backend_get_source (E_BACKEND (backend));
	registry = e_book_backend_get_registry (backend);
	dirname = e_book_backend_decsync_extract_path_from_source (
		registry, source, GET_PATH_DB_DIR);

	/* Support in-tree testing / relocated modules */
	if (modules_env) {
		backend_path = g_build_filename (
			modules_env, "libebookbackenddecsync.so", NULL);
	} else {
		backend_path = g_build_filename (
			BACKENDDIR, "libebookbackenddecsync.so", NULL);
	}

	direct = e_data_book_direct_new (
		backend_path, "EBookBackendDecsyncFactory", dirname);

	g_free (backend_path);
	g_free (dirname);

	return direct;
}

static void
book_backend_decsync_configure_direct (EBookBackend *backend,
                                    const gchar *config)
{
	EBookBackendDecsyncPrivate *priv;

	priv = E_BOOK_BACKEND_DECSYNC (backend)->priv;
	priv->base_directory = g_strdup (config);
}

static void
book_backend_decsync_vcard_changed (EbSqlChangeType change_type,
                                 const gchar *uid,
                                 const gchar *extra,
                                 const gchar *vcard,
                                 gpointer user_data)
{
	EBookBackend *backend = E_BOOK_BACKEND (user_data);
	EContact     *contact;

	if (change_type == EBSQL_CHANGE_LOCALE_CHANGED) {
		contact = e_contact_new_from_vcard_with_uid (vcard, uid);
		e_book_backend_notify_update (backend, contact);
		g_object_unref (contact);
	}
}

static gboolean
book_backend_decsync_set_locale (EBookBackend *backend,
                              const gchar *locale,
                              GCancellable *cancellable,
                              GError **error)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	gboolean success;
	GList *l;

	g_rw_lock_writer_lock (&(bf->priv->lock));

	success = e_book_sqlite_lock (
		bf->priv->sqlitedb,
		EBSQL_LOCK_WRITE,
		cancellable, error);
	if (!success) {
		g_rw_lock_writer_unlock (&(bf->priv->lock));
		return FALSE;
	}

	success = e_book_sqlite_set_locale (
		bf->priv->sqlitedb, locale,
		cancellable, error);

	if (success)
		success = e_book_backend_decsync_bump_revision (bf, error);

	if (success) {
		success = e_book_sqlite_unlock (
			bf->priv->sqlitedb,
			EBSQL_UNLOCK_COMMIT,
			error);

	} else {
		GError *local_error = NULL;

		/* Rollback transaction */
		e_book_sqlite_unlock (
			bf->priv->sqlitedb,
			EBSQL_UNLOCK_ROLLBACK,
			&local_error);

		if (local_error != NULL) {
			g_warning (
				"Failed to rollback transaction "
				"after failing to set locale: %s",
				local_error->message);
			g_clear_error (&local_error);
		}
	}

	/* This must be done outside the EBookSqlite lock,
	 * as it may try to acquire the lock as well. */
	for (l = bf->priv->cursors; success && l; l = l->next) {
		EDataBookCursor *cursor = l->data;

		success = e_data_book_cursor_load_locale (
			cursor, NULL, cancellable, error);
	}

	/* We set the new locale, now update our local variable */
	if (success) {
		g_free (bf->priv->locale);
		bf->priv->locale = g_strdup (locale);
	}

	g_rw_lock_writer_unlock (&(bf->priv->lock));

	return success;
}

static gchar *
book_backend_decsync_dup_locale (EBookBackend *backend)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	gchar *locale;

	g_rw_lock_reader_lock (&(bf->priv->lock));
	locale = g_strdup (bf->priv->locale);
	g_rw_lock_reader_unlock (&(bf->priv->lock));

	return locale;
}

static EDataBookCursor *
book_backend_decsync_create_cursor (EBookBackend *backend,
                                 EContactField *sort_fields,
                                 EBookCursorSortType *sort_types,
                                 guint n_fields,
                                 GError **error)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	EDataBookCursor *cursor;

	g_rw_lock_writer_lock (&(bf->priv->lock));

	cursor = e_data_book_cursor_sqlite_new (
		backend,
		bf->priv->sqlitedb,
		SQLITE_REVISION_KEY,
		sort_fields,
		sort_types,
		n_fields,
		error);

	if (cursor != NULL) {
		bf->priv->cursors =
			g_list_prepend (bf->priv->cursors, cursor);
	}

	g_rw_lock_writer_unlock (&(bf->priv->lock));

	return cursor;
}

static gboolean
book_backend_decsync_delete_cursor (EBookBackend *backend,
                                 EDataBookCursor *cursor,
                                 GError **error)
{
	EBookBackendDecsync *bf = E_BOOK_BACKEND_DECSYNC (backend);
	GList *link;

	g_rw_lock_writer_lock (&(bf->priv->lock));

	link = g_list_find (bf->priv->cursors, cursor);

	if (link != NULL) {
		bf->priv->cursors = g_list_delete_link (bf->priv->cursors, link);
		g_object_unref (cursor);
	} else {
		g_set_error_literal (
			error,
			E_CLIENT_ERROR,
			E_CLIENT_ERROR_INVALID_ARG,
			_("Requested to delete an unrelated cursor"));
	}

	g_rw_lock_writer_unlock (&(bf->priv->lock));

	return link != NULL;
}


/****************************************************************
 *                         DecSync updates                      *
 ****************************************************************/

typedef struct {
	EBookBackend *backend;
} Extra;

static void
deleteBook (Extra *extra)
{
	ESource *source;

	source = e_backend_get_source (E_BACKEND (extra->backend));
	e_source_remove_sync (source, NULL, NULL);
}

static void
updateName (Extra *extra, const gchar *name)
{
	EBookBackend *backend;
	ESource *source;
	const gchar *old_name;

	backend = E_BOOK_BACKEND (extra->backend);
	source = e_backend_get_source (E_BACKEND (backend));

	old_name = e_source_get_display_name (source);
	if (g_strcmp0 (old_name, name)) {
		e_source_set_display_name (source, name);
		e_source_write_sync (source, NULL, NULL);
	}
}

static void
updateContacts (const gchar *uid, const gchar *vcard, Extra *extra)
{
	EBookBackend *backend;
	EBookBackendSync *backend_sync;
	EBookBackendDecsync *bf;
	gboolean success, exists;
	const gchar *uids[2], *vcards[2];
	GSList *out_contacts = NULL, *link;

	backend = E_BOOK_BACKEND (extra->backend);
	backend_sync = E_BOOK_BACKEND_SYNC (backend);
	bf = E_BOOK_BACKEND_DECSYNC (backend);

	g_rw_lock_reader_lock (&(bf->priv->lock));
	success = e_book_sqlite_has_contact (bf->priv->sqlitedb, uid, &exists, NULL);
	g_rw_lock_reader_unlock (&(bf->priv->lock));
	if (!success) return;

	uids[0] = uid;
	uids[1] = '\0';
	vcards[0] = vcard;
	vcards[1] = '\0';
	if (exists)
		book_backend_decsync_modify_contacts_sync_with_decsync (backend_sync, vcards, uids, 0, &out_contacts, NULL, NULL, FALSE);
	else
		book_backend_decsync_create_contacts_sync_with_decsync (backend_sync, vcards, uids, 0, &out_contacts, NULL, NULL, FALSE);
	for (link = out_contacts; link; link = g_slist_next (link)) {
		e_book_backend_notify_update (backend, E_CONTACT (link->data));
	}

	g_slist_free_full (out_contacts, g_object_unref);
}

static void
removeContacts (const gchar *uid, Extra *extra)
{
	EBookBackend *backend;
	EBookBackendSync *backend_sync;
	const gchar *uids[2];
	GSList *out_uids;

	backend = E_BOOK_BACKEND (extra->backend);
	backend_sync = E_BOOK_BACKEND_SYNC (backend);
	uids[0] = uid;
	uids[1] = '\0';
	book_backend_decsync_remove_contacts_sync_with_decsync (backend_sync, uids, 0, &out_uids, NULL, NULL, FALSE);
	e_book_backend_notify_remove (backend, uid);

	g_slist_free_full (out_uids, g_free);
}

static void
infoListener (const gchar **path, int len, const char *datetime, const char *key_string, const char *value_string, void *extra_void)
{
	Extra *extra;
	const gchar *info;
	JsonNode *key_node, *value_node;
	GError *error = NULL;

	extra = (Extra*)extra_void;
	key_node = json_from_string (key_string, &error);
	if (error != NULL) {
		g_warning ("Invalid JSON for info key: %s", key_string);
		g_error_free (error);
		return;
	}
	value_node = json_from_string (value_string, &error);
	if (error != NULL) {
		g_warning ("Invalid JSON for info value: %s", value_string);
		json_node_free (key_node);
		g_error_free (error);
		return;
	}
	info = json_node_get_string (key_node);
	if (g_strcmp0 (info, "deleted") == 0) {
		if (json_node_get_boolean (value_node) == TRUE) {
			deleteBook (extra);
		}
	} else if (g_strcmp0 (info, "name") == 0) {
		updateName(extra, json_node_get_string (value_node));
	} else {
		g_warning ("Unknown info key: %s", info);
	}
	json_node_free (key_node);
	json_node_free (value_node);
}

static void
resourcesListener (const gchar **path, int len, const char *datetime, const char *key_string, const char *value_string, void *extra_void)
{
	Extra *extra;
	const gchar *uid, *vcard;
	JsonNode *key_node, *value_node;
	GError *error = NULL;

	extra = (Extra*)extra_void;
	key_node = json_from_string (key_string, &error);
	if (error != NULL) {
		g_warning ("Invalid JSON for resource key: %s", key_string);
		g_error_free (error);
		return;
	}
	value_node = json_from_string (value_string, &error);
	if (error != NULL) {
		g_warning ("Invalid JSON for resource value: %s", value_string);
		json_node_free (key_node);
		g_error_free (error);
		return;
	}
	if (len != 1) {
		g_warning ("Invalid resources path size %i", len);
		return;
	}
	uid = path[0];
	if (json_node_is_null (value_node)) {
		removeContacts(uid, extra);
	} else {
		vcard = json_node_get_string (value_node);
		updateContacts(uid, vcard, extra);
	}
	json_node_free (key_node);
	json_node_free (value_node);
}

static gboolean
getDecsyncFromSource (EBookBackendDecsyncPrivate *priv, ESource *source)
{
	ESourceDecsync *decsync_extension;
	const gchar *extension_name, *decsync_dir, *collection, *appid, *path[1];
	int error;

	extension_name = E_SOURCE_EXTENSION_DECSYNC_BACKEND;
	decsync_extension = e_source_get_extension (source, extension_name);
	decsync_dir = e_source_decsync_get_decsync_dir (decsync_extension);
	collection = e_source_decsync_get_collection (decsync_extension);
	appid = e_source_decsync_get_appid (decsync_extension);
	error = decsync_new (&priv->decsync, decsync_dir, "contacts", collection, appid);
	if (error != 0) {
		return FALSE;
	}
	path[0] = "info";
	decsync_add_listener (priv->decsync, path, 1, infoListener);
	path[0] = "resources";
	decsync_add_listener (priv->decsync, path, 1, resourcesListener);
	decsync_init_done (priv->decsync);
	return TRUE;
}

static gboolean
book_backend_decsync_refresh_cb (gpointer backend)
{
	EBookBackendDecsync *bf;
	Extra extra;

	bf = E_BOOK_BACKEND_DECSYNC (backend);
	extra = (Extra) {backend};
	decsync_execute_all_new_entries (bf->priv->decsync, &extra);
	return TRUE;
}

static gboolean
book_backend_decsync_refresh_start (EBookBackendDecsync *bf)
{
	ESource *source;
	ESourceRefresh *extension;
	const gchar *extension_name;
	guint interval_in_minutes = 0;

	source = e_backend_get_source (E_BACKEND (bf));

	extension_name = E_SOURCE_EXTENSION_REFRESH;
	extension = e_source_get_extension (source, extension_name);

	if (e_source_refresh_get_enabled (extension)) {
		interval_in_minutes = e_source_refresh_get_interval_minutes (extension);
		if (interval_in_minutes == 0)
			interval_in_minutes = 30;
	}

	if (interval_in_minutes > 0) {
		e_named_timeout_add_seconds (interval_in_minutes * 60, book_backend_decsync_refresh_cb, bf);
	}
	return FALSE;
}

static void
book_backend_decsync_refresh (EBookBackend *backend,
                              EDataBook *book,
                              guint32 opid,
                              GCancellable *cancellable)
{
	book_backend_decsync_refresh_cb (backend);
}

static gboolean
book_backend_decsync_initable_init (GInitable *initable,
                                 GCancellable *cancellable,
                                 GError **error)
{
	EBookBackendDecsyncPrivate *priv;
	ESourceBackendSummarySetup *setup_extension;
	ESourceRegistry *registry;
	ESource *source;
	const gchar *extension_name;
	gchar *dirname, *fullpath;
	gboolean success = TRUE;

	priv = E_BOOK_BACKEND_DECSYNC (initable)->priv;

	source = e_backend_get_source (E_BACKEND (initable));
	registry = e_book_backend_get_registry (E_BOOK_BACKEND (initable));

	g_type_ensure (E_TYPE_SOURCE_BACKEND_SUMMARY_SETUP);
	extension_name = E_SOURCE_EXTENSION_BACKEND_SUMMARY_SETUP;
	setup_extension = e_source_get_extension (source, extension_name);

	if (priv->base_directory)
		dirname = g_strdup (priv->base_directory);
	else
		dirname = e_book_backend_decsync_extract_path_from_source (
			registry, source, GET_PATH_DB_DIR);

	fullpath = g_build_filename (dirname, "contacts.db", NULL);

	success = getDecsyncFromSource (priv, source);

	if (!success)
		goto exit;

	/* If we already have a handle on this, it means there
	 * was an old BDB migrated and no need to reopen it. */
	if (priv->sqlitedb == NULL) {
		gint populated = 0;
		GError *local_error = NULL;

		/* Ensure the directory exists first. */
		success = create_directory (dirname, error);

		if (!success)
			goto exit;

		/* Create the sqlitedb. */
		priv->sqlitedb = e_book_sqlite_new_full (
			fullpath, source, setup_extension,
			NULL,
			book_backend_decsync_vcard_changed,
			initable, NULL, cancellable, error);

		if (priv->sqlitedb == NULL) {
			success = FALSE;
			goto exit;
		}

		/* An sqlite DB only 'exists' if the populated flag is set. */
		e_book_sqlite_get_key_value_int (
			priv->sqlitedb,
			E_BOOK_SQL_IS_POPULATED_KEY,
			&populated,
			&local_error);

		if (local_error != NULL) {
			g_propagate_error (error, local_error);
			success = FALSE;
			goto exit;
		}

		if (!populated) {
			/* Set the populated flag. */
			success = e_book_sqlite_set_key_value_int (
				priv->sqlitedb,
				E_BOOK_SQL_IS_POPULATED_KEY,
				TRUE,
				error);

			if (!success)
				goto exit;
		}
	}

	/* Load the locale */
	e_book_backend_decsync_load_locale (E_BOOK_BACKEND_DECSYNC (initable));

	/* Resolve the photo directory here. */
	priv->photo_dirname =
		e_book_backend_decsync_extract_path_from_source (
		registry, source, GET_PATH_PHOTO_DIR);
	success = create_directory (priv->photo_dirname, error);

exit:
	g_free (dirname);
	g_free (fullpath);

	return success;
}

static void
e_book_backend_decsync_class_init (EBookBackendDecsyncClass *class)
{
	GObjectClass *object_class;
	EBookBackendClass *backend_class;
	EBookBackendSyncClass *backend_sync_class;

	object_class = G_OBJECT_CLASS (class);
	object_class->dispose = book_backend_decsync_dispose;
	object_class->finalize = book_backend_decsync_finalize;

	backend_sync_class = E_BOOK_BACKEND_SYNC_CLASS (class);
	backend_sync_class->open_sync = book_backend_decsync_open_sync;
	backend_sync_class->create_contacts_sync = book_backend_decsync_create_contacts_sync;
	backend_sync_class->modify_contacts_sync = book_backend_decsync_modify_contacts_sync;
	backend_sync_class->remove_contacts_sync = book_backend_decsync_remove_contacts_sync;
	backend_sync_class->get_contact_sync = book_backend_decsync_get_contact_sync;
	backend_sync_class->get_contact_list_sync = book_backend_decsync_get_contact_list_sync;
	backend_sync_class->get_contact_list_uids_sync = book_backend_decsync_get_contact_list_uids_sync;
	backend_sync_class->contains_email_sync = book_backend_decsync_contains_email_sync;

	backend_class = E_BOOK_BACKEND_CLASS (class);
	backend_class->impl_get_backend_property = book_backend_decsync_get_backend_property;
	backend_class->impl_start_view = book_backend_decsync_start_view;
	backend_class->impl_stop_view = book_backend_decsync_stop_view;
	backend_class->impl_get_direct_book = book_backend_decsync_get_direct_book;
	backend_class->impl_configure_direct = book_backend_decsync_configure_direct;
	backend_class->impl_set_locale = book_backend_decsync_set_locale;
	backend_class->impl_dup_locale = book_backend_decsync_dup_locale;
	backend_class->impl_create_cursor = book_backend_decsync_create_cursor;
	backend_class->impl_delete_cursor = book_backend_decsync_delete_cursor;
	backend_class->impl_refresh = book_backend_decsync_refresh;

	E_TYPE_SOURCE_DECSYNC;
}

static void
e_book_backend_decsync_initable_init (GInitableIface *iface)
{
	iface->init = book_backend_decsync_initable_init;
}

static void
e_book_backend_decsync_init (EBookBackendDecsync *backend)
{
	backend->priv = e_book_backend_decsync_get_instance_private (backend);

	g_rw_lock_init (&(backend->priv->lock));
}

