Blob Blame History Raw
/** \file
 *
 * \author Copyright 2000 Scott Fritzinger
 * \author Contributions Lutz Mueller <lutz@users.sf.net> (2001)
 * \author Copyright 2009 Marcus Meissner
 *
 * \par License
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * \par
 * This library 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
 * Lesser General Public License for more details.
 *
 * \par
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

#define _BSD_SOURCE

#include "config.h"
#include <gphoto2/gphoto2-filesys.h>

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include <gphoto2/gphoto2-result.h>
#include <gphoto2/gphoto2-port-log.h>
#include <gphoto2/gphoto2-setting.h>

#include <limits.h>

#ifdef HAVE_LIBEXIF
#  include <libexif/exif-data.h>
#endif

#ifdef ENABLE_NLS
#  include <libintl.h>
#  undef _
#  define _(String) dgettext (GETTEXT_PACKAGE, String)
#  ifdef gettext_noop
#    define N_(String) gettext_noop (String)
#  else
#    define N_(String) (String)
#  endif
#else
#  define textdomain(String) (String)
#  define gettext(String) (String)
#  define dgettext(Domain,Message) (Message)
#  define dcgettext(Domain,Message,Type) (Message)
#  define bindtextdomain(Domain,Directory) (Domain)
#  define _(String) (String)
#  define N_(String) (String)
#endif

#ifndef PATH_MAX
# define PATH_MAX 4096
#endif

typedef struct _CameraFilesystemFile {
	char *name;

	int info_dirty;

	CameraFileInfo info;

	struct _CameraFilesystemFile *lru_prev;
	struct _CameraFilesystemFile *lru_next;
	CameraFile *preview;
	CameraFile *normal;
	CameraFile *raw;
	CameraFile *audio;
	CameraFile *exif;
	CameraFile *metadata;

	struct _CameraFilesystemFile *next; /* in folder */
} CameraFilesystemFile;

typedef struct _CameraFilesystemFolder {
	char *name;

	int files_dirty;
	int folders_dirty;

	struct _CameraFilesystemFolder *next; /* chain in same folder */
	struct _CameraFilesystemFolder *folders; /* childchain of this folder */
	struct _CameraFilesystemFile *files; /* of this folder */
} CameraFilesystemFolder;

/**
 * The default number of pictures to keep in the internal cache,
 * can be overriden by settings.
 */
#define PICTURES_TO_KEEP	2
/**
 * The current number of pictures to keep in the internal cache,
 * either from #PICTURES_TO_KEEP or from the settings.
 */
static int pictures_to_keep = -1;

static int gp_filesystem_lru_clear (CameraFilesystem *fs);
static int gp_filesystem_lru_remove_one (CameraFilesystem *fs, CameraFilesystemFile *item);
static int gp_filesystem_lru_update (CameraFilesystem *fs,
			  const char *folder, const char *filename,
			  CameraFileType type,
			  CameraFile *file, GPContext *context);

#ifdef HAVE_LIBEXIF

static int gp_filesystem_get_file_impl (CameraFilesystem *, const char *,
		const char *, CameraFileType, CameraFile *, GPContext *);

static time_t
get_time_from_exif_tag(ExifEntry *e) {
	struct tm ts;

	e->data[4] = e->data[ 7] = e->data[10] = e->data[13] = e->data[16] = 0;
	ts.tm_year = atoi ((char*)e->data) - 1900;
	ts.tm_mon  = atoi ((char*)(e->data + 5)) - 1;
	ts.tm_mday = atoi ((char*)(e->data + 8));
	ts.tm_hour = atoi ((char*)(e->data + 11));
	ts.tm_min  = atoi ((char*)(e->data + 14));
	ts.tm_sec  = atoi ((char*)(e->data + 17));

	return mktime (&ts);
}

static time_t
get_exif_mtime (const unsigned char *data, unsigned long size)
{
	ExifData *ed;
	ExifEntry *e;
	time_t t, t1 = 0, t2 = 0, t3 = 0;

	ed = exif_data_new_from_data (data, size);
	if (!ed) {
		GP_LOG_E ("Could not parse data for EXIF information.");
		return 0;
	}

	/*
	 * HP PhotoSmart C30 has the date and time in ifd_exif.
	 */
#ifdef HAVE_LIBEXIF_IFD
	e = exif_content_get_entry (ed->ifd[EXIF_IFD_0], EXIF_TAG_DATE_TIME);
	if (e)
		t1 = get_time_from_exif_tag(e);
	e = exif_content_get_entry (ed->ifd[EXIF_IFD_EXIF],
				    EXIF_TAG_DATE_TIME_ORIGINAL);
	if (e)
		t2 = get_time_from_exif_tag(e);
	e = exif_content_get_entry (ed->ifd[EXIF_IFD_EXIF],
				    EXIF_TAG_DATE_TIME_DIGITIZED);
	if (e)
		t3 = get_time_from_exif_tag(e);
#else
	e = exif_content_get_entry (ed->ifd0, EXIF_TAG_DATE_TIME);
	if (e) {
		t1 = get_time_from_exif_tag(e);
		exif_data_unref (e);
	}
	e = exif_content_get_entry (ed->ifd_exif,
				    EXIF_TAG_DATE_TIME_ORIGINAL);
	if (e) {
		t2 = get_time_from_exif_tag(e);
		exif_data_unref (e);
	}
	e = exif_content_get_entry (ed->ifd_exif,
				    EXIF_TAG_DATE_TIME_DIGITIZED);
	if (e) {
		t3 = get_time_from_exif_tag(e);
		exif_data_unref (e);
	}
#endif
	exif_data_unref (ed);
	if (!t1 && !t2 && !t3) {
		GP_LOG_D ("EXIF data has not date/time tags.");
		return 0;
	}

	/* Perform some sanity checking on those tags */
	t = t1; /* "last modified" */

	if (t2 > t)	/* "image taken" > "last modified" ? can not be */
		t = t2;
	if (t3 > t)	/* "image digitized" > max(last two) ? can not be */
		t = t3;

	GP_LOG_D ("Found time in EXIF data: '%s'.", asctime (localtime (&t)));
	return (t);
}

static time_t
gp_filesystem_get_exif_mtime (CameraFilesystem *fs, const char *folder,
			      const char *filename)
{
	CameraFile *file;
	const char *data = NULL;
	unsigned long int size = 0;
	time_t t;

	if (!fs)
		return 0;

	/* This is only useful for JPEGs. Avoid querying it for other types. */
	if (	!strstr(filename,"jpg")  && !strstr(filename,"JPG") &&
		!strstr(filename,"jpeg") && !strstr(filename,"JPEG")
	)
		return 0;

	gp_file_new (&file);
	if (gp_filesystem_get_file (fs, folder, filename,
				GP_FILE_TYPE_EXIF, file, NULL) != GP_OK) {
		GP_LOG_E ("Could not get EXIF data of '%s' in folder '%s'.",
			  filename, folder);
		gp_file_unref (file);
		return 0;
	}

	gp_file_get_data_and_size (file, &data, &size);
	t = get_exif_mtime ((unsigned char*)data, size);
	gp_file_unref (file);

	return (t);
}
#endif

/**
 * \brief The internal camera filesystem structure
 *
 * The internals of the #CameraFilesystem are only visible to gphoto2. You
 * can only access them using the functions provided by gphoto2.
 **/
struct _CameraFilesystem {
	CameraFilesystemFolder *rootfolder;

	CameraFilesystemFile *lru_first;
	CameraFilesystemFile *lru_last;
	unsigned long int lru_size;

	CameraFilesystemGetInfoFunc get_info_func;
	CameraFilesystemSetInfoFunc set_info_func;
	CameraFilesystemListFunc file_list_func;
	CameraFilesystemListFunc folder_list_func;
	CameraFilesystemGetFileFunc get_file_func;
	CameraFilesystemReadFileFunc read_file_func;
	CameraFilesystemDeleteFileFunc delete_file_func;
	CameraFilesystemPutFileFunc put_file_func;
	CameraFilesystemDeleteAllFunc delete_all_func;
	CameraFilesystemDirFunc make_dir_func;
	CameraFilesystemDirFunc remove_dir_func;
	CameraFilesystemStorageInfoFunc	storage_info_func;

	void *data;
};

#undef  MIN
#define MIN(a, b)  (((a) < (b)) ? (a) : (b))

#define CR(result)           {int __r = (result); if (__r < 0) return (__r);}

#define CL(result,list)			\
{					\
	int __r = (result);		\
					\
	 if (__r < 0) {			\
		gp_list_free (list);	\
		return (__r);		\
	}				\
}

#define CU(result,file)			\
{					\
	int __r = (result);		\
					\
	if (__r < 0) {			\
		gp_file_unref (file);	\
		return (__r);		\
	}				\
}

#define CC(context)							\
{									\
	if (gp_context_cancel (context) == GP_CONTEXT_FEEDBACK_CANCEL)	\
		return GP_ERROR_CANCEL;					\
}

#define CA(f,c)							\
{									\
	if ((f)[0] != '/') {						\
		gp_context_error ((c),					\
			_("The path '%s' is not absolute."), (f));	\
		return (GP_ERROR_PATH_NOT_ABSOLUTE);			\
	}								\
}

