[almanah] Make import/export operations asynchronous



commit 2f9fd8078783368beb3744e79c25269a76836503
Author: Philip Withnall <philip tecnocode co uk>
Date:   Sun May 16 16:24:02 2010 +0100

    Make import/export operations asynchronous

 configure.ac               |    2 +-
 data/almanah.ui            |    7 +-
 src/Makefile.am            |    4 +
 src/export-operation.c     |  257 ++++++++++++++++++++
 src/export-operation.h     |   60 +++++
 src/import-export-dialog.c |  558 +++++++------------------------------------
 src/import-export-dialog.h |   11 +-
 src/import-operation.c     |  510 ++++++++++++++++++++++++++++++++++++++++
 src/import-operation.h     |   72 ++++++
 src/main.c                 |    1 +
 10 files changed, 1004 insertions(+), 478 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 275e8a5..ed81fcf 100644
--- a/configure.ac
+++ b/configure.ac
@@ -79,7 +79,7 @@ dnl Dependencies
 dnl ***************************************************************************
 
 dnl Required dependencies
-PKG_CHECK_MODULES(STANDARD, glib-2.0 gtk+-2.0 >= 2.14 gmodule-2.0 gio-2.0 sqlite3 cairo gconf-2.0 atk dbus-glib-1)
+PKG_CHECK_MODULES(STANDARD, glib-2.0 gtk+-2.0 >= 2.14 gmodule-2.0 gthread-2.0 gio-2.0 sqlite3 cairo gconf-2.0 atk dbus-glib-1)
 AC_SUBST(STANDARD_CFLAGS)
 AC_SUBST(STANDARD_LIBS)
 
diff --git a/data/almanah.ui b/data/almanah.ui
index c3fe756..a58af93 100644
--- a/data/almanah.ui
+++ b/data/almanah.ui
@@ -937,8 +937,11 @@
 
 	<object class="GtkListStore" id="almanah_ied_mode_store">
 		<columns>
-			<column type="gchararray"/><!-- Name -->
 			<column type="guint"/><!-- Type -->
+			<column type="gchararray"/><!-- Name -->
+			<column type="gchararray"/><!-- Description -->
+			<column type="guint"/><!-- Action -->
+			<column type="gchararray"/><!-- Title -->
 		</columns>
 	</object>
 
@@ -978,7 +981,7 @@
 										<child>
 											<object class="GtkCellRendererText" id="almanah_ied_mode_renderer"/>
 											<attributes>
-												<attribute name="text">0</attribute>
+												<attribute name="text">1</attribute>
 											</attributes>
 										</child>
 									</object>
diff --git a/src/Makefile.am b/src/Makefile.am
index 6ed8ea7..93e9f92 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -33,6 +33,10 @@ almanah_SOURCES = \
 	date-entry-dialog.h		\
 	import-export-dialog.c		\
 	import-export-dialog.h		\
+	import-operation.c		\
+	import-operation.h		\
+	export-operation.c		\
+	export-operation.h		\
 	definitions/file.c		\
 	definitions/file.h		\
 	definitions/note.c		\
