Sending Files

9.1.1. Creating the New Channel

To set up a file transfer with a remote contact, we need to create a new channel of type org.freedesktop.Telepathy.Channel.Type.FileTransfer using CreateChannel (see Section 6.1 ― Requesting Channels). Besides the standard properties ChannelType, TargetHandleType and TargetHandle/TargetID, metadata pertaining to the file transfer must be provided. At least Filename, Size and ContentType must be provided, but there are other properties that may be provided, summarised in Table 9-1. Example 9-1 shows an example map of properties that could be used to request this channel.

Property Type Description Notes
Filename String The name of the file on the sender's side, the suggested filename for the receiver. Required
ContentType String The MIME type of the file. Required
Size uint64 The size of the file in bytes. Required. Although it must be set accurately, receivers should not trust this value.
ContentHashType File_Hash_Type Specifies the hashing function used for the ContentHash property.
ContentHash String Hash of the file transfer contents. ContentHashType must be set to the appropriate hashing algorithm.
Description String A string description of the file transfer.
Date int64 (Unix_Timestamp64) The last modification time (mtime) of the file.
Example 9-1Example Properties for Creating a File Transfer Channel
org.freedesktop.Telepathy.Channel.ChannelType org.freedesktop.Telepathy.Channel.Type.FileTransfer
org.freedesktop.Telepathy.Channel.TargetHandleType Handle_Type_Contact
org.freedesktop.Telepathy.Channel.TargetID "bob@example.com"
org.freedesktop.Telepathy.Channel.Type.FileTransfer.Filename "cat.jpg"
org.freedesktop.Telepathy.Channel.Type.FileTransfer.Size 32768
org.freedesktop.Telepathy.Channel.Type.FileTransfer.ContentType image/jpeg
Using GLib's GFileInfo

If using telepathy-glib, all of the required information to create this channel can be determined using g_file_query_info.

Example 9-2g_file_query_info()
GFile *file = g_file_new_for_commandline_arg (argv[3]);
GFileInfo *info = g_file_query_info (file,
                "standard::*",
                G_FILE_QUERY_INFO_NONE,
                NULL, &error);
handle_error (error);

GHashTable *props = tp_asv_new (
        TP_PROP_CHANNEL_CHANNEL_TYPE,
        G_TYPE_STRING,
        TP_IFACE_CHANNEL_TYPE_FILE_TRANSFER,

        TP_PROP_CHANNEL_TARGET_HANDLE_TYPE,
        G_TYPE_UINT,
        TP_HANDLE_TYPE_CONTACT,

        TP_PROP_CHANNEL_TARGET_HANDLE,
        G_TYPE_UINT,
        handle,

        TP_PROP_CHANNEL_TYPE_FILE_TRANSFER_FILENAME,
        G_TYPE_STRING,
        g_file_info_get_display_name (info),

        TP_PROP_CHANNEL_TYPE_FILE_TRANSFER_CONTENT_TYPE,
        G_TYPE_STRING,
        g_file_info_get_content_type (info),

        TP_PROP_CHANNEL_TYPE_FILE_TRANSFER_SIZE,
        G_TYPE_UINT64,
        g_file_info_get_size (info),

        NULL);

tp_cli_connection_interface_requests_call_create_channel (
                conn, -1, props,
                create_ft_channel_cb,
                NULL, NULL, NULL);

g_hash_table_destroy (props);
g_object_unref (info);
g_object_unref (file);

Complete Source Code

