changeset 3609:4faf84dfdda2

[gaim-migrate @ 3722] First draft of the file transfer prpl interface. committer: Tailor Script <tailor@pidgin.im>
author Rob Flynn <gaim@robflynn.com>
date Wed, 09 Oct 2002 04:55:29 +0000
parents 7703b2689791
children 9313e146daf4
files src/Makefile.am src/ft.c src/prpl.h
diffstat 3 files changed, 666 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/src/Makefile.am	Tue Oct 08 23:45:20 2002 +0000
+++ b/src/Makefile.am	Wed Oct 09 04:55:29 2002 +0000
@@ -10,6 +10,7 @@
 		conversation.c \
 		core.c \
 		dialogs.c \
+		ft.c \
 		gaimrc.c \
 		gtkimhtml.c \
 		gtkspell.c \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/ft.c	Wed Oct 09 04:55:29 2002 +0000
@@ -0,0 +1,641 @@
+/*
+ * gaim - file transfer functions
+ *
+ * Copyright (C) 2002, Wil Mahan <wtm2@duke.edu>
+ * 
+ * This program 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+
+#define FT_BUFFER_SIZE (4096)
+
+#include <gtk/gtk.h>
+#include "gaim.h"
+#include "proxy.h"
+#include "prpl.h"
+
+/* Completely describes a file transfer.  Opaque to callers. */
+struct file_transfer {
+	enum { FILE_TRANSFER_TYPE_SEND, FILE_TRANSFER_TYPE_RECEIVE } type;
+
+	enum {
+		FILE_TRANSFER_STATE_ASK, /* waiting for confirmation */
+		FILE_TRANSFER_STATE_CHOOSEFILE, /* waiting for file dialog */
+		FILE_TRANSFER_STATE_TRANSFERRING, /* in actual transfer */
+		FILE_TRANSFER_STATE_INTERRUPTED, /* other user canceled */
+		FILE_TRANSFER_STATE_CANCELED, /* we canceled */
+		FILE_TRANSFER_STATE_DONE, /* transfer complete */
+		FILE_TRANSFER_STATE_CLEANUP /* freeing memory */
+	} state;
+
+	char *who;
+
+	/* file selection dialog */
+	GtkWidget *w;
+	char **names;
+	int *sizes;
+	char *dir;
+	char *initname;
+	FILE *file;
+
+	/* connection info */
+	struct gaim_connection *gc;
+	int fd;
+	int watcher;
+
+	/* state */
+	int totfiles;
+	int filesdone;
+	int totsize;
+	int bytessent;
+	int bytesleft;
+};
+
+static int ft_choose_file(gpointer a, struct file_transfer *xfer);
+static void ft_cancel(gpointer w, struct file_transfer *xfer);
+static void ft_delete(struct file_transfer *xfer);
+static void ft_callback(gpointer data, gint source,
+		GaimInputCondition condition);
+static void ft_nextfile(struct file_transfer *xfer);
+static int ft_mkdir(const char *name);
+static int ft_mkdir_help(char *dir);
+
+static struct file_transfer *ft_new(int type, struct gaim_connection *gc,
+		const char *who)
+{
+	struct file_transfer *xfer = g_new0(struct file_transfer, 1);
+	xfer->type = type;
+	xfer->state = FILE_TRANSFER_STATE_ASK;
+	xfer->gc = gc;
+	xfer->who = g_strdup(who);
+	xfer->filesdone = 0;
+
+	return xfer;
+}
+
+struct file_transfer *transfer_in_add(struct gaim_connection *gc,
+		const char *who, const char *initname, int totsize,
+		int totfiles, const char *msg)
+{
+	struct file_transfer *xfer = ft_new(FILE_TRANSFER_TYPE_RECEIVE, gc,
+			who);
+	char *buf;
+	static const char *sizestr[4] = { "bytes", "KB", "MB", "GB" };
+	float sizemag = (float)totsize;
+	int szindex = 0;
+
+	xfer->initname = g_strdup(initname);
+	xfer->totsize = totsize;
+	xfer->totfiles = totfiles;
+	xfer->filesdone = 0;
+
+	/* Ask the user whether he or she accepts the transfer. */
+	while ((szindex < 4) && (sizemag > 1024)) {
+		sizemag /= 1024;
+		szindex++;
+	}
+
+	if (xfer->totfiles == 1) 
+		buf = g_strdup_printf(_("%s requests that %s accept a file: %s (%.3g %s)"),
+		who, xfer->gc->username, initname,
+		sizemag, sizestr[szindex]);
+	else
+		buf = g_strdup_printf(_("%s requests that %s accept %d files: %s (%.3g %s)"),
+		who, xfer->gc->username, xfer->totfiles,
+		initname, sizemag, sizestr[szindex]);
+
+	if (msg) {
+		char *newmsg = g_strconcat(buf, ": ", msg, NULL);
+		g_free(buf);
+		buf = newmsg;
+	}
+
+	do_ask_dialog(buf, xfer, ft_choose_file, ft_cancel);
+	g_free(buf);
+
+	return xfer;
+}
+
+struct file_transfer *transfer_out_add(struct gaim_connection *gc,
+		const char *who)
+{
+	struct file_transfer *xfer = ft_new(FILE_TRANSFER_TYPE_SEND, gc,
+			who);
+
+	ft_choose_file(NULL, xfer);
+
+	return xfer;
+}
+
+/* We canceled the transfer, either by declining the initial
+ * confirmation dialog or canceling the file dialog.
+ */
+static void ft_cancel(gpointer w, struct file_transfer *xfer)
+{
+	/* Make sure we weren't aborted while waiting for
+	 * confirmation from the user.
+	 */
+	if (xfer->state == FILE_TRANSFER_STATE_INTERRUPTED) {
+		xfer->state = FILE_TRANSFER_STATE_CLEANUP;
+		transfer_abort(xfer, NULL);
+		return;
+	}
+
+	xfer->state = FILE_TRANSFER_STATE_CANCELED;
+	if (xfer->w) {
+		gtk_widget_destroy(xfer->w);
+		xfer->w = NULL;
+	}
+
+	if (xfer->gc->prpl->file_transfer_cancel)
+		xfer->gc->prpl->file_transfer_cancel(xfer->gc, xfer);
+
+	ft_delete(xfer);
+}
+
+/* This is called when the other user aborts the transfer,
+ * possibly in the middle of a transfer.
+ */
+int transfer_abort(struct file_transfer *xfer, const char *why)
+{
+	if (xfer->state == FILE_TRANSFER_STATE_INTERRUPTED) {
+		/* If for some reason we have already been
+		 * here and are waiting on some event before
+		 * cleaning up, but we get another abort request,
+		 * we don't need to do anything else.
+		 */
+		return 1;
+	}
+	else if (xfer->state == FILE_TRANSFER_STATE_ASK) {
+		/* Kludge:  since there is no way to kill a
+		 * do_ask_dialog() window, we just note the
+		 * status here and clean up after the user
+		 * makes a selection.
+		 */
+		xfer->state = FILE_TRANSFER_STATE_INTERRUPTED;
+		return 1;
+	}
+	else if (xfer->state == FILE_TRANSFER_STATE_TRANSFERRING) {
+		if (xfer->watcher) {
+			gaim_input_remove(xfer->watcher);
+			xfer->watcher = 0;
+		}
+		if (xfer->file) {
+			fclose(xfer->file);
+			xfer->file = NULL;
+		}
+		/* XXX theoretically, there is a race condition here,
+		 * because we could be inside ft_callback() when we
+		 * free xfer below, with undefined results.  Since
+		 * we use non-blocking IO, this doesn't seem to be
+		 * a problem, but it still makes me nervous--I don't
+		 * know how to fix it other than using locks, though.
+		 * -- wtm
+		 */
+	}
+	else if (xfer->state == FILE_TRANSFER_STATE_CHOOSEFILE) {
+		/* It's safe to clean up now.  Just make sure we
+		 * destroy the dialog window first.
+		 */
+		if (xfer->w) {
+			gtk_signal_disconnect_by_func(GTK_OBJECT(xfer->w),
+					GTK_SIGNAL_FUNC(ft_cancel), xfer);
+			gtk_widget_destroy(xfer->w);
+			xfer->w = NULL;
+		}
+	}
+
+	/* Let the user know that we were aborted, unless we already
+	 * finished or the user aborted first.
+	 */
+	/* if ((xfer->state != FILE_TRANSFER_STATE_DONE) &&
+			(xfer->state != FILE_TRANSFER_STATE_CANCELED)) { */
+	if (why) {
+		char *msg;
+
+		if (xfer->type == FILE_TRANSFER_TYPE_SEND)
+			msg = g_strdup_printf(_("File transfer to %s aborted."), xfer->who);
+		else
+			msg = g_strdup_printf(_("File transfer from %s aborted."), xfer->who);
+		do_error_dialog(msg, why, GAIM_ERROR);
+		g_free(msg);
+	}
+
+	ft_delete(xfer);
+
+	return 0;
+}
+
+
+static void ft_delete(struct file_transfer *xfer)
+{
+	if (xfer->names)
+		g_strfreev(xfer->names);
+	if (xfer->initname)
+		g_free(xfer->initname);
+	if (xfer->who)
+		g_free(xfer->who);
+	if (xfer->sizes)
+		g_free(xfer->sizes);
+	g_free(xfer);
+}
+
+static void ft_choose_ok(gpointer a, struct file_transfer *xfer) {
+	gboolean exists, is_dir;
+	struct stat st;
+	const char *err = NULL;
+	
+	xfer->names = gtk_file_selection_get_selections(GTK_FILE_SELECTION(xfer->w));
+	exists = !stat(*xfer->names, &st);
+	is_dir = (exists) ? S_ISDIR(st.st_mode) : 0;
+
+	if (exists) {
+		if (xfer->type == FILE_TRANSFER_TYPE_RECEIVE)
+			/* XXX overwrite/append/cancel prompt */
+			err = _("That file already exists; please choose another name.");
+		else { /* (xfer->type == FILE_TRANSFER_TYPE_SEND) */
+			char **cur;
+			/* First find the total number of files,
+			 * so we know how much space to allocate.
+			 */
+			xfer->totfiles = 0;
+			for (cur = xfer->names; *cur; cur++) {
+				xfer->totfiles++;
+			}
+
+			/* Now get sizes for each file. */
+			xfer->totsize = st.st_size;
+			xfer->sizes = g_malloc(xfer->totfiles
+					* sizeof(*xfer->sizes));
+			xfer->sizes[0] = st.st_size;
+			for (cur = xfer->names + 1; *cur; cur++) {
+				exists = !stat(*cur, &st);
+				if (!exists) {
+					err = _("File not found.");
+					break;
+				}
+				xfer->sizes[cur - xfer->names] =
+					st.st_size;
+				xfer->totsize += st.st_size;
+			}
+		}
+	}
+	else { /* doesn't exist */
+		if (xfer->type == FILE_TRANSFER_TYPE_SEND)
+			err = _("File not found.");
+		else if (xfer->totfiles > 1) {
+			if (!xfer->names[0] || xfer->names[1]) {
+				err = _("You may only choose one new directory.");
+			}
+			else {
+				if (ft_mkdir(*xfer->names))
+					err = _("Unable to create directory.");
+				else
+					xfer->dir = g_strconcat(xfer->names[0],
+						"/", NULL);
+			}
+		}
+	}
+
+	if (err)
+		do_error_dialog(err, NULL, GAIM_ERROR);
+	else {
+		/* File name looks valid */
+		gtk_widget_destroy(xfer->w);
+		xfer->w = NULL;
+
+		if (xfer->type == FILE_TRANSFER_TYPE_SEND) {
+			char *desc;
+			if (xfer->totfiles == 1)
+				desc = *xfer->names;
+			else
+				/* XXX what else? */
+				desc = "*";
+				/* desc = g_path_get_basename(g_path_get_dirname(*xfer->names)); */
+			xfer->gc->prpl->file_transfer_out(xfer->gc, xfer,
+					desc, xfer->totfiles,
+					xfer->totsize);
+		}
+		else
+			xfer->gc->prpl->file_transfer_in(xfer->gc, xfer,
+					0); /* XXX */
+	}
+}
+
+/* Called on outgoing transfers to get information about the
+ * current file.
+ */
+int transfer_get_file_info(struct file_transfer *xfer, int *size,
+		char **name)
+{
+	*size = xfer->sizes[xfer->filesdone];
+	*name = xfer->names[xfer->filesdone];
+	return 0;
+}
+
+static int ft_choose_file(gpointer a, struct file_transfer *xfer)
+{
+	char *curdir = g_get_current_dir(); /* should be freed */
+	char *initstr;
+
+	/* If the connection is interrupted while we are waiting
+	 * for do_ask_dialog(), then we can't clean up until we
+	 * get here, after the user makes a selection.
+	 */
+	if (xfer->state == FILE_TRANSFER_STATE_INTERRUPTED) {
+		xfer->state = FILE_TRANSFER_STATE_CLEANUP;
+		transfer_abort(xfer, NULL);
+		return 1;
+	}
+
+	xfer->state = FILE_TRANSFER_STATE_CHOOSEFILE;
+	if (xfer->type == FILE_TRANSFER_TYPE_RECEIVE)
+		xfer->w = gtk_file_selection_new(_("Gaim - Save As..."));
+	else /* (xfer->type == FILE_TRANSFER_TYPE_SEND) */ {
+		xfer->w = gtk_file_selection_new(_("Gaim - Open..."));
+		gtk_file_selection_set_select_multiple(GTK_FILE_SELECTION(xfer->w),
+				1);
+	}
+
+	if (xfer->initname) {
+		initstr = g_strdup_printf("%s/%s", curdir, xfer->initname);
+	} else 
+		initstr = g_strconcat(curdir, "/", NULL);
+	g_free(curdir);
+
+	gtk_file_selection_set_filename(GTK_FILE_SELECTION(xfer->w),
+			initstr);
+	g_free(initstr);
+
+	gtk_signal_connect(GTK_OBJECT(xfer->w), "delete_event",
+			GTK_SIGNAL_FUNC(ft_cancel), xfer);
+        gtk_signal_connect(GTK_OBJECT(GTK_FILE_SELECTION(xfer->w)->cancel_button),
+			"clicked", GTK_SIGNAL_FUNC(ft_cancel), xfer);
+	gtk_signal_connect(GTK_OBJECT(GTK_FILE_SELECTION(xfer->w)->ok_button),
+			"clicked", GTK_SIGNAL_FUNC(ft_choose_ok), xfer);
+	gtk_widget_show(xfer->w);
+
+	return 0;
+}
+
+static int ft_open_file(struct file_transfer *xfer, const char *filename,
+		int offset)
+{
+	char *err = NULL;
+
+	if (xfer->type == FILE_TRANSFER_TYPE_RECEIVE) {
+		xfer->file = fopen(filename,
+				(offset > 0) ? "a" : "w");
+
+		if (!xfer->file)
+			err = g_strdup_printf(_("Could not open %s for writing: %s"),
+					filename, g_strerror(errno));
+
+	}
+	else /* (xfer->type == FILE_TRANSFER_TYPE_SEND) */ {
+		xfer->file = fopen(filename, "r");
+		if (!xfer->file)
+			err = g_strdup_printf(_("Could not open %s for reading: %s"),
+					filename, g_strerror(errno));
+	}
+
+	if (err) {
+		do_error_dialog(err, NULL, GAIM_ERROR);
+		g_free(err);
+		return -1;
+	}
+
+	fseek(xfer->file, offset, SEEK_SET);
+
+	return 0;
+}
+
+/* Takes a full file name, and creates any directories above it
+ * that don't exist already.
+ */
+static int ft_mkdir(const char *name) {
+	int ret = 0;
+	struct stat st;
+	mode_t m = umask(0077);
+	char *dir;
+
+	dir = g_path_get_dirname(name);
+	if (stat(dir, &st))
+		ret = ft_mkdir_help(dir);
+
+	g_free(dir);
+	umask(m);
+	return ret;
+}
+
+/* Two functions, one recursive, just to make a directory.  Yuck. */
+static int ft_mkdir_help(char *dir) {
+	int ret;
+
+	ret = mkdir(dir, 0777);
+	if (ret) {
+		char *index = strrchr(dir, G_DIR_SEPARATOR);
+		if (!index)
+			return -1;
+		*index = '\0';
+		ret = ft_mkdir_help(dir);
+		*index = G_DIR_SEPARATOR;
+		if (!ret)
+			ret = mkdir(dir, 0777);
+	}
+
+	return ret;
+}
+ 
+int transfer_in_do(struct file_transfer *xfer, int fd,
+		const char *filename, int size)
+{
+	char *fullname;
+
+	xfer->state = FILE_TRANSFER_STATE_TRANSFERRING;
+	xfer->fd = fd;
+	xfer->bytesleft = size;
+
+	/* XXX implement resuming incoming transfers */
+#if 0
+	if (xfer->sizes)
+		xfer->bytesleft -= xfer->sizes[0];
+#endif
+
+	/* Security check */
+	if (g_strrstr(filename, "..")) {
+		xfer->state = FILE_TRANSFER_STATE_CLEANUP;
+		transfer_abort(xfer, _("Invalid incoming filename component"));
+		return -1;
+	}
+
+	if (xfer->totfiles > 1)
+		fullname = g_strconcat(xfer->dir, filename, NULL);
+	else
+		/* Careful:  filename is the name on the *other*
+		 * end; don't use it here. */
+		fullname = g_strdup(xfer->names[xfer->filesdone]);
+
+	
+	if (ft_mkdir(fullname)) {
+		xfer->state = FILE_TRANSFER_STATE_CLEANUP;
+		transfer_abort(xfer, _("Invalid incoming filename"));
+		return -1;
+	}
+
+	if (!ft_open_file(xfer, fullname, 0)) {
+		/* Special case:  if we are receiving an empty file,
+		 * we would never enter the callback.  Just avoid the
+		 * callback altogether.
+		 */
+		if (xfer->bytesleft == 0)
+			ft_nextfile(xfer);
+		else
+			xfer->watcher = gaim_input_add(fd,
+					GAIM_INPUT_READ,
+					ft_callback, xfer);
+	} else {
+		/* Error opening file */
+		xfer->state = FILE_TRANSFER_STATE_CLEANUP;
+		transfer_abort(xfer, NULL);
+		g_free(fullname);
+		return -1;
+	}
+
+	g_free(fullname);
+	return 0;
+}
+
+int transfer_out_do(struct file_transfer *xfer, int fd, int offset) {
+	xfer->state = FILE_TRANSFER_STATE_TRANSFERRING;
+	xfer->fd = fd;
+	xfer->bytesleft = xfer->sizes[xfer->filesdone] - offset;
+
+	if (!ft_open_file(xfer, xfer->names[xfer->filesdone], offset)) {
+		/* Special case:  see transfer_in_do().
+		 */
+		if (xfer->bytesleft == 0)
+			ft_nextfile(xfer);
+		else
+			xfer->watcher = gaim_input_add(fd,
+					GAIM_INPUT_WRITE, ft_callback,
+					xfer);
+	}
+	else {
+		/* Error opening file */
+		xfer->state = FILE_TRANSFER_STATE_CLEANUP;
+		transfer_abort(xfer, NULL);
+		return -1;
+	}
+
+	return 0;
+}
+
+static void ft_callback(gpointer data, gint source,
+		GaimInputCondition condition)
+{
+	struct file_transfer *xfer = (struct file_transfer *)data;
+	int rt, i;
+	char buf[FT_BUFFER_SIZE];
+
+	if (condition & GAIM_INPUT_READ) {
+		rt = read(xfer->fd, buf, MIN(xfer->bytesleft, FT_BUFFER_SIZE));
+		/* XXX What if the transfer is interrupted while we
+		 * are inside read()?  How can this be handled safely?
+		 * -- wtm
+		 */
+		if (rt > 0) {
+			xfer->bytesleft -= rt;
+			for (i = 0; i < rt; i++) {
+				fprintf(xfer->file, "%c", buf[i]);
+			}
+		}
+
+	}
+	else /* (condition & GAIM_INPUT_WRITE) */ {
+		int remain = MIN(xfer->bytesleft, FT_BUFFER_SIZE);
+
+		for (i = 0; i < remain; i++)
+			fscanf(xfer->file, "%c", &buf[i]);
+
+		rt = write(xfer->fd, buf, remain);
+		if (rt > 0)
+			xfer->bytesleft -= rt;
+	}
+
+	if (rt < 0)
+		return;
+
+	xfer->bytessent += rt;
+
+	if (xfer->gc->prpl->file_transfer_data_chunk)
+		xfer->gc->prpl->file_transfer_data_chunk(xfer->gc, xfer, buf, rt);
+
+	if (rt > 0 && xfer->bytesleft == 0) {
+		/* We are done with this file! */
+		gaim_input_remove(xfer->watcher);
+		xfer->watcher = 0;
+		fclose(xfer->file);
+		xfer->file = 0;
+		ft_nextfile(xfer);
+	}
+}
+
+static void ft_nextfile(struct file_transfer *xfer)
+{
+	debug_printf("file transfer %d of %d done\n",
+			xfer->filesdone + 1, xfer->totfiles);
+
+	if (++xfer->filesdone == xfer->totfiles) {
+		char *msg;
+		char *msg2;
+
+		xfer->gc->prpl->file_transfer_done(xfer->gc, xfer);
+
+		if (xfer->type == FILE_TRANSFER_TYPE_RECEIVE)
+			msg = g_strdup_printf(_("File transfer from %s to %s completed successfully."),
+					xfer->who, xfer->gc->username);
+		else 
+			msg = g_strdup_printf(_("File transfer from %s to %s completed successfully."),
+					xfer->gc->username, xfer->who);
+		xfer->state = FILE_TRANSFER_STATE_DONE;
+
+		if (xfer->totfiles > 1)
+			msg2 = g_strdup_printf(_("%d files transferred."), 
+						xfer->totfiles);
+		else
+			msg2 = NULL;
+
+		do_error_dialog(msg, msg2, GAIM_INFO);
+		g_free(msg);
+		if (msg2)
+			g_free(msg2);
+
+		ft_delete(xfer);
+	}
+	else {
+		xfer->gc->prpl->file_transfer_nextfile(xfer->gc, xfer);
+	}
+}
+
--- a/src/prpl.h	Tue Oct 08 23:45:20 2002 +0000
+++ b/src/prpl.h	Wed Oct 09 04:55:29 2002 +0000
@@ -79,6 +79,8 @@
 
 typedef void (*proto_init)(struct prpl *);
 
