/*
 *  cache-file.c
 *  mod_musicindex
 *
 *  $Id: cache-file.c 1011 2012-08-07 20:23:39Z varenet $
 *
 *  Created by Thibaut VARENE on Wed Feb 23 2005.
 *  Copyright (c) 2003-2007,2009-2010 Thibaut VARENE
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License version 2.1,
 *  as published by the Free Software Foundation.
 *
 */

/**
 * @file
 * Flat file cache management subsystem.
 *
 * @author Thibaut Varene
 * @version $Revision: 1011 $
 * @date 2003-2010
 *
 * @warning 'errno' seems not to be seen as volatile by the compiler, therefore we
 *	cannot do "if(mkdir(foo) && errno == EEXIST)" for instance.
 * @bug No flat file cache support for Solaris.
 *
 * @warning We do rely on success of chdir() calls.
 *
 * @todo O P T I M I Z E ! (maybe)
 */

#include "playlist.h"
#include "cache-file.h"

#ifdef HAVE_DIRENT_H
#include <dirent.h>	/* opendir & friends */
#endif
#include <errno.h>
#include <stdio.h>	/* fprintf / fscanf / fileno */
#include <sys/file.h>	/* flock */
#include <fcntl.h>	/* open */
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>	/* file handling */
#endif
#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>	/* file handling */
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>	/* file handling */
#endif
#ifdef HAVE_STRING_H
#include <string.h>	/* strerror() */
#endif

#define CACHE_VERS	12
#define CACHE_NF	15

#define CA_OK		0
#define CA_FATAL	10
#define CA_MISSARG	20
#define CA_CREATE	30
#define CA_LOCKED	40

/** test for "." and ".." dir entries. x is a char * */
#define ISDOT(x)	( (x)[0] == '.' && (!(x)[1] || ((x)[1] == '.' && !(x)[2])) )
/** if char *x is NULL, print a printable ascii char (white space would be trashed by fscanf alas) */
#define BLANKSTR(x)	( !(x) ? "#" : (x) )
/** Detection of our "home made" encoding of NULL strings */
#define ISBLANK(x)	( ((x)[0] == '#') && ((x)[1] == '\0') )

/**
 * Handles error for the flat file cache subsystem.
 *
 * This function handles various errors depending on errno's value.
 *
 * @param r Apache request_rec struct to handle log writings.
 * @param caller A string (eg. calling function name) used in messages sent.
 *
 * @todo Many things.
 */
static void error_handler(request_rec *r, const char *const caller)
{
	if (!r)
		return;
	
	switch (errno) {
		case EPERM:
			/* The filesystem containing pathname does not support the creation of directories. */
			mi_rerror("(%s) Can't create/delete directory.", caller);
			break;
#if 0
		case EISDIR:
			/* pathname refers to a directory.  (This is  the  non-POSIX  value returned by Linux since 2.1.132.) */
		case EINVAL:
			/* mode  requested  creation of something other than a normal file, device special file or FIFO */
		case EEXIST:
			/* pathname already exists (not necessarily as a directory). */
		case EFAULT:
			/* pathname points outside your accessible address space. */
#endif
		case EACCES:
			/* The parent directory does not allow write permission to the  process,  or  one  of  the
			directories in pathname did not allow search (execute) permission. */
			mi_rerror("(%s) Permission denied.", caller);
			break;
		case EMFILE:
			/* Too many file descriptors in use by process. */
		case ENFILE:
			/* Too many files are currently open in the system. */
			mi_rerror("(%s) Too many open files!", caller);
			break;
		case ENAMETOOLONG:
			/* pathname was too long. */
			mi_rerror("(%s) Pathname was too long.", caller);
			break;
		case ENOENT:
			/* A directory component in pathname does not exist or is a dangling symbolic link. */
			break;
#if 0
		case ENOTDIR:
			/* A component used as a directory in pathname is not, in fact, a directory. */
		case ENOTEMPTY:
			/* pathname contains entries other than . and .. . */
#endif
		case ENOMEM:
			/* Insufficient kernel memory was available. */
			mi_rerror("(%s) Out Of Memory!", caller);
			break;
		case EROFS:
			/* pathname refers to a file on a read-only filesystem. */
			mi_rerror("(%s) Read-Only filesystem!", caller);
			break;
		case ELOOP:
			/* Too many symbolic links were encountered in resolving pathname. */
			mi_rerror("(%s) Too many symbolic links.", caller);
			break;
		case EIO:
			/* An I/O error occured. */
			mi_rerror("(%s) I/O error.", caller);
			break;
		case ENOSPC:
			/* The device containing pathname has no room for the new directory.
			The new directory cannot be created because the user's disk quota is exhausted. */
			mi_rerror("(%s) No space left on device!", caller);
			break;
		default:
			mi_rerror("(%s) - error_handler! %s", caller, strerror(errno));
			break;
	}
	return;
}