diff --git a/src/export-operation.c b/src/export-operation.c
new file mode 100644
index 0000000..032f55f
--- /dev/null
+++ b/src/export-operation.c
@@ -0,0 +1,257 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * Almanah
+ * Copyright (C) Philip Withnall 2010 <philip tecnocode co uk>
+ *
+ * Almanah is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Almanah is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Almanah.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "export-operation.h"
+#include "entry.h"
+#include "storage-manager.h"
+#include "main.h"
+
+typedef gboolean (*ExportFunc) (AlmanahExportOperation *self, GFile *destination, GCancellable *cancellable, GError **error);
+
+typedef struct {
+	const gchar *name; /* translatable */
+	const gchar *description; /* translatable */
+	GtkFileChooserAction action;
+	const gchar *file_chooser_title; /* translatable */
+	ExportFunc export_func;
+} ExportModeDetails;
+
+static gboolean export_text_files (AlmanahExportOperation *self, GFile *destination, GCancellable *cancellable, GError **error);
+static gboolean export_database (AlmanahExportOperation *self, GFile *destination, GCancellable *cancellable, GError **error);
+
+static const ExportModeDetails export_modes[] = {
+	{ N_("Text Files"),
+	  N_("Select a _folder to export the entries to as text files, one per entry, with names in the format 'yyyy-mm-dd', and no extension. "
+	     "All entries will be exported, unencrypted in plain text format."),
+	  GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
+	  N_("Select a Folder"),
+	  export_text_files },
+	{ N_("Database"),
+	  N_("Select a _filename for a complete copy of the unencrypted Almanah Diary database to be given."),
+	  GTK_FILE_CHOOSER_ACTION_SAVE, /* TODO: Need to change the file chooser for this */
+	  N_("Select a File"),
+	  export_database }
+};
+
+static void almanah_export_operation_dispose (GObject *object);
+
+struct _AlmanahExportOperationPrivate {
+	gint current_mode; /* index into export_modes */
+	GFile *destination;
+};
+
+G_DEFINE_TYPE (AlmanahExportOperation, almanah_export_operation, G_TYPE_OBJECT)
+#define ALMANAH_EXPORT_OPERATION_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), ALMANAH_TYPE_EXPORT_OPERATION, AlmanahExportOperationPrivate))
+
+static void
+almanah_export_operation_class_init (AlmanahExportOperationClass *klass)
+{
+	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+	g_type_class_add_private (klass, sizeof (AlmanahExportOperationPrivate));
+
+	gobject_class->dispose = almanah_export_operation_dispose;
+}
+
+static void
+almanah_export_operation_init (AlmanahExportOperation *self)
+{
+	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, ALMANAH_TYPE_EXPORT_OPERATION, AlmanahExportOperationPrivate);
+	self->priv->current_mode = -1; /* no mode selected */
+}
+
+static void
+almanah_export_operation_dispose (GObject *object)
+{
+	AlmanahExportOperationPrivate *priv = ALMANAH_EXPORT_OPERATION (object)->priv;
+
+	if (priv->destination != NULL)
+		g_object_unref (priv->destination);
+	priv->destination = NULL;
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (almanah_export_operation_parent_class)->dispose (object);
+}
+
+AlmanahExportOperation *
+almanah_export_operation_new (AlmanahExportOperationType type_id, GFile *destination)
+{
+	AlmanahExportOperation *export_operation = g_object_new (ALMANAH_TYPE_EXPORT_OPERATION, NULL);
+	export_operation->priv->current_mode = type_id;
+	export_operation->priv->destination = g_object_ref (destination);
+
+	return export_operation;
+}
+
+static gboolean
+export_text_files (AlmanahExportOperation *self, GFile *destination, GCancellable *cancellable, GError **error)
+{
+	AlmanahStorageManagerIter iter;
+	AlmanahEntry *entry;
+	GtkTextBuffer *buffer;
+	gboolean success = FALSE;
+	GError *child_error = NULL;
+
+	/* Build a text buffer to use when getting all the entries */
+	buffer = gtk_text_buffer_new (NULL);
+
+	/* Iterate through the entries */
+	almanah_storage_manager_iter_init (&iter);
+	while ((entry = almanah_storage_manager_get_entries (almanah->storage_manager, &iter)) != NULL) {
+		GDate date;
+		gchar *filename, *content;
+		GFile *file;
+		GtkTextIter start_iter, end_iter;
+
+		/* Get the filename */
+		almanah_entry_get_date (entry, &date);
+		filename = g_strdup_printf ("%04u-%02u-%02u", g_date_get_year (&date), g_date_get_month (&date), g_date_get_day (&date));
+		file = g_file_get_child (destination, filename);
+		g_free (filename);
+
+		/* Get the entry contents */
+		if (almanah_entry_get_content (entry, buffer, TRUE, &child_error) == FALSE) {
+			/* Error */
+			g_object_unref (file);
+			g_object_unref (entry);
+			break;
+		}
+
+		g_object_unref (entry);
+
+		gtk_text_buffer_get_bounds (buffer, &start_iter, &end_iter);
+		content = gtk_text_buffer_get_text (buffer, &start_iter, &end_iter, FALSE);
+
+		/* Create the file */
+		if (g_file_replace_contents (file, content, strlen (content), NULL, FALSE,
+		                             G_FILE_CREATE_PRIVATE | G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, &child_error) == FALSE) {
+			/* Error */
+			g_object_unref (file);
+			g_free (content);
+			break;
+		}
+
+		g_object_unref (file);
+		g_free (content);
+	}
+
+	/* Check if the loop was broken due to an error */
+	if (child_error != NULL) {
+		g_propagate_error (error, child_error);
+		goto finish;
+	}
+
+	success = TRUE;
+
+finish:
+	g_object_unref (buffer);
+
+	return success;
+}
+
+static gboolean
+export_database (AlmanahExportOperation *self, GFile *destination, GCancellable *cancellable, GError **error)
+{
+	GFile *source;
+	gboolean success;
+
+	/* Get the input file (current unencrypted database) */
+	source = g_file_new_for_path (almanah_storage_manager_get_filename (almanah->storage_manager, TRUE));
+
+	/* Copy the current database to that location */
+	success = g_file_copy (source, destination, G_FILE_COPY_OVERWRITE, cancellable, NULL, NULL, error);
+
+	g_object_unref (source);
+
+	return success;
+}
+
+static void
+export_thread (GSimpleAsyncResult *result, AlmanahExportOperation *operation, GCancellable *cancellable)
+{
+	GError *error = NULL;
+
+	/* Check to see if the operation's been cancelled already */
+	if (g_cancellable_set_error_if_cancelled (cancellable, &error) == TRUE) {
+		g_simple_async_result_set_from_error (result, error);
+		g_error_free (error);
+		return;
+	}
+
+	/* Export and return */
+	if (export_modes[operation->priv->current_mode].export_func (operation, operation->priv->destination, cancellable, &error) == FALSE) {
+		g_simple_async_result_set_from_error (result, error);
+		g_error_free (error);
+	}
+}
+
+void
+almanah_export_operation_run (AlmanahExportOperation *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data)
+{
+	GSimpleAsyncResult *result;
+
+	g_return_if_fail (ALMANAH_IS_EXPORT_OPERATION (self));
+	g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+	result = g_simple_async_result_new (G_OBJECT (self), callback, user_data, almanah_export_operation_run);
+	g_simple_async_result_run_in_thread (result, (GSimpleAsyncThreadFunc) export_thread, G_PRIORITY_DEFAULT, cancellable);
+	g_object_unref (result);
+}
+
+gboolean
+almanah_export_operation_finish (AlmanahExportOperation *self, GAsyncResult *async_result, GError **error)
+{
+	GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT (async_result);
+
+	g_return_val_if_fail (ALMANAH_IS_EXPORT_OPERATION (self), FALSE);
+	g_return_val_if_fail (G_IS_ASYNC_RESULT (async_result), FALSE);
+	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+	g_warn_if_fail (g_simple_async_result_get_source_tag (result) == almanah_export_operation_run);
+
+	if (g_simple_async_result_propagate_error (result, error) == TRUE)
+		return FALSE;
+
+	return TRUE;
+}
+
+void
+almanah_export_operation_populate_model (GtkListStore *store, guint type_id_column, guint name_column, guint description_column, guint action_column,
+                                         guint file_chooser_title_column)
+{
+	guint i;
+
+	for (i = 0; i < G_N_ELEMENTS (export_modes); i++) {
+		GtkTreeIter iter;
+
+		gtk_list_store_append (store, &iter);
+		gtk_list_store_set (store, &iter,
+		                    type_id_column, i,
+		                    name_column, _(export_modes[i].name),
+		                    description_column, _(export_modes[i].description),
+		                    action_column, export_modes[i].action,
+		                    file_chooser_title_column, _(export_modes[i].file_chooser_title),
+		                    -1);
+	}
+}
diff --git a/src/export-operation.h b/src/export-operation.h
new file mode 100644
index 0000000..f0990c2
--- /dev/null
+++ b/src/export-operation.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * Almanah
+ * Copyright (C) Philip Withnall 2010 <philip tecnocode co uk>
+ *
+ * Almanah is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Almanah is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Almanah.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ALMANAH_EXPORT_OPERATION_H
+#define ALMANAH_EXPORT_OPERATION_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef guint AlmanahExportOperationType;
+
+#define ALMANAH_TYPE_EXPORT_OPERATION		(almanah_export_operation_get_type ())
+#define ALMANAH_EXPORT_OPERATION(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), ALMANAH_TYPE_EXPORT_OPERATION, AlmanahExportOperation))
+#define ALMANAH_EXPORT_OPERATION_CLASS(k)	(G_TYPE_CHECK_CLASS_CAST((k), ALMANAH_TYPE_EXPORT_OPERATION, AlmanahExportOperationClass))
+#define ALMANAH_IS_EXPORT_OPERATION(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), ALMANAH_TYPE_EXPORT_OPERATION))
+#define ALMANAH_IS_EXPORT_OPERATION_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), ALMANAH_TYPE_EXPORT_OPERATION))
+#define ALMANAH_EXPORT_OPERATION_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), ALMANAH_TYPE_EXPORT_OPERATION, AlmanahExportOperationClass))
+
+typedef struct _AlmanahExportOperationPrivate	AlmanahExportOperationPrivate;
+
+typedef struct {
+	GObject parent;
+	AlmanahExportOperationPrivate *priv;
+} AlmanahExportOperation;
+
+typedef struct {
+	GObjectClass parent;
+} AlmanahExportOperationClass;
+
+GType almanah_export_operation_get_type (void) G_GNUC_CONST;
+
+AlmanahExportOperation *almanah_export_operation_new (AlmanahExportOperationType type_id, GFile *destination) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+
+void almanah_export_operation_run (AlmanahExportOperation *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data);
+gboolean almanah_export_operation_finish (AlmanahExportOperation *self, GAsyncResult *async_result, GError **error);
+
+void almanah_export_operation_populate_model (GtkListStore *list_store, guint type_id_column, guint name_column, guint description_column,
+                                              guint action_column, guint file_chooser_title_column);
+
+G_END_DECLS
+
+#endif /* !ALMANAH_EXPORT_OPERATION_H */
diff --git a/src/import-export-dialog.c b/src/import-export-dialog.c
index 48af0c9..4883c55 100644
--- a/src/import-export-dialog.c
+++ b/src/import-export-dialog.c
@@ -23,48 +23,12 @@
 #include <gtk/gtk.h>
 
 #include "import-export-dialog.h"
+#include "import-operation.h"
+#include "export-operation.h"
 #include "interface.h"
 #include "main.h"
 #include "main-window.h"
 
-typedef gboolean (*ImportFunc) (AlmanahImportExportDialog *self, AlmanahImportResultsDialog *results_dialog, GError **error);
-typedef gboolean (*ExportFunc) (AlmanahImportExportDialog *self, GError **error);
-
-typedef struct {
-	const gchar *name; /* translatable */
-	const gchar *import_description; /* translatable */
-	const gchar *export_description; /* translatable */
-	GtkFileChooserAction action;
-	const gchar *file_chooser_title; /* translatable */
-	ImportFunc import_func;
-	ExportFunc export_func;
-} ImportExportModeDetails;
-
-static gboolean import_text_files (AlmanahImportExportDialog *self, AlmanahImportResultsDialog *results_dialog, GError **error);
-static gboolean export_text_files (AlmanahImportExportDialog *self, GError **error);
-
-static gboolean import_database (AlmanahImportExportDialog *self, AlmanahImportResultsDialog *results_dialog, GError **error);
-static gboolean export_database (AlmanahImportExportDialog *self, GError **error);
-
-static const ImportExportModeDetails import_export_modes[] = {
-	{ N_("Text Files"),
-	  N_("Select a _folder containing text files, one per entry, with names in the format 'yyyy-mm-dd', and no extension. "
-	     "Any and all such files will be imported."),
-	  N_("Select a _folder to export the entries to as text files, one per entry, with names in the format 'yyyy-mm-dd', and no extension. "
-	     "All entries will be exported, unencrypted in plain text format."),
-	  GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
-	  N_("Select a Folder"),
-	  import_text_files,
-	  export_text_files },
-	{ N_("Database"),
-	  N_("Select a database _file created by Almanah Diary to import."),
-	  N_("Select a _filename for a complete copy of the unencrypted Almanah Diary database to be given."),
-	  GTK_FILE_CHOOSER_ACTION_OPEN,
-	  N_("Select a File"),
-	  import_database,
-	  export_database }
-};
-
 static void response_cb (GtkDialog *dialog, gint response_id, AlmanahImportExportDialog *self);
 
 /* GtkBuilder callbacks */
