/*
    libgutenfetch - a small library to help developers of utilities to list
	and fetch books available through Project Gutenberg.

    Copyright (C) 2001, 2002, 2003, 2004 Russell Francis 

    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

Last updated on 
$Date: 2004/07/07 02:41:22 $
$Author: johntabularasa $.

*/

#include "stddefs.h"
#include "gutenfetch.h"
#include "libgutenfetch_cache.h"
#include "libgutenfetch_init.h"
#include "libgutenfetch_utility.h"
#include <curl/curl.h>
#ifdef HAVE_FCNTL_H
#	include <fcntl.h>
#endif /* HAVE_FCNTL_H */
#ifdef HAVE_SYS_TYPES_H
#	include <sys/types.h>
#endif /* HAVE_SYS_TYPES_H */
#ifdef HAVE_SYS_STAT_H
#	include <sys/stat.h>
#endif /* HAVE_SYS_STAT_H */
#ifdef HAVE_ERRNO_H
#	include <errno.h>
#endif /* HAVE_ERRNO_H */
#ifdef HAVE_ASSERT_H
#	include <assert.h>
#endif /* HAVE_ASSERT_H */
#ifdef HAVE_STRING_H
#	include <string.h>
#endif /* HAVE_STRING_H */
#ifdef HAVE_STRINGS_H
#	include <strings.h>
#endif /* HAVE_STRINGS_H */

static int
	gutenfetch_curl_progress_func(void *, double, double, double, double);

typedef struct {
	void *data;
	int (*func)(void *, double, double, double, const char *);
	char msg[4096];
} progress_struct_t;

extern int errno;
static int cache_enabled = FALSE;
static char *cache_base_dir = NULL;
static time_t expires = 0;


/**
 * gutenfetch_curl_write
 *
 * This is a private function which is called by
 * CURL periodically to write the downloaded data
 * to the file descriptor passed in through data.
 *
 * @param ptr The buffer of data which CURL downloaded.
 * @param size The size of each element in the buffer.
 * @param nelem The number of elements in the buffer.
 * @param data User supplied pointer to file descriptor where
 *			buffer should be written to.
 * @return Same as write, number of bytes written on success,
 *			-1 and errno is set on failure.  CURL will abort
 *			a transfer on failure.
 */
size_t
gutenfetch_curl_write(
	void *ptr,
	size_t size,
	size_t nelem,
	void *data)
{
	int fd;
	
	if (data == NULL) {
		return -1;
	}
	
	fd = *(int*)data;
	
	if (fd < 0) {
		return -1;
	}
	
	return write(fd, ptr, size * nelem);
}


/**
 * gutenfetch_curl_progress_func
 *
 * Called periodically by CURL to display the progress of 
 * a current download.
 *
 * @param data User specified data to pass to func.
 * @param dltotal The total filesize of the download.
 * @param dlnow The amount we have downloaded.
 * @param ultotal The total filesize of the upload.
 * @param ulnow The amount we have uploaded.
 * @return An integer from the user supplied function.
 *		returning anything but 0 will cause CURL to
 *		abort the transfer.
 */
int 
gutenfetch_curl_progress_func(
	void *data, double dltotal, double dlnow, double ultotal, double ulnow)
{
	progress_struct_t *pstruct = data;
	double total_progress = 0.0;

	assert(pstruct != NULL);
	assert(pstruct->func != NULL);

	if (dltotal != 0) {
		total_progress = dlnow / dltotal;
	}	

	return pstruct->func(
		pstruct->data, total_progress,
		dltotal, dlnow, pstruct->msg);
}


/**
 * gutenfetch_cache_init()
 *
 * Initialize resources used by the cacheing module
 * of libgutenfetch.
 *
 * @param should_enable TRUE if we should enable caching.
 * @return
 */
gutenfetch_error_t
gutenfetch_cache_init(int should_enable)
{
	return gutenfetch_cache_enable(should_enable);
}


/**
 * gutenfetch_cache_shutdown
 *
 * Release all resources held by the cache.
 *
 */
void
gutenfetch_cache_shutdown(void)
{	
	FREE_NULL(cache_base_dir);
}

gutenfetch_error_t
gutenfetch_cache_enable(int e)
{
	char *home_dir;
	int retval;

	if (e == TRUE) {
		home_dir = gutenfetch_util_get_home_directory();
		if (home_dir != NULL) {
			FREE_NULL(cache_base_dir);
			cache_base_dir = gutenfetch_util_strcat(home_dir,
				"/.libgutenfetch-cache", NULL);
			if (cache_base_dir != NULL) {
				retval = mkdir(cache_base_dir, S_IRUSR | S_IWUSR | S_IXUSR );
				if ((retval == 0) ||
				((retval == -1) && (errno == EEXIST))){  /* Success */
					home_dir = gutenfetch_util_strcat(cache_base_dir, DIR_SEPARATOR, NULL);
					FREE_NULL(cache_base_dir);
					cache_base_dir = home_dir;
					cache_enabled = TRUE;
					expires = (60 * 60 * 24 * 7); // 1 week	
				} else {
					cache_enabled = FALSE;
					FREE_NULL(cache_base_dir);
					return GUTENFETCH_UNABLE_TO_INIT_CACHE;
				}
			}	
		}
	} else {
		cache_enabled = FALSE;
		FREE_NULL(cache_base_dir);
	}
	return GUTENFETCH_OK;
}

int
gutenfetch_cache_is_enabled(void)
{
	return cache_enabled;
}

void 
gutenfetch_cache_set_expires(time_t t)
{
	expires = t;
}

