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. |
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 |
If using telepathy-glib, all of the required information to create this channel can be determined using g_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);
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.
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.
/* 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);
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.
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.
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.
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.
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);
A similar implementation can be used to receive files.