/**
 * Creates cache subdirectories.
 *
 * This subroutine takes care of creating all requested directories and
 * subdirectories if they don't already exist and if possible.
 *
 * @warning If dirpath begins with a "/" the function will return immediately.
 *
 * @param r Apache request_rec struct to handle log writings.
 * @param dirpath A string representing a path to create.
 *
 * @return CA_OK on succes, CA_FATAL otherwise.
 */
static short file_cache_make_dir(request_rec *r, const char *const dirpath)
{
	register unsigned short l = 0, m = 0;
	char *tempdir = NULL;
	short ret = CA_FATAL;

	do {	/* We build the path subdirs by subdirs, in a "mkdir -p" fashion */
		tempdir = realloc(tempdir, (m + (l = strcspn(dirpath + m, "/")) + 1));
		if (!tempdir)
			goto error_out;
		
		/* XXX TODO make better use of realloc() by using strncat() here */
		strncpy(tempdir, dirpath, m + l);
		tempdir[m+l] = '\0';
		m += l;

		if (!l)
			break;

		/* skipping (potentially multiple) slashes */
		while (dirpath[m] == '/')
			m++;

		if (mkdir(tempdir, S_IRWXU)) {
			if (errno != EEXIST)
				goto error_out;
		}
	} while (1);

	ret = CA_OK;
	
error_out:
	free(tempdir);
	if (ret == CA_FATAL)	// on est susceptible d'avoir de la merde en cas d'out of mem... pas grave
		error_handler(r, __FUNCTION__);
	return ret;
}

/**
 * Removes cache subdirectories.
 *
 * This subroutine takes care of removing any given directory and
 * its content (recursively) if any, and if possible.
 *
 * @param r Apache request_rec struct to handle log writings.
 * @param cachedir A DIR stream corresponding to the directory to remove.
 * @param curdir A string representing the absolute path of the corresponding
 *	parent directory on the "original" filesystem.
 */
static void file_cache_remove_dir(request_rec *r, DIR *cachedir, const char *const curdir)
{
	DIR		*subdir = NULL;
	struct dirent	*cachedirent = NULL;
	struct stat	origdirstat;
	const char 	*origdir = NULL;

	if (unlikely(fchdir(dirfd(cachedir))))
		return;		/* on se place dans le repertoire de cache */

	while ((cachedirent = readdir(cachedir))) {	/* on parcourt le repertoire */
		if (ISDOT(cachedirent->d_name))		/* We'd rather avoid trying to remove the whole filesystem... */
			continue;

		if (unlink(cachedirent->d_name)) {	/* We try to remove any entry (actually we will only remove regular files) */
				if ((errno == EISDIR) || (errno == EPERM)) {
				/* On BSDs unlink() returns EPERM on non empty directories.
				 * This shouldn't lead to infloop because of subsequent tests.
				 * If it's a directory, we check that the "original" still exists.
				 * If not, we remove it recursively.
				 * Reminder: "errno == (EISDIR || EPERM)" doesn't work */
				origdir = apr_pstrcat(r->pool, curdir, "/", cachedirent->d_name, NULL);
				if (stat(origdir, &origdirstat)) {
					if (rmdir(cachedirent->d_name)) {	/* stat() sets errno. We have to split */
						if (errno == ENOTEMPTY) {			/* il est pas vide, bigre! */
							subdir = opendir(cachedirent->d_name);	/* on ouvre le vilain repertoire pour en supprimer le contenu */
							file_cache_remove_dir(r, subdir, origdir);	/* en rappelant recursivement la fonction sur son contenu. */
							closedir(subdir);			/* a noter que dans ce cas la il y a un test inutile, celui qui verifie si l'original existe tjrs. Mais bon. */
							if (fchdir(dirfd(cachedir)));		/* on retourne au repertoire precedent */
							rmdir(cachedirent->d_name);		/* maintenant il est vide, et on peut pas avoir d'erreur vu les tests precedants */
						}
						else
							error_handler(r, __FUNCTION__);		/* Oops, on est tombe sur une merde */
					}
				}
			}
			else
				error_handler(r, __FUNCTION__);		/* Oops, on est tombe sur une merde, mais plus tot */
		}
	}

	return;
}

