view src/thumb_standard.c @ 1388:2496a345c452

Try to improve tab completion: when a key is pressed while tab completion menu is shown, the number of entries is reduced, dhowing all matching entries but the menu is no more closed after each key pressure. Number of possible entries in this menu was increased from 500 to 1000. Pressing TAB when path entry is empty now adds / (root directory).
author zas_
date Fri, 06 Mar 2009 22:34:38 +0000
parents 249bf204004a
children a3d3208b0c50
line wrap: on
line source

/*
 * Geeqie
 * (C) 2006 John Ellis
 * Copyright (C) 2008 - 2009 The Geeqie Team
 *
 * Author: John Ellis
 *
 * This software is released under the GNU General Public License (GNU GPL).
 * Please read the included file COPYING for more information.
 * This software comes with no warranty of any kind, use at your own risk!
 */


#include "main.h"
#include "thumb_standard.h"

#include "image-load.h"
#include "md5-util.h"
#include "pixbuf_util.h"
#include "ui_fileops.h"
#include "filedata.h"
#include "exif.h"
#include "metadata.h"


/*
 * This thumbnail caching implementation attempts to conform
 * to the Thumbnail Managing Standard proposed on freedesktop.org
 * The standard is documented here:
 *   http://triq.net/~jens/thumbnail-spec/index.html
 *  (why isn't it actually hosted on freedesktop.org?)
 *
 * This code attempts to conform to version 0.7.0 of the standard.
 *
 * Notes:
 *   > Validation of the thumb's embedded uri is a simple strcmp between our
 *     version of the escaped uri and the thumb's escaped uri. But not all uri
 *     escape functions escape the same set of chars, comparing the unescaped
 *     versions may be more accurate.
 *   > Only Thumb::URI and Thumb::MTime are stored in a thumb at this time.
 *     Storing the Size, Width, Height should probably be implemented.
 */


#define THUMB_SIZE_NORMAL 128
#define THUMB_SIZE_LARGE  256

#define THUMB_MARKER_URI    "tEXt::Thumb::URI"
#define THUMB_MARKER_MTIME  "tEXt::Thumb::MTime"
#define THUMB_MARKER_SIZE   "tEXt::Thumb::Size"
#define THUMB_MARKER_WIDTH  "tEXt::Thumb::Image::Width"
#define THUMB_MARKER_HEIGHT "tEXt::Thumb::Image::Height"
#define THUMB_MARKER_APP    "tEXt::Software"

#define THUMB_PERMS_FOLDER 0700
#define THUMB_PERMS_THUMB  0600



/*
 *-----------------------------------------------------------------------------
 * thumbnail loader
 *-----------------------------------------------------------------------------
 */


static void thumb_loader_std_error_cb(ImageLoader *il, gpointer data);
static gint thumb_loader_std_setup(ThumbLoaderStd *tl, FileData *fd);
static gint thumb_loader_std_setup_path(ThumbLoaderStd *tl, const gchar *path);


ThumbLoaderStd *thumb_loader_std_new(gint width, gint height)
{
	ThumbLoaderStd *tl;

	tl = g_new0(ThumbLoaderStd, 1);

	tl->standard_loader = TRUE;
	tl->requested_width = width;
	tl->requested_height = height;
	tl->cache_enable = options->thumbnails.enable_caching;

	return tl;
}

void thumb_loader_std_set_callbacks(ThumbLoaderStd *tl,
				    ThumbLoaderStdFunc func_done,
				    ThumbLoaderStdFunc func_error,
				    ThumbLoaderStdFunc func_progress,
				    gpointer data)
{
	if (!tl) return;

	tl->func_done = func_done;
	tl->func_error = func_error;
	tl->func_progress = func_progress;
	tl->data = data;
}

static void thumb_loader_std_reset(ThumbLoaderStd *tl)
{
	image_loader_free(tl->il);
	tl->il = NULL;

	file_data_unref(tl->fd);
	tl->fd = NULL;

	g_free(tl->thumb_path);
	tl->thumb_path = NULL;

	g_free(tl->thumb_uri);
	tl->thumb_uri = NULL;
	tl->local_uri = NULL;

	tl->thumb_path_local = FALSE;

	tl->cache_hit = FALSE;

	tl->source_mtime = 0;
	tl->source_size = 0;
	tl->source_mode = 0;

	tl->progress = 0.0;
}