@@ -76,7 +40,7 @@ struct _AlmanahImportExportDialogPrivate {
 	gboolean import; /* TRUE if we're in import mode, FALSE otherwise */
 	GtkComboBox *mode_combo_box;
 	GtkListStore *mode_store;
-	guint current_mode; /* index into import_export_modes */
+	gint current_mode;
 	GtkFileChooser *file_chooser;
 	GtkWidget *import_export_button;
 	GtkLabel *description_label;
@@ -104,20 +68,6 @@ almanah_import_export_dialog_init (AlmanahImportExportDialog *self)
 	gtk_window_set_transient_for (GTK_WINDOW (self), GTK_WINDOW (almanah->main_window));
 }
 
-static void
-populate_mode_store (GtkListStore *store)
-{
-	guint i;
-
-	/* Add all the available import/export modes to the combo box */
-	for (i = 0; i < G_N_ELEMENTS (import_export_modes); i++) {
-		GtkTreeIter iter;
-
-		gtk_list_store_append (store, &iter);
-		gtk_list_store_set (store, &iter, 0, _(import_export_modes[i].name), 1, i, -1);
-	}
-}
-
 /**
  * almanah_import_export_dialog_new:
  * @import: %TRUE to set the dialog up for importing, %FALSE to set it up for exporting
@@ -186,7 +136,10 @@ almanah_import_export_dialog_new (gboolean import)
 	gtk_button_set_label (GTK_BUTTON (priv->import_export_button), (import == TRUE) ? _("_Import") : _("_Export"));
 
 	/* Populate the mode combo box */
-	populate_mode_store (priv->mode_store);
+	if (import == TRUE)
+		almanah_import_operation_populate_model (priv->mode_store, 0, 1, 2, 3, 4);
+	else
+		almanah_export_operation_populate_model (priv->mode_store, 0, 1, 2, 3, 4);
 	gtk_combo_box_set_active (priv->mode_combo_box, 0);
 
 	g_object_unref (builder);
@@ -194,398 +147,77 @@ almanah_import_export_dialog_new (gboolean import)
 	return import_export_dialog;
 }
 
-/**
- * set_entry:
- * @self: an #AlmanahImportExportDialog
- * @imported_entry: an #AlmanahEntry created for an entry being imported
- * @import_source: a string representing the source of the imported entry
- * @message: return location for an error or informational message, or %NULL; free with g_free()
- *
- * Stores the given entry in the database, merging it with any existing one where appropriate.
- *
- * @import_source should represent where the imported entry came from, so it could be the filename of a text file which held the entry, or of
- * the entry's previous database.
- *
- * If @message is non-%NULL, error or informational messages can be allocated and returned in it. They will be returned irrespective of the return
- * value of the function, as some errors can be overcome to result in a successful import. The message should be freed with g_free() by the caller.
- *
- * Return value: %ALMANAH_IMPORT_STATUS_MERGED if the entry was merged with an existing one, %ALMANAH_IMPORT_STATUS_IMPORTED if it was imported
- * without merging, %ALMANAH_IMPORT_STATUS_FAILED otherwise
- **/
-static AlmanahImportStatus
-set_entry (AlmanahImportExportDialog *self, AlmanahEntry *imported_entry, const gchar *import_source, gchar **message)
+static void
+import_progress_cb (const GDate *date, AlmanahImportStatus status, const gchar *message, AlmanahImportResultsDialog *results_dialog)
 {
-	GDate entry_date, existing_last_edited, imported_last_edited;
-	AlmanahEntry *existing_entry;
-	GtkTextBuffer *existing_buffer, *imported_buffer;
-	GtkTextIter existing_start, existing_end, imported_start, imported_end;
-	gchar *header_string;
-	GError *error = NULL;
-
-	/* Check to see if there's a conflict first */
-	almanah_entry_get_date (imported_entry, &entry_date);
-	existing_entry = almanah_storage_manager_get_entry (almanah->storage_manager, &entry_date);
-
-	if (existing_entry == NULL) {
-		/* Add the entry to the proper database and return, ignoring failure */
-		almanah_storage_manager_set_entry (almanah->storage_manager, imported_entry);
-
-		return ALMANAH_IMPORT_STATUS_IMPORTED;
-	}
-
-	/* Load both entries into buffers */
-	imported_buffer = gtk_text_buffer_new (NULL);
-	if (almanah_entry_get_content (imported_entry, imported_buffer, TRUE, &error) == FALSE) {
-		if (message != NULL)
-			*message = g_strdup_printf (_("Error deserializing imported entry into buffer: %s"), (error != NULL) ? error->message : NULL);
-
-		g_error_free (error);
-		g_object_unref (imported_buffer);
-		g_object_unref (existing_entry);
-
-		return ALMANAH_IMPORT_STATUS_FAILED;
-	}
-
-	/* The two buffers have to use the same tag table for gtk_text_buffer_insert_range() to work */
-	existing_buffer = gtk_text_buffer_new (gtk_text_buffer_get_tag_table (imported_buffer));
-	if (almanah_entry_get_content (existing_entry, existing_buffer, FALSE, NULL) == FALSE) {
-		/* Deserialising the existing entry failed; use the imported entry instead */
-		almanah_storage_manager_set_entry (almanah->storage_manager, imported_entry);
-
-		if (message != NULL) {
-			*message = g_strdup_printf (_("Error deserializing existing entry into buffer; overwriting with imported entry: %s"),
-			                            (error != NULL) ? error->message : NULL);
-		}
-
-		g_error_free (error);
-		g_object_unref (imported_buffer);
-		g_object_unref (existing_buffer);
-		g_object_unref (existing_entry);
-
-		return ALMANAH_IMPORT_STATUS_IMPORTED;
-	}
-
-	/* Get the bounds for later use */
-	gtk_text_buffer_get_bounds (existing_buffer, &existing_start, &existing_end);
-	gtk_text_buffer_get_bounds (imported_buffer, &imported_start, &imported_end);
-
-	/* Compare the two buffers --- if they're identical, leave the current entry alone and mark the entries as merged.
-	 * Compare the character counts first so that the comparison is less expensive in the case they aren't identical .*/
-	if (gtk_text_buffer_get_char_count (existing_buffer) == gtk_text_buffer_get_char_count (imported_buffer)) {
-		gchar *existing_text, *imported_text;
-
-		existing_text = gtk_text_buffer_get_text (existing_buffer, &existing_start, &existing_end, FALSE);
-		imported_text = gtk_text_buffer_get_text (imported_buffer, &imported_start, &imported_end, FALSE);
-
-		/* If they're the same, no modifications are required */
-		if (strcmp (existing_text, imported_text) == 0) {
-			g_free (existing_text);
-			g_free (imported_text);
-			g_object_unref (imported_buffer);
-			g_object_unref (existing_buffer);
-			g_object_unref (existing_entry);
-			return ALMANAH_IMPORT_STATUS_MERGED;
-		}
-
-		g_free (existing_text);
-		g_free (imported_text);
-	}
-
-	/* Append some header text for the imported entry */
-	/* Translators: This text is appended to an existing entry when an entry is being imported to the same date.
-	 * The imported entry is appended to this text. */
-	header_string = g_strdup_printf (_("\n\nEntry imported from \"%s\":\n\n"), import_source);
-	gtk_text_buffer_insert (existing_buffer, &existing_end, header_string, -1);
-	g_free (header_string);
-
-	/* Append the imported entry to the end of the existing one */
-	gtk_text_buffer_insert_range (existing_buffer, &existing_end, &imported_start, &imported_end);
-	g_object_unref (imported_buffer);
-
-	/* Store the buffer back in the existing entry and save the entry */
-	almanah_entry_set_content (existing_entry, existing_buffer);
-	g_object_unref (existing_buffer);
-
-	/* Update the last-edited time for the merged entry to be the more recent of the last-edited times for the existing and imported entries */
-	almanah_entry_get_last_edited (existing_entry, &existing_last_edited);
-	almanah_entry_get_last_edited (imported_entry, &imported_last_edited);
-
-	if (g_date_valid (&existing_last_edited) == FALSE || g_date_compare (&existing_last_edited, &imported_last_edited) < 0)
-		almanah_entry_set_last_edited (existing_entry, &imported_last_edited);
-
-	almanah_storage_manager_set_entry (almanah->storage_manager, existing_entry);
-	g_object_unref (existing_entry);
-
-	return ALMANAH_IMPORT_STATUS_MERGED;
+	almanah_import_results_dialog_add_result (results_dialog, date, status, message);
 }
 
