aboutsummaryrefslogblamecommitdiffstats
path: root/modules/composer-autosave/e-autosave-utils.c
blob: 9c5eaa823672e053b4cd1eb715ae101020ea22c2 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

















                                                                             



                    




















                                                              
                                     
















                                                      


                                                        





















































































                                                                           

                                                                          


















                                                                            
                                                                          































                                                                      


                                                         
 

                                  






                                                                     

                                                           




                                                                     













                                                                          

                                                                        
                                      
 












                                                             
                                                     






                                                              






































                                                                              




























































































































































































































                                                                           
/*
 * e-autosave-utils.c
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) version 3.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with the program; if not, see <http://www.gnu.org/licenses/>
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "e-autosave-utils.h"

#include <errno.h>
#include <glib/gstdio.h>
#include <camel/camel.h>

#include <e-util/e-util.h>

#define SNAPSHOT_FILE_KEY   "e-composer-snapshot-file"
#define SNAPSHOT_FILE_PREFIX    ".evolution-composer.autosave"
#define SNAPSHOT_FILE_SEED  SNAPSHOT_FILE_PREFIX "-XXXXXX"

typedef struct _LoadContext LoadContext;
typedef struct _SaveContext SaveContext;

struct _LoadContext {
    EMsgComposer *composer;
};

struct _SaveContext {
    GCancellable *cancellable;
    GOutputStream *output_stream;
};

static void
load_context_free (LoadContext *context)
{
    if (context->composer != NULL)
        g_object_unref (context->composer);

    g_slice_free (LoadContext, context);
}

static void
save_context_free (SaveContext *context)
{
    if (context->cancellable != NULL)
        g_object_unref (context->cancellable);

    if (context->output_stream != NULL)
        g_object_unref (context->output_stream);

    g_slice_free (SaveContext, context);
}

static void
delete_snapshot_file (GFile *snapshot_file)
{
    g_file_delete (snapshot_file, NULL, NULL);
    g_object_unref (snapshot_file);
}

static GFile *
create_snapshot_file (EMsgComposer *composer,
                      GError **error)
{
    GFile *snapshot_file;
    const gchar *user_data_dir;
    gchar *path;
    gint fd;

    snapshot_file = e_composer_get_snapshot_file (composer);

    if (G_IS_FILE (snapshot_file))
        return snapshot_file;

    user_data_dir = e_get_user_data_dir ();
    path = g_build_filename (user_data_dir, SNAPSHOT_FILE_SEED, NULL);

    /* g_mkstemp() modifies the XXXXXX part of the
     * template string to form the actual filename. */
    errno = 0;
    fd = g_mkstemp (path);
    if (fd == -1) {
        g_set_error (
            error, G_FILE_ERROR,
            g_file_error_from_errno (errno),
            "%s", g_strerror (errno));
        g_free (path);
        return FALSE;
    }

    close (fd);

    snapshot_file = g_file_new_for_path (path);

    /* Save the GFile for subsequent snapshots. */
    g_object_set_data_full (
        G_OBJECT (composer),
        SNAPSHOT_FILE_KEY, snapshot_file,
        (GDestroyNotify) delete_snapshot_file);

    return snapshot_file;
}