/**
 * Checks if a directory already exists in the cache.
 *
 * This function takes advantage of the standard behaviour of the cache
 * backend in musicindex: this function is called when the caller wants to know
 * whether the backend handles a given directory. We always return NULL,
 * pretending we don't, as "handling" means being able to provide a full listing
 * straight out of cache, which we don't in this particular incarnation, but we
 * take the opportunity to do our own cooking with our cache data, creating the
 * directory in cache if need be, and checking for cache data sanity.
 * 
 *
 * @param r Apache request_rec struct.
 * @param pack Ignored
 * @param filename current directory path
 * @param uri server-side uri (unused)
 * @param soptions Flags to use for created entries. Ignored.
 *
 * @return NULL.
 */
static void* cache_file_opendir(request_rec *r, mu_pack *const pack,
				const char * const filename, const char * uri, unsigned long soptions)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	DIR		*cachedir = NULL;
	struct stat	cachedirstat, dirstat;

	if (!filename || !conf->cache_setup)
		return NULL;

	/* Bear in mind we're chdir'd from now on. */
	if (unlikely(chdir((char *)(conf->cache_setup))))
		return NULL;

	/* Actually check for the directory in the cache, create it if needed.
	 * "+ 1" offset to suppress leading '/'. */
	if (!(cachedir = opendir(filename + 1))) {			/* on essaye d'ouvrir le repertoire concerne dans le cache (on supprime le leading "/" */
		if (errno == ENOENT) {				/* il n'existe pas mais on peut le creer (ca correspond a ENOENT, a verifier) */
			if (file_cache_make_dir(r, filename + 1))	/* on envoie le chemin prive du leading '/' */
				goto error_out;
		}
		else
			goto error_out;				/* un autre probleme, on degage */
	}
	else {	/* Checking for cache sanity. Has it expired for that folder ? If so, delete its content. */
		fstat(dirfd(cachedir), &cachedirstat);		/* recuperons les stats du repertoire cache. XXX On considere cet appel sans echec vu les tests qu'on a fait avant. */
		stat(filename, &dirstat);				/* recuperons les stats du rep d'origine. XXX pas de test ici, a priori ya pas de raison qu'on puisse pas les recuperer */
		if (cachedirstat.st_mtime < dirstat.st_mtime)	/* si la date de modif du rep de cache est plus vieille que celle du rep original, alors qqc a ete ajoute ou retire ou ecrit */
			file_cache_remove_dir(r, cachedir, filename);	/* alors on le vide proprement de son contenu */
		closedir(cachedir);				/* On en a fini avec le repertoire, on le referme */
		if (file_cache_make_dir(r, filename + 1))	/* on recree le rep */
			goto error_out;		
	}

	return NULL;

error_out:
	error_handler(r, __FUNCTION__);
	return NULL;
}

/** struct type used to reduce memory usage */
struct mi_data_buffer {
	char title[MAX_STRING];
	char album[MAX_STRING];
	char artist[MAX_STRING];
	char genre[MAX_GENRE];
};