-static gboolean
-import_text_files (AlmanahImportExportDialog *self, AlmanahImportResultsDialog *results_dialog, GError **error)
+static void
+import_cb (AlmanahImportOperation *operation, GAsyncResult *async_result, AlmanahImportResultsDialog *results_dialog)
 {
-	gboolean retval = FALSE;
-	GFile *folder;
-	GFileInfo *file_info;
-	GFileEnumerator *enumerator;
-	GtkTextBuffer *buffer;
-	GError *child_error = NULL;
-	AlmanahImportExportDialogPrivate *priv = self->priv;
-
-	/* Get the folder containing all the files to import */
-	folder = gtk_file_chooser_get_file (priv->file_chooser);
-	g_assert (folder != NULL);
-
-	enumerator = g_file_enumerate_children (folder, "standard::name,standard::display-name,standard::is-hidden,time::modified",
-	                                        G_FILE_QUERY_INFO_NONE, NULL, error);
-	g_object_unref (folder);
-	if (enumerator == NULL)
-		return FALSE;
-
-	/* Build a text buffer to use when setting all the entries */
-	buffer = gtk_text_buffer_new (NULL);
-
-	/* Enumerate all the children of the folder */
-	while ((file_info = g_file_enumerator_next_file (enumerator, NULL, &child_error)) != NULL) {
-		AlmanahEntry *entry;
-		GDate parsed_date, last_edited;
-		GTimeVal modification_time;
-		GFile *file;
-		gchar *contents, *message = NULL;
-		gsize length;
-		AlmanahImportStatus status;
-		const gchar *file_name = g_file_info_get_name (file_info);
-		const gchar *display_name = g_file_info_get_display_name (file_info);
-
-		/* Skip the file if it's hidden */
-		if (g_file_info_get_is_hidden (file_info) == TRUE || file_name[strlen (file_name) - 1] == '~') {
-			g_object_unref (file_info);
-			continue;
-		}
-
-		/* Heuristically parse the date, though we recommend using the format: yyyy-mm-dd */
-		g_date_set_parse (&parsed_date, file_name);
-		if (g_date_valid (&parsed_date) == FALSE) {
-			g_object_unref (file_info);
-			continue;
-		}
-
-		/* Get the file */
-		file = g_file_get_child (folder, file_name);
-		g_assert (file != NULL);
-
-		/* Load the content */
-		if (g_file_load_contents (file, NULL, &contents, &length, NULL, &child_error) == FALSE) {
-			g_object_unref (file);
-			g_object_unref (file_info);
-			break; /* let the error get handled by the code just after the loop */
-		}
-		g_object_unref (file);
-
-		/* Create the relevant entry */
-		entry = almanah_entry_new (&parsed_date);
-
-		/* Set the content on the entry */
-		gtk_text_buffer_set_text (buffer, contents, length);
-		almanah_entry_set_content (entry, buffer);
-		g_free (contents);
-
-		/* Set the entry's last-edited date */
-		g_file_info_get_modification_time (file_info, &modification_time);
-		g_date_set_time_val (&last_edited, &modification_time);
-		almanah_entry_set_last_edited (entry, &last_edited);
-
-		/* Store the entry */
-		status = set_entry (self, entry, display_name, &message);
-		almanah_import_results_dialog_add_result (results_dialog, &parsed_date, status, message);
-		g_free (message);
-
-		g_object_unref (entry);
-		g_object_unref (file_info);
-	}
-
-	/* Check if the loop was broken due to an error */
-	if (child_error != NULL) {
-		g_propagate_error (error, child_error);
-		goto finish;
-	}
-
-	/* Success! */
-	retval = TRUE;
+	AlmanahImportExportDialog *self;
+	GError *error = NULL;
 
-finish:
-	g_object_unref (folder);
-	g_object_unref (enumerator);
-	g_object_unref (buffer);
+	self = ALMANAH_IMPORT_EXPORT_DIALOG (gtk_window_get_transient_for (GTK_WINDOW (results_dialog))); /* set in response_cb() */
 
-	return retval;
-}
+	/* Check for errors (e.g. errors opening databases or files; not errors importing individual entries once we have the content to import) */
+	if (almanah_import_operation_finish (operation, async_result, &error) == FALSE) {
+		/* Show an error */
+		GtkWidget *error_dialog = gtk_message_dialog_new (GTK_WINDOW (self), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR,
+		                                                  GTK_BUTTONS_OK, _("Import failed"));
+		gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (error_dialog), "%s", error->message);
+		gtk_dialog_run (GTK_DIALOG (error_dialog));
+		gtk_widget_destroy (error_dialog);
 
-static gboolean
-export_text_files (AlmanahImportExportDialog *self, GError **error)
-{
-	AlmanahImportExportDialogPrivate *priv = self->priv;
-	AlmanahStorageManagerIter iter;
-	GFile *folder;
-	AlmanahEntry *entry;
-	GtkTextBuffer *buffer;
-	gboolean success = FALSE;
-	GError *child_error = NULL;
-
-	/* Get the output folder */
-	folder = gtk_file_chooser_get_file (priv->file_chooser);
-	g_assert (folder != NULL);
-
-	/* Build a text buffer to use when getting all the entries */
-	buffer = gtk_text_buffer_new (NULL);
-
-	/* Iterate through the entries */
-	almanah_storage_manager_iter_init (&iter);
-	while ((entry = almanah_storage_manager_get_entries (almanah->storage_manager, &iter)) != NULL) {
-		GDate date;
-		gchar *filename, *content;
-		GFile *file;
-		GtkTextIter start_iter, end_iter;
-
-		/* Get the filename */
-		almanah_entry_get_date (entry, &date);
-		filename = g_strdup_printf ("%04u-%02u-%02u", g_date_get_year (&date), g_date_get_month (&date), g_date_get_day (&date));
-		file = g_file_get_child (folder, filename);
-		g_free (filename);
-
-		/* Get the entry contents */
-		if (almanah_entry_get_content (entry, buffer, TRUE, &child_error) == FALSE) {
-			/* Error */
-			g_object_unref (file);
-			g_object_unref (entry);
-			break;
-		}
-
-		g_object_unref (entry);
-
-		gtk_text_buffer_get_bounds (buffer, &start_iter, &end_iter);
-		content = gtk_text_buffer_get_text (buffer, &start_iter, &end_iter, FALSE);
-
-		/* Create the file */
-		if (g_file_replace_contents (file, content, strlen (content), NULL, FALSE,
-		                             G_FILE_CREATE_PRIVATE | G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, &child_error) == FALSE) {
-			/* Error */
-			g_object_unref (file);
-			g_free (content);
-			break;
-		}
-
-		g_object_unref (file);
-		g_free (content);
-	}
+		g_error_free (error);
 
-	/* Check if the loop was broken due to an error */
-	if (child_error != NULL) {
-		g_propagate_error (error, child_error);
-		goto finish;
+		gtk_widget_hide (GTK_WIDGET (self));
+	} else {
+		/* Show the results dialogue */
+		gtk_widget_hide (GTK_WIDGET (self));
+		gtk_widget_show_all (GTK_WIDGET (results_dialog));
+		gtk_dialog_run (GTK_DIALOG (results_dialog));
 	}
 
-	success = TRUE;
-
-finish:
-	g_object_unref (folder);
-	g_object_unref (buffer);
-
-	return success;
+	gtk_widget_destroy (GTK_WIDGET (results_dialog));
 }
 