static void
load_snapshot_loaded_cb (GFile *snapshot_file,
                         GAsyncResult *result,
                         GSimpleAsyncResult *simple)
{
    EShell *shell;
    GObject *object;
    LoadContext *context;
    EMsgComposer *composer;
    CamelMimeMessage *message;
    CamelStream *camel_stream;
    gchar *contents = NULL;
    gsize length;
    GError *error = NULL;

    context = g_simple_async_result_get_op_res_gpointer (simple);

    g_file_load_contents_finish (
        snapshot_file, result, &contents, &length, NULL, &error);

    if (error != NULL) {
        g_warn_if_fail (contents == NULL);
        g_simple_async_result_set_from_error (simple, error);
        g_simple_async_result_complete (simple);
        g_error_free (error);
        return;
    }

    /* Create an in-memory buffer for the MIME parser to read from.
     * We have to do this because CamelStreams are syncrhonous-only,
     * and feeding the parser a direct file stream would block. */
    message = camel_mime_message_new ();
    camel_stream = camel_stream_mem_new_with_buffer (contents, length);
    camel_data_wrapper_construct_from_stream_sync (
        CAMEL_DATA_WRAPPER (message), camel_stream, NULL, &error);
    g_object_unref (camel_stream);
    g_free (contents);

    if (error != NULL) {
        g_simple_async_result_set_from_error (simple, error);
        g_simple_async_result_complete (simple);
        g_object_unref (message);
        g_error_free (error);
        return;
    }

    /* g_async_result_get_source_object() returns a new reference. */
    object = g_async_result_get_source_object (G_ASYNC_RESULT (simple));

    /* Create a new composer window from the loaded message and
     * restore its snapshot file so it continues auto-saving to
     * the same file. */
    shell = E_SHELL (object);
    g_object_ref (snapshot_file);
    composer = e_msg_composer_new_with_message (shell, message, NULL);
    g_object_set_data_full (
        G_OBJECT (composer),
        SNAPSHOT_FILE_KEY, snapshot_file,
        (GDestroyNotify) delete_snapshot_file);
    context->composer = g_object_ref_sink (composer);
    g_object_unref (message);

    g_object_unref (object);

    g_simple_async_result_complete (simple);
    g_object_unref (simple);
}

static void
save_snapshot_splice_cb (GOutputStream *output_stream,
                         GAsyncResult *result,
                         GSimpleAsyncResult *simple)
{
    GError *error = NULL;

    g_output_stream_splice_finish (output_stream, result, &error);

    if (error != NULL) {
        g_simple_async_result_set_from_error (simple, error);
        g_error_free (error);
    }

    g_simple_async_result_complete (simple);
    g_object_unref (simple);
}

static void
save_snapshot_get_message_cb (EMsgComposer *composer,
                              GAsyncResult *result,
                              GSimpleAsyncResult *simple)
{
    SaveContext *context;
    CamelMimeMessage *message;
    GInputStream *input_stream;
    CamelStream *camel_stream;
    GByteArray *buffer;
    GError *error = NULL;

    context = g_simple_async_result_get_op_res_gpointer (simple);

    message = e_msg_composer_get_message_draft_finish (
        composer, result, &error);

    if (error != NULL) {
        g_warn_if_fail (message == NULL);
        g_simple_async_result_set_from_error (simple, error);
        g_simple_async_result_complete (simple);
        g_object_unref (simple);
        g_error_free (error);
        return;
    }

    g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message));

    /* Decode the message to an in-memory buffer.  We have to do this
     * because CamelStreams are synchronous-only, and using threads is
     * dangerous because CamelDataWrapper is not reentrant. */
    buffer = g_byte_array_new ();
    camel_stream = camel_stream_mem_new ();
    camel_stream_mem_set_byte_array (
        CAMEL_STREAM_MEM (camel_stream), buffer);
    camel_data_wrapper_decode_to_stream_sync (
        CAMEL_DATA_WRAPPER (message), camel_stream, NULL, NULL);
    g_object_unref (camel_stream);

    g_object_unref (message);

    /* Load the buffer into a GMemoryInputStream. */
    input_stream = g_memory_input_stream_new ();
    if (buffer->len > 0)
        g_memory_input_stream_add_data (
            G_MEMORY_INPUT_STREAM (input_stream),
            buffer->data, (gssize) buffer->len,
            (GDestroyNotify) g_free);
    g_byte_array_free (buffer, FALSE);

    /* Splice the input and output streams. */
    g_output_stream_splice_async (
        context->output_stream, input_stream,
        G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE |
        G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
        G_PRIORITY_DEFAULT, context->cancellable,
        (GAsyncReadyCallback) save_snapshot_splice_cb,
        simple);

    g_object_unref (input_stream);
}

