Implement wide thumbnail cache invalidation

This commit is contained in:
Přemysl Eric Janouch 2022-02-20 13:13:49 +01:00
parent 6c748439ed
commit a28fbf25bc
Signed by: p
GPG Key ID: A0420B94F92B9493
4 changed files with 198 additions and 4 deletions

View File

@ -77,10 +77,17 @@ gdk-pixbuf modules.
<h2>Thumbnails</h2> <h2>Thumbnails</h2>
<p><i>fiv</i> uses a custom way of storing thumbnails, and doesn't currently <p><i>fiv</i> uses a custom means of storing thumbnails, and doesn't currently
provide any means of invalidating this cache. Should you find out that your invalidate this cache automatically. Should you find out that your
<i>~/.cache/thumbnails</i> directory is taking up too much space, run: <i>~/.cache/thumbnails</i> directory is taking up too much space, run:
<pre>
fiv --invalidate-cache
</pre>
<p>to trim it down. Alternatively, if you want to get rid of _all_ thumbnails,
even for existing images:
<pre> <pre>
rm -rf ~/.cache/thumbnails/wide-* rm -rf ~/.cache/thumbnails/wide-*
</pre> </pre>

View File

@ -17,10 +17,11 @@
#include "config.h" #include "config.h"
#include <glib/gstdio.h>
#include <spng.h> #include <spng.h>
#include <webp/demux.h>
#include <webp/encode.h> #include <webp/encode.h>
#include <webp/mux.h> #include <webp/mux.h>
#include <glib/gstdio.h>
#include <errno.h> #include <errno.h>
#include <math.h> #include <math.h>
@ -582,3 +583,179 @@ fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size)
g_free(uri); g_free(uri);
return result; return result;
} }
// --- Invalidation ------------------------------------------------------------
static void
print_error(GFile *file, GError *error)
{
gchar *name = g_file_get_parse_name(file);
g_printerr("%s: %s\n", name, error->message);
g_free(name);
g_error_free(error);
}
static gchar *
identify_wide_thumbnail(GMappedFile *mf, time_t *mtime, GError **error)
{
WebPDemuxer *demux = WebPDemux(&(WebPData) {
.bytes = (const uint8_t *) g_mapped_file_get_contents(mf),
.size = g_mapped_file_get_length(mf),
});
if (!demux) {
set_error(error, "demux failure while reading metadata");
return NULL;
}
WebPChunkIterator chunk_iter = {};
gchar *uri = NULL;
if (!WebPDemuxGetChunk(demux, "THUM", 1, &chunk_iter)) {
set_error(error, "missing THUM chunk");
goto fail;
}
// Similar to check_wide_thumbnail_texts(), but with a different purpose.
const char *p = (const char *) chunk_iter.chunk.bytes,
*end = p + chunk_iter.chunk.size, *key = NULL, *nul = NULL;
for (; (nul = memchr(p, '\0', end - p)); p = ++nul)
if (key) {
if (!strcmp(key, THUMB_URI) && !uri)
uri = g_strdup(p);
if (!strcmp(key, THUMB_MTIME))
*mtime = atol(p);
key = NULL;
} else {
key = p;
}
if (!uri)
set_error(error, "missing target URI");
WebPDemuxReleaseChunkIterator(&chunk_iter);
fail:
WebPDemuxDelete(demux);
return uri;
}
static void
check_wide_thumbnail(GFile *thumbnail, GError **error)
{
// Not all errors are enough of a reason for us to delete something.
GError *tolerable = NULL;
const char *path = g_file_peek_path(thumbnail);
GMappedFile *mf = g_mapped_file_new(path, FALSE, &tolerable);
if (!mf) {
print_error(thumbnail, tolerable);
return;
}
time_t target_mtime = 0;
gchar *target_uri = identify_wide_thumbnail(mf, &target_mtime, error);
g_mapped_file_unref(mf);
if (!target_uri)
return;
// This should not occur at all, we're being pedantic.
gchar *sum = g_compute_checksum_for_string(G_CHECKSUM_MD5, target_uri, -1);
gchar *expected_basename = g_strdup_printf("%s.webp", sum);
gchar *basename = g_path_get_basename(path);
gboolean match = !strcmp(basename, expected_basename);
g_free(basename);
g_free(expected_basename);
g_free(sum);
if (!match) {
set_error(error, "URI checksum mismatch");
g_free(target_uri);
return;
}
GFile *target = g_file_new_for_uri(target_uri);
g_free(target_uri);
GFileInfo *info = g_file_query_info(target,
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_TIME_MODIFIED,
G_FILE_QUERY_INFO_NONE, NULL, &tolerable);
g_object_unref(target);
if (g_error_matches(tolerable, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
g_propagate_error(error, tolerable);
return;
} else if (tolerable) {
print_error(thumbnail, tolerable);
return;
}
GDateTime *mdatetime = g_file_info_get_modification_date_time(info);
g_object_unref(info);
if (!mdatetime) {
set_error(&tolerable, "cannot retrieve file modification time");
print_error(thumbnail, tolerable);
return;
}
if (g_date_time_to_unix(mdatetime) != target_mtime)
set_error(error, "mtime mismatch");
g_date_time_unref(mdatetime);
}
static void
invalidate_wide_thumbnail(GFile *thumbnail)
{
// It's possible to lift that restriction in the future,
// but we need to codify how the modification time should be checked.
const char *path = g_file_peek_path(thumbnail);
if (!path) {
print_error(thumbnail,
g_error_new_literal(G_IO_ERROR, G_IO_ERROR_FAILED,
"thumbnails are expected to be local files"));
return;
}
// You cannot kill what you did not create.
if (!g_str_has_suffix(path, ".webp"))
return;
GError *error = NULL;
check_wide_thumbnail(thumbnail, &error);
if (error) {
g_debug("Deleting %s: %s", path, error->message);
g_clear_error(&error);
if (!g_file_delete(thumbnail, NULL, &error))
print_error(thumbnail, error);
}
}
static void
invalidate_wide_thumbnail_directory(GFile *directory)
{
GError *error = NULL;
GFileEnumerator *enumerator = g_file_enumerate_children(directory,
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
G_FILE_QUERY_INFO_NONE, NULL, &error);
if (!enumerator) {
print_error(directory, error);
return;
}
GFileInfo *info = NULL;
GFile *child = NULL;
while (g_file_enumerator_iterate(enumerator, &info, &child, NULL, &error) &&
info != NULL) {
if (g_file_info_get_file_type(info) == G_FILE_TYPE_REGULAR)
invalidate_wide_thumbnail(child);
}
g_object_unref(enumerator);
if (error)
print_error(directory, error);
}
void
fiv_thumbnail_invalidate(void)
{
gchar *thumbnails_dir = fiv_thumbnail_get_root();
for (int i = 0; i < FIV_THUMBNAIL_SIZE_COUNT; i++) {
const char *name = fiv_thumbnail_sizes[i].thumbnail_spec_name;
gchar *dirname = g_strdup_printf("wide-%s", name);
GFile *dir = g_file_new_build_filename(thumbnails_dir, dirname, NULL);
g_free(dirname);
invalidate_wide_thumbnail_directory(dir);
g_object_unref(dir);
}
g_free(thumbnails_dir);
}

View File

@ -62,3 +62,6 @@ gboolean fiv_thumbnail_produce(
/// Retrieves a thumbnail of the most appropriate quality and resolution /// Retrieves a thumbnail of the most appropriate quality and resolution
/// for the target file. /// for the target file.
cairo_surface_t *fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size); cairo_surface_t *fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size);
/// Invalidate the wide thumbnail cache. May write to standard streams.
void fiv_thumbnail_invalidate(void);

9
fiv.c
View File

@ -1760,7 +1760,7 @@ int
main(int argc, char *argv[]) main(int argc, char *argv[])
{ {
gboolean show_version = FALSE, show_supported_media_types = FALSE, gboolean show_version = FALSE, show_supported_media_types = FALSE,
browse = FALSE; invalidate_cache = FALSE, browse = FALSE;
gchar **path_args = NULL, *thumbnail_size = NULL; gchar **path_args = NULL, *thumbnail_size = NULL;
const GOptionEntry options[] = { const GOptionEntry options[] = {
{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &path_args, {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &path_args,
@ -1774,6 +1774,9 @@ main(int argc, char *argv[])
{"thumbnail", 0, G_OPTION_FLAG_IN_MAIN, {"thumbnail", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_STRING, &thumbnail_size, G_OPTION_ARG_STRING, &thumbnail_size,
"Generate thumbnails for an image, up to the given size", "SIZE"}, "Generate thumbnails for an image, up to the given size", "SIZE"},
{"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &invalidate_cache,
"Invalidate the wide thumbnail cache", NULL},
{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE, {"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
&show_version, "Output version information and exit", NULL}, &show_version, "Output version information and exit", NULL},
{}, {},
@ -1791,6 +1794,10 @@ main(int argc, char *argv[])
g_print("%s\n", *types++); g_print("%s\n", *types++);
return 0; return 0;
} }
if (invalidate_cache) {
fiv_thumbnail_invalidate();
return 0;
}
if (!initialized) if (!initialized)
exit_fatal("%s", error->message); exit_fatal("%s", error->message);