-static gboolean
-import_database (AlmanahImportExportDialog *self, AlmanahImportResultsDialog *results_dialog, GError **error)
+static void
+export_cb (AlmanahExportOperation *operation, GAsyncResult *async_result, AlmanahImportExportDialog *self)
 {
-	GFile *file;
-	GFileInfo *file_info;
-	gchar *path;
-	const gchar *display_name;
-	GSList *i, *definitions;
-	AlmanahEntry *entry;
-	AlmanahStorageManager *database;
-	AlmanahStorageManagerIter iter;
-	AlmanahImportExportDialogPrivate *priv = self->priv;
-
-	/* Get the database file to import */
-	file = gtk_file_chooser_get_file (priv->file_chooser);
-	g_assert (file != NULL);
-
-	/* Get the display name for use with set_entry(), below */
-	file_info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, G_FILE_QUERY_INFO_NONE, NULL, NULL);
-	display_name = g_file_info_get_display_name (file_info);
-
-	/* Open the database */
-	path = g_file_get_path (file);
-	database = almanah_storage_manager_new (path);
-	g_free (path);
-	g_object_unref (file);
-
-	/* Connect to the database */
-	if (almanah_storage_manager_connect (database, error) == FALSE) {
-		g_object_unref (database);
-		return FALSE;
-	}
-
-	/* Iterate through every entry */
-	almanah_storage_manager_iter_init (&iter);
-	while ((entry = almanah_storage_manager_get_entries (database, &iter)) != NULL) {
-		GDate date;
-		gchar *message = NULL;
-		AlmanahImportStatus status;
+	GError *error = NULL;
 
-		almanah_entry_get_date (entry, &date);
+	/* Check for errors (e.g. errors opening databases or files; not errors importing individual entries once we have the content to import) */
+	if (almanah_export_operation_finish (operation, async_result, &error) == FALSE) {
+		/* Show an error */
+		GtkWidget *error_dialog = gtk_message_dialog_new (GTK_WINDOW (self), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR,
+		                                                  GTK_BUTTONS_OK, _("Export failed"));
+		gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (error_dialog), "%s", error->message);
+		gtk_dialog_run (GTK_DIALOG (error_dialog));
+		gtk_widget_destroy (error_dialog);
 
-		status = set_entry (self, entry, display_name, &message);
-		almanah_import_results_dialog_add_result (results_dialog, &date, status, message);
-		g_free (message);
+		g_error_free (error);
 
-		g_object_unref (entry);
-	}
+		gtk_widget_hide (GTK_WIDGET (self));
+	} else {
+		/* Show a success message */
+		GtkWidget *message_dialog;
 
-	/* Query for every definition */
-	definitions = almanah_storage_manager_get_definitions (database);
-	for (i = definitions; i != NULL; i = i->next) {
-		/* Add the definition to the proper database, ignoring failure */
-		almanah_storage_manager_add_definition (almanah->storage_manager, ALMANAH_DEFINITION (i->data));
-		g_object_unref (i->data);
+		gtk_widget_hide (GTK_WIDGET (self));
+		message_dialog = gtk_message_dialog_new (GTK_WINDOW (self), GTK_DIALOG_MODAL, GTK_MESSAGE_INFO,
+		                                         GTK_BUTTONS_OK, _("Export successful"));
+		gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (message_dialog), _("The diary was successfully exported."));
+		gtk_dialog_run (GTK_DIALOG (message_dialog));
+		gtk_widget_destroy (message_dialog);
 	}
-	g_slist_free (definitions);
-
-	almanah_storage_manager_disconnect (database, NULL);
-	g_object_unref (database);
-	g_object_unref (file_info);
-
-	return TRUE;
-}
-
-static gboolean
-export_database (AlmanahImportExportDialog *self, GError **error)
-{
-	AlmanahImportExportDialogPrivate *priv = self->priv;
-	GFile *source, *destination;
-	gboolean success;
-
-	/* Get the output file */
-	destination = gtk_file_chooser_get_file (priv->file_chooser);
-	g_assert (destination != NULL);
-
-	/* Get the input file (current unencrypted database) */
-	source = g_file_new_for_path (almanah_storage_manager_get_filename (almanah->storage_manager, TRUE));
-
-	/* Copy the current database to that location */
-	success = g_file_copy (source, destination, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, error);
-
-	g_object_unref (source);
-	g_object_unref (destination);
-
-	return success;
 }
 
 static void
 response_cb (GtkDialog *dialog, gint response_id, AlmanahImportExportDialog *self)
 {
-	GError *error = NULL;
 	AlmanahImportExportDialogPrivate *priv = self->priv;
-	AlmanahImportResultsDialog *results_dialog = NULL; /* shut up, gcc */
+	GFile *file;
 
 	/* Just return if the user pressed Cancel */
 	if (response_id != GTK_RESPONSE_OK) {
@@ -593,49 +225,29 @@ response_cb (GtkDialog *dialog, gint response_id, AlmanahImportExportDialog *sel
 		return;
 	}
 
+	file = gtk_file_chooser_get_file (priv->file_chooser);
+	g_assert (file != NULL);
+
 	if (priv->import == TRUE) {
-		/* Import the entries according to the selected method.
-		 * It's OK if we block, since the dialogue should be running in its own main loop. */
-		results_dialog = almanah_import_results_dialog_new ();
-		import_export_modes[priv->current_mode].import_func (self, results_dialog, &error);
+		/* Import the entries according to the selected method.*/
+		AlmanahImportOperation *operation;
+		AlmanahImportResultsDialog *results_dialog = almanah_import_results_dialog_new ();
+		gtk_window_set_transient_for (GTK_WINDOW (results_dialog), GTK_WINDOW (self)); /* this is required in import_cb() */
+
+		operation = almanah_import_operation_new (priv->current_mode, file);
+		almanah_import_operation_run (operation, NULL, (AlmanahImportProgressCallback) import_progress_cb, results_dialog,
+		                              (GAsyncReadyCallback) import_cb, results_dialog);
+		g_object_unref (operation);
 	} else {
 		/* Export the entries according to the selected method. */
-		import_export_modes[priv->current_mode].export_func (self, &error);
-	}
-
-	/* Check for errors (e.g. errors opening databases or files; not errors importing individual entries once we have the content to import) */
-	if (error != NULL) {
-		/* Show an error */
-		GtkWidget *error_dialog = gtk_message_dialog_new (GTK_WINDOW (self), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR,
-		                                                  GTK_BUTTONS_OK, (priv->import == TRUE) ? _("Import failed") : _("Export failed"));
-		gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (error_dialog), "%s", error->message);
-		gtk_dialog_run (GTK_DIALOG (error_dialog));
-		gtk_widget_destroy (error_dialog);
+		AlmanahExportOperation *operation;
 
-		g_error_free (error);
-
-		gtk_widget_hide (GTK_WIDGET (self));
-	} else {
-		if (priv->import == TRUE) {
-			/* Show the results dialogue */
-			gtk_widget_hide (GTK_WIDGET (self));
-			gtk_widget_show_all (GTK_WIDGET (results_dialog));
-			gtk_dialog_run (GTK_DIALOG (results_dialog));
-		} else {
-			/* Show a success message */
-			GtkWidget *message_dialog;
-
-			gtk_widget_hide (GTK_WIDGET (self));
-			message_dialog = gtk_message_dialog_new (GTK_WINDOW (self), GTK_DIALOG_MODAL, GTK_MESSAGE_INFO,
-			                                         GTK_BUTTONS_OK, _("Export successful"));
-			gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (message_dialog), _("The diary was successfully exported."));
-			gtk_dialog_run (GTK_DIALOG (message_dialog));
-			gtk_widget_destroy (message_dialog);
-		}
+		operation = almanah_export_operation_new (priv->current_mode, file);
+		almanah_export_operation_run (operation, NULL, (GAsyncReadyCallback) export_cb, self);
+		g_object_unref (operation);
 	}
 
-	if (priv->import == TRUE)
-		gtk_widget_destroy (GTK_WIDGET (results_dialog));
+	g_object_unref (file);
 }
 
 void