static void
save_snapshot_replace_cb (GFile *snapshot_file,
                          GAsyncResult *result,
                          GSimpleAsyncResult *simple)
{
    GObject *object;
    SaveContext *context;
    GFileOutputStream *output_stream;
    GError *error = NULL;

    context = g_simple_async_result_get_op_res_gpointer (simple);

    /* Output stream might be NULL, so don't use cast macro. */
    output_stream = g_file_replace_finish (snapshot_file, result, &error);
    context->output_stream = (GOutputStream *) output_stream;

    if (error != NULL) {
        g_warn_if_fail (output_stream == NULL);
        g_simple_async_result_set_from_error (simple, error);
        g_simple_async_result_complete (simple);
        g_object_unref (simple);
        g_error_free (error);
        return;
    }

    g_return_if_fail (G_IS_OUTPUT_STREAM (output_stream));

    /* g_async_result_get_source_object() returns a new reference. */
    object = g_async_result_get_source_object (G_ASYNC_RESULT (simple));

    /* Extract a MIME message from the composer. */
    e_msg_composer_get_message_draft (
        E_MSG_COMPOSER (object), G_PRIORITY_DEFAULT,
        context->cancellable, (GAsyncReadyCallback)
        save_snapshot_get_message_cb, simple);

    g_object_unref (object);
}

static EMsgComposer *
composer_registry_lookup (GQueue *registry,
                          const gchar *basename)
{
    GList *iter;

    /* Find the composer with the given snapshot filename. */
    for (iter = registry->head; iter != NULL; iter = iter->next) {
        EMsgComposer *composer;
        GFile *snapshot_file;
        gchar *snapshot_name;

        composer = E_MSG_COMPOSER (iter->data);
        snapshot_file = e_composer_get_snapshot_file (composer);

        if (!G_IS_FILE (snapshot_file))
            continue;

        snapshot_name = g_file_get_basename (snapshot_file);
        if (g_strcmp0 (basename, snapshot_name) == 0) {
            g_free (snapshot_name);
            return composer;
        }

        g_free (snapshot_name);
    }

    return NULL;
}

GList *
e_composer_find_orphans (GQueue *registry,
                         GError **error)
{
    GDir *dir;
    const gchar *dirname;
    const gchar *basename;
    GList *orphans = NULL;

    g_return_val_if_fail (registry != NULL, NULL);

    dirname = e_get_user_data_dir ();
    dir = g_dir_open (dirname, 0, error);
    if (dir == NULL)
        return NULL;

    /* Scan the user data directory for snapshot files. */
    while ((basename = g_dir_read_name (dir)) != NULL) {
        const gchar *errmsg;
        gchar *filename;
        struct stat st;

        /* Is this a snapshot file? */
        if (!g_str_has_prefix (basename, SNAPSHOT_FILE_PREFIX))
            continue;

        /* Is this an orphaned snapshot file? */
        if (composer_registry_lookup (registry, basename) != NULL)
            continue;

        filename = g_build_filename (dirname, basename, NULL);

        /* Try to examine the snapshot file.  Failure here
         * is non-fatal; just emit a warning and move on. */
        errno = 0;
        if (g_stat (filename, &st) < 0) {
            errmsg = g_strerror (errno);
            g_warning ("%s: %s", filename, errmsg);
            g_free (filename);
            continue;
        }

        /* If the file is empty, delete it.  Failure here
         * is non-fatal; just emit a warning and move on. */
        if (st.st_size == 0) {
            errno = 0;
            if (g_unlink (filename) < 0) {
                errmsg = g_strerror (errno);
                g_warning ("%s: %s", filename, errmsg);
            }
            g_free (filename);
            continue;
        }

        orphans = g_list_prepend (
            orphans, g_file_new_for_path (filename));

        g_free (filename);
    }

    g_dir_close (dir);

    return g_list_reverse (orphans);
}