The remote user is prompted whether they wish to accept your transfer when the channel is created (not when you call ProvideFile. You should connect the signal FileTransferStateChanged as soon as the channel is available to listen for status changes. If the channel's state switches to Cancelled (File_Transfer_State_Cancelled), that means the user declined to accept the file. If this happens you should close the channel.

9.1.2. Providing the File

Once the channel has been set up and is ready, you should connect to the InitialOffsetDefined signal. This will be emitted before the channel enters the Open state (File_Transfer_State_Open), and represents the file offset that the receiver wants you to continue from. This value should be stored for when we start transferring the file.

ProvideFile takes as its arguments the type of socket we wish to use to transfer the file from the Connection Manager to the client. Different Connection Managers and different OS platforms might support different socket options. The property AvailableSocketTypes lists the supported socket types and their access modes. This property would have been passed in as one of the channel properties returned when the channel was created.

The socket address returned by AcceptFile and ProvideFile is a socket shared between the client and the Connection Manager. It is not the address of the socket between you and the remote client.

The file itself is transferred by the Connection Manager using the most appropriate mechanism to reach the remote host, be it a peer-to-peer socket, SOCKS5 proxy, using ICE or in-band via the server. A Telepathy client doesn't have to be concerned with the mechanism.

The transfer begins once both the sender and the receiver have called ProvideFile and AcceptFile respectively and the channel enters the Open state (File_Transfer_State_Open).

Example 9-3 shows the setup described in this section.

Example 9-3Setting Up The File Transfer Channel
/* File transfers in Telepathy work by opening a socket to the
 * Connection Manager and streaming the file over that socket.
 * Let's find out what manner of sockets are supported by this CM */
GHashTable *sockets = tp_asv_get_boxed (map,
        TP_PROP_CHANNEL_TYPE_FILE_TRANSFER_AVAILABLE_SOCKET_TYPES,
        TP_HASH_TYPE_SUPPORTED_SOCKET_MAP);

struct ft_state *ftstate = g_slice_new (struct ft_state);
ftstate->file = file;
guint access_control;

/* let's try for IPv4 */
if (g_hash_table_lookup (sockets,
                        GINT_TO_POINTER (TP_SOCKET_ADDRESS_TYPE_IPV4)))
{
        ftstate->type = TP_SOCKET_ADDRESS_TYPE_IPV4;
        access_control = TP_SOCKET_ACCESS_CONTROL_LOCALHOST;
}
else if (g_hash_table_lookup (sockets,
                        GINT_TO_POINTER (TP_SOCKET_ADDRESS_TYPE_UNIX)))
{
        ftstate->type = TP_SOCKET_ADDRESS_TYPE_UNIX;
        access_control = TP_SOCKET_ACCESS_CONTROL_LOCALHOST;
}

tp_cli_channel_type_file_transfer_connect_to_initial_offset_defined (
                channel, initial_offset_defined_cb,
                ftstate, NULL, NULL, &error);
handle_error (error);

tp_cli_channel_type_file_transfer_connect_to_file_transfer_state_changed (
                channel, file_transfer_state_changed_cb,
                ftstate, NULL, NULL, &error);
handle_error (error);

/* set up the socket for providing the file */
GValue *value = tp_g_value_slice_new_static_string ("");
tp_cli_channel_type_file_transfer_call_provide_file (channel,
                -1, ftstate->type, access_control,
                value, provide_file_cb,
                ftstate, NULL, NULL);
tp_g_value_slice_free (value);

Complete Source Code

Decoding The Address

The address returned ProvideFile and AcceptFile is a variant type, depending on the type of socket that is being opened.

Socket Type D-Bus Type GLib Type Structure Example
Unix ay DBUS_TYPE_G_UCHAR_ARRAY Address as a character array /tmp/tp-ft-1612616106
Abstract Unix
IPv4 (sq) TP_STRUCT_TYPE_SOCKET_ADDRESS_IPV4 IP address (as a string in the canonical form) and a port number (as an unsigned integer) 127.0.0.1, 12001
IPv6 TP_STRUCT_TYPE_SOCKET_ADDRESS_IPV6 ::1, 22222

Example 9-4 shows how the address might be decoded using telepathy-glib.

Example 9-4Decoding the Address Using telepathy-glib
switch (socket_type)
{
	case TP_SOCKET_ADDRESS_TYPE_UNIX:
	case TP_SOCKET_ADDRESS_TYPE_ABSTRACT_UNIX:
		g_return_if_fail (G_VALUE_HOLDS (addressv, DBUS_TYPE_G_UCHAR_ARRAY));
		GArray *array = g_value_get_boxed (addressv);
		g_print (" > file_transfer_cb (unix:%s)\n", (char*) array->data);
		break;

	case TP_SOCKET_ADDRESS_TYPE_IPV4:
	case TP_SOCKET_ADDRESS_TYPE_IPV6:
		GValueArray *address = g_value_get_boxed (addressv);
		const char *host = g_value_get_string (
			g_value_array_get_nth (address, 0));
		guint port = g_value_get_uint (
			g_value_array_get_nth (address, 1));
		g_print (" > file_transfer_cb (tcp:%s:%i)\n", host, port);
		break;

	default:
		g_return_if_reached ();
}

Be aware that abstract Unix sockets can contain the NUL character (usually as their first character) so should be copied with memcpy instead of strcpy.

If you're using a recent GLib you can connect to the socket using GIO, telepathy-glib provides utilities for converting between GSocketAddress objects and Telepathy's address types. The functions are tp_address_variant_from_g_socket_address and tp_g_socket_address_from_variant.

9.1.3. Transferring the File

When FileTransferStateChanged reports the state of Open (File_Transfer_State_Open), you should connect to the socket address returned by ProvideFile. You should also open the source file and seek to the offset provided by InitialOffsetDefined.

Copy the file from the source file to the socket. Files can be large, so you should do this in a way that doesn't block your mainloop. When you reach the end of the file, close the socket to the Connection Manager. This will indicate that you have completed the transfer. The channel will move into the Completed state (File_Transfer_State_Completed).

If the remote user cancels the channel, it will change into the Cancelled state (File_Transfer_State_Cancelled). If you wish to cancel your own file transfer, simply Close the channel.

9.1.3.1. Using GIO

With GLib's GIO, you can splice transfer between the disk and the network, which makes implementing file transfer very simple. Example 9-5 shows an example of doing this.

Example 9-5Splicing a file stream into a network socket with GIO
GSocketClient *client = g_socket_client_new ();
ftstate->connection = g_socket_client_connect (
                client,
                G_SOCKET_CONNECTABLE (ftstate->address),
                NULL, &error);
handle_error (error);
g_object_unref (client);

/* we can now use the stream like any other GIO stream.
 * Open an output stream for writing to */
GOutputStream *output = g_io_stream_get_output_stream (
                G_IO_STREAM (ftstate->connection));
ftstate->input = G_INPUT_STREAM (
                g_file_read (ftstate->file, NULL, &error));
handle_error (error);

g_seekable_seek (G_SEEKABLE (ftstate->input),
                ftstate->offset, G_SEEK_SET, NULL,
                &error);
handle_error (error);

/* splice the input stream into the output stream and GIO
 * takes care of the rest */
g_output_stream_splice_async (output, ftstate->input,
                G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE,
                0, NULL,
                splice_done_cb, ftstate);

Complete Source Code

A similar implementation can be used to receive files.