time_t
gutenfetch_cache_get_expires(void)
{
	return expires;
}

/**
 * gutenfetch_cache_flush
 *
 * Remove all files in the cache.
 */
gutenfetch_error_t
gutenfetch_cache_flush(void)
{
	if ((cache_enabled == TRUE) && (cache_base_dir != NULL)) {
		gutenfetch_util_rm_below_dir(cache_base_dir);
	}
	return GUTENFETCH_OK;
}

/**
 * gutenfetch_cache_fetch
 *
 * This function looks for a file in the cache, if it
 * finds it, it returns a file descriptor for reading
 * if it doesn't it fetchs the file, stores it in the
 * cache and returns a file descriptor for reading.
 * it it can't do that it returns -1.
 *
 * @param loc Whether we should look in australian or non-australian server.
 * @param file The file we are looking for.
 * @param progress_func NULL or a pointer of a function to display progress
 * 	to.
 * @param progress_func_data Optional user supplied data to progress function.
 * @return A valid file descriptor or -1.
 */
int
gutenfetch_cache_fetch(
	server_location_t loc,
	const char *file,
	int (*progress_func)(void *, double, double, double, const char *),
	void *progress_func_data)
{
	CURL *handle;
	progress_struct_t progress;
	gutenfetch_server_t *server;
	char *cache_filename = NULL;
	char *temp_filename = NULL;
	struct stat sb;
	time_t now;
	int fd = -1;
	char *url;
	
	if (file == NULL)
		return -1;
	
	/* Look for the file in the cache */
	if ((cache_enabled == TRUE) && (cache_base_dir != NULL)) {
		if ((strlen(file) >= 2) && (file[0] == '/')) {
			cache_filename = gutenfetch_util_strcat(cache_base_dir, &file[1], NULL);
		} else {
			cache_filename = gutenfetch_util_strcat(cache_base_dir, file, NULL);
		}	
		if (cache_filename != NULL) {
			if(stat(cache_filename, &sb) != -1) {
				now = time(NULL);
				if ((now - sb.st_mtime) < expires) { /* in cache and valid. */
					fd = open(cache_filename, O_RDONLY);
					FREE_NULL(cache_filename);
					return fd;
				} else {
					unlink(cache_filename);
				}
			}
		}
	}

	
	if (fd == -1) { 	/* The file doesn't exist in the cache */
		// lets try to open a temp file to write this to.
		fd = gutenfetch_util_get_temp_file(&temp_filename);
	}

	/* if we still don't have something, fail! */
	if ((fd == -1) || (temp_filename == NULL)) {
		FREE_NULL(temp_filename);
		FREE_NULL(cache_filename);
		close(fd);
		return -1;
	}

	/* Get the server. */
	/* XXX The australian server is not accessible from the US,
	   if there is anyone who can get to it and wants to test
 	   this code please feel free.
	 */
//	if (loc == NON_AUSTRALIAN) {
		server = gutenfetch_get_active_server();
//	} else {
//		server = gutenfetch_get_aussie_server();
//	}

	if (server == NULL) {
		if (fd != -1) {
			close(fd);
		}	
		FREE_NULL(cache_filename);
		return -1;
	}

	/* Build the URL */
	url = gutenfetch_util_build_URL(server, file);
	gutenfetch_free_server(server);

	handle = gutenfetch_init_curl_handle();
	if (handle == NULL) {
		if (fd != -1) {
			close (fd);
		}
		FREE_NULL(cache_filename);
		FREE_NULL(url);
		return -1;
	}

	// Assign the URL to curl.
	curl_easy_setopt(handle, CURLOPT_URL, url);

	// Setup our progress display function.
	if (progress_func != NULL) {
		progress.data = progress_func_data;
		progress.func = progress_func;
		snprintf(progress.msg, 4096, "Downloading %s", file);
		curl_easy_setopt(handle, CURLOPT_NOPROGRESS, FALSE);
		curl_easy_setopt(handle, CURLOPT_PROGRESSFUNCTION,
			gutenfetch_curl_progress_func);
		curl_easy_setopt(handle, CURLOPT_PROGRESSDATA,
			&progress);
	} else {
		curl_easy_setopt(handle, CURLOPT_NOPROGRESS, TRUE);
	}

	// setup our write function
	curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION,
		gutenfetch_curl_write);

	// fd is guaranteed to be valid here!
	curl_easy_setopt(handle, CURLOPT_WRITEDATA, &fd);
	if (curl_easy_perform(handle) != 0) {
		FREE_NULL(cache_filename);
		close(fd);
	} else {
		lseek(fd, 0, SEEK_SET);
	}
	FREE_NULL(url);

	if ((cache_enabled == TRUE) && (cache_base_dir != NULL)) {
		if(gutenfetch_util_move(temp_filename, cache_filename) == 1) {
			unlink(temp_filename);
			close(fd); /* close the tempfile */
			fd = open(cache_filename, O_RDONLY);
			if (fd > 0) {
				lseek(fd, 0, SEEK_SET);
			}
		}
	}
	
	FREE_NULL(temp_filename);
	FREE_NULL(cache_filename);
	return fd;
}

/**
 * gutenfetch_cache_flush_old
 * 
 * Flush all old entries from the cache.
 */
void
gutenfetch_cache_flush_old(void)
{
	if ((cache_enabled == TRUE) && (cache_base_dir != NULL)) {
		gutenfetch_util_rm_old_below_dir(expires, cache_base_dir);
	}
}


syntax highlighted by Code2HTML, v. 0.9.1