diff options
author | Ryan Lortie <desrt@desrt.ca> | 2008-12-12 05:33:16 +0000 |
---|---|---|
committer | Ryan Lortie <ryanl@src.gnome.org> | 2008-12-12 05:33:16 +0000 |
commit | 1394c3a853596f9a08275e49f8b70f2a4174738f (patch) | |
tree | 85518fe7303fa98704b7ce38b32ac77b0d68ba6d | |
parent | 297cefc8d3215f6400f260ff6ba54a6925bdb471 (diff) | |
download | gvfs-1394c3a853596f9a08275e49f8b70f2a4174738f.tar.gz |
New trash:/ backend.
2008-12-11 Ryan Lortie <desrt@desrt.ca>
New trash:/ backend.
* daemon/trashlib: implementation of the reader side of the fd.o
trash specification
* daemon/gvfsbackendtrash.[ch]: rewrite based on trashlib
* configure.ac: add daemon/trashlib/Makefile to output
* daemon/Makefile.am: add trashlib/ subdir and include in trash
backend libraries
svn path=/trunk/; revision=2132
-rw-r--r-- | ChangeLog | 11 | ||||
-rw-r--r-- | configure.ac | 1 | ||||
-rw-r--r-- | daemon/Makefile.am | 7 | ||||
-rw-r--r-- | daemon/gvfsbackendtrash.c | 2132 | ||||
-rw-r--r-- | daemon/gvfsbackendtrash.h | 56 | ||||
-rw-r--r-- | daemon/trashlib/Makefile.am | 15 | ||||
-rw-r--r-- | daemon/trashlib/dirwatch.c | 317 | ||||
-rw-r--r-- | daemon/trashlib/dirwatch.h | 33 | ||||
-rw-r--r-- | daemon/trashlib/trashdir.c | 318 | ||||
-rw-r--r-- | daemon/trashlib/trashdir.h | 36 | ||||
-rw-r--r-- | daemon/trashlib/trashexpunge.c | 127 | ||||
-rw-r--r-- | daemon/trashlib/trashexpunge.h | 17 | ||||
-rw-r--r-- | daemon/trashlib/trashitem.c | 522 | ||||
-rw-r--r-- | daemon/trashlib/trashitem.h | 56 | ||||
-rw-r--r-- | daemon/trashlib/trashwatcher.c | 332 | ||||
-rw-r--r-- | daemon/trashlib/trashwatcher.h | 23 |
16 files changed, 2323 insertions, 1680 deletions
@@ -1,3 +1,14 @@ +2008-12-11 Ryan Lortie <desrt@desrt.ca> + + New trash:/ backend. + + * daemon/trashlib: implementation of the reader side of the fd.o + trash specification + * daemon/gvfsbackendtrash.[ch]: rewrite based on trashlib + * configure.ac: add daemon/trashlib/Makefile to output + * daemon/Makefile.am: add trashlib/ subdir and include in trash + backend libraries + 2008-12-11 Dan Winship <danw@gnome.org> * configure.ac: require libsoup-gnome, for SoupProxyResolverGNOME diff --git a/configure.ac b/configure.ac index 063e761a..0e20de97 100644 --- a/configure.ac +++ b/configure.ac @@ -553,6 +553,7 @@ AC_OUTPUT([ Makefile common/Makefile client/Makefile +daemon/trashlib/Makefile daemon/Makefile monitor/Makefile monitor/proxy/Makefile diff --git a/daemon/Makefile.am b/daemon/Makefile.am index e54de039..526865ac 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -1,3 +1,5 @@ +SUBDIRS = trashlib + NULL = mountdir = $(datadir)/gvfs/mounts @@ -264,9 +266,10 @@ gvfsd_trash_CPPFLAGS = \ -DBACKEND_HEADER=gvfsbackendtrash.h \ -DDEFAULT_BACKEND_TYPE=trash \ -DMAX_JOB_THREADS=10 \ - -DBACKEND_TYPES='"trash", G_VFS_TYPE_BACKEND_TRASH,' + -DBACKEND_TYPES='"trash", G_VFS_TYPE_BACKEND_TRASH,' \ + -Itrashlib -gvfsd_trash_LDADD = $(libraries) +gvfsd_trash_LDADD = $(libraries) trashlib/libtrash.a gvfsd_computer_SOURCES = \ gvfsbackendcomputer.c gvfsbackendcomputer.h \ diff --git a/daemon/gvfsbackendtrash.c b/daemon/gvfsbackendtrash.c index 0b99e21b..b4b5916a 100644 --- a/daemon/gvfsbackendtrash.c +++ b/daemon/gvfsbackendtrash.c @@ -1,1862 +1,722 @@ -/* GIO - GLib Input, Output and Streaming Library - * - * Copyright (C) 2006-2007 Red Hat, Inc. +/* + * Copyright © 2008 Ryan Lortie * - * 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. - * - * 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. - * - * 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., 59 Temple Place, Suite 330, - * Boston, MA 02111-1307, USA. - * - * Author: Alexander Larsson <alexl@redhat.com> + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. */ +#include "gvfsbackendtrash.h" -#include <config.h> - -#include <sys/types.h> -#include <sys/stat.h> -#include <sys/wait.h> -#include <errno.h> -#include <unistd.h> -#include <fcntl.h> +#include <glib/gi18n.h> /* _() */ #include <string.h> -#include <glib/gstdio.h> -#include <glib/gi18n.h> -#include <gio/gio.h> -#include <gio/gunixmounts.h> -#include <glib/gurifuncs.h> +#include "trashwatcher.h" +#include "trashitem.h" -#include "gvfsbackendtrash.h" -#include "gvfsmonitor.h" +#include "gvfsjobcreatemonitor.h" #include "gvfsjobopenforread.h" -#include "gvfsjobread.h" -#include "gvfsjobseekread.h" -#include "gvfsjobopenforwrite.h" -#include "gvfsjobwrite.h" -#include "gvfsjobclosewrite.h" -#include "gvfsjobseekwrite.h" -#include "gvfsjobsetdisplayname.h" -#include "gvfsjobqueryinfo.h" -#include "gvfsjobdelete.h" #include "gvfsjobqueryfsinfo.h" -#include "gvfsjobqueryattributes.h" +#include "gvfsjobqueryinfo.h" #include "gvfsjobenumerate.h" -#include "gvfsjobcreatemonitor.h" -#include "gvfsdaemonprotocol.h" - -/* This is an implementation of the read side of the freedesktop trash specification. - For more details on that, see: http://www.freedesktop.org/wiki/Specifications/trash-spec - - It supplies the virtual trash: location that contains the merged contents of all the - trash directories currently mounted on the system. In order to not have filename - conflicts in the toplevel trash: directory (as files in different trash dirs can have - the same name) we encode the filenames in the toplevel in a way such that: - 1) There are no conflicts - 2) You can know from the filename itself which trash directory it came from - 3) Files in the trash in your homedir look "nicest" - This is handled by escape_pathname() and unescape_pathname() - - This should be pretty straightforward, but there are two complications: - - * When looking for trash directories we need to look in the root of all mounted - filesystems. This is problematic, beacuse it means as soon as there is at least - one hanged NFS mount the whole process will block until that comes back. This - is pretty common on some kinds of machines. We avoid this by forking and doing the - stats in a child process. - - * Monitoring the root directory is complicated, as its spread over many directories - that all have to be monitored. Furthermore, directories can be added/removed - at any time if something is mounted/unmounted, and in the case of unmounts we - need to generate deleted events for the files we lost. So, we need to keep track - of the current contents of the trash dirs. - - The solution used is to keep a list of all the files currently in the toplevel - directory, and then whenever we re-scan that we send out events based on changes - from the old list to the new list. And, in addition to user read-dirs we re-scan - the toplevel when filesystems are mounted/unmounted and when files are added/deleted - in currently monitored trash directories. - */ - +#include "gvfsjobseekread.h" +#include "gvfsjobread.h" +typedef GVfsBackendClass GVfsBackendTrashClass; -struct _GVfsBackendTrash +struct OPAQUE_TYPE__GVfsBackendTrash { GVfsBackend parent_instance; - GMountSpec *mount_spec; + GVfsMonitor *file_monitor; + GVfsMonitor *dir_monitor; - /* This is only set on the main thread */ - GList *top_files; /* Files in toplevel dir */ - guint num_top_files; - - /* All these are protected by the root_monitor lock */ - GVfsMonitor *file_vfs_monitor; - GVfsMonitor *vfs_monitor; - GList *trash_dir_monitors; /* GFileMonitor objects */ - GUnixMountMonitor *mount_monitor; - gboolean trash_file_update_running; - gboolean trash_file_update_scheduled; - gboolean trash_file_update_monitors_scheduled; + TrashWatcher *watcher; + TrashRoot *root; }; -G_LOCK_DEFINE_STATIC(root_monitor); - -G_DEFINE_TYPE (GVfsBackendTrash, g_vfs_backend_trash, G_VFS_TYPE_BACKEND) +G_DEFINE_TYPE (GVfsBackendTrash, g_vfs_backend_trash, G_VFS_TYPE_BACKEND); -static void schedule_update_trash_files (GVfsBackendTrash *backend, - gboolean update_trash_dirs); -static GList *enumerate_root (GVfsBackend *backend, - GVfsJobEnumerate *job); -static GVfsMonitor *do_create_root_monitor (GVfsBackend *backend); - -static char * -escape_pathname (const char *dir) +static GVfsMonitor * +trash_backend_get_file_monitor (GVfsBackendTrash *backend, + gboolean create) { - const char *p; - char *d, *res; - int count; - char c; - const char *basename; - const char *user_data_dir; - - /* Special case the homedir trash to get nice filenames for that */ - user_data_dir = g_get_user_data_dir (); - if (g_str_has_prefix (dir, user_data_dir) && - (g_str_has_prefix (dir + strlen (user_data_dir), "/Trash/"))) + if (backend->file_monitor == NULL && create == FALSE) + return NULL; + + else if (backend->file_monitor == NULL) { - basename = dir + strlen (user_data_dir) + strlen ("/Trash/"); + /* 'create' is only ever set in the main thread, so we will have + * no possibility here for creating more than one new monitor. + */ + if (backend->dir_monitor == NULL) + trash_watcher_watch (backend->watcher); - res = g_malloc (strlen (basename) + 2); - res[0] = '_'; - strcpy (res + 1, basename); - return res; + backend->file_monitor = g_vfs_monitor_new (G_VFS_BACKEND (backend)); } - /* Skip initial slashes, we don't need those since they are always there */ - while (*dir == '/') - dir++; - - /* count characters that need to be escaped. */ - count = 0; - p = dir; - while (*p) - { - if (*p == '_') - count++; - if (*p == '/') - count++; - if (*p == '%') - count++; - p++; - } - - res = g_malloc (strlen (dir) + count*2 + 1); - - p = dir; - d = res; - while (*p) - { - c = *p++; - if (c == '_') - { - *d++ = '%'; - *d++ = '5'; - *d++ = 'f'; - } - else if (c == '/') - { - *d++ = '%'; - *d++ = '2'; - *d++ = 'f'; - - /* Skip consecutive slashes, they are unnecessary, - and break our escaping */ - while (*p == '/') - p++; - } - else if (c == '%') - { - *d++ = '%'; - *d++ = '2'; - *d++ = '5'; - } - else - *d++ = c; - } - *d = 0; - - return res; + return g_object_ref (backend->file_monitor); } -static char * -unescape_pathname (const char *escaped_dir, int len) +static GVfsMonitor * +trash_backend_get_dir_monitor (GVfsBackendTrash *backend, + gboolean create) { - char *dir, *d; - const char *p, *end; - char c; + if (backend->dir_monitor == NULL && create == FALSE) + return NULL; - if (len == -1) - len = strlen (escaped_dir); - - /* If first char is _ this is a homedir trash file */ - if (len > 1 && *escaped_dir == '_') + else if (backend->dir_monitor == NULL) { - char *trashname; - trashname = g_strndup (escaped_dir + 1, len - 1); - dir = g_build_filename (g_get_user_data_dir (), "Trash", trashname, NULL); - g_free (trashname); - return dir; - } - - dir = g_malloc (len + 1 + 1); + /* 'create' is only ever set in the main thread, so we will have + * no possibility here for creating more than one new monitor. + */ + if (backend->file_monitor == NULL) + trash_watcher_watch (backend->watcher); - p = escaped_dir; - d = dir; - *d++ = '/'; - end = p + len; - while (p < end) - { - c = *p++; - if (c == '%' && p < (end-1)) - { - if (*(p) == '2' && *(p+1) == 'f') - { - *d++ = '/'; - p+=2; - } - else if (*(p) == '2' && *(p+1) == '5') - { - *d++ = '%'; - p+=2; - } - else if (*(p) == '5' && *(p+1) == 'f') - { - *d++ = '_'; - p+=2; - } - } - else - *d++ = c; + backend->dir_monitor = g_vfs_monitor_new (G_VFS_BACKEND (backend)); } - *d = 0; - return dir; + return g_object_ref (backend->dir_monitor); } -static char * -get_top_dir_for_trash_dir (const char *trash_dir) +static void +trash_backend_item_created (TrashItem *item, + gpointer user_data) { - char *basename, *dirname; - char *user_trash_basename; - char *user_sys_dir, *res; + GVfsBackendTrash *backend = user_data; + GVfsMonitor *monitor; - basename = g_path_get_basename (trash_dir); - if (strcmp (basename, "Trash") == 0) - { - /* This is $XDG_DATA_DIR/Trash */ - g_free (basename); - return g_path_get_dirname (trash_dir); - } - - user_trash_basename = g_strdup_printf (".Trash-%u", getuid()); - if (strcmp (basename, user_trash_basename) == 0) - { - g_free (user_trash_basename); - g_free (basename); - return g_path_get_dirname (trash_dir); - } - g_free (user_trash_basename); + monitor = trash_backend_get_dir_monitor (backend, FALSE); - user_sys_dir = g_strdup_printf ("%u", getuid()); - if (strcmp (basename, user_sys_dir) == 0) + if (monitor) { - g_free (user_sys_dir); - dirname = g_path_get_dirname (trash_dir); - g_free (basename); - basename = g_path_get_basename (dirname); + char *slashname; - if (strcmp (basename, ".Trash") == 0) - { - res = g_path_get_dirname (dirname); - g_free (dirname); - g_free (basename); - return res; - } - - g_free (dirname); - } - g_free (user_sys_dir); - g_free (basename); + slashname = g_strconcat ("/", trash_item_get_escaped_name (item), NULL); - /* Weird, but we return something at least */ - return g_strdup (trash_dir); + g_vfs_monitor_emit_event (monitor, G_FILE_MONITOR_EVENT_CREATED, + slashname, NULL); + g_object_unref (monitor); + g_free (slashname); + } } -/* FALSE => root */ -static gboolean -decode_path (const char *filename, char **trashdir, char **trashfile, char **relative_path, char **topdir) +static void +trash_backend_item_deleted (TrashItem *item, + gpointer user_data) { - const char *first_entry, *first_entry_end; - char *first_item; - - if (*filename == 0 || *filename != '/') - return FALSE; - - while (*filename == '/') - filename++; + GVfsBackendTrash *backend = user_data; + GVfsMonitor *monitor; - if (*filename == 0) - return FALSE; + monitor = trash_backend_get_dir_monitor (backend, FALSE); - first_entry = filename; - - while (*filename != 0 && *filename != '/') - filename++; - - first_entry_end = filename; - - while (*filename == '/') - filename++; - - first_item = unescape_pathname (first_entry, first_entry_end - first_entry); - *trashfile = g_path_get_basename (first_item); - *trashdir = g_path_get_dirname (first_item); - *topdir = get_top_dir_for_trash_dir (*trashdir); + if (monitor) + { + char *slashname; - if (*filename) - *relative_path = g_strdup (filename); - else - *relative_path = NULL; - return TRUE; + slashname = g_strconcat ("/", trash_item_get_escaped_name (item), NULL); + g_vfs_monitor_emit_event (monitor, G_FILE_MONITOR_EVENT_DELETED, + slashname, NULL); + g_object_unref (monitor); + g_free (slashname); + } } -typedef enum { - HAS_SYSTEM_DIR = 1<<0, - HAS_USER_DIR = 1<<1, - HAS_TRASH_FILES = 1<<1 -} TopdirInfo; - -static TopdirInfo -check_topdir (const char *topdir) +static void +trash_backend_item_count_changed (gpointer user_data) { - TopdirInfo res; - struct stat statbuf; - char *sysadmin_dir, *sysadmin_dir_uid; - char *user_trash_basename, *user_trash; + GVfsBackendTrash *backend = user_data; + GVfsMonitor *file_monitor; + GVfsMonitor *dir_monitor; - res = 0; - - sysadmin_dir = g_build_filename (topdir, ".Trash", NULL); - if (lstat (sysadmin_dir, &statbuf) == 0 && - S_ISDIR (statbuf.st_mode) && - statbuf.st_mode & S_ISVTX) + file_monitor = trash_backend_get_file_monitor (backend, FALSE); + dir_monitor = trash_backend_get_dir_monitor (backend, FALSE); + + if (file_monitor) { - /* We have a valid sysadmin .Trash dir, look for uid subdir */ - sysadmin_dir_uid = g_strdup_printf ("%s/%u", sysadmin_dir, getuid()); - - if (lstat (sysadmin_dir_uid, &statbuf) == 0 && - S_ISDIR (statbuf.st_mode) && - statbuf.st_uid == getuid()) - { - res |= HAS_SYSTEM_DIR; + g_vfs_monitor_emit_event (file_monitor, + G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED, + "/", NULL); - if (statbuf.st_nlink != 2) - res |= HAS_TRASH_FILES; - } - - g_free (sysadmin_dir_uid); + g_object_unref (file_monitor); } - g_free (sysadmin_dir); - user_trash_basename = g_strdup_printf (".Trash-%u", getuid()); - user_trash = g_build_filename (topdir, user_trash_basename, NULL); - g_free (user_trash_basename); - - if (lstat (user_trash, &statbuf) == 0 && - S_ISDIR (statbuf.st_mode) && - statbuf.st_uid == getuid()) + if (dir_monitor) { - res |= HAS_USER_DIR; - - if (statbuf.st_nlink != 2) - res |= HAS_TRASH_FILES; - } + g_vfs_monitor_emit_event (dir_monitor, + G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED, + "/", NULL); - g_free (user_trash); - - return res; + g_object_unref (dir_monitor); + } } -static gboolean -wait_for_fd_with_timeout (int fd, int timeout_secs) -{ - int res; - - do - { -#ifdef HAVE_POLL - struct pollfd poll_fd; - poll_fd.fd = fd; - poll_fd.events = POLLIN; - res = poll (&poll_fd, 1, timeout_secs * 1000); -#else - struct timeval tv; - fd_set read_fds; - - tv.tv_sec = timeout_secs; - tv.tv_usec = 0; - - FD_ZERO(&read_fds); - FD_SET(fd, &read_fds); - - res = select (fd + 1, &read_fds, NULL, NULL, &tv); -#endif - } while (res == -1 && errno == EINTR); - - return res > 0; -} -/* We do this convoluted fork + pipe thing to avoid hanging - on e.g stuck NFS mounts, which is somewhat common since - we're basically stat:ing all mounted filesystems */ -static GList * -get_topdir_info (GList *topdirs) +static GFile * +trash_backend_get_file (GVfsBackendTrash *backend, + const char *filename, + TrashItem **item_ret, + gboolean *is_toplevel, + GError **error) { - GList *result = NULL; - - while (topdirs) - { - guint32 topdir_info = 0; - pid_t pid; - int pipes[2]; - int status; - - if (pipe (pipes) == -1) - goto error; - - pid = fork (); - if (pid == -1) - { - close (pipes[0]); - close (pipes[1]); - goto error; - } - - if (pid == 0) - { - /* Child */ - close (pipes[0]); - - /* Fork an intermediate child that immediately exits - * so we can waitpid it. This means the final process - * will get owned by init and not go zombie. - */ - pid = fork (); - - if (pid == 0) - { - /* Grandchild */ - while (topdirs) - { - guint32 info; - info = check_topdir ((char *)topdirs->data); - write (pipes[1], (char *)&info, sizeof (guint32)); - topdirs = topdirs->next; - } - } - close (pipes[1]); - _exit (0); - g_assert_not_reached (); - } - - /* Parent */ - close (pipes[1]); + const char *slash; + gboolean is_top; + TrashItem *item; + GFile *file; - /* Wait for the intermidate process to die */ - retry_waitpid: - if (waitpid (pid, &status, 0) < 0) - { - if (errno == EINTR) - goto retry_waitpid; - else if (errno == ECHILD) - ; /* do nothing, child already reaped */ - else - g_warning ("waitpid() should not fail in get_topdir_info"); - } - - while (topdirs) - { - if (!wait_for_fd_with_timeout (pipes[0], 3) || - read (pipes[0], (char *)&topdir_info, sizeof (guint32)) != sizeof (guint32)) - break; - - result = g_list_prepend (result, GUINT_TO_POINTER (topdir_info)); - topdirs = topdirs->next; - } - - close (pipes[0]); - - error: - if (topdirs) - { - topdir_info = 0; - result = g_list_prepend (result, GUINT_TO_POINTER (topdir_info)); - topdirs = topdirs->next; - } - } - - return g_list_reverse (result); -} + file = NULL; + filename++; -static GList * -list_trash_dirs (void) -{ - GList *mounts, *l, *li; - const char *topdir; - char *home_trash; - GUnixMountEntry *mount; - GList *dirs; - GList *topdirs; - GList *topdirs_info; - struct stat statbuf; - gboolean has_trash_files; - int stat_result; - - dirs = NULL; - has_trash_files = FALSE; - - home_trash = g_build_filename (g_get_user_data_dir (), "Trash", NULL); + slash = strchr (filename, '/'); + is_top = slash == NULL; - stat_result = g_lstat (home_trash, &statbuf); + if (is_toplevel) + *is_toplevel = is_top; - /* If the home trash directory doesn't exist at this point, we must create - * it in order to monitor it. */ - if (stat_result != 0) + if (!is_top) { - gchar *home_trash_files = g_build_filename (home_trash, "files", NULL); - gchar *home_trash_info = g_build_filename (home_trash, "info", NULL); + char *toplevel; - g_mkdir_with_parents (home_trash_files, 0700); - g_mkdir_with_parents (home_trash_info, 0700); + g_assert (slash[1]); - g_free (home_trash_files); - g_free (home_trash_info); + toplevel = g_strndup (filename, slash - filename); + if ((item = trash_root_lookup_item (backend->root, toplevel))) + { + file = trash_item_get_file (item); + file = g_file_get_child (file, slash + 1); - stat_result = g_lstat (home_trash, &statbuf); - } + if (item_ret) + *item_ret = item; + else + trash_item_unref (item); + } - if (stat_result == 0 && - S_ISDIR (statbuf.st_mode)) - { - dirs = g_list_prepend (dirs, home_trash); - if (statbuf.st_nlink != 2) - has_trash_files = TRUE; + g_free (toplevel); } else - g_free (home_trash); - - topdirs = NULL; - mounts = g_unix_mounts_get (NULL); - for (l = mounts; l != NULL; l = l->next) { - mount = l->data; - - if (!g_unix_mount_is_system_internal (mount) ) + if ((item = trash_root_lookup_item (backend->root, filename))) { - topdir = g_unix_mount_get_mount_path (mount); - topdirs = g_list_prepend (topdirs, g_strdup (topdir)); - } - - g_unix_mount_free (mount); - } - g_list_free (mounts); - - topdirs_info = get_topdir_info (topdirs); - - for (l = topdirs, li = topdirs_info; l != NULL && li != NULL; l = l->next, li = li->next) - { - TopdirInfo info = GPOINTER_TO_UINT (li->data); - char *basename, *trashdir; - topdir = l->data; + file = g_object_ref (trash_item_get_file (item)); - if (info & HAS_SYSTEM_DIR) - { - basename = g_strdup_printf ("%u", getuid()); - trashdir = g_build_filename (topdir, ".Trash", basename, NULL); - g_free (basename); - dirs = g_list_prepend (dirs, trashdir); - } - - if (info & HAS_USER_DIR) - { - basename = g_strdup_printf (".Trash-%u", getuid()); - trashdir = g_build_filename (topdir, basename, NULL); - g_free (basename); - dirs = g_list_prepend (dirs, trashdir); + if (item_ret) + *item_ret = item; + else + trash_item_unref (item); } - - if (info & HAS_TRASH_FILES) - has_trash_files = TRUE; } - g_list_foreach (topdirs, (GFunc) g_free, NULL); - g_list_free (topdirs); - g_list_free (topdirs_info); - - return g_list_reverse (dirs); -} - -static void -g_vfs_backend_trash_finalize (GObject *object) -{ - GVfsBackendTrash *backend; - - backend = G_VFS_BACKEND_TRASH (object); + if (file == NULL) + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("No such file or directory")); - g_mount_spec_unref (backend->mount_spec); - - - if (G_OBJECT_CLASS (g_vfs_backend_trash_parent_class)->finalize) - (*G_OBJECT_CLASS (g_vfs_backend_trash_parent_class)->finalize) (object); + return file; } -static void -g_vfs_backend_trash_init (GVfsBackendTrash *trash_backend) +/* ======================= method implementations ======================= */ +static gboolean +trash_backend_open_for_read (GVfsBackend *vfs_backend, + GVfsJobOpenForRead *job, + const char *filename) { - GVfsBackend *backend = G_VFS_BACKEND (trash_backend); - GMountSpec *mount_spec; - - /* translators: This is the name of the backend */ - g_vfs_backend_set_display_name (backend, _("Trash")); - g_vfs_backend_set_icon_name (backend, "user-trash"); - g_vfs_backend_set_user_visible (backend, FALSE); + GVfsBackendTrash *backend = G_VFS_BACKEND_TRASH (vfs_backend); + GError *error = NULL; - mount_spec = g_mount_spec_new ("trash"); - g_vfs_backend_set_mount_spec (backend, mount_spec); - trash_backend->mount_spec = mount_spec; -} + if (filename[1] == '\0') + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, + _("Can't open directory")); -static void -do_mount (GVfsBackend *backend, - GVfsJobMount *job, - GMountSpec *mount_spec, - GMountSource *mount_source, - gboolean is_automount) -{ - GVfsBackendTrash *trash_backend = G_VFS_BACKEND_TRASH (backend); - GList *names; + else + { + GFile *real; - names = enumerate_root (backend, NULL); - trash_backend->num_top_files = g_list_length (names); - trash_backend->top_files = g_list_sort (names, (GCompareFunc)strcmp); - do_create_root_monitor (backend); - - g_vfs_job_succeeded (G_VFS_JOB (job)); - } + real = trash_backend_get_file (backend, filename, NULL, NULL, &error); -static void -do_open_for_read (GVfsBackend *backend, - GVfsJobOpenForRead *job, - const char *filename) -{ - char *trashdir, *topdir, *relative_path, *trashfile; + if (real) + { + GFileInputStream *stream; - if (!decode_path (filename, &trashdir, &trashfile, &relative_path, &topdir)) - g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, - G_IO_ERROR_IS_DIRECTORY, - _("Can't open directory")); - else - { - GFile *file; - char *dir; - GError *error; - GFileInputStream *stream; - - dir = g_build_filename (trashdir, "files", trashfile, relative_path, NULL); - file = g_file_new_for_path (dir); + stream = g_file_read (real, NULL, &error); - error = NULL; - stream = g_file_read (file, - G_VFS_JOB (job)->cancellable, - &error); - g_object_unref (file); + if (stream) + { + g_vfs_job_open_for_read_set_handle (job, stream); + g_vfs_job_open_for_read_set_can_seek (job, TRUE); + g_vfs_job_succeeded (G_VFS_JOB (job)); - if (stream) - { - g_vfs_job_open_for_read_set_handle (job, stream); - g_vfs_job_open_for_read_set_can_seek (job, - g_seekable_can_seek (G_SEEKABLE (stream))); - g_vfs_job_succeeded (G_VFS_JOB (job)); - } - else - { - g_vfs_job_failed_from_error (G_VFS_JOB (job), error); - g_error_free (error); + return TRUE; + } } - - g_free (trashdir); - g_free (trashfile); - g_free (relative_path); - g_free (topdir); } + + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); + + return TRUE; } -static void -do_read (GVfsBackend *backend, - GVfsJobRead *job, - GVfsBackendHandle _handle, - char *buffer, - gsize bytes_requested) +static gboolean +trash_backend_read (GVfsBackend *backend, + GVfsJobRead *job, + GVfsBackendHandle handle, + char *buffer, + gsize bytes_requested) { - GInputStream *stream; - gssize res; - GError *error; - - stream = G_INPUT_STREAM (_handle); + GError *error = NULL; + gssize bytes; - error = NULL; - res = g_input_stream_read (stream, - buffer, bytes_requested, - G_VFS_JOB (job)->cancellable, - &error); + bytes = g_input_stream_read (handle, buffer, bytes_requested, + NULL, &error); - if (res != -1) + if (bytes >= 0) { - g_vfs_job_read_set_size (job, res); + g_vfs_job_read_set_size (job, bytes); g_vfs_job_succeeded (G_VFS_JOB (job)); + + return TRUE; } - else - { - g_vfs_job_failed_from_error (G_VFS_JOB (job), error); - g_error_free (error); - } + + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); + + return TRUE; } -static void -do_seek_on_read (GVfsBackend *backend, - GVfsJobSeekRead *job, - GVfsBackendHandle _handle, - goffset offset, - GSeekType type) +static gboolean +trash_backend_seek_on_read (GVfsBackend *backend, + GVfsJobSeekRead *job, + GVfsBackendHandle handle, + goffset offset, + GSeekType type) { - GFileInputStream *stream; - GError *error; - - stream = G_FILE_INPUT_STREAM (_handle); + GError *error = NULL; - error = NULL; - if (g_seekable_seek (G_SEEKABLE (stream), - offset, type, - G_VFS_JOB (job)->cancellable, - &error)) + if (g_seekable_seek (handle, offset, type, NULL, &error)) { - g_vfs_job_seek_read_set_offset (job, - g_seekable_tell (G_SEEKABLE (stream))); + g_vfs_job_seek_read_set_offset (job, g_seekable_tell (handle)); g_vfs_job_succeeded (G_VFS_JOB (job)); + + return TRUE; } - else - { - g_vfs_job_failed_from_error (G_VFS_JOB (job), error); - g_error_free (error); - } + + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); + + return TRUE; } -static void -do_close_read (GVfsBackend *backend, - GVfsJobCloseRead *job, - GVfsBackendHandle _handle) +static gboolean +trash_backend_close_read (GVfsBackend *backend, + GVfsJobCloseRead *job, + GVfsBackendHandle handle) { - GInputStream *stream; - GError *error; - - stream = G_INPUT_STREAM (_handle); + GError *error = NULL; - error = NULL; - if (g_input_stream_close (stream, - G_VFS_JOB (job)->cancellable, - &error)) - g_vfs_job_succeeded (G_VFS_JOB (job)); - else + if (g_input_stream_close (handle, NULL, &error)) { - g_vfs_job_failed_from_error (G_VFS_JOB (job), error); - g_error_free (error); + g_vfs_job_succeeded (G_VFS_JOB (job)); + + return TRUE; } -} -typedef struct { - GVfsBackend *backend; - GList *names; -} SetHasTrashFilesData; + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); + + return TRUE; +} static gboolean -set_trash_files (gpointer _data) +trash_backend_delete (GVfsBackend *vfs_backend, + GVfsJobDelete *job, + const char *filename) { - GVfsMonitor *vfs_monitor, *file_vfs_monitor; - GVfsBackendTrash *trash_backend; - SetHasTrashFilesData *data = _data; - GList *new_list, *old_list; - guint old_count, new_count; - - trash_backend = G_VFS_BACKEND_TRASH (data->backend); - new_list = g_list_sort (data->names, (GCompareFunc)strcmp); - g_slice_free (SetHasTrashFilesData, data); - - old_list = trash_backend->top_files; - old_count = trash_backend->num_top_files; - - /* do the replacement now, before we send notifications. */ - new_count = g_list_length (new_list); - trash_backend->num_top_files = new_count; - trash_backend->top_files = new_list; - - G_LOCK (root_monitor); - vfs_monitor = NULL; - if (trash_backend->vfs_monitor) - vfs_monitor = g_object_ref (trash_backend->vfs_monitor); - - file_vfs_monitor = NULL; - if (trash_backend->file_vfs_monitor) - file_vfs_monitor = g_object_ref (trash_backend->file_vfs_monitor); - G_UNLOCK (root_monitor); + GVfsBackendTrash *backend = G_VFS_BACKEND_TRASH (vfs_backend); + GError *error = NULL; - if (vfs_monitor) + if (filename[1] == '\0') + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, + _("Can't delete trash")); + else { - GList *added, *removed; - GList *new, *old, *l; - char *name; - int cmp; + gboolean is_toplevel; + TrashItem *item; + GFile *real; - added = NULL; - removed = NULL; + real = trash_backend_get_file (backend, filename, + &item, &is_toplevel, &error); - new = new_list; - old = old_list; - - while (new != NULL || old != NULL) + if (real) { - if (new == NULL) - { - /* old deleted */ - removed = g_list_prepend (removed, old->data); - old = old->next; - } - else if (old == NULL) - { - /* new added */ - added = g_list_prepend (added, new->data); - new = new->next; - } - else if ((cmp = strcmp (new->data, old->data)) == 0) - { - old = old->next; - new = new->next; - } - else if (cmp < 0) - { - /* new added */ - added = g_list_prepend (added, new->data); - new = new->next; - } - else if (cmp > 0) + if (!is_toplevel) + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, + _("Items in the trash may not be modified")); + + else { - /* old deleted */ - removed = g_list_prepend (removed, old->data); - old = old->next; - } - } + if (trash_item_delete (item, &error)) + { + g_vfs_job_succeeded (G_VFS_JOB (job)); - for (l = removed; l != NULL; l = l->next) - { - name = g_strconcat ("/", l->data, NULL); - g_vfs_monitor_emit_event (vfs_monitor, - G_FILE_MONITOR_EVENT_DELETED, - name, - NULL); - g_free (name); - } - g_list_free (removed); - - for (l = added; l != NULL; l = l->next) - { - name = g_strconcat ("/", l->data, NULL); - g_vfs_monitor_emit_event (vfs_monitor, - G_FILE_MONITOR_EVENT_CREATED, - name, - NULL); - g_free (name); - } - g_list_free (added); - - if (new_count != old_count) - { - /* trash::item-count changed. */ - /* icon change only ever occurs when item-count also changes */ - g_vfs_monitor_emit_event (vfs_monitor, - G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED, - "/", - NULL); - } - - g_object_unref (vfs_monitor); - } + return TRUE; + } + } - if (file_vfs_monitor) - { - if (new_count != old_count) - { - /* trash::item-count changed. */ - /* icon change only ever occurs when item-count also changes */ - g_vfs_monitor_emit_event (file_vfs_monitor, - G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED, - "/", - NULL); + trash_item_unref (item); } - - g_object_unref (file_vfs_monitor); + } - g_list_foreach (old_list, (GFunc)g_free, NULL); - g_list_free (old_list); + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); - return FALSE; + return TRUE; } + static void -queue_set_trash_files (GVfsBackend *backend, - GList *names) +trash_backend_add_info (TrashItem *item, + GFileInfo *info, + gboolean is_toplevel) { - SetHasTrashFilesData *data; + GFile *original; - data = g_slice_new (SetHasTrashFilesData); - data->backend = backend; - data->names = names; - g_idle_add (set_trash_files, data); -} + if (is_toplevel && item) + { + original = trash_item_get_original (item); -static void -add_extra_trash_info (GFileInfo *file_info, - const char *topdir, - const char *info_dir, - const char *filename, - const char *relative_path) -{ - char *info_filename; - char *info_path; - char *orig_path, *orig_path_key, *orig_path_unescaped, *date; - GKeyFile *keyfile; - char *display_name; - char *desc; - - /* Override all writability */ - g_file_info_set_attribute_boolean (file_info, + if (original) + { + gchar *basename; + + basename = g_file_get_basename (original); + /* XXX: utf8ify or something... */ + g_file_info_set_display_name (info, basename); + g_free (basename); + } + } + + g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, FALSE); - g_file_info_set_attribute_boolean (file_info, + g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE, FALSE); - g_file_info_set_attribute_boolean (file_info, + g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME, FALSE); - g_file_info_set_attribute_boolean (file_info, + g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH, FALSE); - - /* But we can delete */ - g_file_info_set_attribute_boolean (file_info, + g_file_info_set_attribute_boolean (info, G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE, - TRUE); - - info_filename = g_strconcat (filename, ".trashinfo", NULL); - info_path = g_build_filename (info_dir, info_filename, NULL); - g_free (info_filename); + is_toplevel); +} + +static gboolean +trash_backend_enumerate (GVfsBackend *vfs_backend, + GVfsJobEnumerate *job, + const char *filename, + GFileAttributeMatcher *attribute_matcher, + GFileQueryInfoFlags flags) +{ + GVfsBackendTrash *backend = G_VFS_BACKEND_TRASH (vfs_backend); + + g_assert (filename[0] == '/'); - keyfile = g_key_file_new (); - if (g_key_file_load_from_file (keyfile, info_path, G_KEY_FILE_NONE, NULL)) + trash_watcher_rescan (backend->watcher); + + if (filename[1]) + /* not root case */ { - orig_path_key = g_key_file_get_string (keyfile, "Trash Info", "Path", NULL); - if (orig_path_key) + GError *error = NULL; + GFile *real; + + real = trash_backend_get_file (backend, filename, NULL, NULL, &error); + + if (real) { - orig_path_unescaped = g_uri_unescape_string (orig_path_key, ""); + GFileEnumerator *enumerator; + + enumerator = g_file_enumerate_children (real, job->attributes, + job->flags, NULL, &error); - if (orig_path_unescaped) + if (enumerator) { - if (g_path_is_absolute (orig_path_unescaped)) - orig_path = g_build_filename (orig_path_unescaped, relative_path, NULL); - else - orig_path = g_build_filename (topdir, orig_path_unescaped, relative_path, NULL); - g_free (orig_path_unescaped); - - /* Set display name and edit name based of original basename */ - display_name = g_filename_display_basename (orig_path); - g_file_info_set_edit_name (file_info, display_name); - - if (strstr (display_name, "\357\277\275") != NULL) + GFileInfo *info; + + g_vfs_job_succeeded (G_VFS_JOB (job)); + + while ((info = g_file_enumerator_next_file (enumerator, + NULL, &error))) { - char *p = display_name; - display_name = g_strconcat (display_name, _(" (invalid encoding)"), NULL); - g_free (p); - - g_file_info_set_attribute_string (file_info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME, NULL); + trash_backend_add_info (NULL, info, FALSE); + g_vfs_job_enumerate_add_info (job, info); + g_object_unref (info); } - else - { - g_file_info_set_attribute_string (file_info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME, display_name); - g_file_info_set_attribute_string (file_info, G_FILE_ATTRIBUTE_STANDARD_COPY_NAME, display_name); - } - - g_file_info_set_display_name (file_info, display_name); - - desc = g_strdup_printf (_("%s (in trash)"), display_name); - g_file_info_set_attribute_string (file_info, G_FILE_ATTRIBUTE_STANDARD_DESCRIPTION, desc); - g_free (desc); - - g_free (display_name); - - g_file_info_set_attribute_byte_string (file_info, - "trash::orig-path", - orig_path); - g_free (orig_path); - } - g_free (orig_path_key); + g_object_unref (enumerator); + + if (!error) + { + g_vfs_job_enumerate_done (job); + + return TRUE; + } + } } - - date = g_key_file_get_string (keyfile, "Trash Info", "DeletionDate", NULL); - if (date && g_utf8_validate (date, -1, NULL)) - g_file_info_set_attribute_string (file_info, - "trash::deletion-date", - date); - g_free (date); - } - g_key_file_free (keyfile); - g_free (info_path); -} -static void -enumerate_root_trashdir (GVfsBackend *backend, - GVfsJobEnumerate *job, - const char *topdir, - const char *trashdir, - GList **names) -{ - GFile *file, *files_file; - GFileEnumerator *enumerator; - char *info_dir; - - info_dir = g_build_filename (trashdir, "info", NULL); - - file = g_file_new_for_path (trashdir); - files_file = g_file_get_child (file, "files"); - enumerator = - g_file_enumerate_children (files_file, - job ? job->attributes : G_FILE_ATTRIBUTE_STANDARD_NAME, - job ? job->flags : 0, - job ? G_VFS_JOB (job)->cancellable : NULL, - NULL); - g_object_unref (files_file); - g_object_unref (file); - - if (enumerator) + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); /* wrote when drunk at uds, plz check later, k thx. XXX */ + } + else { - GFileInfo *info; + GCancellable *cancellable; + GList *items; + GList *node; + + cancellable = G_VFS_JOB (job)->cancellable; + g_vfs_job_succeeded (G_VFS_JOB (job)); + + items = trash_root_get_items (backend->root); - while ((info = g_file_enumerator_next_file (enumerator, - job ? G_VFS_JOB (job)->cancellable : NULL, - NULL)) != NULL) + for (node = items; node; node = node->next) { - const char *name; - char *new_name, *new_name_escaped; + TrashItem *item = node->data; + GFileInfo *info; + GFile *original; - name = g_file_info_get_name (info); + info = g_file_query_info (trash_item_get_file (item), + job->attributes, + flags, cancellable, NULL); - /* Get the display name, etc */ - add_extra_trash_info (info, - topdir, - info_dir, - name, - NULL); + g_file_info_set_attribute_mask (info, attribute_matcher); + trash_backend_add_info (item, info, TRUE); + g_file_info_set_name (info, trash_item_get_escaped_name (item)); + + original = trash_item_get_original (item); + if (original) + { + char *basename; - /* Update the name to also have the trash dir */ - new_name = g_build_filename (trashdir, name, NULL); - new_name_escaped = escape_pathname (new_name); - g_free (new_name); - g_file_info_set_name (info, new_name_escaped); + basename = g_file_get_basename (original); - *names = g_list_prepend (*names, new_name_escaped); /* Takes over ownership */ + /* XXX utf8 */ + g_file_info_set_display_name (info, basename); + g_free (basename); + } - if (job) - g_vfs_job_enumerate_add_info (job, info); + g_vfs_job_enumerate_add_info (job, info); g_object_unref (info); } - - g_file_enumerator_close (enumerator, - job ? G_VFS_JOB (job)->cancellable : NULL, - NULL); - g_object_unref (enumerator); } - g_free (info_dir); -} - -static GList * -enumerate_root (GVfsBackend *backend, - GVfsJobEnumerate *job) -{ - GList *trashdirs, *l; - char *trashdir; - char *topdir; - GList *names; - - trashdirs = list_trash_dirs (); + g_vfs_job_enumerate_done (job); - names = NULL; - for (l = trashdirs; l != NULL; l = l->next) - { - trashdir = l->data; - topdir = get_top_dir_for_trash_dir (trashdir); + return TRUE; +} - enumerate_root_trashdir (backend, job, topdir, trashdir, &names); - g_free (trashdir); - g_free (topdir); - } - g_list_free (trashdirs); +static gboolean +trash_backend_mount (GVfsBackend *vfs_backend, + GVfsJobMount *job, + GMountSpec *mount_spec, + GMountSource *mount_source, + gboolean is_automount) +{ + GVfsBackendTrash *backend = G_VFS_BACKEND_TRASH (vfs_backend); + + backend->file_monitor = NULL; + backend->dir_monitor = NULL; + backend->root = trash_root_new (trash_backend_item_created, + trash_backend_item_deleted, + trash_backend_item_count_changed, + backend); + backend->watcher = trash_watcher_new (backend->root); - if (job) - g_vfs_job_enumerate_done (job); + g_vfs_job_succeeded (G_VFS_JOB (job)); - return names; + return TRUE; } -static void -do_enumerate (GVfsBackend *backend, - GVfsJobEnumerate *job, - const char *filename, - GFileAttributeMatcher *attribute_matcher, - GFileQueryInfoFlags flags) +static gboolean +trash_backend_query_info (GVfsBackend *vfs_backend, + GVfsJobQueryInfo *job, + const char *filename, + GFileQueryInfoFlags flags, + GFileInfo *info, + GFileAttributeMatcher *matcher) { - char *trashdir, *topdir, *relative_path, *trashfile; - GList *names; + GVfsBackendTrash *backend = G_VFS_BACKEND_TRASH (vfs_backend); - if (!decode_path (filename, &trashdir, &trashfile, &relative_path, &topdir)) - { - /* Always succeeds */ - g_vfs_job_succeeded (G_VFS_JOB (job)); - - names = enumerate_root (backend, job); - queue_set_trash_files (backend, names); - } - else + g_assert (filename[0] == '/'); + + if (filename[1]) { - GFile *file; - GFileEnumerator *enumerator; - GFileInfo *info; - char *dir; - GError *error; - - dir = g_build_filename (trashdir, "files", trashfile, relative_path, NULL); - file = g_file_new_for_path (dir); - error = NULL; - enumerator = - g_file_enumerate_children (file, - job->attributes, - job->flags, - G_VFS_JOB (job)->cancellable, - &error); - g_free (dir); - g_object_unref (file); - - if (enumerator) + GError *error = NULL; + gboolean is_toplevel; + TrashItem *item; + GFile *real; + + real = trash_backend_get_file (backend, filename, + &item, &is_toplevel, &error); + + if (real) { - g_vfs_job_succeeded (G_VFS_JOB (job)); - - while ((info = g_file_enumerator_next_file (enumerator, - G_VFS_JOB (job)->cancellable, - NULL)) != NULL) + GFileInfo *real_info; + + real_info = g_file_query_info (real, job->attributes, + flags, NULL, &error); + + if (real_info) { - g_vfs_job_enumerate_add_info (job, info); - g_object_unref (info); + g_file_info_copy_into (real_info, info); + trash_backend_add_info (item, info, is_toplevel); + g_vfs_job_succeeded (G_VFS_JOB (job)); + + return TRUE; } - - g_file_enumerator_close (enumerator, - G_VFS_JOB (job)->cancellable, - NULL); - g_object_unref (enumerator); - - g_vfs_job_enumerate_done (job); - } - else - { - g_vfs_job_failed_from_error (G_VFS_JOB (job), error); - g_error_free (error); + + trash_item_unref (item); } - g_free (trashdir); - g_free (trashfile); - g_free (relative_path); - g_free (topdir); + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); } -} - -static void -do_query_info (GVfsBackend *backend, - GVfsJobQueryInfo *job, - const char *filename, - GFileQueryInfoFlags flags, - GFileInfo *info, - GFileAttributeMatcher *matcher) -{ - char *trashdir, *topdir, *relative_path, *trashfile; - GIcon *icon; - - if (!decode_path (filename, &trashdir, &trashfile, &relative_path, &topdir)) + else { - GVfsBackendTrash *trash_backend = G_VFS_BACKEND_TRASH (backend); + GIcon *icon; + int n_items; + + n_items = trash_root_get_n_items (backend->root); - /* The trash:/// root */ g_file_info_set_file_type (info, G_FILE_TYPE_DIRECTORY); g_file_info_set_name (info, "/"); /* Translators: this is the display name of the backend */ g_file_info_set_display_name (info, _("Trash")); g_file_info_set_content_type (info, "inode/directory"); - if (trash_backend->top_files != NULL) - icon = g_themed_icon_new ("user-trash-full"); - else - icon = g_themed_icon_new ("user-trash"); - + icon = g_themed_icon_new (n_items ? "user-trash-full" : "user-trash"); g_file_info_set_icon (info, icon); g_object_unref (icon); - - g_file_info_set_attribute_boolean (info, - G_FILE_ATTRIBUTE_STANDARD_IS_VIRTUAL, - TRUE); - g_file_info_set_attribute_boolean (info, - G_FILE_ATTRIBUTE_ACCESS_CAN_READ, - TRUE); - g_file_info_set_attribute_boolean (info, - G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, - FALSE); - g_file_info_set_attribute_boolean (info, - G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE, - FALSE); - g_file_info_set_attribute_boolean (info, - G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE, - FALSE); - g_file_info_set_attribute_boolean (info, - G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH, - FALSE); - g_file_info_set_attribute_boolean (info, - G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME, - FALSE); - g_file_info_set_attribute_uint32 (info, - G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT, - trash_backend->num_top_files); - - g_vfs_job_succeeded (G_VFS_JOB (job)); - } - else - { - GFile *file; - GFileInfo *local_info; - char *path; - GError *error; - char *info_dir; - char *basename; - - path = g_build_filename (trashdir, "files", trashfile, relative_path, NULL); - file = g_file_new_for_path (path); - g_free (path); - - error = NULL; - local_info = g_file_query_info (file, - job->attributes, - job->flags, - G_VFS_JOB (job)->cancellable, - &error); - g_object_unref (file); - - if (local_info) - { - g_file_info_copy_into (local_info, info); - basename = g_path_get_basename (filename); - g_file_info_set_name (info, basename); - g_free (basename); - - info_dir = g_build_filename (trashdir, "info", NULL); - add_extra_trash_info (info, - topdir, - info_dir, - trashfile, - relative_path); - g_free (info_dir); - - g_object_unref (local_info); - g_vfs_job_succeeded (G_VFS_JOB (job)); - } - else - { - g_vfs_job_failed_from_error (G_VFS_JOB (job), error); - g_error_free (error); - } - - g_free (trashdir); - g_free (trashfile); - g_free (relative_path); - g_free (topdir); - } -} + g_file_info_set_attribute_uint32 (info, "trash::item-count", n_items); -static void -do_delete (GVfsBackend *backend, - GVfsJobDelete *job, - const char *filename) -{ - char *trashdir, *topdir, *relative_path, *trashfile; - - if (!decode_path (filename, &trashdir, &trashfile, &relative_path, &topdir)) - g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, - G_IO_ERROR_PERMISSION_DENIED, - _("Can't delete trash")); - else - { - GFile *file; - GError *error; - char *path, *info_filename, *info_path; - - path = g_build_filename (trashdir, "files", trashfile, relative_path, NULL); - file = g_file_new_for_path (path); - g_free (path); - - error = NULL; - if (g_file_delete (file, - G_VFS_JOB (job)->cancellable, - &error)) - { - g_vfs_job_succeeded (G_VFS_JOB (job)); - - if (relative_path == NULL) - { - info_filename = g_strconcat (trashfile, ".trashinfo", NULL); - info_path = g_build_filename (trashdir, "info", info_filename, NULL); - g_free (info_filename); - g_unlink (info_path); - g_free (info_path); - } - } - else - { - g_vfs_job_failed_from_error (G_VFS_JOB (job), error); - g_error_free (error); - } - - g_object_unref (file); - - g_free (trashdir); - g_free (trashfile); - g_free (relative_path); - g_free (topdir); - } -} - -typedef struct { - GVfsMonitor *vfs_monitor; - GObject *monitor; - GFile *base_file; - char *base_path; -} MonitorProxy; - -static void -monitor_proxy_free (MonitorProxy *proxy) -{ - g_object_unref (proxy->monitor); - g_object_unref (proxy->base_file); - g_free (proxy->base_path); - g_free (proxy); -} - -static char * -proxy_get_trash_path (MonitorProxy *proxy, - GFile *file) -{ - char *file_path, *basename; - - if (g_file_equal (file, proxy->base_file)) - file_path = g_strdup (proxy->base_path); - else - { - basename = g_file_get_relative_path (proxy->base_file, file); - file_path = g_build_filename (proxy->base_path, basename, NULL); - g_free (basename); + g_vfs_job_succeeded (G_VFS_JOB (job)); } - return file_path; -} - -static void -proxy_changed (GFileMonitor* monitor, - GFile* file, - GFile* other_file, - GFileMonitorEvent event_type, - MonitorProxy *proxy) -{ - char *file_path; - char *other_file_path; - - file_path = proxy_get_trash_path (proxy, file); - - if (other_file) - other_file_path = proxy_get_trash_path (proxy, other_file); - else - other_file_path = NULL; - - g_vfs_monitor_emit_event (proxy->vfs_monitor, - event_type, - file_path, - other_file_path); - - g_free (file_path); - g_free (other_file_path); -} - -static void -trash_dir_changed (GFileMonitor *monitor, - GFile *child, - GFile *other_file, - GFileMonitorEvent event_type, - GVfsBackendTrash *backend) -{ - if (event_type == G_FILE_MONITOR_EVENT_DELETED || - event_type == G_FILE_MONITOR_EVENT_CREATED) - schedule_update_trash_files (backend, FALSE); + return TRUE; } -static void -update_trash_dir_monitors (GVfsBackendTrash *backend) -{ - GFile *file; - char *trashdir; - GFileMonitor *monitor; - GList *monitors, *l, *trashdirs; - - /* Remove old monitors */ - - G_LOCK (root_monitor); - monitors = backend->trash_dir_monitors; - backend->trash_dir_monitors = NULL; - G_UNLOCK (root_monitor); - - for (l = monitors; l != NULL; l = l->next) - { - monitor = l->data; - g_object_unref (monitor); - } - - g_list_free (monitors); +static gboolean +trash_backend_query_fs_info (GVfsBackend *vfs_backend, + GVfsJobQueryFsInfo *job, + const char *filename, + GFileInfo *info, + GFileAttributeMatcher *matcher) +{ + g_file_info_set_attribute_string (info, + G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, + "trash"); + + g_file_info_set_attribute_boolean (info, + G_FILE_ATTRIBUTE_FILESYSTEM_READONLY, + FALSE); - monitors = NULL; + g_file_info_set_attribute_uint32 (info, + G_FILE_ATTRIBUTE_FILESYSTEM_USE_PREVIEW, + G_FILESYSTEM_PREVIEW_TYPE_IF_LOCAL); - /* Add new ones for all trash dirs */ - - trashdirs = list_trash_dirs (); - for (l = trashdirs; l != NULL; l = l->next) - { - char *filesdir; - - trashdir = l->data; - - filesdir = g_build_filename (trashdir, "files", NULL); - file = g_file_new_for_path (filesdir); - g_free (filesdir); - monitor = g_file_monitor_directory (file, 0, NULL, NULL); - g_object_unref (file); - - if (monitor) - { - g_signal_connect (monitor, "changed", G_CALLBACK (trash_dir_changed), backend); - monitors = g_list_prepend (monitors, monitor); - } - g_free (trashdir); - } - - g_list_free (trashdirs); + g_vfs_job_succeeded (G_VFS_JOB (job)); - G_LOCK (root_monitor); - backend->trash_dir_monitors = g_list_concat (backend->trash_dir_monitors, monitors); - G_UNLOCK (root_monitor); + return TRUE; } - -static gpointer -update_trash_files_in_thread (GVfsBackendTrash *backend) +static gboolean +trash_backend_create_dir_monitor (GVfsBackend *vfs_backend, + GVfsJobCreateMonitor *job, + const char *filename, + GFileMonitorFlags flags) { - GList *names; - gboolean do_monitors; - - G_LOCK (root_monitor); - backend->trash_file_update_running = TRUE; - backend->trash_file_update_scheduled = FALSE; - do_monitors = backend->trash_file_update_monitors_scheduled; - backend->trash_file_update_monitors_scheduled = FALSE; - G_UNLOCK (root_monitor); - - loop: - if (do_monitors) - update_trash_dir_monitors (backend); - - names = enumerate_root (G_VFS_BACKEND (backend), NULL); - queue_set_trash_files (G_VFS_BACKEND (backend), names); + GVfsBackendTrash *backend = G_VFS_BACKEND_TRASH (vfs_backend); + GVfsMonitor *monitor; - G_LOCK (root_monitor); - if (backend->trash_file_update_scheduled) - { - backend->trash_file_update_scheduled = FALSE; - do_monitors = backend->trash_file_update_monitors_scheduled; - backend->trash_file_update_monitors_scheduled = FALSE; - G_UNLOCK (root_monitor); - goto loop; - } - - backend->trash_file_update_running = FALSE; - G_UNLOCK (root_monitor); - - return NULL; - -} - -static void -schedule_update_trash_files (GVfsBackendTrash *backend, - gboolean update_trash_dirs) -{ - G_LOCK (root_monitor); + if (filename[1]) + monitor = g_vfs_monitor_new (vfs_backend); + else + monitor = trash_backend_get_dir_monitor (backend, TRUE); - backend->trash_file_update_scheduled = TRUE; - backend->trash_file_update_monitors_scheduled |= update_trash_dirs; - - if (!backend->trash_file_update_running) - g_thread_create ((GThreadFunc)update_trash_files_in_thread, backend, FALSE, NULL); - - G_UNLOCK (root_monitor); -} + g_vfs_job_create_monitor_set_monitor (job, monitor); + g_vfs_job_succeeded (G_VFS_JOB (job)); + g_object_unref (monitor); -static void -mounts_changed (GUnixMountMonitor *mount_monitor, - GVfsBackendTrash *backend) -{ - schedule_update_trash_files (backend, TRUE); + return TRUE; } -static void -vfs_monitor_gone (gpointer data, - GObject *where_the_object_was) +static gboolean +trash_backend_create_file_monitor (GVfsBackend *vfs_backend, + GVfsJobCreateMonitor *job, + const char *filename, + GFileMonitorFlags flags) { - GVfsBackendTrash *backend; + GVfsBackendTrash *backend = G_VFS_BACKEND_TRASH (vfs_backend); + GVfsMonitor *monitor; - backend = G_VFS_BACKEND_TRASH (data); - - G_LOCK (root_monitor); - g_assert ((void *)backend->vfs_monitor == (void *)where_the_object_was); - backend->vfs_monitor = NULL; - g_object_unref (backend->mount_monitor); - backend->mount_monitor = NULL; - - g_list_foreach (backend->trash_dir_monitors, (GFunc)g_object_unref, NULL); - g_list_free (backend->trash_dir_monitors); - backend->trash_dir_monitors = NULL; - - backend->trash_file_update_scheduled = FALSE; - backend->trash_file_update_monitors_scheduled = FALSE; - - G_UNLOCK (root_monitor); -} + if (filename[1]) + monitor = g_vfs_monitor_new (vfs_backend); + else + monitor = trash_backend_get_file_monitor (backend, TRUE); -static GVfsMonitor * -do_create_root_monitor (GVfsBackend *backend) -{ - GVfsBackendTrash *trash_backend; - GVfsMonitor *vfs_monitor; - gboolean created; + g_vfs_job_create_monitor_set_monitor (job, monitor); + g_vfs_job_succeeded (G_VFS_JOB (job)); + g_object_unref (monitor); - trash_backend = G_VFS_BACKEND_TRASH (backend); - - created = FALSE; - G_LOCK (root_monitor); - if (trash_backend->vfs_monitor == NULL) - { - trash_backend->vfs_monitor = g_vfs_monitor_new (backend); - created = TRUE; - } - - vfs_monitor = trash_backend->vfs_monitor; - G_UNLOCK (root_monitor); - - if (created) - { - update_trash_dir_monitors (trash_backend); - trash_backend->mount_monitor = g_unix_mount_monitor_new (); - g_signal_connect (trash_backend->mount_monitor, - "mounts_changed", G_CALLBACK (mounts_changed), backend); - - g_object_weak_ref (G_OBJECT (vfs_monitor), - vfs_monitor_gone, - backend); - - } - - return vfs_monitor; + return TRUE; } static void -do_create_dir_monitor (GVfsBackend *backend, - GVfsJobCreateMonitor *job, - const char *filename, - GFileMonitorFlags flags) +trash_backend_finalize (GObject *object) { - char *trashdir, *topdir, *relative_path, *trashfile; - GVfsMonitor *vfs_monitor; + GVfsBackendTrash *backend = G_VFS_BACKEND_TRASH (object); - if (!decode_path (filename, &trashdir, &trashfile, &relative_path, &topdir)) - { - /* The trash:/// root */ - vfs_monitor = do_create_root_monitor (backend); - - g_vfs_job_create_monitor_set_monitor (job, - vfs_monitor); - g_vfs_job_succeeded (G_VFS_JOB (job)); - - g_object_unref (vfs_monitor); - } - else - { - GFile *file; - char *path; - GFileMonitor *monitor; - MonitorProxy *proxy; - - path = g_build_filename (trashdir, "files", trashfile, relative_path, NULL); - file = g_file_new_for_path (path); - g_free (path); - - monitor = g_file_monitor_directory (file, - flags, - G_VFS_JOB (job)->cancellable, - NULL); - - if (monitor) - { - proxy = g_new0 (MonitorProxy, 1); - proxy->vfs_monitor = g_vfs_monitor_new (backend); - proxy->monitor = G_OBJECT (monitor); - proxy->base_path = g_strdup (filename); - proxy->base_file = g_object_ref (file); - - g_object_set_data_full (G_OBJECT (proxy->vfs_monitor), "monitor-proxy", proxy, - (GDestroyNotify) monitor_proxy_free); - g_signal_connect (monitor, "changed", G_CALLBACK (proxy_changed), proxy); - - g_vfs_job_create_monitor_set_monitor (job, - proxy->vfs_monitor); - g_object_unref (proxy->vfs_monitor); - - g_vfs_job_succeeded (G_VFS_JOB (job)); - } - else - { - g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, - G_IO_ERROR_NOT_SUPPORTED, - _("Trash directory notification not supported")); - } - g_object_unref (file); - - g_free (trashdir); - g_free (trashfile); - g_free (relative_path); - g_free (topdir); - } -} + /* get rid of these first to stop a flood of event notifications + * from being emitted while we're tearing down the TrashWatcher + */ + if (backend->file_monitor) + g_object_unref (backend->file_monitor); + backend->file_monitor = NULL; -static void -do_create_file_monitor (GVfsBackend *backend, - GVfsJobCreateMonitor *job, - const char *filename, - GFileMonitorFlags flags) -{ - GVfsBackendTrash *trash_backend; - char *trashdir, *topdir, *relative_path, *trashfile; - GVfsMonitor *vfs_monitor; + if (backend->dir_monitor) + g_object_unref (backend->dir_monitor); + backend->dir_monitor = NULL; - trash_backend = G_VFS_BACKEND_TRASH (backend); - - if (!decode_path (filename, &trashdir, &trashfile, &relative_path, &topdir)) - { - /* The trash:/// root */ - G_LOCK (root_monitor); - if (trash_backend->file_vfs_monitor == NULL) - trash_backend->file_vfs_monitor = g_vfs_monitor_new (backend); - - vfs_monitor = trash_backend->file_vfs_monitor; - g_object_add_weak_pointer (G_OBJECT (vfs_monitor), (gpointer *)&trash_backend->file_vfs_monitor); - G_UNLOCK (root_monitor); - - g_vfs_job_create_monitor_set_monitor (job, vfs_monitor); - g_vfs_job_succeeded (G_VFS_JOB (job)); - g_object_unref (vfs_monitor); - } - else - { - GFile *file; - char *path; - GFileMonitor *monitor; - MonitorProxy *proxy; - - path = g_build_filename (trashdir, "files", trashfile, relative_path, NULL); - file = g_file_new_for_path (path); - g_free (path); - - monitor = g_file_monitor_file (file, - flags, - G_VFS_JOB (job)->cancellable, - NULL); - - if (monitor) - { - proxy = g_new0 (MonitorProxy, 1); - proxy->vfs_monitor = g_vfs_monitor_new (backend); - proxy->monitor = G_OBJECT (monitor); - proxy->base_path = g_strdup (filename); - proxy->base_file = g_object_ref (file); - - g_object_set_data_full (G_OBJECT (proxy->vfs_monitor), "monitor-proxy", proxy, (GDestroyNotify) monitor_proxy_free); - g_signal_connect (monitor, "changed", G_CALLBACK (proxy_changed), proxy); - - g_vfs_job_create_monitor_set_monitor (job, - proxy->vfs_monitor); - g_object_unref (proxy->vfs_monitor); - - g_vfs_job_succeeded (G_VFS_JOB (job)); - } - else - { - g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, - G_IO_ERROR_NOT_SUPPORTED, - _("Trash directory notification not supported")); - } - g_object_unref (file); - - g_free (trashdir); - g_free (trashfile); - g_free (relative_path); - g_free (topdir); - } + trash_watcher_free (backend->watcher); + trash_root_free (backend->root); } static void -do_pull (GVfsBackend *backend, - GVfsJobPull *job, - const char *filename, - const char *local_destination, - GFileCopyFlags flags, - gboolean remove_source, - GFileProgressCallback progress_callback, - gpointer progress_callback_data) +g_vfs_backend_trash_init (GVfsBackendTrash *backend) { - GFile *dst_file; - GFile *src_file; - char *trashdir, *topdir, *relative_path, *trashfile; - char *src_path; - gboolean res; - GError *error = NULL; - - res = decode_path (filename, &trashdir, &trashfile, &relative_path, &topdir); - - if (res == FALSE) - { - g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, - G_IO_ERROR_IS_DIRECTORY, - _("Can't open directory")); - return; - } - - dst_file = g_file_new_for_path (local_destination); - - src_path = g_build_filename (trashdir, "files", trashfile, relative_path, NULL); - src_file = g_file_new_for_path (src_path); - - if (remove_source) - res = g_file_move (src_file, - dst_file, - flags, - G_VFS_JOB (job)->cancellable, - progress_callback, - progress_callback_data, - &error); - else - res = g_file_copy (src_file, - dst_file, - flags, - G_VFS_JOB (job)->cancellable, - progress_callback, - progress_callback_data, - &error); - - if (res == FALSE) - { - g_vfs_job_failed_from_error (G_VFS_JOB (job), error); - g_error_free (error); - } - else - { - g_vfs_job_succeeded (G_VFS_JOB (job)); - - if (relative_path == NULL) - { - char *info_filename, *info_path; - - info_filename = g_strconcat (trashfile, ".trashinfo", NULL); - info_path = g_build_filename (trashdir, "info", info_filename, NULL); - g_free (info_filename); - g_unlink (info_path); - g_free (info_path); - } - } + GVfsBackend *vfs_backend = G_VFS_BACKEND (backend); + GMountSpec *mount_spec; - g_object_unref (src_file); - g_object_unref (dst_file); + /* translators: This is the name of the backend */ + g_vfs_backend_set_display_name (vfs_backend, _("Trash")); + g_vfs_backend_set_icon_name (vfs_backend, "user-trash"); + g_vfs_backend_set_user_visible (vfs_backend, FALSE); - g_free (trashdir); - g_free (trashfile); - g_free (relative_path); - g_free (topdir); + mount_spec = g_mount_spec_new ("trash"); + g_vfs_backend_set_mount_spec (vfs_backend, mount_spec); + g_mount_spec_unref (mount_spec); } - static void -g_vfs_backend_trash_class_init (GVfsBackendTrashClass *klass) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS (klass); - GVfsBackendClass *backend_class = G_VFS_BACKEND_CLASS (klass); - - gobject_class->finalize = g_vfs_backend_trash_finalize; - - backend_class->mount = do_mount; - backend_class->open_for_read = do_open_for_read; - backend_class->read = do_read; - backend_class->seek_on_read = do_seek_on_read; - backend_class->close_read = do_close_read; - backend_class->query_info = do_query_info; - backend_class->enumerate = do_enumerate; - backend_class->delete = do_delete; - backend_class->create_dir_monitor = do_create_dir_monitor; - backend_class->create_file_monitor = do_create_file_monitor; - backend_class->pull = do_pull; +g_vfs_backend_trash_class_init (GVfsBackendTrashClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (class); + GVfsBackendClass *backend_class = G_VFS_BACKEND_CLASS (class); + + gobject_class->finalize = trash_backend_finalize; + + backend_class->try_mount = trash_backend_mount; + backend_class->try_open_for_read = trash_backend_open_for_read; + backend_class->try_read = trash_backend_read; + backend_class->try_seek_on_read = trash_backend_seek_on_read; + backend_class->try_close_read = trash_backend_close_read; + backend_class->try_query_info = trash_backend_query_info; + backend_class->try_query_fs_info = trash_backend_query_fs_info; + backend_class->try_enumerate = trash_backend_enumerate; + backend_class->try_delete = trash_backend_delete; + backend_class->try_create_dir_monitor = trash_backend_create_dir_monitor; + backend_class->try_create_file_monitor = trash_backend_create_file_monitor; } diff --git a/daemon/gvfsbackendtrash.h b/daemon/gvfsbackendtrash.h index ee13ae37..dc6f25b3 100644 --- a/daemon/gvfsbackendtrash.h +++ b/daemon/gvfsbackendtrash.h @@ -1,50 +1,22 @@ -/* GIO - GLib Input, Output and Streaming Library - * - * Copyright (C) 2006-2007 Red Hat, Inc. +/* + * Copyright © 2008 Ryan Lortie * - * 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. - * - * 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. - * - * 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., 59 Temple Place, Suite 330, - * Boston, MA 02111-1307, USA. - * - * Author: Alexander Larsson <alexl@redhat.com> + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. */ -#ifndef __G_VFS_BACKEND_TRASH_H__ -#define __G_VFS_BACKEND_TRASH_H__ +#ifndef _gvfsbackendtrash_h_ +#define _gvfsbackendtrash_h_ #include <gvfsbackend.h> -#include <gmountspec.h> - -G_BEGIN_DECLS - -#define G_VFS_TYPE_BACKEND_TRASH (g_vfs_backend_trash_get_type ()) -#define G_VFS_BACKEND_TRASH(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), G_VFS_TYPE_BACKEND_TRASH, GVfsBackendTrash)) -#define G_VFS_BACKEND_TRASH_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), G_VFS_TYPE_BACKEND_TRASH, GVfsBackendTrashClass)) -#define G_VFS_IS_BACKEND_TRASH(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), G_VFS_TYPE_BACKEND_TRASH)) -#define G_VFS_IS_BACKEND_TRASH_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), G_VFS_TYPE_BACKEND_TRASH)) -#define G_VFS_BACKEND_TRASH_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), G_VFS_TYPE_BACKEND_TRASH, GVfsBackendTrashClass)) - -typedef struct _GVfsBackendTrash GVfsBackendTrash; -typedef struct _GVfsBackendTrashClass GVfsBackendTrashClass; - -struct _GVfsBackendTrashClass -{ - GVfsBackendClass parent_class; -}; -GType g_vfs_backend_trash_get_type (void) G_GNUC_CONST; +#define G_VFS_TYPE_BACKEND_TRASH (g_vfs_backend_trash_get_type ()) +#define G_VFS_BACKEND_TRASH(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), \ + G_VFS_TYPE_BACKEND_TRASH, \ + GVfsBackendTrash)) -G_END_DECLS +typedef struct OPAQUE_TYPE__GVfsBackendTrash GVfsBackendTrash; +GType g_vfs_backend_trash_get_type (void); -#endif /* __G_VFS_BACKEND_TRASH_H__ */ +#endif /* _gvfsbackendtrash_h_ */ diff --git a/daemon/trashlib/Makefile.am b/daemon/trashlib/Makefile.am new file mode 100644 index 00000000..ca907fe7 --- /dev/null +++ b/daemon/trashlib/Makefile.am @@ -0,0 +1,15 @@ +noinst_LIBRARIES = libtrash.a + +libtrash_a_CFLAGS = $(GLIB_CFLAGS) + +libtrash_a_SOURCES = \ + dirwatch.h \ + dirwatch.c \ + trashdir.h \ + trashdir.c \ + trashitem.h \ + trashitem.c \ + trashwatcher.h \ + trashwatcher.c \ + trashexpunge.h \ + trashexpunge.c diff --git a/daemon/trashlib/dirwatch.c b/daemon/trashlib/dirwatch.c new file mode 100644 index 00000000..d64992c7 --- /dev/null +++ b/daemon/trashlib/dirwatch.c @@ -0,0 +1,317 @@ +/* + * Copyright © 2008 Ryan Lortie + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. + */ + +#include <sys/stat.h> + +#include "dirwatch.h" + +/* DirWatch + * + * a directory watcher utility for use by the trash:/ backend. + * + * A DirWatch monitors a given directory for existence under a very + * specific set of circumstances. When the directory comes into + * existence, the create() callback is invoked. When the directory + * stops existing the destroy() callback is invoked. If the directory + * initially exists, then create() is invoked before the call to + * dir_watch_new() returns. + * + * The directory to watch is considered to exist only if it is a + * directory (and not a symlink) and its parent directory also exists. + * A topdir must be given, which is always assumed to "exist". + * + * For example, if '/mnt/disk/.Trash/1000/files/' is monitored with + * '/mnt/disk/' as a topdir then the following conditions must be true + * in order for the directory to be reported as existing: + * + * /mnt/disk/ is blindly assumed to exist + * /mnt/disk/.Trash must be a directory (not a symlink) + * /mnt/disk/.Trash/1000 must be a directory (not a symlink) + * /mnt/disk/.Trash/1000/files must be a directory (not a symlink) + * + * If any of these ceases to be true (even momentarily), the directory + * will be reported as having been destroyed. create() and destroy() + * callbacks are never issued spuriously (ie: two calls to one + * callback will never occur in a row). Events where the directory + * exists momentarily might be missed, but events where the directory + * stops existing momentarily will (hopefully) always be reported. + * The first call (if it happens) will always be to create(). + * + * This implementation is currently tweaked a bit for how GFileMonitor + * currently works with inotify. If GFileMonitor's implementation is + * changed it might be a good idea to take another look at this code. + */ + +struct OPAQUE_TYPE__DirWatch +{ + GFile *directory; + GFile *topdir; + + DirWatchFunc create; + DirWatchFunc destroy; + gpointer user_data; + gboolean state; + gboolean active; + + DirWatch *parent; + + GFileMonitor *parent_monitor; +}; + +#ifdef DIR_WATCH_DEBUG +# define dir_watch_created(watch) \ + G_STMT_START { \ + char *path = g_file_get_path ((watch)->directory); \ + g_print (">> created '%s'\n", path); \ + g_free (path); \ + (watch)->create ((watch)->user_data); \ + } G_STMT_END + +# define dir_watch_destroyed(watch) \ + G_STMT_START { \ + char *path = g_file_get_path ((watch)->directory); \ + g_print (">> destroyed '%s'\n", path); \ + g_free (path); \ + (watch)->destroy ((watch)->user_data); \ + } G_STMT_END +#else +# define dir_watch_created(watch) (watch)->create ((watch)->user_data) +# define dir_watch_destroyed(watch) (watch)->destroy ((watch)->user_data) +#endif + +#ifdef DIR_WATCH_DEBUG +#include <errno.h> +#include <string.h> +#endif + +static gboolean +dir_exists (GFile *file) +{ + gboolean result; + struct stat buf; + char *path; + + path = g_file_get_path (file); +#ifdef DIR_WATCH_DEBUG + errno = 0; +#endif + result = !lstat (path, &buf) && S_ISDIR (buf.st_mode); + +#ifdef DIR_WATCH_DEBUG + g_print (" lstat ('%s') -> is%s a directory (%s)\n", + path, result ? "" : " not", strerror (errno)); +#endif + g_free (path); + + return result; +} + +static void +dir_watch_parent_changed (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + DirWatch *watch = user_data; + + g_assert (watch->parent_monitor == monitor); + + if (!g_file_equal (file, watch->directory)) + return; + + if (event_type == G_FILE_MONITOR_EVENT_CREATED) + { + if (watch->state) + return; + + /* we were just created. ensure that it's a directory. */ + if (dir_exists (file)) + { + /* we're official now. report it. */ + watch->state = TRUE; + dir_watch_created (watch); + } + } + else if (event_type == G_FILE_MONITOR_EVENT_DELETED) + { + if (!watch->state) + return; + + watch->state = FALSE; + dir_watch_destroyed (watch); + } +} + +static void +dir_watch_recursive_create (gpointer user_data) +{ + DirWatch *watch = user_data; + GFile *parent; + + g_assert (watch->parent_monitor == NULL); + + parent = g_file_get_parent (watch->directory); + watch->parent_monitor = g_file_monitor_directory (parent, 0, + NULL, NULL); + g_object_unref (parent); + g_signal_connect (watch->parent_monitor, "changed", + G_CALLBACK (dir_watch_parent_changed), watch); + + /* check if directory was created before we started to monitor */ + if (dir_exists (watch->directory)) + { + watch->state = TRUE; + dir_watch_created (watch); + } +} + +static void +dir_watch_recursive_destroy (gpointer user_data) +{ + DirWatch *watch = user_data; + + /* exactly one monitor should be active */ + g_assert (watch->parent_monitor != NULL); + + /* if we were monitoring the directory... */ + if (watch->state) + { + dir_watch_destroyed (watch); + watch->state = FALSE; + } + + g_object_unref (watch->parent_monitor); + watch->parent_monitor = NULL; +} + +DirWatch * +dir_watch_new (GFile *directory, + GFile *topdir, + gboolean watching, + DirWatchFunc create, + DirWatchFunc destroy, + gpointer user_data) +{ + DirWatch *watch; + + watch = g_slice_new0 (DirWatch); + watch->create = create; + watch->destroy = destroy; + watch->user_data = user_data; + + watch->directory = g_object_ref (directory); + watch->topdir = g_object_ref (topdir); + + /* the top directory always exists */ + if (g_file_equal (directory, topdir)) + { + if (watching) + dir_watch_created (watch); + + watch->state = TRUE; + } + + else + { + GFile *parent; + + parent = g_file_get_parent (directory); + g_assert (parent != NULL); + + watch->parent = dir_watch_new (parent, topdir, watching, + dir_watch_recursive_create, + dir_watch_recursive_destroy, + watch); + + g_object_unref (parent); + + if (!watching) + watch->state = watch->parent->state && dir_exists (directory); + } + + return watch; +} + +void +dir_watch_free (DirWatch *watch) +{ + if (watch != NULL) + { + if (watch->parent_monitor) + g_object_unref (watch->parent_monitor); + + g_object_unref (watch->directory); + g_object_unref (watch->topdir); + + dir_watch_free (watch->parent); + } +} + +void +dir_watch_enable (DirWatch *watch) +{ + /* topdir always exists. say so. */ + if (watch->parent == NULL) + dir_watch_created (watch); + + else + dir_watch_enable (watch->parent); +} + +void +dir_watch_disable (DirWatch *watch) +{ + if (watch->parent_monitor) + g_object_unref (watch->parent_monitor); + + watch->parent_monitor = NULL; + + if (watch->parent) + dir_watch_disable (watch->parent); +} + +gboolean +dir_watch_is_valid (DirWatch *watch) +{ + return watch->state; +} + +gboolean +dir_watch_double_check (DirWatch *watch) +{ + gboolean old_state; + + old_state = watch->state; + + if (watch->parent == NULL) + { + g_assert (watch->state == TRUE); + return TRUE; + } + + if (dir_watch_double_check (watch->parent)) + { + if (dir_watch_is_valid (watch->parent)) + watch->state = dir_exists (watch->directory); + else + watch->state = FALSE; + + + if (!old_state && watch->state && watch->parent_monitor) + dir_watch_created (watch); + + else if (old_state && !watch->state && watch->parent_monitor) + dir_watch_destroyed (watch); + + else if (old_state || watch->state) + return TRUE; + } + + return FALSE; +} diff --git a/daemon/trashlib/dirwatch.h b/daemon/trashlib/dirwatch.h new file mode 100644 index 00000000..c1b7f46e --- /dev/null +++ b/daemon/trashlib/dirwatch.h @@ -0,0 +1,33 @@ +/* + * Copyright © 2008 Ryan Lortie + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. + */ + +#ifndef _dirwatch_h_ +#define _dirwatch_h_ + +#include <gio/gio.h> + +typedef void (*DirWatchFunc) (gpointer user_data); +typedef struct OPAQUE_TYPE__DirWatch DirWatch; + +DirWatch *dir_watch_new (GFile *directory, + GFile *topdir, + gboolean watching, + DirWatchFunc create, + DirWatchFunc destroy, + gpointer user_data); + +void dir_watch_enable (DirWatch *watch); +void dir_watch_disable (DirWatch *watch); +gboolean dir_watch_double_check (DirWatch *watch); + +gboolean dir_watch_is_valid (DirWatch *watch); +gboolean dir_watch_needs_update (DirWatch *watch); + +void dir_watch_free (DirWatch *watch); + +#endif /* _dirwatch_h_ */ diff --git a/daemon/trashlib/trashdir.c b/daemon/trashlib/trashdir.c new file mode 100644 index 00000000..bdc64e0e --- /dev/null +++ b/daemon/trashlib/trashdir.c @@ -0,0 +1,318 @@ +/* + * Copyright © 2008 Ryan Lortie + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. + */ + +#include "trashdir.h" + +#include <string.h> + +#include "dirwatch.h" + +struct OPAQUE_TYPE__TrashDir +{ + TrashRoot *root; + GSList *items; + + GFile *directory; + GFile *topdir; + gboolean is_homedir; + + DirWatch *watch; + GFileMonitor *monitor; +}; + +static gint +compare_basename (gconstpointer a, + gconstpointer b) +{ + GFile *file_a, *file_b; + char *name_a, *name_b; + gint result; + + file_a = (GFile *) a; + file_b = (GFile *) b; + + name_a = g_file_get_basename (file_a); + name_b = g_file_get_basename (file_b); + + result = strcmp (name_a, name_b); + + g_free (name_a); + g_free (name_b); + + return result; +} + +static void +trash_dir_set_files (TrashDir *dir, + GSList *items) +{ + GSList **old, *new; + + items = g_slist_sort (items, (GCompareFunc) compare_basename); + old = &dir->items; + new = items; + + while (new || *old) + { + int result; + + if ((result = (new == NULL) - (*old == NULL)) == 0) + result = compare_basename (new->data, (*old)->data); + + if (result < 0) + { + /* new entry. add it. */ + *old = g_slist_prepend (*old, new->data); /* take reference */ + old = &(*old)->next; + trash_root_add_item (dir->root, new->data, dir->is_homedir); + new = new->next; + } + else if (result > 0) + { + /* old entry. remove it. */ + trash_root_remove_item (dir->root, (*old)->data, dir->is_homedir); + g_object_unref ((*old)->data); + *old = g_slist_delete_link (*old, *old); + } + else + { + /* match. no change. */ + old = &(*old)->next; + g_object_unref (new->data); + new = new->next; + } + } + + g_slist_free (items); + + trash_root_thaw (dir->root); +} + +static void +trash_dir_empty (TrashDir *dir) +{ + trash_dir_set_files (dir, NULL); +} + +static void +trash_dir_enumerate (TrashDir *dir) +{ + GFileEnumerator *enumerator; + GSList *files = NULL; + + enumerator = g_file_enumerate_children (dir->directory, + G_FILE_ATTRIBUTE_STANDARD_NAME, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, NULL); + + if (enumerator) + { + GFileInfo *info; + + while ((info = g_file_enumerator_next_file (enumerator, NULL, NULL))) + { + GFile *file; + + file = g_file_get_child (dir->directory, + g_file_info_get_name (info)); + files = g_slist_prepend (files, file); + + g_object_unref (info); + } + + g_object_unref (enumerator); + } + + trash_dir_set_files (dir, files); /* consumes files */ +} + +static void +trash_dir_changed (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + TrashDir *dir = user_data; + + if (event_type == G_FILE_MONITOR_EVENT_CREATED) + trash_root_add_item (dir->root, file, dir->is_homedir); + + else if (event_type == G_FILE_MONITOR_EVENT_DELETED) + trash_root_remove_item (dir->root, file, dir->is_homedir); + + else + { + static gboolean already_did_warning; + char *dirname; + char *name; + + g_warning ("*** Unsupported operation detected on trash directory"); + if (!already_did_warning) + { + g_warning (" A trash files/ directory should only have files " + "linked or unlinked (via moves or deletes). Some other " + "operation has been detected on a file in the directory " + "(eg: a file has been modified). Likely, the data " + "reported by the trash backend will now be " + "inconsistent."); + already_did_warning = TRUE; + } + + name = file ? g_file_get_basename (file) : NULL; + dirname = g_file_get_path (dir->directory); + g_warning (" dir: %s, file: %s, type: %d\n\n", + dirname, name, event_type); + } + + trash_root_thaw (dir->root); +} + +static void +trash_dir_created (gpointer user_data) +{ + TrashDir *dir = user_data; + + g_assert (dir->monitor == NULL); + dir->monitor = g_file_monitor_directory (dir->directory, 0, NULL, NULL); + g_signal_connect (dir->monitor, "changed", + G_CALLBACK (trash_dir_changed), dir); + trash_dir_enumerate (dir); +} + +static void +trash_dir_destroyed (gpointer user_data) +{ + TrashDir *dir = user_data; + + g_assert (dir->monitor != NULL); + g_object_unref (dir->monitor); + dir->monitor = NULL; + + trash_dir_empty (dir); +} + +void +trash_dir_watch (TrashDir *dir) +{ + g_assert (dir->monitor == NULL); + + /* start monitoring after a period of not monitoring. + * + * there are two possible cases here: + * 1) the directory now exists + * - we have to rescan the directory to ensure that we notice + * any changes that have occured since we last looked + * + * 2) the directory does not exist + * - if it existed last time we looked then we may have stale + * toplevel items that need to be removed. + * + * in case 1, trash_dir_created() will be called from + * dir_watch_new(). it calls trash_dir_rescan() itself. + * + * in case 2, no other function will be called and we must manually + * call trash_dir_empty(). + * + * we can tell if case 1 happened because trash_dir_created() also + * sets the dir->monitor. + */ + dir_watch_enable (dir->watch); + + if (dir->monitor == NULL) + /* case 2 */ + trash_dir_empty (dir); +} + +void +trash_dir_unwatch (TrashDir *dir) +{ + /* stop monitoring. + * + * in all cases, we just fall silent. + */ + + if (dir->monitor != NULL) + { + g_object_unref (dir->monitor); + dir->monitor = NULL; + } + + dir_watch_disable (dir->watch); +} + +void +trash_dir_rescan (TrashDir *dir) +{ + if (dir_watch_double_check (dir->watch)) + { + if (dir_watch_is_valid (dir->watch)) + trash_dir_enumerate (dir); + else + trash_dir_empty (dir); + } +} + +static trash_dir_ui_hook ui_hook; + +TrashDir * +trash_dir_new (TrashRoot *root, + gboolean watching, + gboolean is_homedir, + const char *mount_point, + const char *format, + ...) +{ + TrashDir *dir; + va_list ap; + char *rel; + + va_start (ap, format); + rel = g_strdup_vprintf (format, ap); + va_end (ap); + + dir = g_slice_new (TrashDir); + + dir->root = root; + dir->items = NULL; + dir->topdir = g_file_new_for_path (mount_point); + dir->directory = g_file_get_child (dir->topdir, rel); + dir->monitor = NULL; + dir->is_homedir = is_homedir; + + dir->watch = dir_watch_new (dir->directory, dir->topdir, watching, + trash_dir_created, trash_dir_destroyed, + dir); + + if (ui_hook) + ui_hook (dir, dir->directory); + + return dir; +} + +void +trash_dir_set_ui_hook (trash_dir_ui_hook _ui_hook) +{ + ui_hook = _ui_hook; +} + +void +trash_dir_free (TrashDir *dir) +{ + dir_watch_free (dir->watch); + + if (dir->monitor) + g_object_unref (dir->monitor); + + trash_dir_set_files (dir, NULL); + + g_object_unref (dir->directory); + g_object_unref (dir->topdir); + + g_slice_free (TrashDir, dir); +} diff --git a/daemon/trashlib/trashdir.h b/daemon/trashlib/trashdir.h new file mode 100644 index 00000000..5612f9ba --- /dev/null +++ b/daemon/trashlib/trashdir.h @@ -0,0 +1,36 @@ +/* + * Copyright © 2008 Ryan Lortie + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. + */ + +#ifndef _trashdir_h_ +#define _trashdir_h_ + +#include <gio/gio.h> + +#include "trashitem.h" + +typedef struct OPAQUE_TYPE__TrashDir TrashDir; + +typedef void (*trash_dir_ui_hook) (TrashDir *dir, + GFile *directory); + +TrashDir *trash_dir_new (TrashRoot *root, + gboolean watching, + gboolean is_homedir, + const char *mount_point, + const char *format, + ...); + +void trash_dir_free (TrashDir *dir); + +void trash_dir_watch (TrashDir *dir); +void trash_dir_unwatch (TrashDir *dir); +void trash_dir_rescan (TrashDir *dir); + +void trash_dir_set_ui_hook (trash_dir_ui_hook ui_hook); + +#endif /* _trashdir_h_ */ diff --git a/daemon/trashlib/trashexpunge.c b/daemon/trashlib/trashexpunge.c new file mode 100644 index 00000000..b54b3b2e --- /dev/null +++ b/daemon/trashlib/trashexpunge.c @@ -0,0 +1,127 @@ +/* + * Copyright © 2008 Ryan Lortie + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. + */ + +#include "trashexpunge.h" + +static gsize trash_expunge_initialised; +static GHashTable *trash_expunge_queue; +static gboolean trash_expunge_alive; +static GMutex *trash_expunge_lock; +static GCond *trash_expunge_wait; + +static void +trash_expunge_delete_everything_under (GFile *directory) +{ + GFileEnumerator *enumerator; + + enumerator = g_file_enumerate_children (directory, + G_FILE_ATTRIBUTE_STANDARD_NAME, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, NULL); + + if (enumerator) + { + GFileInfo *info; + + while ((info = g_file_enumerator_next_file (enumerator, NULL, NULL))) + { + const gchar *basename; + GFile *sub; + + basename = g_file_info_get_name (info); + sub = g_file_get_child (directory, basename); + trash_expunge_delete_everything_under (sub); + + /* do the delete here */ + g_file_delete (sub, NULL, NULL); + + g_object_unref (sub); + } + g_object_unref (enumerator); + } +} + +static gboolean +just_return_true (gpointer a, + gpointer b, + gpointer c) +{ + return TRUE; +} + +static gpointer +trash_expunge_thread (gpointer data) +{ + GTimeVal timeval; + + g_mutex_lock (trash_expunge_lock); + + do + { + while (g_hash_table_size (trash_expunge_queue)) + { + GFile *directory; + + directory = g_hash_table_find (trash_expunge_queue, + just_return_true, NULL); + g_hash_table_remove (trash_expunge_queue, directory); + + g_mutex_unlock (trash_expunge_lock); + trash_expunge_delete_everything_under (directory); + g_mutex_lock (trash_expunge_lock); + + g_object_unref (directory); + } + + g_get_current_time (&timeval); + g_time_val_add (&timeval, 60 * 1000000); /* 1min */ + } + while (g_cond_timed_wait (trash_expunge_wait, + trash_expunge_lock, + &timeval)); + + trash_expunge_alive = FALSE; + + g_mutex_unlock (trash_expunge_lock); + + return NULL; +} + +void +trash_expunge (GFile *directory) +{ + if G_UNLIKELY (g_once_init_enter (&trash_expunge_initialised)) + { + trash_expunge_queue = g_hash_table_new (g_file_hash, + (GEqualFunc) g_file_equal); + trash_expunge_lock = g_mutex_new (); + trash_expunge_wait = g_cond_new (); + + g_once_init_leave (&trash_expunge_initialised, 1); + } + + g_mutex_lock (trash_expunge_lock); + + if (!g_hash_table_lookup (trash_expunge_queue, directory)) + g_hash_table_insert (trash_expunge_queue, + g_object_ref (directory), + directory); + + if (trash_expunge_alive == FALSE) + { + GThread *thread; + + thread = g_thread_create (trash_expunge_thread, NULL, FALSE, NULL); + g_assert (thread != NULL); + trash_expunge_alive = TRUE; + } + else + g_cond_signal (trash_expunge_wait); + + g_mutex_unlock (trash_expunge_lock); +} diff --git a/daemon/trashlib/trashexpunge.h b/daemon/trashlib/trashexpunge.h new file mode 100644 index 00000000..d6ab4f8f --- /dev/null +++ b/daemon/trashlib/trashexpunge.h @@ -0,0 +1,17 @@ +/* + * Copyright © 2008 Ryan Lortie + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. + */ + +#ifndef _trashexpunger_h_ +#define _trashexpunger_h_ + +#include <gio/gio.h> + +typedef struct OPAQUE_TYPE__TrashExpunger TrashExpunger; +void trash_expunge (GFile *expunge_directory); + +#endif /* _trashexpunger_h_ */ diff --git a/daemon/trashlib/trashitem.c b/daemon/trashlib/trashitem.c new file mode 100644 index 00000000..dc01a929 --- /dev/null +++ b/daemon/trashlib/trashitem.c @@ -0,0 +1,522 @@ +/* + * Copyright © 2008 Ryan Lortie + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. + */ + +#include "trashexpunge.h" +#include "trashitem.h" + +#include <glib/gstdio.h> + +typedef struct +{ + trash_item_notify func; + TrashItem *item; + gpointer user_data; +} NotifyClosure; + +struct OPAQUE_TYPE__TrashRoot +{ + GStaticRWLock lock; + GQueue *notifications; + + trash_item_notify create_notify; + trash_item_notify delete_notify; + trash_size_change size_change; + gpointer user_data; + + GHashTable *item_table; + gboolean is_homedir; + int old_size; +}; + +struct OPAQUE_TYPE__TrashItem +{ + TrashRoot *root; + gint ref_count; + + char *escaped_name; + GFile *file; + + GFile *original; + char *delete_date; +}; + +static char * +trash_item_escape_name (GFile *file, + gboolean in_homedir) +{ + /* + * make unique names as follows: + * + * - items in home directory use their basename (never starts with '/') + * + * - if the basename starts with '\' then it is changed to '`\' + * + * - if the basename starts with '`' then it is changed to '``' + * + * - this means that home directory items never start with '\' + * + * - items in others use full path name (always starts with '/') + * + * - each '/' (including the first) is changed to '\' + * + * - this means that all of these items start with '\' + * + * - each '\' is changed to '`\' + * + * - each '`' is changed to '``' + */ +#define ESCAPE_SYMBOL1 '\\' +#define ESCAPE_SYMBOL2 '`' + + if (in_homedir) + { + char *basename; + char *escaped; + + basename = g_file_get_basename (file); + + if (basename[0] != ESCAPE_SYMBOL1 && basename[0] != ESCAPE_SYMBOL2) + return basename; + + escaped = g_strdup_printf ("%c%s", ESCAPE_SYMBOL2, basename); + g_free (basename); + + return escaped; + } + else + { + char *uri, *src, *dest, *escaped; + int need_bytes = 0; + + uri = g_file_get_uri (file); + g_assert (g_str_has_prefix (uri, "file:///")); + + src = uri + 7; /* keep the first '/' */ + while (*src) + { + if (*src == ESCAPE_SYMBOL1 || *src == ESCAPE_SYMBOL2) + need_bytes += 2; + else + need_bytes++; + + src++; + } + + escaped = g_malloc (need_bytes + 1); + + dest = escaped; + src = uri + 7; + while (*src) + { + if (*src == ESCAPE_SYMBOL1 || *src == ESCAPE_SYMBOL2) + { + *dest++ = ESCAPE_SYMBOL2; + *dest++ = *src; + } + else if (*src == '/') + *dest++ = ESCAPE_SYMBOL1; + else + *dest++ = *src; + + src++; + } + + g_free (uri); + *src = '\0'; + + return escaped; + } +} + +static void +trash_item_get_trashinfo (GFile *path, + GFile **original, + char **date) +{ + GFile *files, *trashdir; + GKeyFile *keyfile; + char *trashpath; + char *trashinfo; + char *basename; + + files = g_file_get_parent (path); + trashdir = g_file_get_parent (files); + trashpath = g_file_get_path (trashdir); + g_object_unref (files); + + basename = g_file_get_basename (path); + + trashinfo = g_strdup_printf ("%s/info/%s.trashinfo", trashpath, basename); + g_free (trashpath); + g_free (basename); + + keyfile = g_key_file_new (); + + *original = NULL; + *date = NULL; + + if (g_key_file_load_from_file (keyfile, trashinfo, 0, NULL)) + { + char *orig; + + orig = g_key_file_get_string (keyfile, + "Trash Info", "Path", + NULL); + + if (orig == NULL) + *original = NULL; + + else if (g_path_is_absolute (orig)) + *original = g_file_new_for_path (orig); + + else + { + GFile *rootdir; + + rootdir = g_file_get_parent (trashdir); + *original = g_file_get_child (rootdir, orig); + } + + g_free (orig); + + *date = g_key_file_get_string (keyfile, + "Trash Info", "DeletionDate", + NULL); + } + + g_object_unref (trashdir); + g_key_file_free (keyfile); + g_free (trashinfo); +} + +static TrashItem * +trash_item_new (TrashRoot *root, + GFile *file, + gboolean in_homedir) +{ + TrashItem *item; + + item = g_slice_new (TrashItem); + item->root = root; + item->ref_count = 1; + item->file = g_object_ref (file); + item->escaped_name = trash_item_escape_name (file, in_homedir); + trash_item_get_trashinfo (item->file, &item->original, &item->delete_date); + + return item; +} + +static TrashItem * +trash_item_ref (TrashItem *item) +{ + g_atomic_int_inc (&item->ref_count); + return item; +} + +void +trash_item_unref (TrashItem *item) +{ + if (g_atomic_int_dec_and_test (&item->ref_count)) + { + g_object_unref (item->file); + + if (item->original) + g_object_unref (item->original); + + g_free (item->delete_date); + g_free (item->escaped_name); + + g_slice_free (TrashItem, item); + } +} + +const char * +trash_item_get_escaped_name (TrashItem *item) +{ + return item->escaped_name; +} + +const char * +trash_item_get_delete_date (TrashItem *item) +{ + return item->delete_date; +} + +GFile * +trash_item_get_original (TrashItem *item) +{ + return item->original; +} + +GFile * +trash_item_get_file (TrashItem *item) +{ + return item->file; +} + +static void +trash_item_queue_notify (TrashItem *item, + trash_item_notify func) +{ + NotifyClosure *closure; + + closure = g_slice_new (NotifyClosure); + closure->func = func; + closure->item = trash_item_ref (item); + closure->user_data = item->root->user_data; + + g_queue_push_tail (item->root->notifications, closure); +} + +static void +trash_item_invoke_closure (NotifyClosure *closure) +{ + closure->func (closure->item, closure->user_data); + trash_item_unref (closure->item); + g_slice_free (NotifyClosure, closure); +} + +void +trash_root_thaw (TrashRoot *root) +{ + NotifyClosure *closure; + gboolean size_changed; + int size; + + /* send notifications until we have none */ + while (TRUE) + { + g_static_rw_lock_writer_lock (&root->lock); + if (g_queue_is_empty (root->notifications)) + break; + + closure = g_queue_pop_head (root->notifications); + g_static_rw_lock_writer_unlock (&root->lock); + + trash_item_invoke_closure (closure); + } + + /* still holding lock... */ + size = g_hash_table_size (root->item_table); + size_changed = root->old_size != size; + root->old_size = size; + + g_static_rw_lock_writer_unlock (&root->lock); + + if (size_changed) + root->size_change (root->user_data); +} + +static void +trash_item_removed (gpointer data) +{ + TrashItem *item = data; + + trash_item_queue_notify (item, item->root->delete_notify); + trash_item_unref (item); +} + +TrashRoot * +trash_root_new (trash_item_notify create, + trash_item_notify delete, + trash_size_change size_change, + gpointer user_data) +{ + TrashRoot *root; + + root = g_slice_new (TrashRoot); + g_static_rw_lock_init (&root->lock); + root->create_notify = create; + root->delete_notify = delete; + root->size_change = size_change; + root->user_data = user_data; + root->notifications = g_queue_new (); + root->item_table = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, trash_item_removed); + root->old_size = 0; + + return root; +} + +void +trash_root_free (TrashRoot *root) +{ + g_hash_table_destroy (root->item_table); + + while (!g_queue_is_empty (root->notifications)) + { + NotifyClosure *closure; + + closure = g_queue_pop_head (root->notifications); + trash_item_unref (closure->item); + g_slice_free (NotifyClosure, closure); + } + g_queue_free (root->notifications); + + g_slice_free (TrashRoot, root); +} + +void +trash_root_add_item (TrashRoot *list, + GFile *file, + gboolean in_homedir) +{ + TrashItem *item; + + item = trash_item_new (list, file, in_homedir); + + g_static_rw_lock_writer_lock (&list->lock); + + if (g_hash_table_lookup (list->item_table, item->escaped_name)) + { + g_static_rw_lock_writer_unlock (&list->lock); + + /* already exists... */ + trash_item_unref (item); + return; + } + + g_hash_table_insert (list->item_table, item->escaped_name, item); + trash_item_queue_notify (item, item->root->create_notify); + + g_static_rw_lock_writer_unlock (&list->lock); +} + +void +trash_root_remove_item (TrashRoot *list, + GFile *file, + gboolean in_homedir) +{ + char *escaped; + + escaped = trash_item_escape_name (file, in_homedir); + + g_static_rw_lock_writer_lock (&list->lock); + g_hash_table_remove (list->item_table, escaped); + g_static_rw_lock_writer_unlock (&list->lock); + + g_free (escaped); +} + +GList * +trash_root_get_items (TrashRoot *root) +{ + GList *items, *node; + + g_static_rw_lock_reader_lock (&root->lock); + + items = g_hash_table_get_values (root->item_table); + for (node = items; node; node = node->next) + trash_item_ref (node->data); + + g_static_rw_lock_reader_unlock (&root->lock); + + return items; +} + +void +trash_item_list_free (GList *list) +{ + GList *node; + + for (node = list; node; node = node->next) + trash_item_unref (node->data); + g_list_free (list); +} + +TrashItem * +trash_root_lookup_item (TrashRoot *root, + const char *escaped) +{ + TrashItem *item; + + g_static_rw_lock_reader_lock (&root->lock); + + if ((item = g_hash_table_lookup (root->item_table, escaped))) + trash_item_ref (item); + + g_static_rw_lock_reader_unlock (&root->lock); + + return item; +} + +int +trash_root_get_n_items (TrashRoot *root) +{ + int size; + + g_static_rw_lock_reader_lock (&root->lock); + size = g_hash_table_size (root->item_table); + g_static_rw_lock_reader_unlock (&root->lock); + + return size; +} + +gboolean +trash_item_delete (TrashItem *item, + GError **error) +{ + gboolean success; + GFile *expunged; + guint unique; + guint i; + + expunged = g_file_resolve_relative_path (item->file, + "../../expunged"); + g_file_make_directory_with_parents (expunged, NULL, NULL); + unique = g_random_int (); + + for (success = FALSE, i = 0; !success && i < 1000; i++) + { + GFile *temp_name; + char buffer[16]; + + g_sprintf (buffer, "%u", unique + i); + temp_name = g_file_get_child (expunged, buffer); + + if (g_file_move (item->file, temp_name, + G_FILE_COPY_OVERWRITE | + G_FILE_COPY_NOFOLLOW_SYMLINKS | + G_FILE_COPY_NO_FALLBACK_FOR_MOVE, + NULL, NULL, NULL, NULL)) + { + g_static_rw_lock_writer_lock (&item->root->lock); + g_hash_table_remove (item->root->item_table, item->escaped_name); + g_static_rw_lock_writer_unlock (&item->root->lock); + + { + GFile *trashinfo; + gchar *basename; + gchar *relname; + + basename = g_file_get_basename (item->file); + relname = g_strdup_printf ("../../info/%s.trashinfo", basename); + trashinfo = g_file_resolve_relative_path (item->file, relname); + g_free (basename); + g_free (relname); + + g_file_delete (trashinfo, NULL, NULL); + } + + trash_expunge (expunged); + success = TRUE; + } + + g_object_unref (temp_name); + } + + g_object_unref (expunged); + + if (!success) + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to delete the item from the trash"); + + trash_root_thaw (item->root); + + return success; +} diff --git a/daemon/trashlib/trashitem.h b/daemon/trashlib/trashitem.h new file mode 100644 index 00000000..785ef8aa --- /dev/null +++ b/daemon/trashlib/trashitem.h @@ -0,0 +1,56 @@ +/* + * Copyright © 2008 Ryan Lortie + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. + */ + +#ifndef _trashitem_h_ +#define _trashitem_h_ + +#include <gio/gio.h> + +typedef struct OPAQUE_TYPE__TrashRoot TrashRoot; +typedef struct OPAQUE_TYPE__TrashItem TrashItem; + +typedef void (*trash_item_notify) (TrashItem *item, + gpointer user_data); +typedef void (*trash_size_change) (gpointer user_data); + +/* trash root -- the set of all toplevel trash items */ +TrashRoot *trash_root_new (trash_item_notify create, + trash_item_notify delete, + trash_size_change size_change, + gpointer user_data); +void trash_root_free (TrashRoot *root); + +/* add/remove trash items (safe only from one thread) */ +void trash_root_add_item (TrashRoot *root, + GFile *file, + gboolean in_homedir); +void trash_root_remove_item (TrashRoot *root, + GFile *file, + gboolean in_homedir); +void trash_root_thaw (TrashRoot *root); + +/* query trash items, holding references (safe from any thread) */ +int trash_root_get_n_items (TrashRoot *root); +GList *trash_root_get_items (TrashRoot *root); +TrashItem *trash_root_lookup_item (TrashRoot *root, + const char *escaped); + +void trash_item_list_free (GList *list); +void trash_item_unref (TrashItem *item); + +/* query a trash item (safe while holding a reference to it) */ +const char *trash_item_get_escaped_name (TrashItem *item); +const char *trash_item_get_delete_date (TrashItem *item); +GFile *trash_item_get_original (TrashItem *item); +GFile *trash_item_get_file (TrashItem *item); + +/* delete a trash item (safe while holding a reference to it) */ +gboolean trash_item_delete (TrashItem *item, + GError **error); + +#endif /* _trashitem_h_ */ diff --git a/daemon/trashlib/trashwatcher.c b/daemon/trashlib/trashwatcher.c new file mode 100644 index 00000000..e10d6e3d --- /dev/null +++ b/daemon/trashlib/trashwatcher.c @@ -0,0 +1,332 @@ +/* + * Copyright © 2008 Ryan Lortie + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. + */ + +#include "trashwatcher.h" + +#include <gio/gunixmounts.h> +#include <gio/gio.h> +#include <unistd.h> +#include <string.h> + +#include "trashitem.h" +#include "trashdir.h" + +typedef enum +{ + TRASH_WATCHER_TRUSTED, + TRASH_WATCHER_WATCH, + TRASH_WATCHER_NO_WATCH, +} WatchType; + +/* decide_watch_type: + * + * This function is responsible for determining what sort of watching + * we should do on a given mountpoint according to the type of + * filesystem. It must return one of the WatchType constants above. + * + * TRASH_WATCHER_TRUSTED: + * + * This is used for filesystems on which notification is supported + * and all file events are reliably reported. After initialisation + * the trash directories are never manually rescanned since any + * changes are already known to us from the notifications we + * received about them (that's where the "trust" comes in). + * + * This should be used for local filesystems such as ext3. + * + * TRASH_WATCHER_WATCH: + * + * This is used for filesystems on which notification is supported + * but works unreliably. Some changes to the filesystem may not + * be delivered by the operating system. The events which are + * delivered are immediately reported but events which are not + * delivered are not reported until the directory is manually + * rescanned (ie: trash_watcher_rescan() is called). + * + * This should be used for filesystems like NFS where local + * changes are reported by the kernel but changes made on other + * hosts are not. + * + * TRASH_WATCHER_NO_WATCH: + * + * Don't bother watching at all. No change events are ever + * delivered except while running trash_watcher_rescan(). + * + * This should be used for filesystems where change notification + * is unsupported or is supported, but buggy enough to cause + * problems when using the other two options. + */ +static WatchType +decide_watch_type (GUnixMountEntry *mount, + gboolean is_home_trash) +{ + return TRASH_WATCHER_TRUSTED; +} + +/* find the mount entry for the directory containing 'file'. + * used to figure out what sort of filesystem the home trash + * folder is sitting on. + */ +static GUnixMountEntry * +find_mount_entry_for_file (GFile *file) +{ + GUnixMountEntry *entry; + char *pathname; + + pathname = g_file_get_path (file); + do + { + char *slash; + + slash = strrchr (pathname, '/'); + + /* leave the leading '/' in place */ + if (slash == pathname) + slash++; + + *slash = '\0'; + + entry = g_unix_mount_at (pathname, NULL); + } + while (entry == NULL && pathname[1]); + + g_free (pathname); + + /* if the GUnixMount stuff is gummed up, this might fail. we can't + * really proceed, since decide_watch_type() needs to know this. + */ + g_assert (entry != NULL); + + return entry; +} + +typedef struct _TrashMount TrashMount; + +struct OPAQUE_TYPE__TrashWatcher +{ + TrashRoot *root; + + GUnixMountMonitor *mount_monitor; + TrashMount *mounts; + + TrashDir *homedir_trashdir; + WatchType homedir_type; + + gboolean watching; +}; + +struct _TrashMount +{ + GUnixMountEntry *mount_entry; + TrashDir *dirs[2]; + WatchType type; + + TrashMount *next; +}; + +static void +trash_mount_insert (TrashWatcher *watcher, + TrashMount ***mount_ptr_ptr, + GUnixMountEntry *mount_entry) +{ + const char *mountpoint; + gboolean watching; + TrashMount *mount; + + mountpoint = g_unix_mount_get_mount_path (mount_entry); + + mount = g_slice_new (TrashMount); + mount->mount_entry = mount_entry; + mount->type = decide_watch_type (mount_entry, FALSE); + + watching = watcher->watching && mount->type != TRASH_WATCHER_NO_WATCH; + + /* """ + * For showing trashed files, implementations SHOULD support (1) and + * (2) at the same time (i.e. if both $topdir/.Trash/$uid and + * $topdir/.Trash-$uid are present, it should list trashed files + * from both of them). + * """ + */ + + /* (1) */ + mount->dirs[0] = trash_dir_new (watcher->root, watching, FALSE, mountpoint, + ".Trash/%d/files", (int) getuid ()); + + /* (2) */ + mount->dirs[1] = trash_dir_new (watcher->root, watching, FALSE, mountpoint, + ".Trash-%d/files", (int) getuid ()); + + mount->next = **mount_ptr_ptr; + + **mount_ptr_ptr = mount; + *mount_ptr_ptr = &mount->next; +} + +static void +trash_mount_remove (TrashMount **mount_ptr) +{ + TrashMount *mount = *mount_ptr; + + /* first, the dirs */ + trash_dir_free (mount->dirs[0]); + trash_dir_free (mount->dirs[1]); + + /* detach from list */ + *mount_ptr = mount->next; + + g_unix_mount_free (mount->mount_entry); + g_slice_free (TrashMount, mount); +} + +static void +trash_watcher_remount (TrashWatcher *watcher) +{ + TrashMount **old; + GList *mounts; + GList *new; + + mounts = g_unix_mounts_get (NULL); + mounts = g_list_sort (mounts, (GCompareFunc) g_unix_mount_compare); + + old = &watcher->mounts; + new = mounts; + + /* synchronise the two lists */ + while (*old || new) + { + int result; + + if (new && g_unix_mount_is_system_internal (new->data)) + { + g_unix_mount_free (new->data); + new = new->next; + continue; + } + + if ((result = (new == NULL) - (*old == NULL)) == 0) + result = g_unix_mount_compare (new->data, (*old)->mount_entry); + + if (result < 0) + { + /* new entry. add it. */ + trash_mount_insert (watcher, &old, new->data); + new = new->next; + } + else if (result > 0) + { + /* old entry. remove it. */ + trash_mount_remove (old); + } + else + { + /* match. no change. */ + g_unix_mount_free (new->data); + + old = &(*old)->next; + new = new->next; + } + } + + g_list_free (mounts); +} + +TrashWatcher * +trash_watcher_new (TrashRoot *root) +{ + GUnixMountEntry *homedir_mount; + GFile *homedir_trashdir; + TrashWatcher *watcher; + GFile *user_datadir; + + watcher = g_slice_new (TrashWatcher); + watcher->root = root; + watcher->mounts = NULL; + watcher->watching = FALSE; + watcher->mount_monitor = g_unix_mount_monitor_new (); + g_signal_connect_swapped (watcher->mount_monitor, "mounts_changed", + G_CALLBACK (trash_watcher_remount), watcher); + + user_datadir = g_file_new_for_path (g_get_user_data_dir ()); + homedir_trashdir = g_file_get_child (user_datadir, "Trash/files"); + homedir_mount = find_mount_entry_for_file (homedir_trashdir); + watcher->homedir_type = decide_watch_type (homedir_mount, TRUE); + watcher->homedir_trashdir = trash_dir_new (watcher->root, + FALSE, TRUE, + g_get_user_data_dir (), + "Trash/files"); + + g_object_unref (homedir_trashdir); + g_object_unref (user_datadir); + + trash_watcher_remount (watcher); + + return watcher; +} + +void +trash_watcher_free (TrashWatcher *watcher) +{ + g_assert_not_reached (); +} + +void +trash_watcher_watch (TrashWatcher *watcher) +{ + TrashMount *mount; + + g_assert (!watcher->watching); + + if (watcher->homedir_type != TRASH_WATCHER_NO_WATCH) + trash_dir_watch (watcher->homedir_trashdir); + + for (mount = watcher->mounts; mount; mount = mount->next) + if (mount->type != TRASH_WATCHER_NO_WATCH) + { + trash_dir_watch (mount->dirs[0]); + trash_dir_watch (mount->dirs[1]); + } + + watcher->watching = TRUE; +} + +void +trash_watcher_unwatch (TrashWatcher *watcher) +{ + TrashMount *mount; + + g_assert (watcher->watching); + + if (watcher->homedir_type != TRASH_WATCHER_NO_WATCH) + trash_dir_unwatch (watcher->homedir_trashdir); + + for (mount = watcher->mounts; mount; mount = mount->next) + if (mount->type != TRASH_WATCHER_NO_WATCH) + { + trash_dir_unwatch (mount->dirs[0]); + trash_dir_unwatch (mount->dirs[1]); + } + + watcher->watching = FALSE; +} + +void +trash_watcher_rescan (TrashWatcher *watcher) +{ + TrashMount *mount; + + if (!watcher->watching || watcher->homedir_type != TRASH_WATCHER_TRUSTED) + trash_dir_rescan (watcher->homedir_trashdir); + + for (mount = watcher->mounts; mount; mount = mount->next) + if (!watcher->watching || mount->type != TRASH_WATCHER_TRUSTED) + { + trash_dir_rescan (mount->dirs[0]); + trash_dir_rescan (mount->dirs[1]); + } +} diff --git a/daemon/trashlib/trashwatcher.h b/daemon/trashlib/trashwatcher.h new file mode 100644 index 00000000..fbe31200 --- /dev/null +++ b/daemon/trashlib/trashwatcher.h @@ -0,0 +1,23 @@ +/* + * Copyright © 2008 Ryan Lortie + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of version 3 of the GNU General Public License as + * published by the Free Software Foundation. + */ + +#ifndef _trashwatcher_h_ +#define _trashwatcher_h_ + +#include "trashitem.h" + +typedef struct OPAQUE_TYPE__TrashWatcher TrashWatcher; + +TrashWatcher *trash_watcher_new (TrashRoot *root); +void trash_watcher_free (TrashWatcher *watcher); + +void trash_watcher_watch (TrashWatcher *watcher); +void trash_watcher_unwatch (TrashWatcher *watcher); +void trash_watcher_rescan (TrashWatcher *watcher); + +#endif /* _trashitem_h_ */ |