static gchar *thumb_std_cache_path(const gchar *path, const gchar *uri, gint local,
				   const gchar *cache_subfolder)
{
	gchar *result = NULL;
	gchar *md5_text;
	guchar digest[16];
	gchar *name;

	if (!path || !uri || !cache_subfolder) return NULL;

	md5_get_digest((guchar *)uri, strlen(uri), digest);
	md5_text = md5_digest_to_text(digest);

	if (!md5_text) return NULL;

	name = g_strconcat(md5_text, THUMB_NAME_EXTENSION, NULL);

	if (local)
		{
		gchar *base = remove_level_from_path(path);

		result = g_build_filename(base, THUMB_FOLDER_LOCAL, cache_subfolder, name, NULL);
		g_free(base);
		}
	else
		{
		result = g_build_filename(homedir(), THUMB_FOLDER_GLOBAL, cache_subfolder, name, NULL);
		}

	g_free(name);
	g_free(md5_text);

	return result;
}

static gchar *thumb_loader_std_cache_path(ThumbLoaderStd *tl, gint local, GdkPixbuf *pixbuf, gint fail)
{
	const gchar *folder;
	gint w, h;

	if (!tl->fd || !tl->thumb_uri) return NULL;

	if (pixbuf)
		{
		w = gdk_pixbuf_get_width(pixbuf);
		h = gdk_pixbuf_get_height(pixbuf);
		}
	else
		{
		w = tl->requested_width;
		h = tl->requested_height;
		}

	if (fail)
		{
		folder = THUMB_FOLDER_FAIL;
		}
	else if (w > THUMB_SIZE_NORMAL || h > THUMB_SIZE_NORMAL)
		{
		folder = THUMB_FOLDER_LARGE;
		}
	else
		{
		folder = THUMB_FOLDER_NORMAL;
		}

	return thumb_std_cache_path(tl->fd->path,
				    (local) ?  tl->local_uri : tl->thumb_uri,
				    local, folder);
}

static gint thumb_loader_std_fail_check(ThumbLoaderStd *tl)
{
	gchar *fail_path;
	gint result = FALSE;

	fail_path = thumb_loader_std_cache_path(tl, FALSE, NULL, TRUE);
	if (isfile(fail_path))
		{
		GdkPixbuf *pixbuf;

		if (tl->cache_retry)
			{
			pixbuf = NULL;
			}
		else
			{
			gchar *pathl;

			pathl = path_from_utf8(fail_path);
			pixbuf = gdk_pixbuf_new_from_file(pathl, NULL);
			g_free(pathl);
			}

		if (pixbuf)
			{
			const gchar *mtime_str;

			mtime_str = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_MTIME);
			if (mtime_str && strtol(mtime_str, NULL, 10) == tl->source_mtime)
				{
				result = TRUE;
				DEBUG_1("thumb fail valid: %s", tl->fd->path);
				DEBUG_1("           thumb: %s", fail_path);
				}

			g_object_unref(G_OBJECT(pixbuf));
			}

		if (!result) unlink_file(fail_path);
		}
	g_free(fail_path);

	return result;
}

static gint thumb_loader_std_validate(ThumbLoaderStd *tl, GdkPixbuf *pixbuf)
{
	const gchar *valid_uri;
	const gchar *uri;
	const gchar *mtime_str;
	time_t mtime;
	gint w, h;

	if (!pixbuf) return FALSE;

	w = gdk_pixbuf_get_width(pixbuf);
	h = gdk_pixbuf_get_height(pixbuf);

	if (w != THUMB_SIZE_NORMAL && w != THUMB_SIZE_LARGE &&
	    h != THUMB_SIZE_NORMAL && h != THUMB_SIZE_LARGE) return FALSE;

	valid_uri = (tl->thumb_path_local) ? tl->local_uri : tl->thumb_uri;

	uri = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_URI);
	mtime_str = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_MTIME);

	if (!mtime_str || !uri || !valid_uri) return FALSE;
	if (strcmp(uri, valid_uri) != 0) return FALSE;

	mtime = strtol(mtime_str, NULL, 10);
	if (tl->source_mtime != mtime) return FALSE;

	return TRUE;
}