@@ -643,20 +255,32 @@ ied_mode_combo_box_changed_cb (GtkComboBox *combo_box, AlmanahImportExportDialog
 {
 	AlmanahImportExportDialogPrivate *priv = self->priv;
 	gint new_mode;
+	GtkTreeIter iter;
+	GtkTreeModel *model;
+	gchar *description, *title;
+	GtkFileChooserAction action;
 
 	new_mode = gtk_combo_box_get_active (combo_box);
-	if (new_mode == -1 || new_mode == (gint) priv->current_mode)
+	if (new_mode == -1 || new_mode == priv->current_mode)
 		return;
 
 	priv->current_mode = new_mode;
 
 	/* Change the dialogue */
-	gtk_file_chooser_set_action (priv->file_chooser, import_export_modes[priv->current_mode].action);
-	gtk_file_chooser_button_set_title (GTK_FILE_CHOOSER_BUTTON (priv->file_chooser),
-	                                   _(import_export_modes[priv->current_mode].file_chooser_title));
-	gtk_label_set_text_with_mnemonic (priv->description_label,
-	                                  (priv->import == TRUE) ? _(import_export_modes[priv->current_mode].import_description)
-	                                                         : _(import_export_modes[priv->current_mode].export_description));
+	gtk_combo_box_get_active_iter (combo_box, &iter);
+	model = gtk_combo_box_get_model (combo_box);
+	gtk_tree_model_get (model, &iter,
+	                    2, &description,
+	                    3, &action,
+	                    4, &title,
+	                    -1);
+
+	gtk_file_chooser_set_action (priv->file_chooser, action);
+	gtk_file_chooser_button_set_title (GTK_FILE_CHOOSER_BUTTON (priv->file_chooser), title);
+	gtk_label_set_text_with_mnemonic (priv->description_label, description);
+
+	g_free (title);
+	g_free (description);
 }
 
 void
@@ -803,7 +427,7 @@ results_selection_changed_cb (GtkTreeSelection *tree_selection, GtkWidget *butto
 }
 
 void
-almanah_import_results_dialog_add_result (AlmanahImportResultsDialog *self, GDate *date, AlmanahImportStatus status, const gchar *message)
+almanah_import_results_dialog_add_result (AlmanahImportResultsDialog *self, const GDate *date, AlmanahImportStatus status, const gchar *message)
 {
 	GtkTreeIter iter;
 	gchar formatted_date[100];
diff --git a/src/import-export-dialog.h b/src/import-export-dialog.h
index c7c5666..7cf4203 100644
--- a/src/import-export-dialog.h
+++ b/src/import-export-dialog.h
@@ -24,14 +24,9 @@
 #include <glib-object.h>
 #include <gtk/gtk.h>
 
-G_BEGIN_DECLS
+#include "import-operation.h"
 
-/* These must be kept in synchrony with the rows in almanah_ird_view_store in almanah.ui */
-typedef enum {
-	ALMANAH_IMPORT_STATUS_IMPORTED = 0,
-	ALMANAH_IMPORT_STATUS_MERGED = 1,
-	ALMANAH_IMPORT_STATUS_FAILED = 2
-} AlmanahImportStatus;
+G_BEGIN_DECLS
 
 #define ALMANAH_TYPE_IMPORT_EXPORT_DIALOG		(almanah_import_export_dialog_get_type ())
 #define ALMANAH_IMPORT_EXPORT_DIALOG(o)			(G_TYPE_CHECK_INSTANCE_CAST ((o), ALMANAH_TYPE_IMPORT_EXPORT_DIALOG, AlmanahImportExportDialog))
@@ -76,7 +71,7 @@ typedef struct {
 GType almanah_import_results_dialog_get_type (void) G_GNUC_CONST;
 
 AlmanahImportResultsDialog *almanah_import_results_dialog_new (void) G_GNUC_WARN_UNUSED_RESULT;
-void almanah_import_results_dialog_add_result (AlmanahImportResultsDialog *self, GDate *date, AlmanahImportStatus status, const gchar *message);
+void almanah_import_results_dialog_add_result (AlmanahImportResultsDialog *self, const GDate *date, AlmanahImportStatus status, const gchar *message);
 
 G_END_DECLS
 
diff --git a/src/import-operation.c b/src/import-operation.c
new file mode 100644
index 0000000..87d9b5a
--- /dev/null
+++ b/src/import-operation.c
@@ -0,0 +1,510 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * Almanah
+ * Copyright (C) Philip Withnall 2010 <philip tecnocode co uk>
+ *
+ * Almanah is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Almanah is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Almanah.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "import-operation.h"
+#include "entry.h"
+#include "storage-manager.h"
+#include "main.h"
+
+typedef gboolean (*ImportFunc) (AlmanahImportOperation *self, GFile *source, AlmanahImportProgressCallback callback, gpointer user_data,
+                                GCancellable *cancellable, GError **error);
+
+typedef struct {
+	const gchar *name; /* translatable */
+	const gchar *description; /* translatable */
+	GtkFileChooserAction action;
+	const gchar *file_chooser_title; /* translatable */
+	ImportFunc import_func;
+} ImportModeDetails;
+
+static gboolean import_text_files (AlmanahImportOperation *self, GFile *source, AlmanahImportProgressCallback callback, gpointer user_data,
+                                   GCancellable *cancellable, GError **error);
+static gboolean import_database (AlmanahImportOperation *self, GFile *source, AlmanahImportProgressCallback callback, gpointer user_data,
+                                 GCancellable *cancellable, GError **error);
+
+static const ImportModeDetails import_modes[] = {
+	{ N_("Text Files"),
+	  N_("Select a _folder containing text files, one per entry, with names in the format 'yyyy-mm-dd', and no extension. "
+	     "Any and all such files will be imported."),
+	  GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
+	  N_("Select a Folder"),
+	  import_text_files },
+	{ N_("Database"),
+	  N_("Select a database _file created by Almanah Diary to import."),
+	  GTK_FILE_CHOOSER_ACTION_OPEN,
+	  N_("Select a File"),
+	  import_database }
+};
+
+static void almanah_import_operation_dispose (GObject *object);
+
+struct _AlmanahImportOperationPrivate {
+	gint current_mode; /* index into import_modes */
+	GFile *source;
+};
+
+G_DEFINE_TYPE (AlmanahImportOperation, almanah_import_operation, G_TYPE_OBJECT)
+#define ALMANAH_IMPORT_OPERATION_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), ALMANAH_TYPE_IMPORT_OPERATION, AlmanahImportOperationPrivate))
+
+static void
+almanah_import_operation_class_init (AlmanahImportOperationClass *klass)
+{
+	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+	g_type_class_add_private (klass, sizeof (AlmanahImportOperationPrivate));
+
+	gobject_class->dispose = almanah_import_operation_dispose;
+}
+
+static void
+almanah_import_operation_init (AlmanahImportOperation *self)
+{
+	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, ALMANAH_TYPE_IMPORT_OPERATION, AlmanahImportOperationPrivate);
+	self->priv->current_mode = -1; /* no mode selected */
+}
+
+static void
+almanah_import_operation_dispose (GObject *object)
+{
+	AlmanahImportOperationPrivate *priv = ALMANAH_IMPORT_OPERATION (object)->priv;
+
+	if (priv->source != NULL)
+		g_object_unref (priv->source);
+	priv->source = NULL;
+
+	/* Chain up to the parent class */
+	G_OBJECT_CLASS (almanah_import_operation_parent_class)->dispose (object);
+}
+
+AlmanahImportOperation *
+almanah_import_operation_new (AlmanahImportOperationType type_id, GFile *source)
+{
+	AlmanahImportOperation *import_operation = g_object_new (ALMANAH_TYPE_IMPORT_OPERATION, NULL);
+	import_operation->priv->current_mode = type_id;
+	import_operation->priv->source = g_object_ref (source);
+
+	return import_operation;
+}
+
+typedef struct {
+	AlmanahImportProgressCallback callback;
+	gpointer user_data;
+	GDate *date;
+	AlmanahImportStatus status;
+	gchar *message;
+} ProgressData;
+
+static gboolean
+progress_idle_callback_cb (ProgressData *data)
+{
+	g_assert (data->callback != NULL);
+	data->callback (data->date, data->status, data->message, data->user_data);
+
+	/* Free the data */
+	g_free (data->message);
+	g_date_free (data->date);
+	g_slice_free (ProgressData, data);
+
+	return FALSE;
+}
+
+static void
+progress_idle_callback (AlmanahImportProgressCallback callback, gpointer user_data, const GDate *date, AlmanahImportStatus status,
+                        const gchar *message)
+{
+	ProgressData *data = g_slice_new (ProgressData);
+	data->callback = callback;
+	data->user_data = user_data;
+	data->date = g_date_new_dmy (g_date_get_day (date), g_date_get_month (date), g_date_get_year (date));
+	data->status = status;
+	data->message = g_strdup (message);
+	
+	g_idle_add ((GSourceFunc) progress_idle_callback_cb, data);
+}
+
+/**
+ * set_entry:
+ * @self: an #AlmanahImportOperation
+ * @imported_entry: an #AlmanahEntry created for an entry being imported
+ * @import_source: a string representing the source of the imported entry
+ * @message: return location for an error or informational message, or %NULL; free with g_free()
+ *
+ * Stores the given entry in the database, merging it with any existing one where appropriate.
+ *
+ * @import_source should represent where the imported entry came from, so it could be the filename of a text file which held the entry, or of
+ * the entry's previous database.
+ *
+ * If @message is non-%NULL, error or informational messages can be allocated and returned in it. They will be returned irrespective of the return
+ * value of the function, as some errors can be overcome to result in a successful import. The message should be freed with g_free() by the caller.
+ *
+ * Return value: %ALMANAH_IMPORT_STATUS_MERGED if the entry was merged with an existing one, %ALMANAH_IMPORT_STATUS_IMPORTED if it was imported
+ * without merging, %ALMANAH_IMPORT_STATUS_FAILED otherwise
+ **/
+static AlmanahImportStatus
+set_entry (AlmanahImportOperation *self, AlmanahEntry *imported_entry, const gchar *import_source, gchar **message)
+{
+	GDate entry_date, existing_last_edited, imported_last_edited;
+	AlmanahEntry *existing_entry;
+	GtkTextBuffer *existing_buffer, *imported_buffer;
+	GtkTextIter existing_start, existing_end, imported_start, imported_end;
+	gchar *header_string;
+	GError *error = NULL;
+
+	/* Check to see if there's a conflict first */
+	almanah_entry_get_date (imported_entry, &entry_date);
+	existing_entry = almanah_storage_manager_get_entry (almanah->storage_manager, &entry_date);
+
+	if (existing_entry == NULL) {
+		/* Add the entry to the proper database and return, ignoring failure */
+		almanah_storage_manager_set_entry (almanah->storage_manager, imported_entry);
+
+		return ALMANAH_IMPORT_STATUS_IMPORTED;
+	}
+
+	/* Load both entries into buffers */
+	imported_buffer = gtk_text_buffer_new (NULL);
+	if (almanah_entry_get_content (imported_entry, imported_buffer, TRUE, &error) == FALSE) {
+		if (message != NULL)
+			*message = g_strdup_printf (_("Error deserializing imported entry into buffer: %s"), (error != NULL) ? error->message : NULL);
+
+		g_error_free (error);
+		g_object_unref (imported_buffer);
+		g_object_unref (existing_entry);
+
+		return ALMANAH_IMPORT_STATUS_FAILED;
+	}
+
+	/* The two buffers have to use the same tag table for gtk_text_buffer_insert_range() to work */
+	existing_buffer = gtk_text_buffer_new (gtk_text_buffer_get_tag_table (imported_buffer));
+	if (almanah_entry_get_content (existing_entry, existing_buffer, FALSE, NULL) == FALSE) {
+		/* Deserialising the existing entry failed; use the imported entry instead */
+		almanah_storage_manager_set_entry (almanah->storage_manager, imported_entry);
+
+		if (message != NULL) {
+			*message = g_strdup_printf (_("Error deserializing existing entry into buffer; overwriting with imported entry: %s"),
+			                            (error != NULL) ? error->message : NULL);
+		}
+
+		g_error_free (error);
+		g_object_unref (imported_buffer);
+		g_object_unref (existing_buffer);
+		g_object_unref (existing_entry);
+
+		return ALMANAH_IMPORT_STATUS_IMPORTED;
+	}
+
+	/* Get the bounds for later use */
+	gtk_text_buffer_get_bounds (existing_buffer, &existing_start, &existing_end);
+	gtk_text_buffer_get_bounds (imported_buffer, &imported_start, &imported_end);
+
+	/* Compare the two buffers --- if they're identical, leave the current entry alone and mark the entries as merged.
+	 * Compare the character counts first so that the comparison is less expensive in the case they aren't identical .*/
+	if (gtk_text_buffer_get_char_count (existing_buffer) == gtk_text_buffer_get_char_count (imported_buffer)) {
+		gchar *existing_text, *imported_text;
+
+		existing_text = gtk_text_buffer_get_text (existing_buffer, &existing_start, &existing_end, FALSE);
+		imported_text = gtk_text_buffer_get_text (imported_buffer, &imported_start, &imported_end, FALSE);
+
+		/* If they're the same, no modifications are required */
+		if (strcmp (existing_text, imported_text) == 0) {
+			g_free (existing_text);
+			g_free (imported_text);
+			g_object_unref (imported_buffer);
+			g_object_unref (existing_buffer);
+			g_object_unref (existing_entry);
+			return ALMANAH_IMPORT_STATUS_MERGED;
+		}
+
+		g_free (existing_text);
+		g_free (imported_text);
+	}
+
+	/* Append some header text for the imported entry */
+	/* Translators: This text is appended to an existing entry when an entry is being imported to the same date.
+	 * The imported entry is appended to this text. */
+	header_string = g_strdup_printf (_("\n\nEntry imported from \"%s\":\n\n"), import_source);
+	gtk_text_buffer_insert (existing_buffer, &existing_end, header_string, -1);
+	g_free (header_string);
+
+	/* Append the imported entry to the end of the existing one */
+	gtk_text_buffer_insert_range (existing_buffer, &existing_end, &imported_start, &imported_end);
+	g_object_unref (imported_buffer);
+
+	/* Store the buffer back in the existing entry and save the entry */
+	almanah_entry_set_content (existing_entry, existing_buffer);
+	g_object_unref (existing_buffer);
+
+	/* Update the last-edited time for the merged entry to be the more recent of the last-edited times for the existing and imported entries */
+	almanah_entry_get_last_edited (existing_entry, &existing_last_edited);
+	almanah_entry_get_last_edited (imported_entry, &imported_last_edited);
+
+	if (g_date_valid (&existing_last_edited) == FALSE || g_date_compare (&existing_last_edited, &imported_last_edited) < 0)
+		almanah_entry_set_last_edited (existing_entry, &imported_last_edited);
+
+	almanah_storage_manager_set_entry (almanah->storage_manager, existing_entry);
+	g_object_unref (existing_entry);
+
+	return ALMANAH_IMPORT_STATUS_MERGED;
+}
+
+static gboolean
+import_text_files (AlmanahImportOperation *self, GFile *source, AlmanahImportProgressCallback progress_callback, gpointer progress_user_data,
+                   GCancellable *cancellable, GError **error)
+{
+	gboolean retval = FALSE;
+	GFileInfo *file_info;
+	GFileEnumerator *enumerator;
+	GtkTextBuffer *buffer;
+	GError *child_error = NULL;
+
+	enumerator = g_file_enumerate_children (source, "standard::name,standard::display-name,standard::is-hidden,time::modified",
+	                                        G_FILE_QUERY_INFO_NONE, NULL, error);
+	if (enumerator == NULL)
+		return FALSE;
+
+	/* Build a text buffer to use when setting all the entries */
+	buffer = gtk_text_buffer_new (NULL);
+
+	/* Enumerate all the children of the folder */
+	while ((file_info = g_file_enumerator_next_file (enumerator, NULL, &child_error)) != NULL) {
+		AlmanahEntry *entry;
+		GDate parsed_date, last_edited;
+		GTimeVal modification_time;
+		GFile *file;
+		gchar *contents, *message = NULL;
+		gsize length;
+		AlmanahImportStatus status;
+		const gchar *file_name = g_file_info_get_name (file_info);
+		const gchar *display_name = g_file_info_get_display_name (file_info);
+
+		/* Skip the file if it's hidden */
+		if (g_file_info_get_is_hidden (file_info) == TRUE || file_name[strlen (file_name) - 1] == '~') {
+			g_object_unref (file_info);
+			continue;
+		}
+
+		/* Heuristically parse the date, though we recommend using the format: yyyy-mm-dd */
+		g_date_set_parse (&parsed_date, file_name);
+		if (g_date_valid (&parsed_date) == FALSE) {
+			g_object_unref (file_info);
+			continue;
+		}
+
+		/* Get the file */
+		file = g_file_get_child (source, file_name);
+		g_assert (file != NULL);
+
+		/* Load the content */
+		if (g_file_load_contents (file, NULL, &contents, &length, NULL, &child_error) == FALSE) {
+			g_object_unref (file);
+			g_object_unref (file_info);
+			break; /* let the error get handled by the code just after the loop */
+		}
+		g_object_unref (file);
+
+		/* Create the relevant entry */
+		entry = almanah_entry_new (&parsed_date);
+
+		/* Set the content on the entry */
+		gtk_text_buffer_set_text (buffer, contents, length);
+		almanah_entry_set_content (entry, buffer);
+		g_free (contents);
+
+		/* Set the entry's last-edited date */
+		g_file_info_get_modification_time (file_info, &modification_time);
+		g_date_set_time_val (&last_edited, &modification_time);
+		almanah_entry_set_last_edited (entry, &last_edited);
+
+		/* Store the entry */
+		status = set_entry (self, entry, display_name, &message);
+		progress_idle_callback (progress_callback, progress_user_data, &parsed_date, status, message);
+		g_free (message);
+
+		g_object_unref (entry);
+		g_object_unref (file_info);
+	}
+
+	/* Check if the loop was broken due to an error */
+	if (child_error != NULL) {
+		g_propagate_error (error, child_error);
+		goto finish;
+	}
+
+	/* Success! */
+	retval = TRUE;
+
+finish:
+	g_object_unref (enumerator);
+	g_object_unref (buffer);
+
+	return retval;
+}
+
+static gboolean
+import_database (AlmanahImportOperation *self, GFile *source, AlmanahImportProgressCallback progress_callback, gpointer progress_user_data,
+                 GCancellable *cancellable, GError **error)
+{
+	GFileInfo *file_info;
+	gchar *path;
+	const gchar *display_name;
+	GSList *i, *definitions;
+	AlmanahEntry *entry;
+	AlmanahStorageManager *database;
+	AlmanahStorageManagerIter iter;
+
+	/* Get the display name for use with set_entry(), below */
+	file_info = g_file_query_info (source, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, G_FILE_QUERY_INFO_NONE, NULL, NULL);
+	display_name = g_file_info_get_display_name (file_info);
+
+	/* Open the database */
+	path = g_file_get_path (source);
+	database = almanah_storage_manager_new (path);
+	g_free (path);
+
+	/* Connect to the database */
+	if (almanah_storage_manager_connect (database, error) == FALSE) {
+		g_object_unref (database);
+		return FALSE;
+	}
+
+	/* Iterate through every entry */
+	almanah_storage_manager_iter_init (&iter);
+	while ((entry = almanah_storage_manager_get_entries (database, &iter)) != NULL) {
+		GDate date;
+		gchar *message = NULL;
+		AlmanahImportStatus status;
+
+		almanah_entry_get_date (entry, &date);
+
+		status = set_entry (self, entry, display_name, &message);
+		progress_idle_callback (progress_callback, progress_user_data, &date, status, message);
+		g_free (message);
+
+		g_object_unref (entry);
+	}
+
+	/* Query for every definition */
+	definitions = almanah_storage_manager_get_definitions (database);
+	for (i = definitions; i != NULL; i = i->next) {
+		/* Add the definition to the proper database, ignoring failure */
+		almanah_storage_manager_add_definition (almanah->storage_manager, ALMANAH_DEFINITION (i->data));
+		g_object_unref (i->data);
+	}
+	g_slist_free (definitions);
+
+	almanah_storage_manager_disconnect (database, NULL);
+	g_object_unref (database);
+	g_object_unref (file_info);
+
+	return TRUE;
+}
+
+typedef struct {
+	AlmanahImportProgressCallback progress_callback;
+	gpointer progress_user_data;
+} ImportData;
+
+static void
+import_data_free (ImportData *data)
+{
+	g_slice_free (ImportData, data);
+}
+
+static void
+import_thread (GSimpleAsyncResult *result, AlmanahImportOperation *operation, GCancellable *cancellable)
+{
+	GError *error = NULL;
+	ImportData *data = g_simple_async_result_get_op_res_gpointer (result);
+
+	/* Check to see if the operation's been cancelled already */
+	if (g_cancellable_set_error_if_cancelled (cancellable, &error) == TRUE) {
+		g_simple_async_result_set_from_error (result, error);
+		g_error_free (error);
+		return;
+	}
+
+	/* Import and return */
+	if (import_modes[operation->priv->current_mode].import_func (operation, operation->priv->source, data->progress_callback,
+	    data->progress_user_data, cancellable, &error) == FALSE) {
+		g_simple_async_result_set_from_error (result, error);
+		g_error_free (error);
+	}
+}
+
+void
+almanah_import_operation_run (AlmanahImportOperation *self, GCancellable *cancellable, AlmanahImportProgressCallback progress_callback,
+                              gpointer progress_user_data, GAsyncReadyCallback callback, gpointer user_data)
+{
+	GSimpleAsyncResult *result;
+	ImportData *data;
+
+	g_return_if_fail (ALMANAH_IS_IMPORT_OPERATION (self));
+	g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+	data = g_slice_new (ImportData);
+	data->progress_callback = progress_callback;
+	data->progress_user_data = progress_user_data;
+
+	result = g_simple_async_result_new (G_OBJECT (self), callback, user_data, almanah_import_operation_run);
+	g_simple_async_result_set_op_res_gpointer (result, data, (GDestroyNotify) import_data_free);
+	g_simple_async_result_run_in_thread (result, (GSimpleAsyncThreadFunc) import_thread, G_PRIORITY_DEFAULT, cancellable);
+	g_object_unref (result);
+}
+
+gboolean
+almanah_import_operation_finish (AlmanahImportOperation *self, GAsyncResult *async_result, GError **error)
+{
+	GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT (async_result);
+
+	g_return_val_if_fail (ALMANAH_IS_IMPORT_OPERATION (self), FALSE);
+	g_return_val_if_fail (G_IS_ASYNC_RESULT (async_result), FALSE);
+	g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+	g_warn_if_fail (g_simple_async_result_get_source_tag (result) == almanah_import_operation_run);
+
+	if (g_simple_async_result_propagate_error (result, error) == TRUE)
+		return FALSE;
+
+	return TRUE;
+}
+
+void
+almanah_import_operation_populate_model (GtkListStore *store, guint type_id_column, guint name_column, guint description_column, guint action_column,
+                                         guint file_chooser_title_column)
+{
+	guint i;
+
+	for (i = 0; i < G_N_ELEMENTS (import_modes); i++) {
+		GtkTreeIter iter;
+
+		gtk_list_store_append (store, &iter);
+		gtk_list_store_set (store, &iter,
+		                    type_id_column, i,
+		                    name_column, _(import_modes[i].name),
+		                    description_column, _(import_modes[i].description),
+		                    action_column, import_modes[i].action,
+		                    file_chooser_title_column, _(import_modes[i].file_chooser_title),
+		                    -1);
+	}
+}
diff --git a/src/import-operation.h b/src/import-operation.h
new file mode 100644
index 0000000..72c2c80
--- /dev/null
+++ b/src/import-operation.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * Almanah
+ * Copyright (C) Philip Withnall 2010 <philip tecnocode co uk>
+ *
+ * Almanah is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Almanah is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Almanah.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ALMANAH_IMPORT_OPERATION_H
+#define ALMANAH_IMPORT_OPERATION_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+/* TODO: These must be kept in synchrony with the rows in almanah_ird_view_store in almanah.ui */
+typedef enum {
+	ALMANAH_IMPORT_STATUS_IMPORTED = 0,
+	ALMANAH_IMPORT_STATUS_MERGED = 1,
+	ALMANAH_IMPORT_STATUS_FAILED = 2
+} AlmanahImportStatus;
+
+typedef guint AlmanahImportOperationType;
+
+typedef void (*AlmanahImportProgressCallback) (const GDate *date, AlmanahImportStatus status, const gchar *message, gpointer user_data);
+
+#define ALMANAH_TYPE_IMPORT_OPERATION		(almanah_import_operation_get_type ())
+#define ALMANAH_IMPORT_OPERATION(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), ALMANAH_TYPE_IMPORT_OPERATION, AlmanahImportOperation))
+#define ALMANAH_IMPORT_OPERATION_CLASS(k)	(G_TYPE_CHECK_CLASS_CAST((k), ALMANAH_TYPE_IMPORT_OPERATION, AlmanahImportOperationClass))
+#define ALMANAH_IS_IMPORT_OPERATION(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), ALMANAH_TYPE_IMPORT_OPERATION))
+#define ALMANAH_IS_IMPORT_OPERATION_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), ALMANAH_TYPE_IMPORT_OPERATION))
+#define ALMANAH_IMPORT_OPERATION_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), ALMANAH_TYPE_IMPORT_OPERATION, AlmanahImportOperationClass))
+
+typedef struct _AlmanahImportOperationPrivate	AlmanahImportOperationPrivate;
+
+typedef struct {
+	GObject parent;
+	AlmanahImportOperationPrivate *priv;
+} AlmanahImportOperation;
+
+typedef struct {
+	GObjectClass parent;
+} AlmanahImportOperationClass;
+
+GType almanah_import_operation_get_type (void) G_GNUC_CONST;
+
+AlmanahImportOperation *almanah_import_operation_new (AlmanahImportOperationType type_id, GFile *source) G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+
+void almanah_import_operation_run (AlmanahImportOperation *self, GCancellable *cancellable,
+                                   AlmanahImportProgressCallback progress_callback, gpointer progress_user_data,
+                                   GAsyncReadyCallback callback, gpointer user_data);
+gboolean almanah_import_operation_finish (AlmanahImportOperation *self, GAsyncResult *async_result, GError **error);
+
+void almanah_import_operation_populate_model (GtkListStore *list_store, guint type_id_column, guint name_column, guint description_column,
+                                              guint action_column, guint file_chooser_title_column);
+
+G_END_DECLS
+
+#endif /* !ALMANAH_IMPORT_OPERATION_H */
diff --git a/src/main.c b/src/main.c
index b208397..77001fa 100644
--- a/src/main.c
+++ b/src/main.c
@@ -111,6 +111,7 @@ main (int argc, char *argv[])
 	textdomain (GETTEXT_PACKAGE);
 #endif
 
+	g_thread_init (NULL);
 	gtk_set_locale ();
 	gtk_init (&argc, &argv);
 	g_set_application_name (_("Almanah Diary"));



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]