/* The GIMP -- an image manipulation program * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * Recent File Storage, * see http://freedesktop.org/Standards/recent-file-spec/ * * This code is taken from libegg and has been adapted to the GIMP needs. * The original author is James Willcox , * responsible for bugs in this version is Sven Neumann . * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "config.h" #include #include #include #include #ifdef HAVE_UNISTD_H #include #endif #include #include #include #include #ifndef G_OS_WIN32 /* This code doesn't compile on win32 and the use of * the freedesktop standard doesn't make much sense * there anyway. If someone wants to contribute a win32 * specific implementation, that would be appreciated. */ #include "config/config-types.h" #include "config/gimpxmlparser.h" #include "gimprecentitem.h" #include "gimprecentlist.h" #define GIMP_RECENT_LIST_FILE_NAME ".recently-used" #define GIMP_RECENT_LIST_MAX_ITEMS 500 #define GIMP_RECENT_LIST_GROUP_GIMP "gimp" #define GIMP_RECENT_ITEM_LIST_UNREF(list) \ g_list_foreach (list, (GFunc) gimp_recent_item_unref, NULL); \ g_list_free (list); typedef struct { GSList *states; GList *items; GimpRecentItem *current_item; } ParseInfo; typedef enum { STATE_START, STATE_RECENT_FILES, STATE_RECENT_ITEM, STATE_URI, STATE_MIME_TYPE, STATE_TIMESTAMP, STATE_PRIVATE, STATE_GROUPS, STATE_GROUP } ParseState; #define TAG_RECENT_FILES "RecentFiles" #define TAG_RECENT_ITEM "RecentItem" #define TAG_URI "URI" #define TAG_MIME_TYPE "Mime-Type" #define TAG_TIMESTAMP "Timestamp" #define TAG_PRIVATE "Private" #define TAG_GROUPS "Groups" #define TAG_GROUP "Group" static void start_element_handler (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error); static void end_element_handler (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error); static void text_handler (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error); static const GMarkupParser markup_parser = { start_element_handler, end_element_handler, text_handler, NULL, NULL }; static void gimp_recent_list_add_new_groups (GimpRecentItem *item, GimpRecentItem *upd_item) { const GList *tmp; for (tmp = gimp_recent_item_get_groups (upd_item); tmp; tmp = tmp->next) { const gchar *group = tmp->data; if (! gimp_recent_item_in_group (item, group)) gimp_recent_item_add_group (item, group); } } static gboolean gimp_recent_list_update_item (GList *items, GimpRecentItem *upd_item) { const char *uri = gimp_recent_item_get_uri (upd_item); GList *tmp; for (tmp = items; tmp; tmp = tmp->next) { GimpRecentItem *item = tmp->data; /* gnome_vfs_uris_match (gimp_recent_item_get_uri (item), uri) */ if (strcmp (gimp_recent_item_get_uri (item), uri) == 0) { gimp_recent_item_set_timestamp (item, (time_t) -1); gimp_recent_list_add_new_groups (item, upd_item); return TRUE; } } return FALSE; } static void parse_info_init (ParseInfo *info) { info->states = g_slist_prepend (NULL, GINT_TO_POINTER (STATE_START)); info->items = NULL; } static void parse_info_free (ParseInfo *info) { g_slist_free (info->states); } static void push_state (ParseInfo *info, ParseState state) { info->states = g_slist_prepend (info->states, GINT_TO_POINTER (state)); } static void pop_state (ParseInfo *info) { g_return_if_fail (info->states != NULL); info->states = g_slist_remove (info->states, info->states->data); } static ParseState peek_state (ParseInfo *info) { g_return_val_if_fail (info->states != NULL, STATE_START); return GPOINTER_TO_INT (info->states->data); } #define ELEMENT_IS(name) (strcmp (element_name, (name)) == 0) static void start_element_handler (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error) { ParseInfo *info = user_data; if (ELEMENT_IS (TAG_RECENT_FILES)) { push_state (info, STATE_RECENT_FILES); } else if (ELEMENT_IS (TAG_RECENT_ITEM)) { info->current_item = gimp_recent_item_new (); push_state (info, STATE_RECENT_ITEM); } else if (ELEMENT_IS (TAG_URI)) { push_state (info, STATE_URI); } else if (ELEMENT_IS (TAG_MIME_TYPE)) { push_state (info, STATE_MIME_TYPE); } else if (ELEMENT_IS (TAG_TIMESTAMP)) { push_state (info, STATE_TIMESTAMP); } else if (ELEMENT_IS (TAG_PRIVATE)) { push_state (info, STATE_PRIVATE); gimp_recent_item_set_private (info->current_item, TRUE); } else if (ELEMENT_IS (TAG_GROUPS)) { push_state (info, STATE_GROUPS); } else if (ELEMENT_IS (TAG_GROUP)) { push_state (info, STATE_GROUP); } } static void end_element_handler (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error) { ParseInfo *info = user_data; switch (peek_state (info)) { case STATE_RECENT_ITEM: info->items = g_list_prepend (info->items, info->current_item); break; default: break; } pop_state (info); } static void text_handler (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error) { ParseInfo *info = user_data; switch (peek_state (info)) { case STATE_START: case STATE_RECENT_FILES: case STATE_RECENT_ITEM: case STATE_PRIVATE: case STATE_GROUPS: break; case STATE_URI: gimp_recent_item_set_uri (info->current_item, text); break; case STATE_MIME_TYPE: gimp_recent_item_set_mime_type (info->current_item, text); break; case STATE_TIMESTAMP: gimp_recent_item_set_timestamp (info->current_item, (time_t) atoi (text)); break; case STATE_GROUP: gimp_recent_item_add_group (info->current_item, text); break; } } static void gimp_recent_list_enforce_limit (GList *list, gint limit) { gint len; GList *end; /* limit < 0 means unlimited */ if (limit <= 0) return; len = g_list_length (list); if (len > limit) { GList *next; end = g_list_nth (list, limit-1); next = end->next; end->next = NULL; GIMP_RECENT_ITEM_LIST_UNREF (next); } } static GList * gimp_recent_list_read (gint fd) { GimpXmlParser *parser; GList *list; ParseInfo info; GError *error = NULL; lseek (fd, 0, SEEK_SET); parse_info_init (&info); parser = gimp_xml_parser_new (&markup_parser, &info); if (! gimp_xml_parser_parse_fd (parser, fd, &error)) { g_printerr ("%s", error->message); g_error_free (error); } gimp_xml_parser_free (parser); list = info.items; parse_info_free (&info); return g_list_reverse (list); } static gboolean gimp_recent_list_write_raw (gint fd, const gchar *content, gssize len) { struct stat sbuf; gssize remaining = len; lseek (fd, 0, SEEK_SET); if (fstat (fd, &sbuf) < 0) { g_warning ("Couldn't stat XML document."); } else { if ((off_t) len < sbuf.st_size) ftruncate (fd, len); } while (remaining > 0) { gssize written = write (fd, content, remaining); if (written < 0 && errno != EINTR) return FALSE; remaining -= written; } fsync (fd); return TRUE; } static gboolean gimp_recent_list_write (gint fd, GList *list) { GString *string; gboolean success; string = g_string_new ("\n"); string = g_string_append (string, "<" TAG_RECENT_FILES ">\n"); while (list) { GimpRecentItem *item = list->data; const GList *groups; gchar *uri; const gchar *mime_type; gchar *escaped_uri; time_t timestamp; uri = gimp_recent_item_get_uri_utf8 (item); escaped_uri = g_markup_escape_text (uri, strlen (uri)); g_free (uri); mime_type = gimp_recent_item_get_mime_type (item); timestamp = gimp_recent_item_get_timestamp (item); string = g_string_append (string, " <" TAG_RECENT_ITEM ">\n"); g_string_append_printf (string, " <" TAG_URI ">%s\n", escaped_uri); if (mime_type) g_string_append_printf (string, " <" TAG_MIME_TYPE ">%s\n", mime_type); else g_string_append_printf (string, " <" TAG_MIME_TYPE ">\n"); g_string_append_printf (string, " <" TAG_TIMESTAMP ">%d\n", (gint) timestamp); if (gimp_recent_item_get_private (item)) string = g_string_append (string, " <" TAG_PRIVATE "/>\n"); groups = gimp_recent_item_get_groups (item); if (groups) { /* write the groups */ string = g_string_append (string, " <" TAG_GROUPS ">\n"); if (groups == NULL && gimp_recent_item_get_private (item)) g_warning ("Item with URI \"%s\" marked as private, but" " does not belong to any groups.\n", uri); while (groups) { const gchar *group = groups->data; gchar *escaped_group; escaped_group = g_markup_escape_text (group, strlen(group)); g_string_append_printf (string, " <" TAG_GROUP ">%s\n", escaped_group); g_free (escaped_group); groups = groups->next; } string = g_string_append (string, " \n"); } string = g_string_append (string, " \n"); g_free (escaped_uri); list = list->next; } string = g_string_append (string, ""); success = gimp_recent_list_write_raw (fd, string->str, string->len); g_string_free (string, TRUE); return success; } static gboolean gimp_recent_list_lock_file (gint fd) { gint i; /* Attempt to lock the file 5 times, * waiting a random interval (< 1 second) * in between attempts. * We should really be doing asynchronous * locking, but requires substantially larger * changes. */ lseek (fd, 0, SEEK_SET); for (i = 0; i < 5; i++) { gint rand_interval; if (lockf (fd, F_TLOCK, 0) == 0) return TRUE; rand_interval = 1 + (gint) (10.0 * rand () / (RAND_MAX + 1.0)); g_usleep (100000 * rand_interval); } return FALSE; } static gboolean gimp_recent_list_unlock_file (gint fd) { lseek (fd, 0, SEEK_SET); return (lockf (fd, F_ULOCK, 0) == 0) ? TRUE : FALSE; } static gboolean gimp_recent_list_add_item (GimpRecentItem *item) { const gchar *home; gchar *filename; gint fd; gboolean success = FALSE; gboolean created = FALSE; gboolean updated = FALSE; home = g_get_home_dir (); if (! home) return FALSE; filename = g_build_filename (home, GIMP_RECENT_LIST_FILE_NAME, NULL); fd = open (filename, O_RDWR); if (fd < 0) { fd = creat (filename, S_IRUSR | S_IWUSR); created = TRUE; } g_free (filename); if (fd < 0) return FALSE; if (gimp_recent_list_lock_file (fd)) { GList *list = NULL; if (! created) list = gimp_recent_list_read (fd); /* if it's already there, we just update it */ updated = gimp_recent_list_update_item (list, item); if (!updated) { list = g_list_prepend (list, item); gimp_recent_list_enforce_limit (list, GIMP_RECENT_LIST_MAX_ITEMS); } /* write new stuff */ if (!gimp_recent_list_write (fd, list)) g_warning ("Write failed: %s", g_strerror (errno)); if (!updated) list = g_list_remove (list, item); GIMP_RECENT_ITEM_LIST_UNREF (list); success = TRUE; } else { g_warning ("Failed to lock: %s", g_strerror (errno)); return FALSE; } if (! gimp_recent_list_unlock_file (fd)) g_warning ("Failed to unlock: %s", strerror (errno)); close (fd); return success; } /** * gimp_recent_list_add_uri: * @uri: an URI * @mime_type: a MIME type * * This function adds an item to the list of recently used URIs. * See http://freedesktop.org/Standards/recent-file-spec/. * * On the Win32 platform, this call is unimplemented and will always * fail. * * Returns: %TRUE on success, %FALSE otherwise */ gboolean gimp_recent_list_add_uri (const gchar *uri, const gchar *mime_type) { GimpRecentItem *item; gboolean success; g_return_val_if_fail (uri != NULL, FALSE); if (! mime_type || ! strlen (mime_type) || ! g_utf8_validate (mime_type, -1, NULL)) return FALSE; item = gimp_recent_item_new_from_uri (uri); if (! item) return FALSE; gimp_recent_item_set_mime_type (item, mime_type); gimp_recent_item_set_timestamp (item, -1); gimp_recent_item_add_group (item, GIMP_RECENT_LIST_GROUP_GIMP); success = gimp_recent_list_add_item (item); gimp_recent_item_unref (item); return success; } #else /* G_OS_WIN32 */ gboolean gimp_recent_list_add_uri (const gchar *uri, const gchar *mime_type) { return FALSE; } #endif