static void thumb_loader_std_save(ThumbLoaderStd *tl, GdkPixbuf *pixbuf)
{
	gchar *base_path;
	gchar *tmp_path;
	gint fail;

	if (!tl->cache_enable || tl->cache_hit) return;
	if (tl->thumb_path) return;

	if (!pixbuf)
		{
		/* local failures are not stored */
		if (tl->cache_local) return;

		pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 1, 1);
		fail = TRUE;
		}
	else
		{
		g_object_ref(G_OBJECT(pixbuf));
		fail = FALSE;
		}

	tl->thumb_path = thumb_loader_std_cache_path(tl, tl->cache_local, pixbuf, fail);
	if (!tl->thumb_path)
		{
		g_object_unref(G_OBJECT(pixbuf));
		return;
		}
	tl->thumb_path_local = tl->cache_local;

	/* create thumbnail dir if needed */
	base_path = remove_level_from_path(tl->thumb_path);
	if (tl->cache_local)
		{
		if (!isdir(base_path))
			{
			struct stat st;
			gchar *source_base;

			source_base = remove_level_from_path(tl->fd->path);
			if (stat_utf8(source_base, &st))
				{
				recursive_mkdir_if_not_exists(base_path, st.st_mode);
				}
			g_free(source_base);
			}
		}
	else
		{
		recursive_mkdir_if_not_exists(base_path, THUMB_PERMS_FOLDER);
		}
	g_free(base_path);

	DEBUG_1("thumb saving: %s", tl->fd->path);
	DEBUG_1("       saved: %s", tl->thumb_path);

	/* save thumb, using a temp file then renaming into place */
	tmp_path = unique_filename(tl->thumb_path, ".tmp", "_", 2);
	if (tmp_path)
		{
		const gchar *mark_uri;
		gchar *mark_app;
		gchar *mark_mtime;
		gchar *pathl;
		gint success;

		mark_uri = (tl->cache_local) ? tl->local_uri :tl->thumb_uri;

		mark_app = g_strdup_printf("%s %s", GQ_APPNAME, VERSION);
		mark_mtime = g_strdup_printf("%lu", tl->source_mtime);

		pathl = path_from_utf8(tmp_path);
		success = gdk_pixbuf_save(pixbuf, pathl, "png", NULL,
					  THUMB_MARKER_URI, mark_uri,
					  THUMB_MARKER_MTIME, mark_mtime,
					  THUMB_MARKER_APP, mark_app,
					  NULL);
		if (success)
			{
			chmod(pathl, (tl->cache_local) ? tl->source_mode : THUMB_PERMS_THUMB);
			success = rename_file(tmp_path, tl->thumb_path);
			}

		g_free(pathl);

		g_free(mark_mtime);
		g_free(mark_app);

		g_free(tmp_path);
		if (!success)
			{
			DEBUG_1("thumb save failed: %s", tl->fd->path);
			DEBUG_1("            thumb: %s", tl->thumb_path);
			}

		}

	g_object_unref(G_OBJECT(pixbuf));
}

static void thumb_loader_std_set_fallback(ThumbLoaderStd *tl)
{
	if (tl->fd->thumb_pixbuf) g_object_unref(tl->fd->thumb_pixbuf);
	tl->fd->thumb_pixbuf = pixbuf_fallback(tl->fd, tl->requested_width, tl->requested_height);
}