static int
delete_all_files (CameraFilesystem *fs, CameraFilesystemFolder *folder)
{
	CameraFilesystemFile	*file;

	C_PARAMS (folder);
	GP_LOG_D ("Delete all files in folder %p/%s", folder, folder->name);

	file = folder->files;
	while (file) {
		CameraFilesystemFile	*next;
		/* Get rid of cached files */
		gp_filesystem_lru_remove_one (fs, file);
		if (file->preview) {
			gp_file_unref (file->preview);
			file->preview = NULL;
		}
		if (file->normal) {
			gp_file_unref (file->normal);
			file->normal = NULL;
		}
		if (file->raw) {
			gp_file_unref (file->raw);
			file->raw = NULL;
		}
		if (file->audio) {
			gp_file_unref (file->audio);
			file->audio = NULL;
		}
		if (file->exif) {
			gp_file_unref (file->exif);
			file->exif = NULL;
		}
		if (file->metadata) {
			gp_file_unref (file->metadata);
			file->metadata = NULL;
		}
		next = file->next;
		free (file->name);
		free (file);
		file = next;
	}
	folder->files = NULL;
	return (GP_OK);
}

static int
delete_folder (CameraFilesystem *fs, CameraFilesystemFolder **folder)
{
	CameraFilesystemFolder *next;
	C_PARAMS (folder);

	GP_LOG_D ("Delete one folder %p/%s", *folder, (*folder)->name);
	next = (*folder)->next;
	delete_all_files (fs, *folder);
	free ((*folder)->name);
	free (*folder);
	*folder = next;
	return (GP_OK);
}

static CameraFilesystemFolder*
lookup_folder (
	CameraFilesystem *fs,
	CameraFilesystemFolder *folder, const char *foldername,
	GPContext *context
) {
	CameraFilesystemFolder	*f;
	const char	*curpt = foldername;
	const char	*s;

	GP_LOG_D ("Lookup folder '%s'...", foldername);
	while (folder) {
		/* handle multiple slashes, and slashes at the end */
		while (curpt[0]=='/')
			curpt++;
		if (!curpt[0]) {
			GP_LOG_D ("Found! %s is %p", foldername, folder);
			return folder;
		}

		s = strchr(curpt,'/');
		/* Check if we need to load the folder ... */
		if (folder->folders_dirty) {
			CameraList	*list;
			char		*copy = strdup (foldername);
			int		ret;
			/*
			 * The parent folder is dirty. List the folders in the parent 
			 * folder to make it clean.
			 */
			/* the character _before_ curpt is a /, overwrite it temporary with \0 */
			copy[curpt-foldername] = '\0';
			GP_LOG_D ("Folder %s is dirty. "
				"Listing folders in there to make folder clean...", copy);
			ret = gp_list_new (&list);
			if (ret == GP_OK) {
				ret = gp_filesystem_list_folders (fs, copy, list, context);
				gp_list_free (list);
				GP_LOG_D ("Done making folder %s clean...", copy);
			} else {
				GP_LOG_D ("Making folder %s clean failed: %d", copy, ret);
			}
			free (copy);
		}
		f = folder->folders;
		while (f) {
			if (s) {
				if (!strncmp(f->name,curpt, (s-curpt)) &&
				    (strlen(f->name) == (s-curpt))
				) {
					folder = f;
					curpt = s;
					break;
				}
			} else {
				if (!strcmp(f->name,curpt))
					return f;
			}
			f = f->next;
		}
		folder = f;
	}
	return NULL;
}

static int
lookup_folder_file (
	CameraFilesystem *fs,
	const char *folder, const char *filename,
	CameraFilesystemFolder **xfolder, CameraFilesystemFile **xfile,
	GPContext *context
) {
	CameraFilesystemFolder*	xf;
	CameraFilesystemFile*	f;

	GP_LOG_D ("Lookup folder %s file %s", folder, filename);
	xf = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!xf) return GP_ERROR_DIRECTORY_NOT_FOUND;
	/* Check if we need to load the filelist of the folder ... */
	if (xf->files_dirty) {
		CameraList	*list;
		int		ret;
		/*
                 * The folder is dirty. List the files in it to make it clean.
		 */
		GP_LOG_D ("Folder %s is dirty. "
			"Listing files in there to make folder clean...", folder);
		ret = gp_list_new (&list);
		if (ret == GP_OK) {
			ret = gp_filesystem_list_files (fs, folder, list, context);
			gp_list_free (list);
			GP_LOG_D ("Done making folder %s clean...", folder);
		}
		if (ret != GP_OK)
			GP_LOG_D ("Making folder %s clean failed: %d", folder, ret);
	}

	f = xf->files;
	while (f) {
		if (!strcmp (f->name, filename)) {
			*xfile = f;
			*xfolder = xf;
			return GP_OK;
		}
		f = f->next;
	}
	return GP_ERROR_FILE_NOT_FOUND;
}

/* delete all folder content */
static int
recurse_delete_folder (CameraFilesystem *fs, CameraFilesystemFolder *folder) {
	CameraFilesystemFolder  **f;

	GP_LOG_D ("Recurse delete folder %p/%s", folder, folder->name);
	f = &folder->folders;
	while (*f) {
		recurse_delete_folder (fs, *f);
		delete_folder (fs, f); /* will also advance to next */
	}
	return GP_OK;
}

static int
delete_all_folders (CameraFilesystem *fs, const char *foldername,
		    GPContext *context)
{
	CameraFilesystemFolder	*folder;

	GP_LOG_D ("Internally deleting "
		"all folders from '%s'...", foldername);

	C_PARAMS (fs && foldername);
	CC (context);
	CA (foldername, context);

	folder = lookup_folder (fs, fs->rootfolder, foldername, context);
	if (!folder) return GP_OK;
	return recurse_delete_folder (fs, folder);
}

/* create and append 1 new folder entry to the current folder */
static int
append_folder_one (
	CameraFilesystemFolder *folder,
	const char *name,
	CameraFilesystemFolder **newfolder
) {
	CameraFilesystemFolder *f;

	GP_LOG_D ("Append one folder %s", name);
	C_MEM (f = calloc(1, sizeof(CameraFilesystemFolder)));
	f->name = strdup (name);
	if (!f->name) {
		free (f);
		return GP_ERROR_NO_MEMORY;
	}
	f->files_dirty = 1;
	f->folders_dirty = 1;

	/* Link into the current chain...  perhaps later alphabetically? */
	f->next = folder->folders;
	folder->folders = f;
	if (newfolder) *newfolder = f;
	return (GP_OK);
}

/* This is a mix between lookup and folder creator */
static int
append_to_folder (CameraFilesystemFolder *folder,
	const char *foldername,
	CameraFilesystemFolder **newfolder
) {
	CameraFilesystemFolder	*f;
	char	*s;

	GP_LOG_D ("Append to folder %p/%s - %s", folder, folder->name, foldername);
	/* Handle multiple slashes, and slashes at the end */
	while (foldername[0]=='/')
		foldername++;
	if (!foldername[0]) {
		if (newfolder) *newfolder = folder;
		return (GP_OK);
	}

	s = strchr(foldername,'/');
	f = folder->folders;
	while (f) {
		if (s) {
			if (!strncmp(f->name,foldername, (s-foldername)) &&
			    (strlen(f->name) == (s-foldername))
			)
				return append_to_folder (f, s+1, newfolder);
		} else {
			if (!strcmp(f->name,foldername)) {
				if (newfolder) *newfolder = f;
				return (GP_OK);
			}
		}
		f = f->next;
	}
	/* Not found ... create new folder */
	if (s) {
		char *x;
		C_MEM (x = calloc ((s-foldername)+1,1));
		memcpy (x, foldername, (s-foldername));
		x[s-foldername] = 0;
		CR (append_folder_one (folder, x, newfolder));
		free (x);
	} else {
		CR (append_folder_one (folder, foldername, newfolder));
	}
	return (GP_OK);
}

static int
append_folder (CameraFilesystem *fs,
	const char *folder,
	CameraFilesystemFolder **newfolder,
	GPContext *context
) {
	GP_LOG_D ("Appending folder %s...", folder);
	C_PARAMS (fs);
	C_PARAMS (folder);
	CC (context);
	CA (folder, context);
	return append_to_folder (fs->rootfolder, folder, newfolder);
}

static int
append_file (CameraFilesystem *fs, CameraFilesystemFolder *folder, const char *name, CameraFile *file, GPContext *context)
{
	CameraFilesystemFile **new;

	C_PARAMS (fs && file);
	GP_LOG_D ("Appending file %s...", name);

	new = &folder->files;
	while (*new) {
		if (!strcmp((*new)->name, name)) {
			GP_LOG_E ("File %s already exists!", name);
			return (GP_ERROR);
		}
		new = &((*new)->next);
	}
	/* new now points to the location of the last ->next pointer,
	 * if we write to it, we set last->next */
	C_MEM ((*new) = calloc (1, sizeof (CameraFilesystemFile)));
	C_MEM ((*new)->name = strdup (name));
	(*new)->info_dirty = 1;
	(*new)->normal = file;
	gp_file_ref (file);
	return (GP_OK);
}