/**
 * Fills in the information fields about a music file from the cache.
 *
 * This function reads the tags from the cache file
 * and fills in the struct mu_ent fields accordingly.
 *
 * @warning @b MUST close in file handler on success.
 *
 * @param r Apache request_rec struct to handle log writings.
 * @param pool Apache pool
 * @param in Not used (except for closing).
 * @param filename current filename.
 *
 * @return When possible, struct mu_ent correctly set up, file stream closed,
 * 		NULL otherwise.
 */
static mu_ent *file_make_cache_entry(request_rec *r, apr_pool_t *pool, FILE *const in,
	const char *const filename)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	mu_ent		*p = NULL;
	short 		result = 0;
	unsigned short	track, posn, flags, cvers = 0;
	signed short	filetype;
	int		fdesc;
	FILE		*cache_file = NULL;
	struct mi_data_buffer	*data_buffer = NULL;

	/* Bear in mind we're chdir'd from now on. */
	if (unlikely(chdir((char *)(conf->cache_setup))))
		return p;

	/* Actually check for the file in the cache, open it if possible.
	 * "+ 1" offset to suppress leading '/'.
	 * Dev note: O_SHLOCK is BSD specific */
	fdesc = open(filename + 1, O_RDONLY|O_NONBLOCK);
	if (unlikely(fdesc < 0)) {
		if (likely((errno == ENOENT) || (errno == EWOULDBLOCK) || (errno == EAGAIN)))
			return p;	/* Creation of the file is handled separately (playlist.c) */
		else
			goto error_out;	/* game over */
	}
	
	/* We acquire a shared advisory lock on the file to be (almost) certain of its integrity.
	 * This will prevent reading from incomplete cache files. The lock in non blocking:
	 * if we can't get it, we won't wait to read the file, we'll delegate to the original handler. */
	if (flock(fdesc, LOCK_SH|LOCK_NB)) {
		close(fdesc);
		return p;
	}
	
	cache_file = fdopen(fdesc, "r");
	if (unlikely(!cache_file))
		goto error_out;

	/* Dev note: mixing unix and std IO is ugly, but there's no flockfile() counterpart to the shared
	 * advisory lock, alas, see flockfile(3), flock(2) and lockf(3). Besides, we have to lock, since
	 * fread()/fwrite() are thread safe, but not fscanf() and fprintf() */

	p = NEW_ENT(pool);
	if (likely(p)) {
		data_buffer = (struct mi_data_buffer *)malloc(sizeof(struct mi_data_buffer)); /* This should save some memory */
		if (likely(data_buffer)) {
			result = fscanf(cache_file, "album: %[^\n]\nartist: %[^\n]\n"
				"title: %[^\n]\ndate: %hu\ntrack: %hu\nposn: %hu\n"
				"length: %hu\nbitrate: %lu\nfreq: %hu\nsize: %lu\n"
				"filetype: %hi\ngenre: %[^\n]\nmtime: %lu\nflags: %hx\n"
				"cvers: %hu\n",
				data_buffer->album, data_buffer->artist, data_buffer->title, &p->date, &track, &posn, &p->length,
				&p->bitrate, &p->freq, &p->size, &filetype, data_buffer->genre, &p->mtime, &flags, &cvers);
		
			/* Check whether the cache is somehow corrupted */
			if (unlikely((result != CACHE_NF) || (cvers != CACHE_VERS))) {	/* fscanf() returns the number of input items assigned */
				p = NULL;					/* hopefuly p allocs should be cleaned by apache */
			}
			else {
				p->title = apr_pstrdup(pool, data_buffer->title);
				if (!ISBLANK(data_buffer->album))
					p->album = apr_pstrdup(pool, data_buffer->album);
				if (!ISBLANK(data_buffer->artist))
					p->artist = apr_pstrdup(pool, data_buffer->artist);
				if (!ISBLANK(data_buffer->genre))
					p->genre = apr_pstrdup(pool, data_buffer->genre);

				/* We have to use that trick, fscanf won't work on char variables */
				p->filetype = filetype;
				p->flags = flags;
				p->track = track;
				p->posn = posn;
			}
		
			free(data_buffer);
		}
		else
			p = NULL;	/* something failed, return non-bogus data. p will be cleaned up by apache's GC */
	}

	/* fclose() will also close() fdesc and thus release the lock */
	fclose(cache_file);

	if (likely(p))
		fclose(in);	/* this part of the cache subsystem is (uglily) seen as part of the playlist system,
				and has to behave as such. This is why we close the input file if we took advantage of it. */

	return p;