static GdkPixbuf *thumb_loader_std_finish(ThumbLoaderStd *tl, GdkPixbuf *pixbuf, gint shrunk)
{
	GdkPixbuf *pixbuf_thumb = NULL;
	GdkPixbuf *result;
	GdkPixbuf *rotated = NULL;
	gint sw, sh;


	if (!tl->cache_hit && options->image.exif_rotate_enable)
		{
		if (!tl->fd->exif_orientation)
			{
			tl->fd->exif_orientation = metadata_read_int(tl->fd, "Exif.Image.Orientation", EXIF_ORIENTATION_TOP_LEFT);
			}
		
		if (tl->fd->exif_orientation != EXIF_ORIENTATION_TOP_LEFT)
			{
			rotated = pixbuf_apply_orientation(pixbuf, tl->fd->exif_orientation);
			pixbuf = rotated;
			}
		}

	sw = gdk_pixbuf_get_width(pixbuf);
	sh = gdk_pixbuf_get_height(pixbuf);

	if (tl->cache_enable)
		{
		if (!tl->cache_hit)
			{
			gint cache_w, cache_h;

			if (tl->requested_width > THUMB_SIZE_NORMAL || tl->requested_height > THUMB_SIZE_NORMAL)
				{
				cache_w = cache_h = THUMB_SIZE_LARGE;
				}
			else
				{
				cache_w = cache_h = THUMB_SIZE_NORMAL;
				}

			if (sw > cache_w || sh > cache_h || shrunk)
				{
				gint thumb_w, thumb_h;

				if (pixbuf_scale_aspect(cache_w, cache_h, sw, sh,
								  &thumb_w, &thumb_h))
					{
					pixbuf_thumb = gdk_pixbuf_scale_simple(pixbuf, thumb_w, thumb_h,
									       (GdkInterpType)options->thumbnails.quality);
					}
				else
					{
					pixbuf_thumb = pixbuf;
					g_object_ref(G_OBJECT(pixbuf_thumb));
					}

				thumb_loader_std_save(tl, pixbuf_thumb);
				}
			}
		else if (tl->cache_hit &&
			 tl->cache_local && !tl->thumb_path_local)
			{
			/* A local cache save was requested, but a valid thumb is in $HOME,
			 * so specifically save as a local thumbnail.
			 */
			g_free(tl->thumb_path);
			tl->thumb_path = NULL;

			tl->cache_hit = FALSE;

			DEBUG_1("thumb copied: %s", tl->fd->path);

			thumb_loader_std_save(tl, pixbuf);
			}
		}

	if (sw <= tl->requested_width && sh <= tl->requested_height)
		{
		result = pixbuf;
		g_object_ref(result);
		}
	else
		{
		gint thumb_w, thumb_h;

		if (pixbuf_thumb)
			{
			pixbuf = pixbuf_thumb;
			sw = gdk_pixbuf_get_width(pixbuf);
			sh = gdk_pixbuf_get_height(pixbuf);
			}

		if (pixbuf_scale_aspect(tl->requested_width, tl->requested_height, sw, sh,
						  &thumb_w, &thumb_h))
			{
			result = gdk_pixbuf_scale_simple(pixbuf, thumb_w, thumb_h,
							 (GdkInterpType)options->thumbnails.quality);
			}
		else
			{
			result = pixbuf;
			g_object_ref(result);
			}
		}

	if (pixbuf_thumb) g_object_unref(pixbuf_thumb);
	if (rotated) g_object_unref(rotated);

	return result;
}

static gint thumb_loader_std_next_source(ThumbLoaderStd *tl, gint remove_broken)
{
	image_loader_free(tl->il);
	tl->il = NULL;

	if (tl->thumb_path)
		{
		if (!tl->thumb_path_local && remove_broken)
			{
			DEBUG_1("thumb broken, unlinking: %s", tl->thumb_path);
			unlink_file(tl->thumb_path);
			}

		g_free(tl->thumb_path);
		tl->thumb_path = NULL;

		if (!tl->thumb_path_local)
			{
			tl->thumb_path = thumb_loader_std_cache_path(tl, TRUE, NULL, FALSE);
			if (isfile(tl->thumb_path) && thumb_loader_std_setup_path(tl, tl->thumb_path))
				{
				tl->thumb_path_local = TRUE;
				return TRUE;
				}

			g_free(tl->thumb_path);
			tl->thumb_path = NULL;
			}

		if (thumb_loader_std_setup(tl, tl->fd)) return TRUE;
		}

	thumb_loader_std_save(tl, NULL);
	return FALSE;
}