/**
 * \brief Clear the filesystem
 * \param fs the filesystem to be cleared
 *
 * Resets the filesystem. All cached information including the folder tree
 * will get lost and will be queried again on demand.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_reset (CameraFilesystem *fs)
{
	GP_LOG_D ("resetting filesystem");
	CR (gp_filesystem_lru_clear (fs));
	CR (delete_all_folders (fs, "/", NULL));
	if (fs->rootfolder) {
		fs->rootfolder->files_dirty = 1;
		fs->rootfolder->folders_dirty = 1;
	} else {
		GP_LOG_E ("root folder is gone?");
	}
	return (GP_OK);
}

/**
 * \brief Create a new filesystem struct
 *
 * \param fs a pointer to a #CameraFilesystem
 *
 * Creates a new empty #CameraFilesystem
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_new (CameraFilesystem **fs)
{
	C_PARAMS (fs);

	C_MEM (*fs = calloc (1, sizeof (CameraFilesystem)));

	(*fs)->rootfolder = calloc (sizeof (CameraFilesystemFolder), 1);
	if (!(*fs)->rootfolder) {
		free (*fs);
		return (GP_ERROR_NO_MEMORY);
	}
	(*fs)->rootfolder->name = strdup("/");
	if (!(*fs)->rootfolder->name) {
		free ((*fs)->rootfolder);
		free (*fs);
		return (GP_ERROR_NO_MEMORY);
	}
	(*fs)->rootfolder->files_dirty = 1;
	(*fs)->rootfolder->folders_dirty = 1;
	return (GP_OK);
}

/**
 * \brief Free filesystem struct
 * \param fs a #CameraFilesystem
 *
 * Frees the #CameraFilesystem
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_free (CameraFilesystem *fs)
{
	/* We don't care for success or failure */
	gp_filesystem_reset (fs);

	/* Now, we've only got left over the root folder. Free that and
	 * the filesystem. */
	free (fs->rootfolder->name);
	free (fs->rootfolder);
	free (fs);
	return (GP_OK);
}

/**
 * \brief Append a file to a folder in a filesystem
 * \param fs a #CameraFilesystem
 * \param folder the folder where to put the file in
 * \param filename filename of the file
 * \param context a #GPContext
 *
 * Tells the fs that there is a file called filename in folder
 * called folder. Usually camera drivers will call this function after
 * capturing an image in order to tell the fs about the new file.
 * A front-end should not use this function.
 *
 * \return a gphoto2 error code.
 **/
static int
internal_append (CameraFilesystem *fs, CameraFilesystemFolder *f,
		      const char *filename, GPContext *context)
{
	CameraFilesystemFile **new;

	C_PARAMS (fs && f);

	GP_LOG_D ("Internal append %s to folder %s", filename, f->name);
	/* Check folder for existence, if not, create it. */
	new = &f->files;
	while (*new) {
		if (!strcmp((*new)->name, filename)) break;
		new = &((*new)->next);
	}
	if (*new)
		return (GP_ERROR_FILE_EXISTS);

	C_MEM ((*new) = calloc (sizeof (CameraFilesystemFile), 1));
	(*new)->name = strdup (filename);
	if (!(*new)->name) {
		free (*new);
		*new = NULL;
		return (GP_ERROR_NO_MEMORY);
	}
	(*new)->info_dirty = 1;
	return (GP_OK);
}

int
gp_filesystem_append (CameraFilesystem *fs, const char *folder,
		      const char *filename, GPContext *context)
{
	CameraFilesystemFolder *f;
	int ret;

	C_PARAMS (fs && folder);
	CC (context);
	CA (folder, context);

	GP_LOG_D ("Append %s/%s to filesystem", folder, filename);
	/* Check folder for existence, if not, create it. */
	f = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!f)
		CR (append_folder (fs, folder, &f, context));
	if (f->files_dirty) { /* Need to load folder from driver first ... capture case */
		CameraList	*xlist;
		int ret;

		ret = gp_list_new (&xlist);
		if (ret != GP_OK) return ret;
		ret = gp_filesystem_list_files (fs, folder, xlist, context);
		gp_list_free (xlist);
		if (ret != GP_OK) return ret;
	}
	ret = internal_append (fs, f, filename, context);
	if (ret == GP_ERROR_FILE_EXISTS) /* not an error here ... just in case we add files twice to the list */
		ret = GP_OK;
	return ret;
}


static void
recursive_fs_dump (CameraFilesystemFolder *folder, int depth) {
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*xfile;

	GP_LOG_D ("%*sFolder %s", depth, " ", folder->name);

	xfile = folder->files;
	while (xfile) {
		GP_LOG_D ("%*s    %s", depth, " ", xfile->name);
		xfile = xfile->next;
	}
	
	f = folder->folders;
	while (f) {
		recursive_fs_dump (f, depth+4);
		f = f->next;
	}
}
/**
 * \brief Dump the current filesystem.
 * \param fs the #CameraFilesystem
 * \return a gphoto error code
 *
 * Internal function to dump the current filesystem.
 */
int
gp_filesystem_dump (CameraFilesystem *fs)
{
	GP_LOG_D ("Dumping Filesystem:");
	recursive_fs_dump (fs->rootfolder, 0);
	return (GP_OK);
}

static int
delete_file (CameraFilesystem *fs, CameraFilesystemFolder *folder, CameraFilesystemFile *file)
{
	CameraFilesystemFile **prev;

	gp_filesystem_lru_remove_one (fs, file);
	/* Get rid of cached files */
	if (file->preview) {
		gp_file_unref (file->preview);
		file->preview = NULL;
	}
	if (file->normal) {
		gp_file_unref (file->normal);
		file->normal = NULL;
	}
	if (file->raw) {
		gp_file_unref (file->raw);
		file->raw = NULL;
	}
	if (file->audio) {
		gp_file_unref (file->audio);
		file->audio = NULL;
	}
	if (file->exif) {
		gp_file_unref (file->exif);
		file->exif = NULL;
	}
	if (file->metadata) {
		gp_file_unref (file->metadata);
		file->metadata = NULL;
	}

	prev = &(folder->files);
	while ((*prev) && ((*prev) != file))
		prev = &((*prev)->next);
	if (!*prev)
		return GP_ERROR;
	*prev = file->next;
	file->next = NULL;
	free (file->name);
	free (file);
	return (GP_OK);
}

static int
gp_filesystem_delete_all_one_by_one (CameraFilesystem *fs, const char *folder,
				     GPContext *context)
{
	CameraList *list;
	int count, x;
	const char *name;

	GP_LOG_D ("Deleting all 1 by 1 from %s", folder);
	CR (gp_list_new (&list));
	CL (gp_filesystem_list_files (fs, folder, list, context), list);
	CL (count = gp_list_count (list), list);
	for (x = count ; x--; ) {
		CL (gp_list_get_name (list, x, &name), list);
		CL (gp_filesystem_delete_file (fs, folder, name, context),list);
	}
	gp_list_free(list);
	return (GP_OK);
}

/**
 * \brief Delete all files in specified folder.
 *
 * \param fs a #CameraFilesystem
 * \param folder the folder in which to delete all files
 * \param context a #GPContext
 *
 * Deletes all files in the given folder from the fs. If the fs has not
 * been supplied with a delete_all_func, it tries to delete the files
 * one by one using the delete_file_func. If that function has not been
 * supplied neither, an error is returned.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_delete_all (CameraFilesystem *fs, const char *folder,
			  GPContext *context)
{
	int r;
	CameraFilesystemFolder *f;

	C_PARAMS (fs && folder);
	CC (context);
	CA (folder, context);

	GP_LOG_D ("Deleting all from %s", folder);
	/* Make sure this folder exists */
	f = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!f) return (GP_ERROR_DIRECTORY_NOT_FOUND);

	if (!fs->delete_all_func)
		return gp_filesystem_delete_all_one_by_one (fs, folder, context);
	/*
	 * Mark the folder dirty - it could be that an error
	 * happens, and then we don't know which files have been
	 * deleted and which not.
	 */
	f->files_dirty = 1;
	/*
	 * First try to use the delete_all function. If that fails,
	 * fall back to deletion one-by-one.
	 */
	r = fs->delete_all_func (fs, folder, fs->data, context);
	if (r < 0) {
		GP_LOG_D (
			"delete_all failed (%s). Falling back to "
			"deletion one-by-one.",
			gp_result_as_string (r));
		CR (gp_filesystem_delete_all_one_by_one (fs, folder,
							 context));
	} else {
		/* delete from filesystem view too now */
		CR (delete_all_files (fs, f));
	}
	/*
	 * No error happened. We can be sure that all files have been
	 * deleted.
	 */
	f->files_dirty = 0;
	return (GP_OK);
}