+struct file_transfer;
+
 struct _prpl_smiley {
 	char *key;
 	char **xpm;
@@ -180,6 +182,14 @@
 	void (* convo_closed)   (struct gaim_connection *, char *who);
 
 	char *(* normalize)(const char *);
+
+	/* transfer files */
+	void (* file_transfer_cancel)    (struct gaim_connection *, struct file_transfer *);
+	void (* file_transfer_in)  (struct gaim_connection *, struct file_transfer *, int);
+	void (* file_transfer_out) (struct gaim_connection *, struct file_transfer *, const char *, int, int);
+	void (* file_transfer_nextfile)  (struct gaim_connection *, struct file_transfer *);
+	void (* file_transfer_data_chunk)(struct gaim_connection *, struct file_transfer *, const char *, int);
+	void (* file_transfer_done)      (struct gaim_connection *, struct file_transfer *);
 };
 
 extern GSList *protocols;
@@ -210,4 +220,18 @@
 
 extern GSList *add_smiley(GSList *, char *, char **, int) ;
 
+/* file transfer stuff */
+extern struct file_transfer *transfer_in_add(struct gaim_connection *gc,
+		const char *who, const char *filename, int totsize,
+		int totfiles, const char *msg);
+extern struct file_transfer *transfer_out_add(struct gaim_connection *gc,
+		const char *who);
+extern int transfer_abort(struct file_transfer *xfer, const char *why);
+extern int transfer_out_do(struct file_transfer *xfer, int fd,
+		int offset);
+extern int transfer_in_do(struct file_transfer *xfer, int fd,
+		const char *filename, int size);
+int transfer_get_file_info(struct file_transfer *xfer, int *size,
+		char **name);
+
 #endif /* _PRPL_H_ */