static void thumb_loader_std_done_cb(ImageLoader *il, gpointer data)
{
	ThumbLoaderStd *tl = data;
	GdkPixbuf *pixbuf;

	DEBUG_1("thumb image done: %s", tl->fd ? tl->fd->path : "???");
	DEBUG_1("            from: %s", image_loader_get_fd(tl->il)->path);

	pixbuf = image_loader_get_pixbuf(tl->il);
	if (!pixbuf)
		{
		DEBUG_1("...but no pixbuf");
		thumb_loader_std_error_cb(il, data);
		return;
		}

	if (tl->thumb_path && !thumb_loader_std_validate(tl, pixbuf))
		{
		if (thumb_loader_std_next_source(tl, TRUE)) return;

		if (tl->func_error) tl->func_error(tl, tl->data);
		return;
		}

	tl->cache_hit = (tl->thumb_path != NULL);

	if (tl->fd)
		{
		if (tl->fd->thumb_pixbuf) g_object_unref(tl->fd->thumb_pixbuf);
		tl->fd->thumb_pixbuf = thumb_loader_std_finish(tl, pixbuf, image_loader_get_shrunk(il));
		}

	if (tl->func_done) tl->func_done(tl, tl->data);
}

static void thumb_loader_std_error_cb(ImageLoader *il, gpointer data)
{
	ThumbLoaderStd *tl = data;

	/* if at least some of the image is available, go to done */
	if (image_loader_get_pixbuf(tl->il) != NULL)
		{
		thumb_loader_std_done_cb(il, data);
		return;
		}
	
	DEBUG_1("thumb image error: %s", tl->fd->path);
	DEBUG_1("             from: %s", image_loader_get_fd(tl->il)->path);

	if (thumb_loader_std_next_source(tl, TRUE)) return;

	thumb_loader_std_set_fallback(tl);
	
	if (tl->func_error) tl->func_error(tl, tl->data);
}

static void thumb_loader_std_progress_cb(ImageLoader *il, gdouble percent, gpointer data)
{
	ThumbLoaderStd *tl = data;

	tl->progress = (gdouble)percent;

	if (tl->func_progress) tl->func_progress(tl, tl->data);
}

static gint thumb_loader_std_setup(ThumbLoaderStd *tl, FileData *fd)
{
	tl->il = image_loader_new(fd);
	image_loader_set_priority(tl->il, G_PRIORITY_LOW);

	if (options->thumbnails.fast)
		{
		/* this will speed up jpegs by up to 3x in some cases */
		if (tl->requested_width <= THUMB_SIZE_NORMAL &&
		    tl->requested_height <= THUMB_SIZE_NORMAL)
			{
			image_loader_set_requested_size(tl->il, THUMB_SIZE_NORMAL, THUMB_SIZE_NORMAL);
			}
		else
			{
			image_loader_set_requested_size(tl->il, THUMB_SIZE_LARGE, THUMB_SIZE_LARGE);
			}
		}

	g_signal_connect(G_OBJECT(tl->il), "error", (GCallback)thumb_loader_std_error_cb, tl);
	if (tl->func_progress)
		{
		g_signal_connect(G_OBJECT(tl->il), "percent", (GCallback)thumb_loader_std_progress_cb, tl);
		}
	g_signal_connect(G_OBJECT(tl->il), "done", (GCallback)thumb_loader_std_done_cb, tl);

	if (image_loader_start(tl->il))
		{
		return TRUE;
		}

	image_loader_free(tl->il);
	tl->il = NULL;
	return FALSE;
}

static gint thumb_loader_std_setup_path(ThumbLoaderStd *tl, const gchar *path)
{
	FileData *fd = file_data_new_simple(path);
	gint ret = thumb_loader_std_setup(tl, fd);
	file_data_unref(fd);
	return ret;
}

/*
 * Note: Currently local_cache only specifies where to save a _new_ thumb, if
 *       a valid existing thumb is found anywhere the local thumb will not be created.
 */
void thumb_loader_std_set_cache(ThumbLoaderStd *tl, gint enable_cache, gint local, gint retry_failed)
{
	if (!tl) return;

	tl->cache_enable = enable_cache;
	tl->cache_local = local;
	tl->cache_retry = retry_failed;
}