error_out:
	error_handler(r, __FUNCTION__);
	close(fdesc);
	return p;
}

/**
 * Creates and writes cache file information.
 *
 * This function creates a new cache file (for #filename), and
 * fills it with the data contained in the mu_ent p structure.
 *
 * @param r Apache request_rec struct to handle log writings.
 * @param p A mu_ent struct containing the data to store.
 * @param filename current filename of the file to cache.
 */
static void cache_file_write(request_rec *r, const mu_ent *const p, const char *const filename)
{
	const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
	int	fdesc;
	FILE	*cache_file = NULL;

	/* we don't deal with directories themselves */
	if (p->filetype < 0)
		return;
	
	if (chdir((char *)conf->cache_setup))
		return;
	
	/* Dev note: O_EXLOCK is BSD specific. */
	fdesc = open(filename + 1, O_WRONLY|O_NONBLOCK|O_CREAT, S_IRUSR|S_IWUSR);
	if (fdesc < 0) {
		if ((errno == EWOULDBLOCK) || (errno == EAGAIN))	/* does this work? */
			return;	/* Creation of the file is handled separately (playlist.c) */
		else
			goto error_out;	/* game over */
	}
	
	/* We acquire an exclusive advisory lock on the file to avoid corruption by another process.
	 * This will also prevent reading from incomplete cache. see cache_read_file() comments. */
	if (flock(fdesc, LOCK_EX|LOCK_NB)) {
		fclose(cache_file);
		return;
	}
	
	cache_file = fdopen(fdesc, "w"); /* now open the fdesc with stdio routines */

	/* let's check if something bad happened */
	if (!cache_file)
		goto error_out;

	fprintf(cache_file, "album: %s\nartist: %s\ntitle: %s\ndate: %hu\n"
		"track: %hhu\nposn: %hhu\nlength: %hu\nbitrate: %lu\nfreq: %hu\n"
		"size: %lu\nfiletype: %hi\ngenre: %s\nmtime: %lu\nflags: %hhx\ncvers: %hu\n",
		BLANKSTR(p->album), BLANKSTR(p->artist), p->title, p->date,
		p->track, p->posn, p->length, p->bitrate, p->freq, p->size, p->filetype,
		BLANKSTR(p->genre), p->mtime, (p->flags & EF_FLAGSTOSAVE), CACHE_VERS);

	/* fclose() will also close() fdesc and thus release the lock */
	fclose(cache_file);

	return;

error_out:
	error_handler(r, __FUNCTION__);
	close(fdesc);
}

static const cache_backend cache_backend_file = {
	.opendir =	cache_file_opendir,
	.readdir =	NULL,
	.closedir =	NULL,
	.make_entry =	file_make_cache_entry,
	.write =	cache_file_write,
	.prologue =	NULL,
	.epilogue =	NULL,
};

int cache_file_setup(cmd_parms *cmd, const char *const setup_string, mu_config *const conf)
{
	server_rec *s = cmd->server;
	static const char biniou[] = "file://";
	int ret = 1;

	if (strncmp(biniou, setup_string, 7) == 0) {
		ret = -1;
		char *restrict csetup = apr_pstrdup(cmd->pool, setup_string+6);
		if (!csetup)
			goto exit;
#if 0	/* this is never happening since we check against "file://" */
		if (csetup[0] != '/') {
			/* for now we only work with absolute paths */
			mi_serror("Non absolute cache directory path: %s", csetup);
			goto exit;
		}
#endif
		if ( (access(csetup, X_OK|W_OK)) || (chdir((char *)(csetup))) ) {
			mi_serror("%s", strerror(errno));
			goto exit;
		}
		conf->cache_setup = csetup;
		conf->cache = &cache_backend_file;
		ret = 0;
	}
	
exit:
	if (-1 == ret)
		mi_serror("Error setting up %s cache!", "file");
	return ret;
}
