/* * empathy-ft-handler.c - Source for EmpathyFTHandler * Copyright (C) 2009 Collabora Ltd. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * Author: Cosimo Cecchi */ /* empathy-ft-handler.c */ #include #include #include #include #include "empathy-ft-handler.h" #include "empathy-contact-factory.h" #include "empathy-dispatcher.h" #include "empathy-marshal.h" #include "empathy-utils.h" #define DEBUG_FLAG EMPATHY_DEBUG_FT #include "empathy-debug.h" G_DEFINE_TYPE (EmpathyFTHandler, empathy_ft_handler, G_TYPE_OBJECT) #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyFTHandler) #define BUFFER_SIZE 4096 enum { PROP_TP_FILE = 1, PROP_G_FILE, PROP_CONTACT }; enum { HASHING_STARTED, HASHING_PROGRESS, HASHING_DONE, TRANSFER_STARTED, TRANSFER_PROGRESS, TRANSFER_DONE, TRANSFER_ERROR, LAST_SIGNAL }; typedef struct { GInputStream *stream; gboolean done_reading; GError *error; guchar *buffer; GChecksum *checksum; gssize total_read; guint64 total_bytes; EmpathyFTHandler *handler; } HashingData; typedef struct { EmpathyFTHandlerReadyCallback callback; gpointer user_data; EmpathyFTHandler *handler; } CallbacksData; /* private data */ typedef struct { gboolean dispose_run; GFile *gfile; EmpathyTpFile *tpfile; GCancellable *cancellable; /* request for the new transfer */ GHashTable *request; /* transfer properties */ EmpathyContact *contact; gchar *content_type; gchar *filename; gchar *description; guint64 total_bytes; guint64 transferred_bytes; guint64 mtime; gchar *content_hash; EmpFileHashType content_hash_type; } EmpathyFTHandlerPriv; static guint signals[LAST_SIGNAL] = { 0 }; /* prototypes */ static void schedule_hash_chunk (HashingData *hash_data); /* GObject implementations */ static void do_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { EmpathyFTHandlerPriv *priv = GET_PRIV (object); switch (property_id) { case PROP_CONTACT: g_value_set_object (value, priv->contact); break; case PROP_G_FILE: g_value_set_object (value, priv->gfile); break; case PROP_TP_FILE: g_value_set_object (value, priv->tpfile); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } } static void do_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { EmpathyFTHandlerPriv *priv = GET_PRIV (object); switch (property_id) { case PROP_CONTACT: priv->contact = g_value_dup_object (value); break; case PROP_G_FILE: priv->gfile = g_value_dup_object (value); break; case PROP_TP_FILE: priv->tpfile = g_value_dup_object (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } } static void do_dispose (GObject *object) { EmpathyFTHandlerPriv *priv = GET_PRIV (object); if (priv->dispose_run) return; priv->dispose_run = TRUE; if (priv->contact) { g_object_unref (priv->contact); priv->contact = NULL; } if (priv->gfile) { g_object_unref (priv->gfile); priv->gfile = NULL; } if (priv->tpfile) { empathy_tp_file_close (priv->tpfile); g_object_unref (priv->tpfile); priv->tpfile = NULL; } if (priv->cancellable) { g_object_unref (priv->cancellable); priv->cancellable = NULL; } G_OBJECT_CLASS (empathy_ft_handler_parent_class)->dispose (object); } static void do_finalize (GObject *object) { G_OBJECT_CLASS (empathy_ft_handler_parent_class)->finalize (object); } static void empathy_ft_handler_class_init (EmpathyFTHandlerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GParamSpec *param_spec; g_type_class_add_private (klass, sizeof (EmpathyFTHandlerPriv)); object_class->get_property = do_get_property; object_class->set_property = do_set_property; object_class->dispose = do_dispose; object_class->finalize = do_finalize; /* properties */ param_spec = g_param_spec_object ("contact", "contact", "The remote contact", EMPATHY_TYPE_CONTACT, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_CONTACT, param_spec); param_spec = g_param_spec_object ("gfile", "gfile", "The GFile we're handling", G_TYPE_FILE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_G_FILE, param_spec); param_spec = g_param_spec_object ("tp-file", "tp-file", "The file's channel wrapper", EMPATHY_TYPE_TP_FILE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_TP_FILE, param_spec); /* signals */ signals[TRANSFER_STARTED] = g_signal_new ("transfer-started", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, EMPATHY_TYPE_TP_FILE); signals[TRANSFER_DONE] = g_signal_new ("transfer-done", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, EMPATHY_TYPE_TP_FILE); signals[TRANSFER_ERROR] = g_signal_new ("transfer-error", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); signals[TRANSFER_PROGRESS] = g_signal_new ("transfer-progress", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, _empathy_marshal_VOID__OBJECT_UINT64_UINT64, G_TYPE_NONE, 2, EMPATHY_TYPE_TP_FILE, G_TYPE_UINT64, G_TYPE_UINT64); signals[HASHING_STARTED] = g_signal_new ("hashing-started", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); signals[HASHING_PROGRESS] = g_signal_new ("hashing-progress", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, _empathy_marshal_VOID__UINT64_UINT64, G_TYPE_NONE, 2, G_TYPE_UINT64, G_TYPE_UINT64); signals[HASHING_DONE] = g_signal_new ("hashing-done", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void empathy_ft_handler_init (EmpathyFTHandler *self) { EmpathyFTHandlerPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (self, EMPATHY_TYPE_FT_HANDLER, EmpathyFTHandlerPriv); self->priv = priv; } /* private functions */ static void hash_data_free (HashingData *data) { if (data->buffer != NULL) { g_free (data->buffer); data->buffer = NULL; } if (data->stream != NULL) { g_object_unref (data->stream); data->stream = NULL; } if (data->checksum != NULL) { g_checksum_free (data->checksum); data->checksum = NULL; } if (data->error != NULL) { g_error_free (data->error); data->error = NULL; } if (data->handler != NULL) { g_object_unref (data->handler); data->handler = NULL; } g_slice_free (HashingData, data); } static void ft_handler_create_channel_cb (EmpathyDispatchOperation *operation, const GError *error, gpointer user_data) { EmpathyFTHandler *handler = user_data; GError *myerr = NULL; EmpathyFTHandlerPriv *priv = GET_PRIV (handler); DEBUG ("FT: dispatcher create channel CB"); if (error != NULL) { /* TODO: error handling */ return; } priv->tpfile = g_object_ref (empathy_dispatch_operation_get_channel_wrapper (operation)); empathy_tp_file_offer (priv->tpfile, priv->gfile, &myerr); empathy_dispatch_operation_claim (operation); } static void ft_handler_push_to_dispatcher (EmpathyFTHandler *handler) { EmpathyDispatcher *dispatcher; McAccount *account; EmpathyFTHandlerPriv *priv = GET_PRIV (handler); DEBUG ("FT: pushing request to the dispatcher"); dispatcher = empathy_dispatcher_dup_singleton (); account = empathy_contact_get_account (priv->contact); empathy_dispatcher_create_channel (dispatcher, account, priv->request, ft_handler_create_channel_cb, handler); g_object_unref (dispatcher); } static gboolean ft_handler_check_if_allowed (EmpathyFTHandler *handler) { EmpathyDispatcher *dispatcher; EmpathyFTHandlerPriv *priv = GET_PRIV (handler); McAccount *account; GStrv allowed; gboolean res = TRUE; dispatcher = empathy_dispatcher_dup_singleton (); account = empathy_contact_get_account (priv->contact); allowed = empathy_dispatcher_find_channel_class (dispatcher, account, EMP_IFACE_CHANNEL_TYPE_FILE_TRANSFER, TP_HANDLE_TYPE_CONTACT); if (!tp_strv_contains ((const gchar * const *) allowed, TP_IFACE_CHANNEL ".TargetHandle")) res = FALSE; g_object_unref (dispatcher); return res; } static void ft_handler_populate_outgoing_request (EmpathyFTHandler *handler) { guint contact_handle; GHashTable *request; GValue *value; EmpathyFTHandlerPriv *priv = GET_PRIV (handler); request = priv->request = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify) tp_g_value_slice_free); contact_handle = empathy_contact_get_handle (priv->contact); /* org.freedesktop.Telepathy.Channel.ChannelType */ value = tp_g_value_slice_new (G_TYPE_STRING); g_value_set_string (value, EMP_IFACE_CHANNEL_TYPE_FILE_TRANSFER); g_hash_table_insert (request, TP_IFACE_CHANNEL ".ChannelType", value); /* org.freedesktop.Telepathy.Channel.TargetHandleType */ value = tp_g_value_slice_new (G_TYPE_UINT); g_value_set_uint (value, TP_HANDLE_TYPE_CONTACT); g_hash_table_insert (request, TP_IFACE_CHANNEL ".TargetHandleType", value); /* org.freedesktop.Telepathy.Channel.TargetHandle */ value = tp_g_value_slice_new (G_TYPE_UINT); g_value_set_uint (value, contact_handle); g_hash_table_insert (request, TP_IFACE_CHANNEL ".TargetHandle", value); /* org.freedesktop.Telepathy.Channel.Type.FileTransfer.ContentType */ value = tp_g_value_slice_new (G_TYPE_STRING); g_value_set_string (value, priv->content_type); g_hash_table_insert (request, EMP_IFACE_CHANNEL_TYPE_FILE_TRANSFER ".ContentType", value); /* org.freedesktop.Telepathy.Channel.Type.FileTransfer.Filename */ value = tp_g_value_slice_new (G_TYPE_STRING); g_value_set_string (value, priv->filename); g_hash_table_insert (request, EMP_IFACE_CHANNEL_TYPE_FILE_TRANSFER ".Filename", value); /* org.freedesktop.Telepathy.Channel.Type.FileTransfer.Size */ value = tp_g_value_slice_new (G_TYPE_UINT64); g_value_set_uint64 (value, (guint64) priv->total_bytes); g_hash_table_insert (request, EMP_IFACE_CHANNEL_TYPE_FILE_TRANSFER ".Size", value); /* org.freedesktop.Telepathy.Channel.Type.FileTransfer.Date */ value = tp_g_value_slice_new (G_TYPE_UINT64); g_value_set_uint64 (value, (guint64) priv->mtime); g_hash_table_insert (request, EMP_IFACE_CHANNEL_TYPE_FILE_TRANSFER ".Date", value); } static void hash_job_async_close_stream_cb (GObject *source, GAsyncResult *res, gpointer user_data) { HashingData *hash_data = user_data; EmpathyFTHandler *handler = hash_data->handler; EmpathyFTHandlerPriv *priv; GError *error = NULL; GValue *value; DEBUG ("FT: closing stream after hashing."); priv = GET_PRIV (handler); /* if we're here we for sure have done reading, check if we stopped due * to an error. */ g_input_stream_close_finish (hash_data->stream, res, &error); if (error != NULL) { if (hash_data->error != NULL) { /* if we already stopped due to an error, probably we're completely * hosed for some reason. just return the first read error * to the user. */ g_clear_error (&error); error = hash_data->error; } goto cleanup; } if (hash_data->error != NULL) { error = hash_data->error; goto cleanup; } /* set the checksum in the request */ DEBUG ("FT: got file hash %s", g_checksum_get_string (hash_data->checksum)); /* org.freedesktop.Telepathy.Channel.Type.FileTransfer.ContentHash */ value = tp_g_value_slice_new (G_TYPE_STRING); g_value_set_string (value, g_checksum_get_string (hash_data->checksum)); g_hash_table_insert (priv->request, EMP_IFACE_CHANNEL_TYPE_FILE_TRANSFER ".ContentHash", value); cleanup: hash_data_free (hash_data); if (error != NULL) { g_signal_emit (handler, signals[TRANSFER_ERROR], 0, error); g_clear_error (&error); } else { g_signal_emit (handler, signals[HASHING_DONE], 0); /* the request is complete now, push it to the dispatcher */ ft_handler_push_to_dispatcher (handler); } } static void hash_job_async_read_cb (GObject *source, GAsyncResult *res, gpointer user_data) { HashingData *hash_data = user_data; gssize bytes_read; GError *error = NULL; DEBUG ("FT: reading a chunk for hashing."); bytes_read = g_input_stream_read_finish (hash_data->stream, res, &error); if (error != NULL) { hash_data->error = error; hash_data->done_reading = TRUE; goto out; } hash_data->total_read += bytes_read; /* we now have the chunk */ if (bytes_read == 0) { hash_data->done_reading = TRUE; goto out; } else { g_checksum_update (hash_data->checksum, hash_data->buffer, bytes_read); g_signal_emit (hash_data->handler, signals[HASHING_PROGRESS], 0, (guint64) hash_data->total_read, (guint64) hash_data->total_bytes); } out: g_free (hash_data->buffer); hash_data->buffer = NULL; schedule_hash_chunk (hash_data); } static void schedule_hash_chunk (HashingData *hash_data) { EmpathyFTHandlerPriv *priv; priv = GET_PRIV (hash_data->handler); if (hash_data->done_reading) { g_input_stream_close_async (hash_data->stream, G_PRIORITY_DEFAULT, priv->cancellable, hash_job_async_close_stream_cb, hash_data); } else { if (hash_data->buffer == NULL) hash_data->buffer = g_malloc0 (BUFFER_SIZE); g_input_stream_read_async (hash_data->stream, hash_data->buffer, BUFFER_SIZE, G_PRIORITY_DEFAULT, priv->cancellable, hash_job_async_read_cb, hash_data); } } static void ft_handler_read_async_cb (GObject *source, GAsyncResult *res, gpointer user_data) { GFileInputStream *stream; GError *error = NULL; HashingData *hash_data; GValue *value; EmpathyFTHandler *handler = user_data; EmpathyFTHandlerPriv *priv = GET_PRIV (handler); DEBUG ("FT: GFile read async CB."); stream = g_file_read_finish (priv->gfile, res, &error); if (error != NULL) { g_signal_emit (handler, signals[TRANSFER_ERROR], 0, error); g_clear_error (&error); return; } hash_data = g_slice_new0 (HashingData); hash_data->stream = G_INPUT_STREAM (stream); hash_data->done_reading = FALSE; hash_data->total_bytes = priv->total_bytes; hash_data->handler = g_object_ref (handler); /* FIXME: should look at the CM capabilities before setting the * checksum type? */ hash_data->checksum = g_checksum_new (G_CHECKSUM_MD5); /* org.freedesktop.Telepathy.Channel.Type.FileTransfer.ContentHashType */ value = tp_g_value_slice_new (G_TYPE_UINT); g_value_set_uint (value, EMP_FILE_HASH_TYPE_MD5); g_hash_table_insert (priv->request, EMP_IFACE_CHANNEL_TYPE_FILE_TRANSFER ".ContentHashType", value); g_signal_emit (handler, signals[HASHING_STARTED], 0); schedule_hash_chunk (hash_data); } static void ft_handler_complete_request (EmpathyFTHandler *handler) { EmpathyFTHandlerPriv *priv = GET_PRIV (handler); GError *myerr = NULL; /* check if FT is allowed before firing up the I/O machinery */ if (!ft_handler_check_if_allowed (handler)) { g_set_error_literal (&myerr, EMPATHY_FT_ERROR_QUARK, EMPATHY_FT_ERROR_NOT_SUPPORTED, _("File transfer not supported by remote contact")); g_signal_emit (handler, signals[TRANSFER_ERROR], 0, myerr); g_clear_error (&myerr); return; } /* populate the request table with all the known properties */ ft_handler_populate_outgoing_request (handler); /* now start hashing the file */ g_file_read_async (priv->gfile, G_PRIORITY_DEFAULT, priv->cancellable, ft_handler_read_async_cb, handler); } static void callbacks_data_free (gpointer user_data) { CallbacksData *data = user_data; if (data->handler) g_object_unref (data->handler); g_slice_free (CallbacksData, data); } static void ft_handler_gfile_ready_cb (GObject *source, GAsyncResult *res, CallbacksData *cb_data) { GFileInfo *info; GError *error = NULL; GTimeVal mtime; EmpathyFTHandlerPriv *priv = GET_PRIV (cb_data->handler); DEBUG ("FT: got GFileInfo."); info = g_file_query_info_finish (priv->gfile, res, &error); if (error != NULL) goto out; priv->content_type = g_strdup (g_file_info_get_content_type (info)); priv->filename = g_strdup (g_file_info_get_display_name (info)); priv->total_bytes = g_file_info_get_size (info); g_file_info_get_modification_time (info, &mtime); priv->mtime = mtime.tv_sec; priv->transferred_bytes = 0; priv->description = NULL; g_object_unref (info); out: if (error == NULL) { cb_data->callback (cb_data->handler, NULL, cb_data->user_data); } else { cb_data->callback (NULL, error, cb_data->user_data); g_error_free (error); g_object_unref (cb_data->handler); } callbacks_data_free (cb_data); } static void ft_handler_contact_ready_cb (EmpathyContact *contact, const GError *error, gpointer user_data, GObject *weak_object) { CallbacksData *cb_data = user_data; EmpathyFTHandlerPriv *priv = GET_PRIV (weak_object); g_assert (priv->contact != NULL); g_assert (priv->gfile != NULL); DEBUG ("FT: contact is ready."); /* start collecting info about the file */ g_file_query_info_async (priv->gfile, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," G_FILE_ATTRIBUTE_STANDARD_SIZE "," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, NULL, (GAsyncReadyCallback) ft_handler_gfile_ready_cb, cb_data); } static void channel_get_all_properties_cb (TpProxy *proxy, GHashTable *properties, const GError *error, gpointer user_data, GObject *weak_object) { CallbacksData *cb_data = user_data; EmpathyFTHandler *handler = EMPATHY_FT_HANDLER (weak_object); EmpathyFTHandlerPriv *priv = GET_PRIV (handler); EmpathyContactFactory *c_factory; guint c_handle; McAccount *account; if (error != NULL) { cb_data->callback (NULL, error, cb_data->user_data); g_object_unref (handler); return; } priv->total_bytes = g_value_get_uint64 ( g_hash_table_lookup (properties, "Size")); priv->transferred_bytes = g_value_get_uint64 ( g_hash_table_lookup (properties, "TransferredBytes")); priv->filename = g_value_dup_string ( g_hash_table_lookup (properties, "Filename")); priv->content_hash = g_value_dup_string ( g_hash_table_lookup (properties, "ContentHash")); priv->content_hash_type = g_value_get_uint ( g_hash_table_lookup (properties, "ContentHashType")); priv->content_type = g_value_dup_string ( g_hash_table_lookup (properties, "ContentType")); priv->description = g_value_dup_string ( g_hash_table_lookup (properties, "Description")); g_hash_table_destroy (properties); c_factory = empathy_contact_factory_dup_singleton (); account = empathy_channel_get_account (TP_CHANNEL (proxy)); c_handle = tp_channel_get_handle (TP_CHANNEL (proxy), NULL); priv->contact = empathy_contact_factory_get_from_handle (c_factory, account,c_handle); g_object_unref (c_factory); g_object_unref (account); cb_data->callback (handler, NULL, cb_data->user_data); } /* public methods */ void empathy_ft_handler_new_outgoing (EmpathyContact *contact, GFile *source, EmpathyFTHandlerReadyCallback callback, gpointer user_data) { EmpathyFTHandler *handler; CallbacksData *data; EmpathyFTHandlerPriv *priv; g_return_if_fail (EMPATHY_IS_CONTACT (contact)); g_return_if_fail (G_IS_FILE (source)); handler = g_object_new (EMPATHY_TYPE_FT_HANDLER, "contact", contact, "gfile", source, NULL); priv = GET_PRIV (handler); data = g_slice_new0 (CallbacksData); data->callback = callback; data->user_data = user_data; data->handler = g_object_ref (handler); empathy_contact_call_when_ready (priv->contact, EMPATHY_CONTACT_READY_HANDLE, ft_handler_contact_ready_cb, data, NULL, G_OBJECT (handler)); } void empathy_ft_handler_new_incoming (EmpathyTpFile *tp_file, GFile *destination, EmpathyFTHandlerReadyCallback callback, gpointer user_data) { EmpathyFTHandler *handler; TpChannel *channel; CallbacksData *data; g_return_if_fail (EMPATHY_IS_TP_FILE (tp_file)); g_return_if_fail (G_IS_FILE (destination)); handler = g_object_new (EMPATHY_TYPE_FT_HANDLER, "tp-file", tp_file, "gfile", destination, NULL); g_object_get (tp_file, "channel", &channel, NULL); data = g_slice_new0 (CallbacksData); data->callback = callback; data->user_data = user_data; data->handler = g_object_ref (handler); tp_cli_dbus_properties_call_get_all (channel, -1, EMP_IFACE_CHANNEL_TYPE_FILE_TRANSFER, channel_get_all_properties_cb, data, callbacks_data_free, G_OBJECT (handler)); } void empathy_ft_handler_start_transfer (EmpathyFTHandler *handler, GCancellable *cancellable) { EmpathyFTHandlerPriv *priv; GError *error = NULL; g_return_if_fail (EMPATHY_IS_FT_HANDLER (handler)); priv = GET_PRIV (handler); priv->cancellable = g_object_ref (cancellable); if (priv->tpfile == NULL) { ft_handler_complete_request (handler); } else { /* TODO: add support for resume. */ empathy_tp_file_accept (priv->tpfile, 0, priv->gfile, &error); } }