gint thumb_loader_std_start(ThumbLoaderStd *tl, FileData *fd)
{
	static gchar *thumb_cache = NULL;
	struct stat st;

	if (!tl || !fd) return FALSE;

	thumb_loader_std_reset(tl);


	tl->fd = file_data_ref(fd);
	if (!stat_utf8(fd->path, &st))
		{
		thumb_loader_std_set_fallback(tl);
		return FALSE;
		}
	tl->source_mtime = st.st_mtime;
	tl->source_size = st.st_size;
	tl->source_mode = st.st_mode;

	if (!thumb_cache) thumb_cache = g_build_filename(homedir(), THUMB_FOLDER_GLOBAL, NULL);
	if (strncmp(tl->fd->path, thumb_cache, strlen(thumb_cache)) != 0)
		{
		gchar *pathl;

		pathl = path_from_utf8(fd->path);
		tl->thumb_uri = g_filename_to_uri(pathl, NULL, NULL);
		tl->local_uri = filename_from_path(tl->thumb_uri);
		g_free(pathl);
		}

	if (tl->cache_enable)
		{
		gint found;

		tl->thumb_path = thumb_loader_std_cache_path(tl, FALSE, NULL, FALSE);
		tl->thumb_path_local = FALSE;

		found = isfile(tl->thumb_path);
		if (found && thumb_loader_std_setup_path(tl, tl->thumb_path)) return TRUE;

		if (thumb_loader_std_fail_check(tl) ||
		    !thumb_loader_std_next_source(tl, found))
			{
			thumb_loader_std_set_fallback(tl);
			return FALSE;
			}
		return TRUE;
		}

	if (!thumb_loader_std_setup(tl, tl->fd))
		{
		thumb_loader_std_save(tl, NULL);
		thumb_loader_std_set_fallback(tl);
		return FALSE;
		}

	return TRUE;
}

void thumb_loader_std_free(ThumbLoaderStd *tl)
{
	if (!tl) return;

	thumb_loader_std_reset(tl);
	g_free(tl);
}

GdkPixbuf *thumb_loader_std_get_pixbuf(ThumbLoaderStd *tl)
{
	GdkPixbuf *pixbuf;

	if (tl && tl->fd && tl->fd->thumb_pixbuf)
		{
		pixbuf = tl->fd->thumb_pixbuf;
		g_object_ref(pixbuf);
		}
	else
		{
		pixbuf = pixbuf_fallback(NULL, tl->requested_width, tl->requested_height);
		}

	return pixbuf;
}


typedef struct _ThumbValidate ThumbValidate;
struct _ThumbValidate
{
	ThumbLoaderStd *tl;
	gchar *path;
	gint days;

	void (*func_valid)(const gchar *path, gint valid, gpointer data);
	gpointer data;

	gint idle_id;
};

static void thumb_loader_std_thumb_file_validate_free(ThumbValidate *tv)
{
	thumb_loader_std_free(tv->tl);
	g_free(tv->path);
	g_free(tv);
}

void thumb_loader_std_thumb_file_validate_cancel(ThumbLoaderStd *tl)
{
	ThumbValidate *tv;

	if (!tl) return;

	tv = tl->data;

	if (tv->idle_id != -1) g_source_remove(tv->idle_id);
	tv->idle_id = -1;

	thumb_loader_std_thumb_file_validate_free(tv);
}

static void thumb_loader_std_thumb_file_validate_finish(ThumbValidate *tv, gint valid)
{
	if (tv->func_valid) tv->func_valid(tv->path, valid, tv->data);

	thumb_loader_std_thumb_file_validate_free(tv);
}

static void thumb_loader_std_thumb_file_validate_done_cb(ThumbLoaderStd *tl, gpointer data)
{
	ThumbValidate *tv = data;
	GdkPixbuf *pixbuf;
	gint valid = FALSE;

	/* this function is called on success, so the pixbuf should not be a fallback*/
	pixbuf = thumb_loader_std_get_pixbuf(tv->tl);
	if (pixbuf)
		{
		const gchar *uri;
		const gchar *mtime_str;

		uri = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_URI);
		mtime_str = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_MTIME);
		if (uri && mtime_str)
			{
			if (strncmp(uri, "file:", strlen("file:")) == 0)
				{
				struct stat st;
				gchar *target;

				target = g_filename_from_uri(uri, NULL, NULL);
				if (stat(target, &st) == 0 &&
				    st.st_mtime == strtol(mtime_str, NULL, 10))
					{
					valid = TRUE;
					}
				g_free(target);
				}
			else
				{
				struct stat st;

				DEBUG_1("thumb uri foreign, doing day check: %s", uri);

				if (stat_utf8(tv->path, &st))
					{
					time_t now;

					now = time(NULL);
					if (st.st_atime >= now - (time_t)tv->days * 24 * 60 * 60)
						{
						valid = TRUE;
						}
					}
				}
			}

		g_object_unref(pixbuf);
		}

	thumb_loader_std_thumb_file_validate_finish(tv, valid);
}