/**
 * \brief Get the list of files in a folder
 * \param fs a #CameraFilesystem
 * \param folder a folder of which a file list should be generated
 * \param list a #CameraList where to put the list of files into
 * \param context a #GPContext
 *
 * Lists the files in folder using either cached values or (if there
 * aren't any) the file_list_func which (hopefully) has been previously
 * supplied.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_list_files (CameraFilesystem *fs, const char *folder,
			  CameraList *list, GPContext *context)
{
	int count, y;
	const char *name;
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*file;

	GP_LOG_D ("Listing files in %s", folder);

	C_PARAMS (fs && list && folder);
	CC (context);
	CA (folder, context);

	gp_list_reset (list);

	/* Search the folder */
	f = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!f) return (GP_ERROR_DIRECTORY_NOT_FOUND);

	/* If the folder is dirty, delete the contents and query the camera */
	if (f->files_dirty && fs->file_list_func) {
		GP_LOG_D ("Querying folder %s...", folder);
		CR (delete_all_files (fs, f));

		/* set it to non-dirty now, so we do not recurse via _append. */
		f->files_dirty = 0;
		CR (fs->file_list_func (fs, folder, list,
					fs->data, context));

		CR (count = gp_list_count (list));
		for (y = 0; y < count; y++) {
			CR (gp_list_get_name (list, y, &name));
			GP_LOG_D ("Added '%s'", name);
			CR (internal_append (fs, f, name, context));
		}
		gp_list_reset (list);
	}
	/* The folder is clean now */
	f->files_dirty = 0;

	file = f->files;
	while (file) {
		GP_LOG_D (
			"Listed '%s'", file->name);
		CR (gp_list_append (list, file->name, NULL));
		file = file->next;
	}
	return (GP_OK);
}

/**
 * \brief List all subfolders within a filesystem folder
 * \param fs a #CameraFilesystem
 * \param folder a folder
 * \param list a #CameraList where subfolders should be listed
 * \param context a #GPContext
 *
 * Generates a list of subfolders of the supplied folder either using
 * cached values (if there are any) or the folder_list_func if it has been
 * supplied previously. If not, it is assumed that only a root folder
 * exists (which is the case for many cameras).
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_list_folders (CameraFilesystem *fs, const char *folder,
			    CameraList *list, GPContext *context)
{
	int y, count;
	const char *name;
	CameraFilesystemFolder	*f, *new;

	GP_LOG_D ("Listing folders in %s", folder);

	C_PARAMS (fs && folder && list);
	CC (context);
	CA (folder, context);

	gp_list_reset (list);

	/* Search the folder */
	f = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!f) return (GP_ERROR_DIRECTORY_NOT_FOUND);


	/* If the folder is dirty, query the contents. */
	if (f->folders_dirty && fs->folder_list_func) {
		GP_LOG_D ("... is dirty, getting from camera");
		CR (fs->folder_list_func (fs, folder, list,
					  fs->data, context));
		CR (delete_all_folders (fs, folder, context));

		CR (count = gp_list_count (list));
		for (y = 0; y < count; y++) {
			CR (gp_list_get_name (list, y, &name));
			CR (append_folder_one (f, name, NULL));
		}
		/* FIXME: why not just return (GP_OK); ? the list should be fine */
		gp_list_reset (list);
	}

	new = f->folders;
	while (new) {
		CR (gp_list_append (list, new->name, NULL));
		new = new->next;
	}
	/* The folder is clean now */
	f->folders_dirty = 0;
	GP_LOG_D ("Folder %s contains %i subfolders.", folder, gp_list_count (list));
	return (GP_OK);
}

/**
 * \brief Count files a folder of a filesystem.
 * \param fs a #CameraFilesystem
 * \param folder a folder in which to count the files
 * \param context a #GPContext
 *
 * Counts the files in the folder.
 *
 * \return The number of files in the folder or a gphoto2 error code.
 **/
int
gp_filesystem_count (CameraFilesystem *fs, const char *folder,
		     GPContext *context)
{
	int x;
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*file;

	C_PARAMS (fs && folder);
	CC (context);
	CA (folder, context);

	f = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!f) return (GP_ERROR_DIRECTORY_NOT_FOUND);

	x = 0;
	file = f->files;
	while (file) {
		x++;
		file = file->next;
	}
	return x;
}

/**
 * \brief Delete a file from a folder.
 * \param fs a #CameraFilesystem
 * \param folder a folder in which to delete the file
 * \param filename the name of the file to delete
 * \param context a #GPContext
 *
 * If a delete_file_func has been supplied to the fs, this function will
 * be called and, if this function returns without error, the file will be
 * removed from the fs.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_delete_file (CameraFilesystem *fs, const char *folder,
			   const char *filename, GPContext *context)
{
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*file;

	C_PARAMS (fs && folder && filename);
	CC (context);
	CA (folder, context);

	/* First of all, do we support file deletion? */
	if (!fs->delete_file_func) {
		gp_context_error (context, _("You have been trying to delete "
			"'%s' from folder '%s', but the filesystem does not "
			"support deletion of files."), filename, folder);
		return (GP_ERROR_NOT_SUPPORTED);
	}

	/* Search the folder and the file */
	CR (lookup_folder_file (fs, folder, filename, &f, &file, context));

	GP_LOG_D ("Deleting '%s' from folder '%s'...", filename, folder);

	/* Delete the file */
	CR (fs->delete_file_func (fs, folder, filename,
				  fs->data, context));
	CR (delete_file (fs, f, file));
	return (GP_OK);
}

/**
 * \brief Delete a virtal file from a folder in the filesystem
 * \param fs a #CameraFilesystem
 * \param folder a folder in which to delete the file
 * \param filename the name of the file to delete
 * \param context a #GPContext
 *
 * Remove a file from the filesystem. Compared to gp_filesystem_delete_file()
 * this just removes the file from the libgphoto2 view of the filesystem, but
 * does not call the camera driver to delete it from the physical device.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_delete_file_noop (CameraFilesystem *fs, const char *folder,
				const char *filename, GPContext *context)
{
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*file;

	C_PARAMS (fs && folder && filename);
	CC (context);
	CA (folder, context);
	/* Search the folder and the file */
	CR (lookup_folder_file (fs, folder, filename, &f, &file, context));
	return delete_file (fs, f, file);
}

/**
 * \brief Create a subfolder within a folder
 * \param fs a #CameraFilesystem
 * \param folder the folder in which the directory should be created
 * \param name the name of the directory to be created
 * \param context a #GPContext
 *
 * Creates a new directory called name in given folder.
 *
 * \return a gphoto2 error code
 **/
int
gp_filesystem_make_dir (CameraFilesystem *fs, const char *folder,
			const char *name, GPContext *context)
{
	CameraFilesystemFolder	*f;

	C_PARAMS (fs && folder && name);
	CC (context);
	CA (folder, context);

	if (!fs->make_dir_func)
		return (GP_ERROR_NOT_SUPPORTED);

	/* Search the folder */
	f = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!f) return (GP_ERROR_DIRECTORY_NOT_FOUND);

	/* Create the directory */
	CR (fs->make_dir_func (fs, folder, name, fs->data, context));
	/* and append to internal fs */
	return append_folder_one (f, name, NULL);
}

/**
 * \brief Remove a subfolder from within a folder
 * \param fs a #CameraFilesystem
 * \param folder the folder in which the directory should be created
 * \param name the name of the directory to be created
 * \param context a #GPContext
 *
 * Removes a directory called name from the given folder.
 *
 * \return a gphoto2 error code
 **/
int
gp_filesystem_remove_dir (CameraFilesystem *fs, const char *folder,
			  const char *name, GPContext *context)
{
	CameraFilesystemFolder *f;
	CameraFilesystemFolder **prev;

	C_PARAMS (fs && folder && name);
	CC (context);
	CA (folder, context);

	if (!fs->remove_dir_func)
		return (GP_ERROR_NOT_SUPPORTED);

	/*
	 * Make sure there are neither files nor folders in the folder
	 * that is to be removed.
	 */
	f = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!f) return (GP_ERROR_DIRECTORY_NOT_FOUND);
	/* Check if we need to load the folder ... */
	if (f->folders_dirty) {
		CameraList	*list;
		int		ret;
		/*
		 * The owning folder is dirty. List the folders in it 
		 * to make it clean.
		 */
		GP_LOG_D ("Folder %s is dirty. "
			"Listing folders in there to make folder clean...", folder);
		ret = gp_list_new (&list);
		if (ret == GP_OK) {
			ret = gp_filesystem_list_folders (fs, folder, list, context);
			gp_list_free (list);
			GP_LOG_D ("Done making folder %s clean...", folder);
		}
	}
	prev = &(f->folders);
	while (*prev) {
		if (!strcmp (name, (*prev)->name))
			break;
		prev = &((*prev)->next);
	}
	if (!*prev) return (GP_ERROR_DIRECTORY_NOT_FOUND);

	if ((*prev)->folders) {
		gp_context_error (context, _("There are still subfolders in "
			"folder '%s/%s' that you are trying to remove."), folder, name);
		return (GP_ERROR_DIRECTORY_EXISTS);
	}
	if ((*prev)->files) {
		gp_context_error (context, _("There are still files in "
			"folder '%s/%s' that you are trying to remove."), folder,name);
		return (GP_ERROR_FILE_EXISTS);
	}

	/* Remove the directory */
	CR (fs->remove_dir_func (fs, folder, name, fs->data, context));
	CR (delete_folder (fs, prev));
	return (GP_OK);
}

