/*
 * $Id: move.c,v 1.3 2004/10/24 01:32:21 cbiere Exp $
 *
 * Copyright (c) 2002-2003, Raphael Manfredi
 *
 * Asychronous file moving operations.
 *
 *----------------------------------------------------------------------
 * This file is part of gtk-gnutella.
 *
 *  gtk-gnutella 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.
 *
 *  gtk-gnutella 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 gtk-gnutella; if not, write to the Free Software
 *  Foundation, Inc.:
 *      59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *----------------------------------------------------------------------
 */

#include "common.h"

RCSID("$Id: move.c,v 1.3 2004/10/24 01:32:21 cbiere Exp $");

#include "downloads.h"
#include "fileinfo.h"
#include "move.h"

#include "lib/atoms.h"
#include "lib/bg.h"
#include "lib/file.h"
#include "lib/walloc.h"

#include "if/gnet_property.h"
#include "if/gnet_property_priv.h"

#include "lib/override.h"		/* Must be the last header included */

#define COPY_BLOCK_SHIFT	12			/* Power of two of copy unit credit */
#define COPY_BUF_SIZE		65536		/* Size of the reading buffer */

static gpointer move_daemon = NULL;

#define MOVED_MAGIC	0x00c0b100

/*
 * Moving daemon context.
 */
struct moved {
	gint magic;				/* Magic number */
	struct download *d;		/* Download for which we're moving file */
	gint rd;				/* Opened file descriptor for read, -1 if none */
	gint wd;				/* Opened file descriptor for write, -1 if none */
	time_t start;			/* Start time, to determine copying rate */
	filesize_t size;		/* Size of file */
	filesize_t copied;		/* Amount of data copied so far */
	gchar *buffer;			/* Large buffer, where data is read */
	gchar *target;			/* Target file name, in case an error occurs */
	gint error;				/* Error code */
};

/*
 * Work queue entry.
 */
struct work {
	struct download *d;		/* Download to move */
	gchar *dest;			/* Target directory (atom) */
	gchar *ext;				/* Trailing extension (atom) */
};

/*
 * we_alloc
 *
 * Allocate work queue entry.
 */
static struct work *
we_alloc(struct download *d, const gchar *dest, const gchar *ext)
{
	struct work *we;

	we = walloc(sizeof(*we));
	we->d = d;
	we->dest = atom_str_get(dest);
	we->ext = atom_str_get(ext);

	return we;
}

/*
 * we_free
 *
 * Freeing of work queue entry.
 */
static void
we_free(gpointer data)
{
	struct work *we = (struct work *) data;

	atom_str_free(we->dest);
	atom_str_free(we->ext);
	wfree(we, sizeof(*we));
}

/**
 * Signal handler for termination.
 */
static void
d_sighandler(gpointer unused_h, gpointer u, bgsig_t sig)
{
	struct moved *md = (struct moved *) u;

	(void) unused_h;
	g_assert(md->magic == MOVED_MAGIC);

	switch (sig) {
	case BG_SIG_TERM:
		/*
		 * Get rid of incompletely moved file.  Moving will be resumed
		 * when we are relaunched.
		 */

		if (md->target != NULL && -1 == unlink(md->target))
			g_warning("cannot unlink \"%s\": %s",
				md->target, g_strerror(errno));
		break;
	default:
		break;
	}
}

/*
 * d_free
 *
 * Freeing of computation context.
 */
static void
d_free(gpointer ctx)
{
	struct moved *md = (struct moved *) ctx;

	g_assert(md->magic == MOVED_MAGIC);

	if (md->rd != -1)
		close(md->rd);

	if (md->wd != -1)
		close(md->wd);

	G_FREE_NULL(md->buffer);
	wfree(md, sizeof(*md));
}

/**
 * Daemon's notification of start/stop.
 */
static void
d_notify(gpointer unused_h, gboolean on)
{
	(void) unused_h;
	gnet_prop_set_boolean_val(PROP_FILE_MOVING, on);
}

/**
 * Daemon's notification: starting to work on item.
 */
static void
d_start(gpointer h, gpointer ctx, gpointer item)
{
	struct moved *md = (struct moved *) ctx;
	struct work *we = (struct work *) item;
	struct download *d = we->d;
	gchar *source = NULL;
	struct stat buf;
	const gchar *name;

	g_assert(md->magic == MOVED_MAGIC);
	g_assert(md->rd == -1);
	g_assert(md->wd == -1);
	g_assert(md->target == NULL);

	download_move_start(d);
	bg_task_signal(h, BG_SIG_TERM, d_sighandler);

	source = make_pathname(download_path(d), download_outname(d));
	g_return_if_fail(NULL != source);

	md->d = we->d;
	md->rd = file_open(source, O_RDONLY);

	if (md->rd == -1) {
		md->error = errno;
		goto abort_read;
	}

	if (-1 == fstat(md->rd, &buf)) {
		md->error = errno;
		g_warning("can't fstat \"%s\": %s", source, g_strerror(errno));
		goto abort_read;
	}

	if (!S_ISREG(buf.st_mode)) {
		g_warning("file \"%s\" is not a regular file", source);
		goto abort_read;
	}

	/*
	 * Don't keep an URN-like name when the file is done, if possible.
	 */

	name = file_info_readable_filename(d->file_info);

	md->target = unique_filename(we->dest, name, we->ext);
	if (NULL == md->target)
		goto abort_read;

	md->wd = file_create(md->target, O_WRONLY | O_TRUNC, buf.st_mode);

	if (md->wd == -1)
		goto abort_read;

	md->start = time(NULL);
	md->size = download_filesize(d);
	md->copied = 0;
	md->error = 0;

	if (dbg > 1)
		g_message("Moving \"%s\" to \"%s\"",
				download_outname(d), md->target);

	G_FREE_NULL(source);

	return;

abort_read:
	md->error = errno;
	if (md->rd != -1)
		close(md->rd);
	md->rd = -1;
	g_warning("can't copy \"%s\" to \"%s\"", source, we->dest);
	if (NULL != source)
		G_FREE_NULL(source);
	return;
}