static void thumb_loader_std_thumb_file_validate_error_cb(ThumbLoaderStd *tl, gpointer data)
{
	ThumbValidate *tv = data;

	thumb_loader_std_thumb_file_validate_finish(tv, FALSE);
}

static gint thumb_loader_std_thumb_file_validate_idle_cb(gpointer data)
{
	ThumbValidate *tv = data;

	tv->idle_id = -1;
	thumb_loader_std_thumb_file_validate_finish(tv, FALSE);

	return FALSE;
}

ThumbLoaderStd *thumb_loader_std_thumb_file_validate(const gchar *thumb_path, gint allowed_days,
						     void (*func_valid)(const gchar *path, gint valid, gpointer data),
						     gpointer data)
{
	ThumbValidate *tv;

	tv = g_new0(ThumbValidate, 1);

	tv->tl = thumb_loader_std_new(THUMB_SIZE_LARGE, THUMB_SIZE_LARGE);
	thumb_loader_std_set_callbacks(tv->tl,
				       thumb_loader_std_thumb_file_validate_done_cb,
				       thumb_loader_std_thumb_file_validate_error_cb,
				       NULL,
				       tv);
	thumb_loader_std_reset(tv->tl);

	tv->path = g_strdup(thumb_path);
	tv->days = allowed_days;
	tv->func_valid = func_valid;
	tv->data = data;

	if (!thumb_loader_std_setup_path(tv->tl, thumb_path))
		{
		tv->idle_id = g_idle_add(thumb_loader_std_thumb_file_validate_idle_cb, tv);
		}
	else
		{
		tv->idle_id = -1;
		}

	return tv->tl;
}

static void thumb_std_maint_remove_one(const gchar *source, const gchar *uri, gint local,
				       const gchar *subfolder)
{
	gchar *thumb_path;

	thumb_path = thumb_std_cache_path(source,
					  (local) ? filename_from_path(uri) : uri,
					  local, subfolder);
	if (isfile(thumb_path))
		{
		DEBUG_1("thumb removing: %s", thumb_path);
		unlink_file(thumb_path);
		}
	g_free(thumb_path);
}

/* this also removes local thumbnails (the source is gone so it makes sense) */
void thumb_std_maint_removed(const gchar *source)
{
	gchar *uri;
	gchar *sourcel;

	sourcel = path_from_utf8(source);
	uri = g_filename_to_uri(sourcel, NULL, NULL);
	g_free(sourcel);

	/* all this to remove a thumbnail? */

	thumb_std_maint_remove_one(source, uri, FALSE, THUMB_FOLDER_NORMAL);
	thumb_std_maint_remove_one(source, uri, FALSE, THUMB_FOLDER_LARGE);
	thumb_std_maint_remove_one(source, uri, FALSE, THUMB_FOLDER_FAIL);
	thumb_std_maint_remove_one(source, uri, TRUE, THUMB_FOLDER_NORMAL);
	thumb_std_maint_remove_one(source, uri, TRUE, THUMB_FOLDER_LARGE);

	g_free(uri);
}

typedef struct _TMaintMove TMaintMove;
struct _TMaintMove
{
	gchar *source;
	gchar *dest;

	ThumbLoaderStd *tl;
	gchar *source_uri;
	gchar *thumb_path;

	gint pass;
};

static GList *thumb_std_maint_move_list = NULL;
static GList *thumb_std_maint_move_tail = NULL;


static void thumb_std_maint_move_step(TMaintMove *tm);
static gint thumb_std_maint_move_idle(gpointer data);