/**
 * \brief Upload a file to a folder on the device filesystem
 * \param fs a #CameraFilesystem
 * \param folder the folder where to put the file into
 * \param file the file
 * \param context a #GPContext
 *
 * Uploads a file to the camera if a put_file_func has been previously
 * supplied to the fs. If the upload is successful, the file will get
 * cached in the fs.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_put_file (CameraFilesystem *fs,
			const char *folder, const char *filename,
			CameraFileType type,
			CameraFile *file, GPContext *context)
{
	CameraFilesystemFolder	*f;
	int ret;

	C_PARAMS (fs && folder && file);
	CC (context);
	CA (folder, context);

	/* Do we support file upload? */
	if (!fs->put_file_func) {
		gp_context_error (context, _("The filesystem does not support "
			"upload of files."));
		return (GP_ERROR_NOT_SUPPORTED);
	}

	/* Search the folder */
	f = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!f) return (GP_ERROR_DIRECTORY_NOT_FOUND);

	/* Upload the file */
	CR (fs->put_file_func (fs, folder, filename, type, file, fs->data, context));
	/* And upload it to internal structure too */
	ret = append_file (fs, f, filename, file, context);
	if (type != GP_FILE_TYPE_NORMAL) /* FIXME perhaps check before append_file? */
		return GP_OK;
	return ret;
}

/**
 * \brief Lookup the filename of an indexed file within a folder.
 * \param fs a #CameraFilesystem
 * \param folder the folder where to look up the file with the filenumber
 * \param filenumber the number of the file
 * \param filename pointer to a filename where the result is stored
 * \param context a #GPContext
 *
 * Looks up the filename of file with given filenumber in given folder.
 * See gp_filesystem_number for exactly the opposite functionality.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_name (CameraFilesystem *fs, const char *folder, int filenumber,
		    const char **filename, GPContext *context)
{
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*file;
	int count;
	C_PARAMS (fs && folder);
	CC (context);
	CA (folder, context);

	f = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!f) return (GP_ERROR_DIRECTORY_NOT_FOUND);

	file = f->files;
	count = 0;
	while (file) {
		if (filenumber == 0)
			break;
		filenumber--;
		count++;
		file = file->next;
	}

	if (!file) {
		gp_context_error (context, _("Folder '%s' only contains "
			"%i files, but you requested a file with number %i."),
			folder, count, filenumber);
		return (GP_ERROR_FILE_NOT_FOUND);
	}
	*filename = file->name;
	return (GP_OK);
}

/**
 * \brief Get the index of a file in specified folder
 * \param fs a #CameraFilesystem
 * \param folder the folder where to look for file called filename
 * \param filename the file to look for
 * \param context a #GPContext
 *
 * Looks for a file called filename in the given folder. See
 * gp_filesystem_name for exactly the opposite functionality.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_number (CameraFilesystem *fs, const char *folder,
		      const char *filename, GPContext *context)
{
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*file;
	CameraList *list;
	int num;

	C_PARAMS (fs && folder && filename);
	CC (context);
	CA (folder, context);

	f = lookup_folder (fs, fs->rootfolder, folder, context);
	if (!f) return (GP_ERROR_DIRECTORY_NOT_FOUND);

	file = f->files;
	num = 0;
	while (file) {
		if (!strcmp (file->name, filename))
			return num;
		num++;
		file = file->next;
	}

	/* Ok, we didn't find the file. Is the folder dirty? */
	if (!f->files_dirty) {
		gp_context_error (context, _("File '%s' could not be found "
			"in folder '%s'."), filename, folder);
		return (GP_ERROR_FILE_NOT_FOUND);
	}
	/* The folder is dirty. List all files to make it clean */
	CR (gp_list_new(&list));
	CL (gp_filesystem_list_files (fs, folder, list, context), list);
	gp_list_free(list);
	return (gp_filesystem_number (fs, folder, filename, context));
}

static int
gp_filesystem_scan (CameraFilesystem *fs, const char *folder,
		    const char *filename, GPContext *context)
{
	int count, x;
	CameraList *list;
	const char *name;
	char path[128];

	GP_LOG_D ("Scanning %s for %s...", folder, filename);

	C_PARAMS (fs && folder && filename);
	CC (context);
	CA (folder, context);

	CR (gp_list_new (&list));
	CL (gp_filesystem_list_files (fs, folder, list, context), list);
	CL (count = gp_list_count (list), list);
	for (x = 0; x < count; x++) {
		CL (gp_list_get_name (list, x, &name), list);
		if (!strcmp (filename, name)) {
			gp_list_free (list);
			return (GP_OK);
		}
	}

	CL (gp_filesystem_list_folders (fs, folder, list, context), list);
	CL (count = gp_list_count (list), list);
	for (x = 0; x < count; x++) {
		CL (gp_list_get_name (list, x, &name), list);
		strncpy (path, folder, sizeof (path));
		if (path[strlen (path) - 1] != '/')
			strncat (path, "/", sizeof (path) - strlen (path) - 1);
		strncat (path, name, sizeof (path) - strlen (path) - 1);
		CL (gp_filesystem_scan (fs, path, filename, context), list);
	}
	gp_list_free (list);
	return (GP_OK);
}

static int
recursive_folder_scan (
	CameraFilesystemFolder *folder, const char *lookforfile,
	char **foldername
) {
	CameraFilesystemFile	*file;
	CameraFilesystemFolder	*f;
	int ret;

	file = folder->files;
	while (file) {
		if (!strcmp(file->name, lookforfile)) {
			*foldername = strdup (folder->name);
			return GP_OK;
		}
		file = file->next;
	}
	f = folder->folders;
	while (f) {
		char *xfolder;
		ret = recursive_folder_scan (f, lookforfile, &xfolder);
		if (ret == GP_OK) {
			C_MEM ((*foldername) = malloc (strlen (folder->name) + 1 + strlen (xfolder) + 1));
			strcpy ((*foldername),folder->name);
			strcat ((*foldername),"/");
			strcat ((*foldername),xfolder);
			free (xfolder);
			return GP_OK;
		}
		f = f->next;
	}
	/* thorugh all subfoilders */
	return GP_ERROR_FILE_NOT_FOUND;
}
/**
 * \brief Search a folder that contains a given filename
 * \param fs a #CameraFilesystem
 * \param filename the name of the file to search in the fs
 * \param folder pointer to value where the string is stored in
 * \param context a #GPContext
 *
 * Searches a file called filename in the fs and returns the first
 * occurrency. This functionality is needed for camera drivers that cannot
 * figure out where a file gets created after capturing an image although the
 * name of the image is known. Usually, those drivers will call
 * gp_filesystem_reset in order to tell the fs that something has
 * changed and then gp_filesystem_get_folder in order to find the file.
 *
 * Note that you get a reference to the string stored in the filesystem structure,
 * so do not free it yourself.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_get_folder (CameraFilesystem *fs, const char *filename,
			  char **folder, GPContext *context)
{
	int ret;

	C_PARAMS (fs && filename && folder);
	CC (context);

	CR (gp_filesystem_scan (fs, "/", filename, context));

	ret = recursive_folder_scan ( fs->rootfolder, filename, folder);
	if (ret == GP_OK) return ret;
	gp_context_error (context, _("Could not find file '%s'."), filename);
	return (GP_ERROR_FILE_NOT_FOUND);
}

static int
gp_filesystem_get_file_impl (CameraFilesystem *fs, const char *folder,
			     const char *filename, CameraFileType type,
			     CameraFile *file, GPContext *context)
{
	CameraFilesystemFolder	*xfolder;
	CameraFilesystemFile	*xfile;
	int			ret;

	C_PARAMS (fs && folder && file && filename);
	CC (context);
	CA (folder, context);

	GP_LOG_D ("Getting file '%s' from folder '%s' (type %i)...",
		  filename, folder, type);

	CR (gp_file_set_name (file, filename));

	if (!fs->get_file_func) {
		gp_context_error (context,
			_("The filesystem doesn't support getting files"));
		return (GP_ERROR_NOT_SUPPORTED);
	}

	/* Search folder and file */
	CR( lookup_folder_file (fs, folder, filename, &xfolder, &xfile, context));

	ret = GP_ERROR;
	switch (type) {
	case GP_FILE_TYPE_PREVIEW:
		if (xfile->preview)
			ret = gp_file_copy (file, xfile->preview);
		break;
	case GP_FILE_TYPE_NORMAL:
		if (xfile->normal)
			ret = gp_file_copy (file, xfile->normal);
		break;
	case GP_FILE_TYPE_RAW:
		if (xfile->raw)
			ret = gp_file_copy (file, xfile->raw);
		break;
	case GP_FILE_TYPE_AUDIO:
		if (xfile->audio)
			ret = gp_file_copy (file, xfile->audio);
		break;
	case GP_FILE_TYPE_EXIF:
		if (xfile->exif)
			ret = gp_file_copy (file, xfile->exif);
		break;
	case GP_FILE_TYPE_METADATA:
		if (xfile->metadata)
			ret = gp_file_copy (file, xfile->metadata);
		break;
	default:
		gp_context_error (context, _("Unknown file type %i."), type);
		return (GP_ERROR);
	}
	if (ret == GP_OK) {
		GP_LOG_D ("LRU cache used for type %d!", type);
		return GP_OK;
	}

	GP_LOG_D ("Downloading '%s' from folder '%s'...", filename, folder);

	CR (fs->get_file_func (fs, folder, filename, type, file,
			       fs->data, context));

	/* We don't trust the camera drivers */
	CR (gp_file_set_name (file, filename));

#if 0
	/* this disables LRU completely. */
	/* Cache this file */
	CR (gp_filesystem_set_file_noop (fs, folder, filename, type, file, context));
#endif

	/*
	 * Often, thumbnails are of a different mime type than the normal
	 * picture. In this case, we should rename the file.
	 */
	if (type != GP_FILE_TYPE_NORMAL)
		CR (gp_file_adjust_name_for_mime_type (file));

	return (GP_OK);
}

/**
 * \brief Get file data from the filesystem
 * \param fs a #CameraFilesystem
 * \param folder the folder in which the file can be found
 * \param filename the name of the file to download
 * \param type the type of the file
 * \param file the file that receives the data
 * \param context a #GPContext
 *
 * Downloads the file called filename from the folder using the
 * get_file_func if such a function has been previously supplied. If the
 * file has been previously downloaded, the file is retrieved from cache.
 * The result is stored in the passed file structure.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_get_file (CameraFilesystem *fs, const char *folder,
			const char *filename, CameraFileType type,
			CameraFile *file, GPContext *context)
{
	int r;
#ifdef HAVE_LIBEXIF
	CameraFile *efile;
	const char *data = NULL;
	unsigned char *buf;
	unsigned int buf_size;
	unsigned long int size = 0;
	ExifData *ed;
#endif

	r = gp_filesystem_get_file_impl (fs, folder, filename, type,
					 file, context);

	if ((r == GP_ERROR_NOT_SUPPORTED) &&
	    (type == GP_FILE_TYPE_PREVIEW)) {

		/*
		 * Could not get preview (unsupported operation). Some
		 * cameras hide the thumbnail in EXIF data. Check it out.
		 */
#ifdef HAVE_LIBEXIF
		GP_LOG_D ("Getting previews is not supported. Trying EXIF data...");
		CR (gp_file_new (&efile));
		CU (gp_filesystem_get_file_impl (fs, folder, filename,
				GP_FILE_TYPE_EXIF, efile, context), efile);
		CU (gp_file_get_data_and_size (efile, &data, &size), efile);
		ed = exif_data_new_from_data ((unsigned char*)data, size);
		gp_file_unref (efile);
		if (!ed) {
			GP_LOG_E ("Could not parse EXIF data of '%s' in folder '%s'.", filename, folder);
			return (GP_ERROR_CORRUPTED_DATA);
		}
		if (!ed->data) {
			GP_LOG_E ("EXIF data does not contain a thumbnail.");
			exif_data_unref (ed);
			return (r);
		}

		/*
		 * We found a thumbnail in EXIF data! Those
		 * thumbnails are always JPEG. Set up the file.
		 */
		r = gp_file_set_data_and_size (file, (char*)ed->data, ed->size);
		if (r < 0) {
			exif_data_unref (ed);
			return (r);
		}
		ed->data = NULL;
		ed->size = 0;
		exif_data_unref (ed);
		CR (gp_file_set_name (file, filename));
		CR (gp_file_set_mime_type (file, GP_MIME_JPEG));
		CR (gp_filesystem_set_file_noop (fs, folder, filename, GP_FILE_TYPE_PREVIEW, file, context));
		CR (gp_file_adjust_name_for_mime_type (file));
#else
		GP_LOG_D ("Getting previews is not supported and "
			"libgphoto2 has been compiled without exif "
			"support. ");
		return (r);
#endif
	} else if ((r == GP_ERROR_NOT_SUPPORTED) &&
		   (type == GP_FILE_TYPE_EXIF)) {

		/*
		 * Some cameras hide EXIF data in thumbnails (!). Check it
		 * out.
		 */
#ifdef HAVE_LIBEXIF
		GP_LOG_D ("Getting EXIF data is not supported. Trying thumbnail...");
		CR (gp_file_new (&efile));
		CU (gp_filesystem_get_file_impl (fs, folder, filename,
				GP_FILE_TYPE_PREVIEW, efile, context), efile);
		CU (gp_file_get_data_and_size (efile, &data, &size), efile);
		ed = exif_data_new_from_data ((unsigned char*)data, size);
		gp_file_unref (efile);
		if (!ed) {
			GP_LOG_D ("Could not parse EXIF data of thumbnail of "
				"'%s' in folder '%s'.", filename, folder);
			return (GP_ERROR_CORRUPTED_DATA);
		}
		exif_data_save_data (ed, &buf, &buf_size);
		exif_data_unref (ed);
		r = gp_file_set_data_and_size (file, (char*)buf, buf_size);
		if (r < 0) {
			free (buf);
			return (r);
		}
		CR (gp_file_set_name (file, filename));
		CR (gp_file_set_mime_type (file, GP_MIME_EXIF));
		CR (gp_filesystem_set_file_noop (fs, folder, filename, GP_FILE_TYPE_EXIF, file, context));
		CR (gp_file_adjust_name_for_mime_type (file));
#else
		GP_LOG_D ("Getting EXIF data is not supported and libgphoto2 "
			"has been compiled without EXIF support.");
		return (r);
#endif
	} else if (r < 0) {
		GP_LOG_D ("Download of '%s' from '%s' (type %i) failed. "
			"Reason: '%s'", filename, folder, type,
			gp_result_as_string (r));
		return (r);
	}

	return (GP_OK);
}

/**
 * \brief Get partial file data from the filesystem
 * \param fs a #CameraFilesystem
 * \param folder the folder in which the file can be found
 * \param filename the name of the file to download
 * \param type the type of the file
 * \param offset the offset where the data starts
 * \param buf the targetbuffer where the data will be put
 * \param size the size to read and that was read into the buffer 
 * \param context a #GPContext
 *
 * Downloads the file called filename from the folder using the
 * read_file_func if such a function has been previously supplied. If the
 * file has been previously downloaded, the file is retrieved from cache.
 * 
 * The file is read partially into the passed buffer. The read starts
 * at offset on the device and goes for at most size bytes.
 * Reading over the end of the file might give errors, so get the maximum
 * file size via an info function before.
 * 
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_read_file (CameraFilesystem *fs, const char *folder,
			const char *filename, CameraFileType type,
			uint64_t offset, char *buf, uint64_t *size,
			GPContext *context)
{
	int r;
	const char	*xdata;
	unsigned long	xsize;
	CameraFile	*file;

	C_PARAMS (fs && folder && filename && buf && size);
	CC (context);
	CA (folder, context);

	if (fs->read_file_func) {
		r = fs->read_file_func (fs, folder, filename, type,
			offset, buf, size, fs->data, context);
		if (r == GP_OK)
			return r;
	} else {
		return GP_ERROR_NOT_SUPPORTED;
	}
	return r;
	/* fallback code */
	CR (gp_file_new (&file));
	CR (gp_filesystem_get_file (fs, folder, filename, type,
				    file, context));
	CR (gp_file_get_data_and_size (file, &xdata, &xsize));
	if (offset > *size) { /* EOF */
		gp_file_unref (file);
		*size = 0;
		return GP_OK;
	}
	if ((offset != 0) || (offset + *size != xsize)) {
		/* Cache this file in the LRU, but only if the user just
		 * hasn't read all of it at once.
		 */
		CR (gp_filesystem_set_file_noop (fs, folder, filename, type, file, context));
	}
	if (offset + (*size) > xsize)
		*size = xsize-offset;
	memcpy (buf, xdata+offset, *size);
	gp_file_unref (file);
	return GP_OK;
}
/**
 * \brief Set all filesystem related function pointers
 * \param fs a #CameraFilesystem
 * \param funcs pointer to a struct of filesystem functions
 * \param data private data
 *
 * Tells the filesystem which functions to call for camera/filesystem specific
 * functions, like listing, retrieving, uploading files and so on.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_set_funcs	(CameraFilesystem *fs,
			 CameraFilesystemFuncs *funcs,
			 void *data)
{
	C_PARAMS (fs);

	fs->get_info_func	= funcs->get_info_func;
	fs->set_info_func	= funcs->set_info_func;
	fs->put_file_func	= funcs->put_file_func;
	fs->delete_all_func	= funcs->delete_all_func;
	fs->make_dir_func	= funcs->make_dir_func;
	fs->remove_dir_func	= funcs->remove_dir_func;
	fs->file_list_func	= funcs->file_list_func;
	fs->folder_list_func	= funcs->folder_list_func;
	fs->delete_file_func	= funcs->del_file_func;
	fs->get_file_func	= funcs->get_file_func;
	fs->read_file_func	= funcs->read_file_func;
	fs->storage_info_func	= funcs->storage_info_func;
	fs->data = data;
	return (GP_OK);
}

/**
 * \brief Get information about the specified file
 * \param fs a #CameraFilesystem
 * \param folder the folder that contains the file
 * \param filename the filename
 * \param info pointer to #CameraFileInfo that receives the information
 * \param context a #GPContext
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_get_info (CameraFilesystem *fs, const char *folder,
			const char *filename, CameraFileInfo *info,
			GPContext *context)
{
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*file;
#ifdef HAVE_LIBEXIF
	time_t t;
#endif

	C_PARAMS (fs && folder && filename && info);
	CC (context);
	CA (folder, context);

	GP_LOG_D ("Getting information about '%s' in '%s'...", filename,
		  folder);

	if (!fs->get_info_func) {
		gp_context_error (context,
			_("The filesystem doesn't support getting file "
			  "information"));
		return (GP_ERROR_NOT_SUPPORTED);
	}

	/* Search folder and file and get info if needed */
	CR ( lookup_folder_file (fs, folder, filename, &f, &file, context));

	if (file->info_dirty) {
		CR (fs->get_info_func (fs, folder, filename,
						&file->info,
						fs->data, context));
		file->info_dirty = 0;
	}

	/*
	 * If we didn't get GP_FILE_INFO_MTIME, we'll have a look if we
	 * can get it from EXIF data.
	 */