void
e_composer_load_snapshot (EShell *shell,
                          GFile *snapshot_file,
                          GCancellable *cancellable,
                          GAsyncReadyCallback callback,
                          gpointer user_data)
{
    GSimpleAsyncResult *simple;
    LoadContext *context;

    g_return_if_fail (E_IS_SHELL (shell));
    g_return_if_fail (G_IS_FILE (snapshot_file));

    context = g_slice_new0 (LoadContext);

    simple = g_simple_async_result_new (
        G_OBJECT (shell), callback, user_data,
        e_composer_load_snapshot);

    g_simple_async_result_set_op_res_gpointer (
        simple, context, (GDestroyNotify) load_context_free);

    g_file_load_contents_async (
        snapshot_file, cancellable, (GAsyncReadyCallback)
        load_snapshot_loaded_cb, simple);
}

EMsgComposer *
e_composer_load_snapshot_finish (EShell *shell,
                                 GAsyncResult *result,
                                 GError **error)
{
    GSimpleAsyncResult *simple;
    LoadContext *context;

    g_return_val_if_fail (
        g_simple_async_result_is_valid (
            result, G_OBJECT (shell),
            e_composer_load_snapshot), NULL);

    simple = G_SIMPLE_ASYNC_RESULT (result);
    context = g_simple_async_result_get_op_res_gpointer (simple);

    if (g_simple_async_result_propagate_error (simple, error))
        return NULL;

    g_return_val_if_fail (E_IS_MSG_COMPOSER (context->composer), NULL);

    return g_object_ref (context->composer);
}

void
e_composer_save_snapshot (EMsgComposer *composer,
                          GCancellable *cancellable,
                          GAsyncReadyCallback callback,
                          gpointer user_data)
{
    GSimpleAsyncResult *simple;
    SaveContext *context;
    GFile *snapshot_file;
    GError *error = NULL;

    g_return_if_fail (E_IS_MSG_COMPOSER (composer));

    context = g_slice_new0 (SaveContext);

    if (G_IS_CANCELLABLE (cancellable))
        context->cancellable = g_object_ref (cancellable);

    simple = g_simple_async_result_new (
        G_OBJECT (composer), callback, user_data,
        e_composer_save_snapshot);

    g_simple_async_result_set_op_res_gpointer (
        simple, context, (GDestroyNotify) save_context_free);

    snapshot_file = e_composer_get_snapshot_file (composer);

    if (!G_IS_FILE (snapshot_file))
        snapshot_file = create_snapshot_file (composer, &error);

    if (error != NULL) {
        g_warn_if_fail (snapshot_file == NULL);
        g_simple_async_result_set_from_error (simple, error);
        g_simple_async_result_complete (simple);
        g_object_unref (simple);
        g_error_free (error);
        return;
    }

    g_return_if_fail (G_IS_FILE (snapshot_file));

    g_file_replace_async (
        snapshot_file, NULL, FALSE,
        G_FILE_CREATE_PRIVATE, G_PRIORITY_DEFAULT,
        context->cancellable, (GAsyncReadyCallback)
        save_snapshot_replace_cb, simple);
}

gboolean
e_composer_save_snapshot_finish (EMsgComposer *composer,
                                 GAsyncResult *result,
                                 GError **error)
{
    GSimpleAsyncResult *simple;

    g_return_val_if_fail (
        g_simple_async_result_is_valid (
            result, G_OBJECT (composer),
            e_composer_save_snapshot), FALSE);

    simple = G_SIMPLE_ASYNC_RESULT (result);

    /* Success is assumed unless a GError is set. */
    return !g_simple_async_result_propagate_error (simple, error);
}

GFile *
e_composer_get_snapshot_file (EMsgComposer *composer)
{
    g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL);

    return g_object_get_data (G_OBJECT (composer), SNAPSHOT_FILE_KEY);
}