static void thumb_std_maint_move_validate_cb(const gchar *path, gint valid, gpointer data)
{
	TMaintMove *tm = data;
	GdkPixbuf *pixbuf;

	/* this function is called on success, so the pixbuf should not be a fallback*/
	pixbuf = thumb_loader_std_get_pixbuf(tm->tl);
	if (pixbuf)
		{
		const gchar *uri;
		const gchar *mtime_str;

		uri = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_URI);
		mtime_str = gdk_pixbuf_get_option(pixbuf, THUMB_MARKER_MTIME);

		if (uri && mtime_str && strcmp(uri, tm->source_uri) == 0)
			{
			gchar *pathl;

			/* The validation utility abuses ThumbLoader, and we
			 * abuse the utility just to load the thumbnail,
			 * but the loader needs to look sane for the save to complete.
			 */

			tm->tl->cache_enable = TRUE;
			tm->tl->cache_hit = FALSE;
			tm->tl->cache_local = FALSE;

			file_data_unref(tm->tl->fd);
			tm->tl->fd = file_data_new_simple(tm->dest);
			tm->tl->source_mtime = strtol(mtime_str, NULL, 10);

			pathl = path_from_utf8(tm->tl->fd->path);
			g_free(tm->tl->thumb_uri);
			tm->tl->thumb_uri = g_filename_to_uri(pathl, NULL, NULL);
			tm->tl->local_uri = filename_from_path(tm->tl->thumb_uri);
			g_free(pathl);

			g_free(tm->tl->thumb_path);
			tm->tl->thumb_path = NULL;
			tm->tl->thumb_path_local = FALSE;

			DEBUG_1("thumb move attempting save:");

			thumb_loader_std_save(tm->tl, pixbuf);
			}

		DEBUG_1("thumb move unlink: %s", tm->thumb_path);
		unlink_file(tm->thumb_path);
		g_object_unref(pixbuf);
		}

	thumb_std_maint_move_step(tm);
}

static void thumb_std_maint_move_step(TMaintMove *tm)
{
	const gchar *folder;

	tm->pass++;
	if (tm->pass > 2)
		{
		g_free(tm->source);
		g_free(tm->dest);
		g_free(tm->source_uri);
		g_free(tm->thumb_path);
		g_free(tm);

		if (thumb_std_maint_move_list)
			{
			g_idle_add_full(G_PRIORITY_LOW, thumb_std_maint_move_idle, NULL, NULL);
			}

		return;
		}

	folder = (tm->pass == 1) ? THUMB_FOLDER_NORMAL : THUMB_FOLDER_LARGE;

	g_free(tm->thumb_path);
	tm->thumb_path = thumb_std_cache_path(tm->source, tm->source_uri, FALSE, folder);
	tm->tl = thumb_loader_std_thumb_file_validate(tm->thumb_path, 0,
						      thumb_std_maint_move_validate_cb, tm);
}

static gint thumb_std_maint_move_idle(gpointer data)
{
	TMaintMove *tm;
	gchar *pathl;

	if (!thumb_std_maint_move_list) return FALSE;

	tm = thumb_std_maint_move_list->data;

	thumb_std_maint_move_list = g_list_remove(thumb_std_maint_move_list, tm);
	if (!thumb_std_maint_move_list) thumb_std_maint_move_tail = NULL;

	pathl = path_from_utf8(tm->source);
	tm->source_uri = g_filename_to_uri(pathl, NULL, NULL);
	g_free(pathl);

	tm->pass = 0;

	thumb_std_maint_move_step(tm);

	return FALSE;
}

/* This will schedule a move of the thumbnail for source image to dest when idle.
 * We do this so that file renaming or moving speed is not sacrificed by
 * moving the thumbnails at the same time because:
 *
 * This cache design requires the tedious task of loading the png thumbnails and saving them.
 *
 * The thumbnails are processed when the app is idle. If the app
 * exits early well too bad - they can simply be regenerated from scratch.
 *
 * This does not manage local thumbnails (fixme ?)
 */
void thumb_std_maint_moved(const gchar *source, const gchar *dest)
{
	TMaintMove *tm;

	tm = g_new0(TMaintMove, 1);
	tm->source = g_strdup(source);
	tm->dest = g_strdup(dest);

	if (!thumb_std_maint_move_list)
		{
		g_idle_add_full(G_PRIORITY_LOW, thumb_std_maint_move_idle, NULL, NULL);
		}

	if (thumb_std_maint_move_tail)
		{
		thumb_std_maint_move_tail = g_list_append(thumb_std_maint_move_tail, tm);
		thumb_std_maint_move_tail = thumb_std_maint_move_tail->next;
		}
	else
		{
		thumb_std_maint_move_list = g_list_append(thumb_std_maint_move_list, tm);
		thumb_std_maint_move_tail = thumb_std_maint_move_list;
		}
}
/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */