diff --git a/docs/fiv.html b/docs/fiv.html index 3638831..79bf8a6 100644 --- a/docs/fiv.html +++ b/docs/fiv.html @@ -77,10 +77,17 @@ gdk-pixbuf modules.

Thumbnails

-

fiv uses a custom way of storing thumbnails, and doesn't currently -provide any means of invalidating this cache. Should you find out that your +

fiv uses a custom means of storing thumbnails, and doesn't currently +invalidate this cache automatically. Should you find out that your ~/.cache/thumbnails directory is taking up too much space, run: +

+fiv --invalidate-cache
+
+ +

to trim it down. Alternatively, if you want to get rid of _all_ thumbnails, +even for existing images: +

 rm -rf ~/.cache/thumbnails/wide-*
 
diff --git a/fiv-thumbnail.c b/fiv-thumbnail.c index ef84079..a57d016 100644 --- a/fiv-thumbnail.c +++ b/fiv-thumbnail.c @@ -17,10 +17,11 @@ #include "config.h" +#include #include +#include #include #include -#include #include #include @@ -582,3 +583,179 @@ fiv_thumbnail_lookup(GFile *target, FivThumbnailSize size) g_free(uri); 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); +} diff --git a/fiv-thumbnail.h b/fiv-thumbnail.h index 208ccac..8d7dfa0 100644 --- a/fiv-thumbnail.h +++ b/fiv-thumbnail.h @@ -62,3 +62,6 @@ gboolean fiv_thumbnail_produce( /// Retrieves a thumbnail of the most appropriate quality and resolution /// for the target file. 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); diff --git a/fiv.c b/fiv.c index f85f830..6930479 100644 --- a/fiv.c +++ b/fiv.c @@ -1760,7 +1760,7 @@ int main(int argc, char *argv[]) { gboolean show_version = FALSE, show_supported_media_types = FALSE, - browse = FALSE; + invalidate_cache = FALSE, browse = FALSE; gchar **path_args = NULL, *thumbnail_size = NULL; const GOptionEntry options[] = { {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, G_OPTION_ARG_STRING, &thumbnail_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, &show_version, "Output version information and exit", NULL}, {}, @@ -1791,6 +1794,10 @@ main(int argc, char *argv[]) g_print("%s\n", *types++); return 0; } + if (invalidate_cache) { + fiv_thumbnail_invalidate(); + return 0; + } if (!initialized) exit_fatal("%s", error->message);