/**
 * Daemon's notification: finished working on item.
 */
static void
d_end(gpointer h, gpointer ctx, gpointer item)
{
	struct moved *md = (struct moved *) ctx;
	struct download *d = md->d;
	gint elapsed = 0;

	g_assert(md->magic == MOVED_MAGIC);
	g_assert(md->d == ((struct work *) item)->d);

	bg_task_signal(h, BG_SIG_TERM, NULL);

	if (md->rd == -1) {			/* Did not start properly */
		g_assert(md->error);
		goto finish;
	}

	close(md->rd);
	md->rd = -1;

	close(md->wd);
	md->wd = -1;

	/*
	 * If copying went well, get rid of the source file.
	 *
	 * If an error occurred, the target file is removed, whilst the source
	 * file is kept intact.
	 */

	if (md->error == 0) {
		gchar *source;

		g_assert(md->copied == md->size);
		
		source = make_pathname(download_path(md->d), download_outname(md->d));
		if (NULL == source || -1 == unlink(source))
			g_warning("cannot unlink \"%s\": %s",
				download_outname(md->d), g_strerror(errno));
		if (NULL != source)
			G_FREE_NULL(source);
	} else {
		if (md->target != NULL && -1 == unlink(md->target))
			g_warning("cannot unlink \"%s\": %s",
				md->target, g_strerror(errno));
	}

	elapsed = delta_time(time(NULL), md->start);
	elapsed = MAX(1, elapsed);		/* time warp? clock not monotic? */

	if (dbg > 1)
		printf("Moved file \"%s\" at %lu bytes/sec [error=%d]\n",
			download_outname(md->d), (gulong) md->size / elapsed, md->error);

finish:
	G_FREE_NULL(md->target);

	if (md->error == 0)
		download_move_done(d, elapsed);
	else
		download_move_error(d);
}

/**
 * Copy file around, incrementally.
 */
static bgret_t
d_step_copy(gpointer h, gpointer u, gint ticks)
{
	struct moved *md = (struct moved *) u;
	ssize_t r;
	size_t amount;
	guint64 remain;
	gint used;

	g_assert(md->magic == MOVED_MAGIC);

	if (md->rd == -1)			/* Could not open the file */
		return BGR_DONE;		/* Computation done */

	if (md->size == 0)			/* Empty file */
		return BGR_DONE;

	remain = md->size - md->copied;

	g_assert(remain > 0);

	/*
	 * Each tick we have can buy us 2^COPY_BLOCK_SHIFT bytes.
	 *
	 * We read into a COPY_BUF_SIZE bytes buffer, and at most md->size
	 * bytes total, to stop before the fileinfo trailer.
	 */

	amount = ticks << COPY_BLOCK_SHIFT;
	remain = MIN(remain, COPY_BUF_SIZE);
	amount = MIN(amount, remain);

	g_assert(amount > 0);

	r = read(md->rd, md->buffer, amount);
	if ((ssize_t) -1 == r) {
		md->error = errno;
		g_warning("error while reading \"%s\" for moving: %s",
			download_outname(md->d), g_strerror(errno));
		return BGR_DONE;
	} else if (r == 0) {
		g_warning("EOF while reading \"%s\" for moving!",
			download_outname(md->d));
		md->error = -1;
		return BGR_DONE;
	}

	/*
	 * Any partially read block counts as one block, hence the second term.
	 */

	used = (r >> COPY_BLOCK_SHIFT) +
		((r & ((1 << COPY_BLOCK_SHIFT) - 1)) ? 1 : 0);

	if (used != ticks)
		bg_task_ticks_used(h, used);

	r = write(md->wd, md->buffer, amount);
	if (r < 0) {
		md->error = errno;
		g_warning("error while writing for moving \"%s\": %s",
			download_outname(md->d), g_strerror(errno));
		return BGR_DONE;
	} else if ((size_t) r < amount) {
		md->error = -1;
		g_warning("short write whilst moving \"%s\"", download_outname(md->d));
		return BGR_DONE;
	}

	g_assert((size_t) r == amount);

	md->copied += r;
	download_move_progress(md->d, md->copied);

	return md->copied == md->size ? BGR_DONE : BGR_MORE;
}

/**
 * Enqueue completed download file for verification.
 */
void
move_queue(struct download *d, const gchar *dest, const gchar *ext)
{
	struct work *we;

	we = we_alloc(d, dest, ext);
	bg_daemon_enqueue(move_daemon, we);
}

/**
 * Initializes the background moving/copying task.
 */
void
move_init(void)
{
	struct moved *md;
	bgstep_cb_t step = d_step_copy;

	md = walloc(sizeof(*md));
	md->magic = MOVED_MAGIC;
	md->rd = -1;
	md->wd = -1;
	md->buffer = g_malloc(COPY_BUF_SIZE);
	md->target = NULL;

	move_daemon = bg_daemon_create("file moving",
		&step, 1,
		md, d_free,
		d_start, d_end, we_free,
		d_notify);
}

/**
 * Called at shutdown time.
 */
void move_close(void)
{
	bg_task_cancel(move_daemon);
}

/* vi: set ts=4 sw=4 cindent: */