#ifdef HAVE_LIBEXIF
	if (!(file->info.file.fields & GP_FILE_INFO_MTIME)) {
		GP_LOG_D ("Did not get mtime. Trying EXIF information...");
		t = gp_filesystem_get_exif_mtime (fs, folder, filename);
		if (t) {
			file->info.file.mtime = t;
			file->info.file.fields |= GP_FILE_INFO_MTIME;
		}
	}
#endif
	memcpy (info, &file->info, sizeof (CameraFileInfo));
	return (GP_OK);
}

static int
gp_filesystem_lru_clear (CameraFilesystem *fs)
{
	int n = 0;
	CameraFilesystemFile *ptr, *prev;

	GP_LOG_D ("Clearing fscache LRU list...");

	if (fs->lru_first == NULL) {
		GP_LOG_D ("fscache LRU list already empty");
		return (GP_OK);
	}

	ptr = prev = fs->lru_first;
	while (ptr != NULL) {
		n++;
		if (ptr->lru_prev != prev) {
			GP_LOG_D ("fscache LRU list corrupted (%i)", n);
			return (GP_ERROR);
		}
		prev = ptr;
		ptr = ptr->lru_next;

		prev->lru_prev = NULL;
		prev->lru_next = NULL;
	}

	fs->lru_first = NULL;
	fs->lru_last = NULL;
	fs->lru_size = 0;

	GP_LOG_D ("fscache LRU list cleared (removed %i items)", n);

	return (GP_OK);
}

static int
gp_filesystem_lru_remove_one (CameraFilesystem *fs, CameraFilesystemFile *item)
{
	if (item->lru_prev == NULL)
		return (GP_ERROR);

	/* Update the prev and next pointers. */
	if (item->lru_prev) item->lru_prev->lru_next = item->lru_next;
	if (item->lru_next) item->lru_next->lru_prev = item->lru_prev;
	if (fs->lru_last == item) {
		if (fs->lru_first == item) {

			/*
			 * Case 1: ITEM is the only one in the list. We'll
			 * remove it, and the list will be empty afterwards.
			 */
			fs->lru_last  = NULL;
			fs->lru_first = NULL;
		} else {

			/* Case 2: ITEM is the last in the list. */
			fs->lru_last = item->lru_prev;
		}
	} else if (fs->lru_first == item) {

		/* Case 3: ITEM is the first in the list. */
		fs->lru_first = item->lru_next;
		/* the first item prev links back to itself */
		fs->lru_first->lru_prev = fs->lru_first;
	}

	/* Clear the pointers */
	item->lru_prev = NULL;
	item->lru_next = NULL;

	return (GP_OK);
}

static int
gp_filesystem_lru_free (CameraFilesystem *fs)
{
	CameraFilesystemFile *ptr;
	unsigned long int size;

	C_PARAMS (fs && fs->lru_first);

	ptr = fs->lru_first;

	GP_LOG_D ("Freeing cached data for file '%s'...", ptr->name);

	/* Remove it from the list. */
	fs->lru_first = ptr->lru_next;
	if (fs->lru_first)
		fs->lru_first->lru_prev = fs->lru_first;
	else
		fs->lru_last = NULL;

	/* Free its content. */
	if (ptr->normal) {
		CR( gp_file_get_data_and_size (ptr->normal, NULL, &size));
		fs->lru_size -= size;
		gp_file_unref (ptr->normal);
		ptr->normal = NULL;
	}
	if (ptr->raw) {
		CR( gp_file_get_data_and_size (ptr->raw, NULL, &size));
		fs->lru_size -= size;
		gp_file_unref (ptr->raw);
		ptr->raw = NULL;
	}
	if (ptr->audio) {
		CR( gp_file_get_data_and_size (ptr->audio, NULL, &size));
		fs->lru_size -= size;
		gp_file_unref (ptr->audio);
		ptr->audio = NULL;
	}
	ptr->lru_next = ptr->lru_prev = NULL;
	return (GP_OK);
}

static int
gp_filesystem_lru_count (CameraFilesystem *fs)
{
	CameraFilesystemFile *ptr;
	int count = 0;

	if (!fs) return 0;
	ptr = fs->lru_first;
	while (ptr) {
		if (ptr->normal || ptr->raw || ptr->audio)
			count++;
		ptr = ptr->lru_next;
	}
	return count;
}

static int
gp_filesystem_lru_update (CameraFilesystem *fs,
			  const char *folder, const char *filename,
			  CameraFileType type,
			  CameraFile *file, GPContext *context)
{
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*xfile;
	CameraFile *oldfile = NULL;
	unsigned long int size;
	int x;
	char cached_images[1024];

	C_PARAMS (fs && folder && file);

	CR (gp_file_get_data_and_size (file, NULL, &size));

	/*
	 * The following is a very simple case which is used to prune
	 * the LRU. We keep PICTURES_TO_KEEP pictures in the LRU.
	 *
	 * We have 2 main scenarios:
	 *	- query all thumbnails (repeatedly) ... they are cached and
	 *	  are not pruned by lru free.
	 *	- download all images, linear.	no real need for caching.
	 *	- skip back 1 image (in viewers) (really? I don't know.)
	 *
	 * So lets just keep 2 pictures in memory.
	 */
	if (pictures_to_keep == -1) {
		if (gp_setting_get ("libgphoto", "cached-images", cached_images) == GP_OK) {
			pictures_to_keep = atoi(cached_images);
		} else {
			/* store a default setting */
			sprintf (cached_images, "%d", PICTURES_TO_KEEP);
			gp_setting_set ("libgphoto", "cached-images", cached_images);
		}
	}

	if (pictures_to_keep < 0) /* also sanity check, but no upper limit. */
		pictures_to_keep = PICTURES_TO_KEEP;

	x = gp_filesystem_lru_count (fs);
	while (x > pictures_to_keep) {
		CR (gp_filesystem_lru_free (fs));
		x = gp_filesystem_lru_count (fs);
	}

	GP_LOG_D ("Adding file '%s' from folder '%s' to the fscache LRU list "
		  "(type %i)...", filename, folder, type);

	/* Search folder and file */
	CR (lookup_folder_file (fs, folder, filename, &f, &xfile, context));

	/*
	 * If the file is already in the lru, we first remove it. Note that
	 * we will only remove 'normal', 'raw' and 'audio' from cache.
	 * See gp_filesystem_lru_free.
	 */
	if (xfile->lru_prev != NULL) {
		switch (type) {
		case GP_FILE_TYPE_NORMAL:
			oldfile = xfile->normal;
			break;
		case GP_FILE_TYPE_RAW:
			oldfile = xfile->raw;
			break;
		case GP_FILE_TYPE_AUDIO:
			oldfile = xfile->audio;
			break;
		case GP_FILE_TYPE_PREVIEW:
		case GP_FILE_TYPE_EXIF:
		case GP_FILE_TYPE_METADATA:
			break;
		default:
			gp_context_error (context, _("Unknown file type %i."),
					  type);
			return (GP_ERROR);
		}
		if (oldfile) {
			CR( gp_file_get_data_and_size (oldfile, NULL, &size));
			fs->lru_size -= size;
		}

		CR (gp_filesystem_lru_remove_one (fs, xfile));
	}

	/* Then add the file at the end of the LRU. */
	if (fs->lru_first == NULL) {
		fs->lru_first = xfile;
		fs->lru_last = xfile;

		/*
		 * For the first item, prev point it itself to show that the
		 * item is in the list.
		 */
		xfile->lru_prev = xfile;

	} else {
		xfile->lru_next = NULL;
		xfile->lru_prev = fs->lru_last;
		fs->lru_last->lru_next = xfile;
		fs->lru_last = xfile;
	}

	CR( gp_file_get_data_and_size (file, NULL, &size));
	fs->lru_size += size;

	GP_LOG_D ("File '%s' from folder '%s' added in fscache LRU list.",
		  filename, folder);

	return (GP_OK);
}

static int
gp_filesystem_lru_check (CameraFilesystem *fs)
{
	int n = 0;
	CameraFilesystemFile *ptr, *prev;

	GP_LOG_D ("Checking fscache LRU list integrity...");

	if (fs->lru_first == NULL) {
		GP_LOG_D ("fscache LRU list empty");
		return (GP_OK);
	}

	ptr = prev = fs->lru_first;
	while (ptr != NULL) {
		n++;
		if (ptr->lru_prev != prev) {
			GP_LOG_E ("fscache LRU list corrupted (%i)", n);
			return (GP_ERROR);
		}
		prev = ptr;
		ptr = ptr->lru_next;
	}

	GP_LOG_D ("fscache LRU list ok with %i items (%ld bytes)", n,
		  fs->lru_size);

	return (GP_OK);
}

/**
 * \brief Attach file content to a specified file.
 *
 * \param fs a #CameraFilesystem
 * \param folder a folder in the filesystem
 * \param file a #CameraFile
 * \param context: a #GPContext
 *
 * Tells the fs about a file. Typically, camera drivers will call this
 * function in case they get information about a file (i.e. preview) "for free"
 * on gp_camera_capture() or gp_camera_folder_list_files().
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_set_file_noop (CameraFilesystem *fs,
			     const char *folder, const char *filename,
			     CameraFileType type,
			     CameraFile *file, GPContext *context)
{
	CameraFileInfo info;
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*xfile;
	int r;
	time_t t;

	C_PARAMS (fs && folder && file);
	CC (context);
	CA (folder, context);

	GP_LOG_D ("Adding file '%s' to folder '%s' (type %i)...",
		  filename, folder, type);

	/* Search folder and file */
	CR (lookup_folder_file (fs, folder, filename, &f, &xfile, context));

	/*
	 * If we add a significant amount of data in the cache, we put (or
	 * move) a reference to this file in the LRU linked list. We
	 * assume that only GP_FILE_TYPE_[RAW,NORMAL,EXIF] will contain a
	 * significant amount of data.
	 */
	if ((type == GP_FILE_TYPE_RAW) || (type == GP_FILE_TYPE_NORMAL) ||
	    (type == GP_FILE_TYPE_AUDIO))
		CR (gp_filesystem_lru_update (fs, folder, filename, type, file, context));

	/* Redundant sanity check. */
	CR (gp_filesystem_lru_check (fs));

	switch (type) {
	case GP_FILE_TYPE_PREVIEW:
		if (xfile->preview)
			gp_file_unref (xfile->preview);
		xfile->preview = file;
		gp_file_ref (file);
		break;
	case GP_FILE_TYPE_NORMAL:
		if (xfile->normal)
			gp_file_unref (xfile->normal);
		xfile->normal = file;
		gp_file_ref (file);
		break;
	case GP_FILE_TYPE_RAW:
		if (xfile->raw)
			gp_file_unref (xfile->raw);
		xfile->raw = file;
		gp_file_ref (file);
		break;
	case GP_FILE_TYPE_AUDIO:
		if (xfile->audio)
			gp_file_unref (xfile->audio);
		xfile->audio = file;
		gp_file_ref (file);
		break;
	case GP_FILE_TYPE_EXIF:
		if (xfile->exif)
			gp_file_unref (xfile->exif);
		xfile->exif = file;
		gp_file_ref (file);
		break;
	case GP_FILE_TYPE_METADATA:
		if (xfile->metadata)
			gp_file_unref (xfile->metadata);
		xfile->metadata = file;
		gp_file_ref (file);
		break;
	default:
		gp_context_error (context, _("Unknown file type %i."), type);
		return (GP_ERROR);
	}

	/*
	 * If we didn't get a mtime, try to get it from the CameraFileInfo.
	 */
	CR (gp_file_get_mtime (file, &t));
	if (!t) {
		GP_LOG_D ("File does not contain mtime. Trying information on the file...");
		r = gp_filesystem_get_info (fs, folder, filename, &info, NULL);
		if ((r == GP_OK) && (info.file.fields & GP_FILE_INFO_MTIME))
			t = info.file.mtime;
	}

	/*
	 * If we still don't have the mtime and this is a normal
	 * file, check if there is EXIF data in the file that contains
	 * information on the mtime.
	 */
#ifdef HAVE_LIBEXIF
	if (!t && (type == GP_FILE_TYPE_NORMAL)) {
		unsigned long int size;
		const char *data;

		GP_LOG_D ("Searching data for mtime...");
		CR (gp_file_get_data_and_size (file, NULL, &size));
		if (size < 32*1024*1024) { /* just assume stuff above 32MB is not EXIF capable */
			CR (gp_file_get_data_and_size (file, &data, &size));
			t = get_exif_mtime ((unsigned char*)data, size);
		}
	}
	/*
	 * Still no mtime? Let's see if the camera offers us data of type
	 * GP_FILE_TYPE_EXIF that includes information on the mtime.
	 */
	if (!t) {
		GP_LOG_D ("Trying EXIF information...");
		t = gp_filesystem_get_exif_mtime (fs, folder, filename);
	}
#endif

	if (t)
		CR (gp_file_set_mtime (file, t));

	return (GP_OK);
}

/**
 * \brief Store the file information in the virtual fs
 * \param fs a #CameraFilesystem
 * \param folder the foldername
 * \param info the #CameraFileInfo to store
 * \param context a #GPContext
 *
 * In contrast to #gp_filesystem_set_info, #gp_filesystem_set_info_noop
 * will only change the file information in the fs. Typically, camera
 * drivers will use this function in case they get file information "for free"
 * on #gp_camera_capture or #gp_camera_folder_list_files.
 *
 * \return a gphoto2 error code
 **/
int
gp_filesystem_set_info_noop (CameraFilesystem *fs,
			     const char *folder, const char *filename,
			     CameraFileInfo info, GPContext *context)
{
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*xfile;

	C_PARAMS (fs && folder);
	CC (context);
	CA (folder, context);

	/* Search folder and file */
	CR (lookup_folder_file (fs, folder, filename, &f, &xfile, context));

	memcpy (&xfile->info, &info, sizeof (CameraFileInfo));
	xfile->info_dirty = 0;
	return (GP_OK);
}

/**
 * \brief Set information about a file
 * \param fs a #CameraFilesystem
 * \param folder foldername where the file resides
 * \param filename the files name
 * \param info the #CameraFileInfo to set
 * \param context a #GPContext
 *
 * Sets information about a file in the camera.
 *
 * \return a gphoto2 error code.
 **/
int
gp_filesystem_set_info (CameraFilesystem *fs, const char *folder,
			const char *filename, CameraFileInfo info,
			GPContext *context)
{
	int result;
	CameraFilesystemFolder	*f;
	CameraFilesystemFile	*xfile;

	C_PARAMS (fs && folder && filename);
	CC (context);
	CA (folder, context);

	if (!fs->set_info_func) {
		gp_context_error (context,
			_("The filesystem doesn't support setting file "
			  "information"));
		return (GP_ERROR_NOT_SUPPORTED);
	}

	/* Search folder and file */
	CR (lookup_folder_file (fs, folder, filename, &f, &xfile, context));

	/* Check if people want to set read-only attributes */
	if ((info.file.fields    & GP_FILE_INFO_TYPE)   ||
	    (info.file.fields    & GP_FILE_INFO_SIZE)   ||
	    (info.file.fields    & GP_FILE_INFO_WIDTH)  ||
	    (info.file.fields    & GP_FILE_INFO_HEIGHT) ||
	    (info.file.fields    & GP_FILE_INFO_STATUS) ||
	    (info.preview.fields & GP_FILE_INFO_TYPE)   ||
	    (info.preview.fields & GP_FILE_INFO_SIZE)   ||
	    (info.preview.fields & GP_FILE_INFO_WIDTH)  ||
	    (info.preview.fields & GP_FILE_INFO_HEIGHT) ||
	    (info.preview.fields & GP_FILE_INFO_STATUS) ||
	    (info.audio.fields   & GP_FILE_INFO_TYPE)   ||
	    (info.audio.fields   & GP_FILE_INFO_SIZE)   ||
	    (info.audio.fields   & GP_FILE_INFO_STATUS)) {
		gp_context_error (context, _("Read-only file attributes "
			"like width and height can not be changed."));
		return (GP_ERROR_BAD_PARAMETERS);
	}

	/*
	 * Set the info. If anything goes wrong, mark info as dirty,
	 * because the operation could have been partially successful.
	 */
	result = fs->set_info_func (fs, folder, filename, info, fs->data,
				    context);
	if (result < 0) {
		xfile->info_dirty = 1;
		return (result);
	}
	if (info.file.fields & GP_FILE_INFO_PERMISSIONS)
		xfile->info.file.permissions = info.file.permissions;

	return (GP_OK);
}

/**
 * \brief Get the storage information about this filesystem
 * \param fs the filesystem
 * \param storageinfo Pointer to receive a pointer to/array of storage info items
 * \param nrofstorageinfos Pointer to receive number of array entries
 * \param context a #GPContext
 *
 * This function is only called from gp_camera_get_storageinfo(). You may
 * want to make sure this information is consistent with the information on
 * gp_camera_get_storageinfo().
 *
 * Retrieves the storage information, like maximum and free space, for
 * the specified filesystem, if supported by the device. The storage
 * information is returned in an newly allocated array of
 * #CameraStorageInformation objects, to which the pointer pointed to
 * by #storageinfo will be set.
 *
 * The variable pointed to by #nrofstorageinfos will be set to the
 * number of elements in that array.
 *
 * It is the caller's responsibility to free the memory of the array.
 *
 * \return a gphoto error code
 **/
int
gp_filesystem_get_storageinfo (
	CameraFilesystem *fs,
	CameraStorageInformation **storageinfo,
	int *nrofstorageinfos,
	GPContext *context
) {
	C_PARAMS (fs && storageinfo && nrofstorageinfos);
	CC (context);

	if (!fs->storage_info_func) {
		gp_context_error (context,
			_("The filesystem doesn't support getting storage "
			  "information"));
		return (GP_ERROR_NOT_SUPPORTED);
	}
	return fs->storage_info_func (fs,
		storageinfo, nrofstorageinfos,
		fs->data, context);
}