[gvfs/ftp-reorg: 6/8] [FTP] rework cache handling



commit 19e86ee8325d6e60e344aa2999ce868df0a8c8fc
Author: Benjamin Otte <otte gnome org>
Date:   Thu Jun 4 18:41:35 2009 +0200

    [FTP] rework cache handling
    
    The cache handling has been split into a separate file and structure
    now. While the API still isn't perfect, it's much clearer than before.
    It's also faster when looking up lots of symlinks.
---
 daemon/Makefile.am         |    1 +
 daemon/gvfsbackendftp.c    |  771 ++++++++-----------------------------------
 daemon/gvfsbackendftp.h    |   13 +-
 daemon/gvfsftpconnection.c |   19 ++
 daemon/gvfsftpconnection.h |    1 +
 daemon/gvfsftpdircache.c   |  596 ++++++++++++++++++++++++++++++++++
 daemon/gvfsftpdircache.h   |   76 +++++
 daemon/gvfsftpfile.c       |   24 ++-
 daemon/gvfsftpfile.h       |    1 +
 9 files changed, 864 insertions(+), 638 deletions(-)

diff --git a/daemon/Makefile.am b/daemon/Makefile.am
index c9e17ae..9761eff 100644
--- a/daemon/Makefile.am
+++ b/daemon/Makefile.am
@@ -231,6 +231,7 @@ gvfsd_obexftp_LDADD = $(OBEXFTP_LIBS) $(XML_LIBS) $(HAL_LIBS) $(libraries)
 
 gvfsd_ftp_SOURCES = \
 	gvfsftpconnection.c gvfsftpconnection.h \
+	gvfsftpdircache.c gvfsftpdircache.h \
 	gvfsftpfile.c gvfsftpfile.h \
 	gvfsftptask.c gvfsftptask.h \
 	gvfsbackendftp.c gvfsbackendftp.h \
diff --git a/daemon/gvfsbackendftp.c b/daemon/gvfsbackendftp.c
index 03834e4..847266e 100644
--- a/daemon/gvfsbackendftp.c
+++ b/daemon/gvfsbackendftp.c
@@ -50,6 +50,7 @@
 
 #include "ParseFTPList.h"
 #include "gvfsftpconnection.h"
+#include "gvfsftpdircache.h"
 #include "gvfsftpfile.h"
 #include "gvfsftptask.h"
 
@@ -67,30 +68,8 @@
  * paths exactly match those of a TVFS-using FTP server.
  */
 
-typedef struct _FtpDirEntry FtpDirEntry;
-struct _FtpDirEntry {
-  gsize         size;
-  gsize         length;
-  gchar         data[1];
-};
-
-struct FtpDirReader {
-  void		(* init_data)	(GVfsFtpTask *      task,
-				 const GVfsFtpFile *dir);
-  gpointer	(* iter_new)	(GVfsFtpTask *      task);
-  GFileInfo *	(* iter_process)(gpointer           iter,
-				 GVfsFtpTask *      task,
-				 const GVfsFtpFile *dirname,
-				 const GVfsFtpFile *must_match_file,
-				 const char *       line,
-				 char **            symlink);
-  void		(* iter_free)	(gpointer	    iter);
-};
-
 G_DEFINE_TYPE (GVfsBackendFtp, g_vfs_backend_ftp, G_VFS_TYPE_BACKEND)
 
-/*** CODE ***/
-
 static gboolean
 gvfs_backend_ftp_determine_features (GVfsFtpTask *task)
 {
@@ -180,6 +159,44 @@ gvfs_backend_ftp_determine_system (GVfsFtpTask *task)
   g_strfreev (reply);
 }
 
+static GFileInfo *
+g_vfs_bacend_ftp_create_root_file_info (GVfsBackendFtp *ftp)
+{
+  GFileInfo *info;
+  GIcon *icon;
+  char *display_name;
+  
+  info = g_file_info_new ();
+  g_file_info_set_file_type (info, G_FILE_TYPE_DIRECTORY);
+
+  g_file_info_set_name (info, "/");
+  display_name = g_strdup_printf (_("/ on %s"), ftp->host_display_name);
+  g_file_info_set_display_name (info, display_name);
+  g_free (display_name);
+  g_file_info_set_edit_name (info, "/");
+
+  g_file_info_set_content_type (info, "inode/directory");
+  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, "inode/directory");
+
+  icon = g_themed_icon_new ("folder-remote");
+  g_file_info_set_icon (info, icon);
+  g_object_unref (icon);
+
+  return info;
+}
+
+static void
+gvfs_backend_ftp_setup_directory_cache (GVfsBackendFtp *ftp)
+{
+  if (ftp->system == G_VFS_FTP_SYSTEM_UNIX)
+    ftp->dir_funcs = &g_vfs_ftp_dir_cache_funcs_unix;
+  else
+    ftp->dir_funcs = &g_vfs_ftp_dir_cache_funcs_default;
+
+  ftp->dir_cache = g_vfs_ftp_dir_cache_new (ftp->dir_funcs,
+                                            g_vfs_bacend_ftp_create_root_file_info (ftp));
+}
+
 /*** COMMON FUNCTIONS WITH SPECIAL HANDLING ***/
 
 static gboolean
@@ -217,169 +234,6 @@ g_vfs_ftp_task_try_cd (GVfsFtpTask *task, const GVfsFtpFile *file)
 /*** default directory reading ***/
 
 static void
-dir_default_init_data (GVfsFtpTask *task, const GVfsFtpFile *dir)
-{
-  g_vfs_ftp_task_cd (task, dir);
-  g_vfs_ftp_task_open_data_connection (task);
-
-  g_vfs_ftp_task_send (task,
-		       G_VFS_FTP_PASS_100 | G_VFS_FTP_FAIL_200,
-		       (task->backend->system == G_VFS_FTP_SYSTEM_UNIX) ? "LIST -a" : "LIST");
-}
-
-static gpointer
-dir_default_iter_new (GVfsFtpTask *task)
-{
-  return g_slice_new0 (struct list_state);
-}
-
-static GFileInfo *
-dir_default_iter_process (gpointer           iter,
-                          GVfsFtpTask *      task,
-			  const GVfsFtpFile *dir,
-			  const GVfsFtpFile *must_match_file,
-			  const char        *line,
-			  char		   **symlink)
-{
-  struct list_state *state = iter;
-  struct list_result result = { 0, };
-  GTimeVal tv = { 0, 0 };
-  GFileInfo *info;
-  int type;
-  GVfsFtpFile *name;
-  const char *s;
-  char *t;
-
-  type = ParseFTPList (line, state, &result);
-  if (type != 'd' && type != 'f' && type != 'l')
-    return NULL;
-
-  /* don't list . and .. directories
-   * Let's hope they're not important files on some ftp servers
-   */
-  if (type == 'd')
-    {
-      if (result.fe_fnlen == 1 && 
-	  result.fe_fname[0] == '.')
-	return NULL;
-      if (result.fe_fnlen == 2 && 
-	  result.fe_fname[0] == '.' &&
-	  result.fe_fname[1] == '.')
-	return NULL;
-    }
-
-  t = g_strndup (result.fe_fname, result.fe_fnlen);
-  if (dir)
-    {
-      name = g_vfs_ftp_file_new_child  (dir, t, NULL);
-      g_free (t);
-    }
-  else
-    {
-      name = g_vfs_ftp_file_new_from_ftp (task->backend, t);
-      g_free (t);
-    }
-  if (name == NULL)
-    return NULL;
-
-  if (must_match_file && !g_vfs_ftp_file_equal (name, must_match_file))
-    {
-      g_vfs_ftp_file_free (name);
-      return NULL;
-    }
-
-  info = g_file_info_new ();
-
-  s = g_vfs_ftp_file_get_gvfs_path (name);
-
-  t = g_path_get_basename (s);
-  g_file_info_set_name (info, t);
-  g_free (t);
-
-  if (type == 'l')
-    {
-      char *link;
-
-      link = g_strndup (result.fe_lname, result.fe_lnlen);
-
-      /* FIXME: this whole stuff is not GVfsFtpFile save */
-      g_file_info_set_symlink_target (info, link);
-      g_file_info_set_is_symlink (info, TRUE);
-
-      if (symlink)
-	{
-	  char *str = g_path_get_dirname (s);
-	  char *symlink_file = g_build_path ("/", str, link, NULL);
-
-	  g_free (str);
-	  while ((str = strstr (symlink_file, "/../")))
-	    {
-	      char *end = str + 4;
-	      char *start;
-	      start = str - 1;
-	      while (start >= symlink_file && *start != '/')
-		start--;
-
-	      if (start < symlink_file) {
-		      *symlink_file = '/';
-		      start = symlink_file;
-	      }
-
-	      memmove (start + 1, end, strlen (end) + 1);
-	    }
-	  str = symlink_file + strlen (symlink_file) - 1;
-	  while (*str == '/' && str > symlink_file)
-	    *str-- = 0;
-	  *symlink = symlink_file;
-	}
-      g_free (link);
-    }
-  else if (symlink)
-    *symlink = NULL;
-
-  g_file_info_set_size (info, g_ascii_strtoull (result.fe_size, NULL, 10));
-
-  gvfs_file_info_populate_default (info, s,
-				   type == 'f' ? G_FILE_TYPE_REGULAR :
-				   type == 'l' ? G_FILE_TYPE_SYMBOLIC_LINK :
-				   G_FILE_TYPE_DIRECTORY);
-
-  if (task->backend->system == G_VFS_FTP_SYSTEM_UNIX)
-    g_file_info_set_is_hidden (info, result.fe_fnlen > 0 &&
-	                             result.fe_fname[0] == '.');
-
-  g_vfs_ftp_file_free (name);
-
-  /* Workaround:
-   * result.fetime.tm_year contains actual year instead of offset-from-1900,
-   * which mktime expects.
-   */
-  if (result.fe_time.tm_year >= 1900)
-	  result.fe_time.tm_year -= 1900;
-
-  tv.tv_sec = mktime (&result.fe_time);
-  if (tv.tv_sec != -1)
-    g_file_info_set_modification_time (info, &tv);
-
-  return info;
-}
-
-static void
-dir_default_iter_free (gpointer iter)
-{
-  g_slice_free (struct list_state, iter);
-}
-
-static const FtpDirReader dir_default = {
-  dir_default_init_data,
-  dir_default_iter_new,
-  dir_default_iter_process,
-  dir_default_iter_free
-};
-
-/*** BACKEND ***/
-
-static void
 g_vfs_backend_ftp_finalize (GObject *object)
 {
   GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (object);
@@ -392,9 +246,6 @@ g_vfs_backend_ftp_finalize (GObject *object)
   g_cond_free (ftp->cond);
   g_mutex_free (ftp->mutex);
 
-  g_hash_table_destroy (ftp->directory_cache);
-  g_static_rw_lock_free (&ftp->directory_cache_lock);
-
   g_free (ftp->user);
   g_free (ftp->password);
 
@@ -403,46 +254,10 @@ g_vfs_backend_ftp_finalize (GObject *object)
 }
 
 static void
-ftp_dir_entry_free (gpointer entry)
-{
-  g_free (entry);
-}
-
-static FtpDirEntry *
-ftp_dir_entry_grow (FtpDirEntry *entry)
-{
-  entry = g_try_realloc (entry, sizeof (FtpDirEntry) + entry->size + 4096);
-  if (entry == NULL)
-    return NULL;
-  entry->size += 4096;
-  return entry;
-}
-
-static FtpDirEntry *
-ftp_dir_entry_new (void)
-{
-  FtpDirEntry *entry;
-  
-  entry = g_malloc (4096);
-  entry->size = 4096 - sizeof (FtpDirEntry);
-  entry->length = 0;
-
-  return entry;
-}
-
-static void
 g_vfs_backend_ftp_init (GVfsBackendFtp *ftp)
 {
   ftp->mutex = g_mutex_new ();
   ftp->cond = g_cond_new ();
-
-  ftp->directory_cache = g_hash_table_new_full (g_vfs_ftp_file_hash,
-					        g_vfs_ftp_file_equal,
-						g_vfs_ftp_file_free,
-						ftp_dir_entry_free);
-  g_static_rw_lock_init (&ftp->directory_cache_lock);
-
-  ftp->dir_ops = &dir_default;
 }
 
 static void
@@ -600,6 +415,7 @@ try_login:
     }
   g_vfs_ftp_task_setup_connection (&task);
   gvfs_backend_ftp_determine_system (&task);
+  gvfs_backend_ftp_setup_directory_cache (ftp);
 
   /* Save the address of the current connection, so that for future connections,
    * we are sure to connect to the same machine.
@@ -903,30 +719,74 @@ do_start_write (GVfsFtpTask *task,
     }
 }
 
-static void
-gvfs_backend_ftp_purge_cache_directory (GVfsBackendFtp *   ftp,
-					const GVfsFtpFile *dir)
+/* NB: This gets a file info for the given object, no matter if it's a dir 
+ * or a file */
+static GFileInfo *
+create_file_info (GVfsFtpTask *task, GVfsFtpFile *file, gboolean resolve_symlinks)
 {
-  g_static_rw_lock_writer_lock (&ftp->directory_cache_lock);
-  g_hash_table_remove (ftp->directory_cache, dir);
-  g_static_rw_lock_writer_unlock (&ftp->directory_cache_lock);
-}
+  GFileInfo *info;
+  char **reply;
 
-static void
-gvfs_backend_ftp_purge_cache_of_file (GVfsBackendFtp *   ftp,
-				      const GVfsFtpFile *file)
-{
-  GVfsFtpFile *dir = g_vfs_ftp_file_new_parent (file);
+  if (g_vfs_ftp_task_is_in_error (task))
+    return NULL;
 
-  if (!g_vfs_ftp_file_equal (file, dir))
-    gvfs_backend_ftp_purge_cache_directory (ftp, dir);
+  info = g_vfs_ftp_dir_cache_lookup_file (task->backend->dir_cache, task, file, resolve_symlinks);
+  if (info)
+    return info;
 
-  g_vfs_ftp_file_free (dir);
+  g_vfs_ftp_task_clear_error (task);
+
+  /* the directory cache fails when the parent directory of the file is not readable.
+   * This cannot happen on Unix, but it can happen on FTP.
+   * In this case we try to figure out as much as possible about the file (does it even exist?)
+   * using standard ftp commands.
+   */
+  if (g_vfs_ftp_task_try_cd (task, file))
+    {
+      char *tmp;
+
+      info = g_file_info_new ();
+
+      tmp = g_path_get_basename (g_vfs_ftp_file_get_gvfs_path (file));
+      g_file_info_set_name (info, tmp);
+      g_free (tmp);
+
+      gvfs_file_info_populate_default (info, g_vfs_ftp_file_get_gvfs_path (file), G_FILE_TYPE_DIRECTORY);
+
+      g_file_info_set_is_hidden (info, TRUE);
+    }
+  else if (g_vfs_ftp_task_send_and_check (task, 0, NULL, NULL, &reply, "SIZE %s", g_vfs_ftp_file_get_ftp_path (file)))
+    {
+      char *tmp;
+
+      info = g_file_info_new ();
+
+      tmp = g_path_get_basename (g_vfs_ftp_file_get_gvfs_path (file));
+      g_file_info_set_name (info, tmp);
+      g_free (tmp);
+
+      gvfs_file_info_populate_default (info, g_vfs_ftp_file_get_gvfs_path (file), G_FILE_TYPE_REGULAR);
+
+      g_file_info_set_size (info, g_ascii_strtoull (reply[0] + 4, NULL, 0));
+      g_strfreev (reply);
+
+      g_file_info_set_is_hidden (info, TRUE);
+    }
+  else
+    {
+      info = NULL;
+      /* clear error from ftp_connection_send() in else if line above */
+      g_vfs_ftp_task_clear_error (task);
+
+      /* note that there might still be a file/directory, we just have 
+       * no way to figure this out (in particular on ftp servers that 
+       * don't support SIZE.
+       * If you have ways to improve file detection, patches are welcome. */
+    }
+
+  return info;
 }
 
-/* forward declaration */
-static GFileInfo *
-create_file_info (GVfsFtpTask *task, const char *filename, char **symlink);
 
 static void
 do_create (GVfsBackend *backend,
@@ -939,7 +799,8 @@ do_create (GVfsBackend *backend,
   GFileInfo *info;
   GVfsFtpFile *file;
 
-  info = create_file_info (&task, filename, NULL);
+  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
+  info = create_file_info (&task, file, FALSE);
   if (info)
     {
       g_object_unref (info);
@@ -947,12 +808,12 @@ do_create (GVfsBackend *backend,
 		           G_IO_ERROR,
 			   G_IO_ERROR_EXISTS,
 			   _("Target file already exists"));
+      g_vfs_ftp_file_free (file);
       g_vfs_ftp_task_done (&task);
       return;
     }
-  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
   do_start_write (&task, flags, "STOR %s", g_vfs_ftp_file_get_ftp_path (file));
-  gvfs_backend_ftp_purge_cache_of_file (ftp, file);
+  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
   g_vfs_ftp_file_free (file);
 
   g_vfs_ftp_task_done (&task);
@@ -970,7 +831,7 @@ do_append (GVfsBackend *backend,
 
   file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
   do_start_write (&task, flags, "APPE %s", g_vfs_ftp_file_get_ftp_path (file));
-  gvfs_backend_ftp_purge_cache_of_file (ftp, file);
+  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
   g_vfs_ftp_file_free (file);
 
   g_vfs_ftp_task_done (&task);
@@ -1001,7 +862,7 @@ do_replace (GVfsBackend *backend,
 
   file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
   do_start_write (&task, flags, "STOR %s", g_vfs_ftp_file_get_ftp_path (file));
-  gvfs_backend_ftp_purge_cache_of_file (ftp, file);
+  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
   g_vfs_ftp_file_free (file);
 
   g_vfs_ftp_task_done (&task);
@@ -1048,301 +909,6 @@ do_write (GVfsBackend *backend,
   g_vfs_ftp_task_done (&task);
 }
 
-static GFileInfo *
-create_file_info_for_root (GVfsBackendFtp *ftp)
-{
-  GFileInfo *info;
-  GIcon *icon;
-  char *display_name;
-  
-  info = g_file_info_new ();
-  g_file_info_set_file_type (info, G_FILE_TYPE_DIRECTORY);
-
-  g_file_info_set_name (info, "/");
-  display_name = g_strdup_printf (_("/ on %s"), ftp->host_display_name);
-  g_file_info_set_display_name (info, display_name);
-  g_free (display_name);
-  g_file_info_set_edit_name (info, "/");
-
-  g_file_info_set_content_type (info, "inode/directory");
-  g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, "inode/directory");
-
-  icon = g_themed_icon_new ("folder-remote");
-  g_file_info_set_icon (info, icon);
-  g_object_unref (icon);
-
-  return info;
-}
-
-static FtpDirEntry *
-do_enumerate_directory (GVfsFtpTask *task)
-{
-  gssize n_bytes;
-  FtpDirEntry *entry;
-
-  if (g_vfs_ftp_task_is_in_error (task))
-    return NULL;
-
-  entry = ftp_dir_entry_new ();
-
-  do
-    {
-      if (entry->size - entry->length < 128)
-        {
-          entry = ftp_dir_entry_grow (entry);
-          if (entry == NULL)
-	    {
-	      g_set_error_literal (&task->error, G_IO_ERROR, G_IO_ERROR_FAILED,
-			           _("Out of memory while reading directory contents"));
-	      return NULL;
-	    }
-	}
-      n_bytes = g_vfs_ftp_connection_read_data (task->conn,
-                                                entry->data + entry->length,
-                                                entry->size - entry->length - 1,
-                                                task->cancellable,
-                                                &task->error);
-
-      if (n_bytes < 0)
-        {
-          ftp_dir_entry_free (entry);
-          return NULL;
-        }
-
-      entry->length += n_bytes;
-    }
-  while (n_bytes > 0);
-
-  g_vfs_ftp_task_close_data_connection (task);
-  g_vfs_ftp_task_receive (task, 0, NULL);
-  if (g_vfs_ftp_task_is_in_error (task))
-    {
-      ftp_dir_entry_free (entry);
-      return NULL;
-    }
-  /* null-terminate, just because */
-  entry->data[entry->length] = 0;
-
-  return entry;
-}
-
-/* IMPORTANT: SUCK ALARM!
- * locks ftp->directory_cache_lock but only iff it returns !NULL */
-static const FtpDirEntry *
-enumerate_directory (GVfsFtpTask *      task,
-		     const GVfsFtpFile *dir,
-		     gboolean	        use_cache)
-{
-  GVfsBackendFtp *ftp = task->backend;
-  FtpDirEntry *entry;
-
-  g_static_rw_lock_reader_lock (&ftp->directory_cache_lock);
-  do {
-    if (use_cache)
-      entry = g_hash_table_lookup (ftp->directory_cache, dir);
-    else
-      {
-	use_cache = TRUE;
-	entry = NULL;
-      }
-    if (entry == NULL)
-      {
-	g_static_rw_lock_reader_unlock (&ftp->directory_cache_lock);
-	ftp->dir_ops->init_data (task, dir);
-	entry = do_enumerate_directory (task);
-	if (entry == NULL)
-          return NULL;
-	g_static_rw_lock_writer_lock (&ftp->directory_cache_lock);
-	g_hash_table_insert (ftp->directory_cache, g_vfs_ftp_file_copy (dir), entry);
-	g_static_rw_lock_writer_unlock (&ftp->directory_cache_lock);
-	entry = NULL;
-	g_static_rw_lock_reader_lock (&ftp->directory_cache_lock);
-      }
-  } while (entry == NULL);
-
-  return entry;
-}
-
-static GFileInfo *
-create_file_info_from_parent (GVfsFtpTask *      task, 
-                              const GVfsFtpFile *dir,
-                              const GVfsFtpFile *file,
-                              char **            symlink)
-{
-  GFileInfo *info = NULL;
-  gpointer iter;
-  const FtpDirEntry *entry;
-  const char *sol, *eol;
-
-  entry = enumerate_directory (task, dir, TRUE);
-  if (entry == NULL)
-    return NULL;
-
-  iter = task->backend->dir_ops->iter_new (task);
-  for (sol = eol = entry->data; eol; sol = eol + 1)
-    {
-      eol = memchr (sol, '\n', entry->length - (sol - entry->data));
-      info = task->backend->dir_ops->iter_process (iter,
-                                                   task,
-                                                   dir,
-                                                   file,
-                                                   sol,
-                                                   symlink);
-      if (info)
-        break;
-    }
-  task->backend->dir_ops->iter_free (iter);
-  g_static_rw_lock_reader_unlock (&task->backend->directory_cache_lock);
-
-  return info;
-}
-
-static GFileInfo *
-create_file_info_from_file (GVfsFtpTask *task, const GVfsFtpFile *file, 
-    const char *filename, char **symlink)
-{
-  GFileInfo *info;
-  char **reply;
-
-  if (g_vfs_ftp_task_try_cd (task, file))
-    {
-      char *tmp;
-
-      info = g_file_info_new ();
-
-      tmp = g_path_get_basename (filename);
-      g_file_info_set_name (info, tmp);
-      g_free (tmp);
-
-      gvfs_file_info_populate_default (info, filename, G_FILE_TYPE_DIRECTORY);
-
-      g_file_info_set_is_hidden (info, TRUE);
-    }
-  else if (g_vfs_ftp_task_send_and_check (task, 0, NULL, NULL, &reply, "SIZE %s", g_vfs_ftp_file_get_ftp_path (file)))
-    {
-      char *tmp;
-
-      info = g_file_info_new ();
-
-      tmp = g_path_get_basename (filename);
-      g_file_info_set_name (info, tmp);
-      g_free (tmp);
-
-      gvfs_file_info_populate_default (info, filename, G_FILE_TYPE_REGULAR);
-
-      g_file_info_set_size (info, g_ascii_strtoull (reply[0] + 4, NULL, 0));
-      g_strfreev (reply);
-
-      g_file_info_set_is_hidden (info, TRUE);
-    }
-  else
-    {
-      info = NULL;
-      /* clear error from ftp_connection_send() in else if line above */
-      g_vfs_ftp_task_clear_error (task);
-
-      /* note that there might still be a file/directory, we just have 
-       * no way to figure this out (in particular on ftp servers that 
-       * don't support SIZE.
-       * If you have ways to improve file detection, patches are welcome. */
-    }
-
-  return info;
-}
-
-/* NB: This gets a file info for the given object, no matter if it's a dir 
- * or a file */
-static GFileInfo *
-create_file_info (GVfsFtpTask *task, const char *filename, char **symlink)
-{
-  GVfsFtpFile *dir, *file;
-  GFileInfo *info;
-
-  if (symlink)
-    *symlink = NULL;
-
-  if (g_str_equal (filename, "/"))
-    return create_file_info_for_root (task->backend);
-
-  file = g_vfs_ftp_file_new_from_gvfs (task->backend, filename);
-  dir = g_vfs_ftp_file_new_parent (file);
-
-  info = create_file_info_from_parent (task, dir, file, symlink);
-  if (info == NULL)
-    info = create_file_info_from_file (task, file, filename, symlink);
-
-  g_vfs_ftp_file_free (dir);
-  g_vfs_ftp_file_free (file);
-  return info;
-}
-
-static GFileInfo *
-resolve_symlink (GVfsFtpTask *task, GFileInfo *original, const char *filename)
-{
-  GFileInfo *info = NULL;
-  char *symlink, *newlink;
-  guint i;
-  static const char *copy_attributes[] = {
-    G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK,
-    G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
-    G_FILE_ATTRIBUTE_STANDARD_NAME,
-    G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
-    G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME,
-    G_FILE_ATTRIBUTE_STANDARD_COPY_NAME,
-    G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET
-  };
-
-  if (g_vfs_ftp_task_is_in_error (task))
-    return original;
-
-  /* How many symlinks should we follow?
-   * <alex> maybe 8?
-   */
-  symlink = g_strdup (filename);
-  for (i = 0; i < 8 && symlink; i++)
-    {
-      info = create_file_info (task,
-			       symlink,
-			       &newlink);
-      if (!newlink)
-	break;
-
-      g_free (symlink);
-      symlink = newlink;
-    }
-  g_free (symlink);
-
-  if (g_vfs_ftp_task_is_in_error (task))
-    {
-      g_assert (info == NULL);
-      g_vfs_ftp_task_clear_error (task);
-      return original;
-    }
-  if (info == NULL)
-    return original;
-
-  for (i = 0; i < G_N_ELEMENTS (copy_attributes); i++)
-    {
-      GFileAttributeType type;
-      gpointer value;
-
-      if (!g_file_info_get_attribute_data (original,
-					   copy_attributes[i],
-					   &type,
-					   &value,
-					   NULL))
-	continue;
-      
-      g_file_info_set_attribute (info,
-	                         copy_attributes[i],
-				 type,
-				 value);
-    }
-  g_object_unref (original);
-
-  return info;
-}
-
 static void
 do_query_info (GVfsBackend *backend,
 	       GVfsJobQueryInfo *job,
@@ -1353,26 +919,11 @@ do_query_info (GVfsBackend *backend,
 {
   GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
   GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
+  GVfsFtpFile *file;
   GFileInfo *real;
-  char *symlink;
-
-  if (query_flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
-    {
-      real = create_file_info (&task,
-			       filename,
-			       NULL);
-    }
-  else
-    {
-      real = create_file_info (&task,
-			       filename,
-			       &symlink);
-      if (symlink)
-	{
-	  real = resolve_symlink (&task, real, symlink);
-	  g_free (symlink);
-	}
-    }
+  
+  file = g_vfs_ftp_file_new_from_gvfs (ftp, filename);
+  real = create_file_info (&task, file, query_flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS ? FALSE : TRUE);
 
   if (real)
     {
@@ -1400,68 +951,29 @@ do_enumerate (GVfsBackend *backend,
   GVfsBackendFtp *ftp = G_VFS_BACKEND_FTP (backend);
   GVfsFtpTask task = G_VFS_FTP_TASK_INIT (ftp, G_VFS_JOB (job));
   GVfsFtpFile *dir;
-  gpointer iter;
-  GSList *symlink_targets = NULL;
-  GSList *symlink_fileinfos = NULL;
-  GSList *twalk, *fwalk;
-  GFileInfo *info;
-  const FtpDirEntry *entry;
-  const char *sol, *eol;
-
-  /* no need to check for IS_DIR, because the enumeration code will return that
-   * automatically.
-   */
+  GList *list;
 
   dir = g_vfs_ftp_file_new_from_gvfs (ftp, dirname);
-  entry = enumerate_directory (&task, dir, FALSE);
-  if (entry != NULL)
+  list = g_vfs_ftp_dir_cache_lookup_dir (ftp->dir_cache,
+                                         &task,
+                                         dir,
+                                         TRUE,
+                                         query_flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS ? FALSE : TRUE);
+  if (g_vfs_ftp_task_is_in_error (&task))
     {
-      g_vfs_job_succeeded (task.job);
-      task.job = NULL;
+      g_assert (list == NULL);
+      g_vfs_ftp_task_done (&task);
+      return;
+    }
 
-      iter = ftp->dir_ops->iter_new (&task);
-      for (sol = eol = entry->data; eol; sol = eol + 1)
-        {
-          char *symlink = NULL;
-
-          eol = memchr (sol, '\n', entry->length - (sol - entry->data));
-          info = ftp->dir_ops->iter_process (iter,
-                                             &task,
-                                             dir,
-                                             NULL,
-                                             sol,
-                                             query_flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS ? NULL : &symlink);
-          if (symlink)
-            {
-              /* This is necessary due to our locking. 
-               * And we must not unlock here because it might invalidate the list we iterate */
-              symlink_targets = g_slist_prepend (symlink_targets, symlink);
-              symlink_fileinfos = g_slist_prepend (symlink_fileinfos, info);
-            }
-          else if (info)
-            {
-              g_vfs_job_enumerate_add_info (job, info);
-              g_object_unref (info);
-            }
-        }
-      ftp->dir_ops->iter_free (iter);
-      g_static_rw_lock_reader_unlock (&ftp->directory_cache_lock);
-      for (twalk = symlink_targets, fwalk = symlink_fileinfos; twalk; 
-           twalk = twalk->next, fwalk = fwalk->next)
-        {
-          info = resolve_symlink (&task, fwalk->data, twalk->data);
-          g_free (twalk->data);
-          g_vfs_job_enumerate_add_info (job, info);
-          g_object_unref (info);
-        }
-      g_slist_free (symlink_targets);
-      g_slist_free (symlink_fileinfos);
+  g_vfs_ftp_task_done (&task);
 
-      g_vfs_job_enumerate_done (job);
-    }
-  
+  g_vfs_job_enumerate_add_infos (job, list);
+  g_vfs_job_enumerate_done (job);
+
+  g_list_foreach (list, (GFunc) g_object_unref, NULL);
+  g_list_free (list);
   g_vfs_ftp_file_free (dir);
-  g_vfs_ftp_task_done (&task);
 }
 
 static void
@@ -1486,7 +998,7 @@ do_set_display_name (GVfsBackend *backend,
 
   /* FIXME: parse result of RNTO here? */
   g_vfs_job_set_display_name_set_new_path (job, g_vfs_ftp_file_get_gvfs_path (now));
-  gvfs_backend_ftp_purge_cache_directory (ftp, dir);
+  g_vfs_ftp_dir_cache_purge_dir (ftp->dir_cache, dir);
   g_vfs_ftp_file_free (now);
   g_vfs_ftp_file_free (dir);
   g_vfs_ftp_file_free (original);
@@ -1517,14 +1029,19 @@ do_delete (GVfsBackend *backend,
 				      "RMD %s", g_vfs_ftp_file_get_ftp_path (file));
       if (response == 550)
 	{
-	  const FtpDirEntry *entry = enumerate_directory (&task, file, FALSE);
-	  if (entry)
+	  GList *list = g_vfs_ftp_dir_cache_lookup_dir (ftp->dir_cache,
+                                                        &task,
+                                                        file,
+                                                        FALSE,
+                                                        FALSE);
+	  if (list)
 	    {
-	      g_static_rw_lock_reader_unlock (&ftp->directory_cache_lock);
 	      g_set_error_literal (&task.error, 
 				   G_IO_ERROR,
 				   G_IO_ERROR_NOT_EMPTY,
 				   g_strerror (ENOTEMPTY));
+              g_list_foreach (list, (GFunc) g_object_unref, NULL);
+              g_list_free (list);
 	    }
 	  else
             {
@@ -1534,7 +1051,7 @@ do_delete (GVfsBackend *backend,
 	}
     }
 
-  gvfs_backend_ftp_purge_cache_of_file (ftp, file);
+  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
   g_vfs_ftp_file_free (file);
 
   g_vfs_ftp_task_done (&task);
@@ -1560,7 +1077,7 @@ do_make_directory (GVfsBackend *backend,
 
   /* FIXME: Compare created file with name from server result to be sure 
    * it's correct and otherwise fail. */
-  gvfs_backend_ftp_purge_cache_of_file (ftp, file);
+  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, file);
   g_vfs_ftp_file_free (file);
 
   g_vfs_ftp_task_done (&task);
@@ -1614,8 +1131,8 @@ do_move (GVfsBackend *backend,
   if (!(flags & G_FILE_COPY_OVERWRITE))
     {
       GFileInfo *info = create_file_info (&task,
-                                          g_vfs_ftp_file_get_gvfs_path (destfile),
-                                          NULL);
+                                          destfile,
+                                          FALSE);
 
       if (info)
 	{
@@ -1635,8 +1152,8 @@ do_move (GVfsBackend *backend,
 		       0,
 		       "RNTO %s", g_vfs_ftp_file_get_ftp_path (destfile));
 
-  gvfs_backend_ftp_purge_cache_of_file (ftp, srcfile);
-  gvfs_backend_ftp_purge_cache_of_file (ftp, destfile);
+  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, srcfile);
+  g_vfs_ftp_dir_cache_purge_file (ftp->dir_cache, destfile);
 out:
   g_vfs_ftp_file_free (srcfile);
   g_vfs_ftp_file_free (destfile);
diff --git a/daemon/gvfsbackendftp.h b/daemon/gvfsbackendftp.h
index e1df8b0..c29b53f 100644
--- a/daemon/gvfsbackendftp.h
+++ b/daemon/gvfsbackendftp.h
@@ -58,7 +58,9 @@ typedef enum {
   G_VFS_FTP_WORKAROUND_FEAT_AFTER_LOGIN,
 } GVfsFtpWorkaround;
 
-typedef struct FtpDirReader FtpDirReader;
+/* forward declarations */
+typedef struct _GVfsFtpDirCache GVfsFtpDirCache;
+typedef struct _GVfsFtpDirFuncs GVfsFtpDirFuncs;
 
 #define G_VFS_TYPE_BACKEND_FTP         (g_vfs_backend_ftp_get_type ())
 #define G_VFS_BACKEND_FTP(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), G_VFS_TYPE_BACKEND_FTP, GVfsBackendFtp))
@@ -85,8 +87,9 @@ struct _GVfsBackendFtp
   int                   features;               /* GVfsFtpFeatures that are supported */
   int                   workarounds;            /* GVfsFtpWorkarounds in use - int because it's atomic */
 
-  /* vfuncs */
-  const FtpDirReader *	dir_ops;
+  /* directory cache */
+  const GVfsFtpDirFuncs *dir_funcs;             /* functions used in directory cache */
+  GVfsFtpDirCache *     dir_cache;              /* directory cache */
 
   /* connection collection - accessed from gvfsftptask.c */
   GMutex *		mutex;                  /* mutex protecting the following variables */
@@ -94,10 +97,6 @@ struct _GVfsBackendFtp
   GQueue *		queue;                  /* queue containing the connections */
   guint			connections;            /* current number of connections */
   guint			max_connections;        /* upper server limit for number of connections - dynamically generated */
-
-  /* caching results from dir queries */
-  GStaticRWLock		directory_cache_lock;
-  GHashTable *		directory_cache;
 };
 
 struct _GVfsBackendFtpClass
diff --git a/daemon/gvfsftpconnection.c b/daemon/gvfsftpconnection.c
index 485ad68..d6b1274 100644
--- a/daemon/gvfsftpconnection.c
+++ b/daemon/gvfsftpconnection.c
@@ -227,6 +227,25 @@ g_vfs_ftp_connection_close_data_connection (GVfsFtpConnection *conn)
   conn->data = NULL;
 }
 
+/**
+ * g_vfs_ftp_connection_get_data_stream:
+ * @conn: a connection
+ *
+ * Gets the data stream in use by @conn. It is an error to call this function
+ * when no data stream exists. Be sure to check the return value of
+ * g_vfs_ftp_connection_open_data_connection().
+ *
+ * Returns: the data stream of @conn
+ **/
+GIOStream *
+g_vfs_ftp_connection_get_data_stream (GVfsFtpConnection *conn)
+{
+  g_return_val_if_fail (conn != NULL, NULL);
+  g_return_val_if_fail (conn->data != NULL, NULL);
+
+  return conn->data;
+}
+
 gssize
 g_vfs_ftp_connection_write_data (GVfsFtpConnection *conn,
                                  const char *       data,
diff --git a/daemon/gvfsftpconnection.h b/daemon/gvfsftpconnection.h
index 234f4e4..4fbc91b 100644
--- a/daemon/gvfsftpconnection.h
+++ b/daemon/gvfsftpconnection.h
@@ -64,6 +64,7 @@ gboolean                g_vfs_ftp_connection_open_data_connection
                                                                GError **                error);
 void                    g_vfs_ftp_connection_close_data_connection
                                                               (GVfsFtpConnection *      conn);
+GIOStream *             g_vfs_ftp_connection_get_data_stream  (GVfsFtpConnection *      conn);
 gssize                  g_vfs_ftp_connection_write_data       (GVfsFtpConnection *      conn,
                                                                const char *             data,
                                                                gsize                    len,
diff --git a/daemon/gvfsftpdircache.c b/daemon/gvfsftpdircache.c
new file mode 100644
index 0000000..d508278
--- /dev/null
+++ b/daemon/gvfsftpdircache.c
@@ -0,0 +1,596 @@
+/* GIO - GLib Input, Output and Streaming Library
+ * 
+ * Copyright (C) 2009 Benjamin Otte <otte gnome org>
+ *
+ * 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: Benjamin Otte <otte gnome org>
+ */
+
+#include <config.h>
+
+#include <glib/gi18n.h>
+
+#include "gvfsftpdircache.h"
+
+/*** CACHE ENTRY ***/
+
+struct _GVfsFtpDirCacheEntry
+{
+  GHashTable *          files;          /* GVfsFtpFile => GFileInfo mapping */
+  guint                 stamp;          /* cache's stamp when this entry was created */
+  volatile int          refcount;       /* need to refount this struct for thread safety */
+};
+
+static GVfsFtpDirCacheEntry *
+g_vfs_ftp_dir_cache_entry_new (guint stamp)
+{
+  GVfsFtpDirCacheEntry *entry;
+
+  entry = g_slice_new0 (GVfsFtpDirCacheEntry);
+  entry->files = g_hash_table_new_full (g_vfs_ftp_file_hash,
+                                        g_vfs_ftp_file_equal,
+                                        (GDestroyNotify) g_vfs_ftp_file_free,
+                                        g_object_unref);
+  entry->stamp = stamp;
+  entry->refcount = 1;
+
+  return entry;
+}
+
+static GVfsFtpDirCacheEntry *
+g_vfs_ftp_dir_cache_entry_ref (GVfsFtpDirCacheEntry *entry)
+{
+  g_atomic_int_inc (&entry->refcount);
+
+  return entry;
+}
+
+static void
+g_vfs_ftp_dir_cache_entry_unref (GVfsFtpDirCacheEntry *entry)
+{
+  if (!g_atomic_int_dec_and_test (&entry->refcount))
+    return;
+
+  g_hash_table_destroy (entry->files);
+  g_slice_free (GVfsFtpDirCacheEntry, entry);
+}
+
+/**
+ * g_vfs_ftp_dir_cache_entry_add:
+ * @entry: the entry to add data to
+ * @file: the file to add. The function takes ownership of the argument.
+ * @info: the file info of the @file. The function takes ownership of the 
+ *        reference.
+ *
+ * Adds a new file entry to the directory belonging to @entry. This function 
+ * must only be called from a @GVfsFtpListDirFunc.
+ **/
+void
+g_vfs_ftp_dir_cache_entry_add (GVfsFtpDirCacheEntry *entry, GVfsFtpFile *file, GFileInfo *info)
+{
+  g_return_if_fail (entry != NULL);
+  g_return_if_fail (file != NULL);
+  g_return_if_fail (G_IS_FILE_INFO (info));
+
+  g_hash_table_insert (entry->files, file, info);
+}
+
+/*** CACHE ***/
+
+struct _GVfsFtpDirCache
+{
+  GHashTable *          directories;    /* GVfsFtpFile of directory => GVfsFtpDirCacheEntry mapping */
+  guint                 stamp;          /* used to identify validity of cache when flushing */
+  GMutex *              lock;           /* mutex for thread safety of stamp and hash table */
+  GFileInfo *           root;           /* file info for '/' */
+  const GVfsFtpDirFuncs *funcs;         /* functions to call */
+};
+
+GVfsFtpDirCache *
+g_vfs_ftp_dir_cache_new (const GVfsFtpDirFuncs *funcs, GFileInfo *root)
+{
+  GVfsFtpDirCache *cache;
+
+  g_return_val_if_fail (funcs != NULL, NULL);
+  g_return_val_if_fail (G_IS_FILE_INFO (root), NULL);
+
+  cache = g_slice_new0 (GVfsFtpDirCache);
+  cache->directories = g_hash_table_new_full (g_vfs_ftp_file_hash,
+                                              g_vfs_ftp_file_equal,
+                                              (GDestroyNotify) g_vfs_ftp_file_free,
+                                              (GDestroyNotify) g_vfs_ftp_dir_cache_entry_unref);
+  cache->lock = g_mutex_new();
+  cache->funcs = funcs;
+  cache->root = root;
+
+  return cache;
+}
+
+void
+g_vfs_ftp_dir_cache_free (GVfsFtpDirCache *cache)
+{
+  g_return_if_fail (cache != NULL);
+
+  g_hash_table_destroy (cache->directories);
+  g_mutex_free (cache->lock);
+  g_object_unref (cache->root);
+  g_slice_free (GVfsFtpDirCache, cache);
+}
+
+static GVfsFtpDirCacheEntry *
+g_vfs_ftp_dir_cache_lookup_entry (GVfsFtpDirCache *  cache,
+                                  GVfsFtpTask *      task,
+                                  const GVfsFtpFile *dir,
+                                  guint              stamp)
+{
+  GVfsFtpDirCacheEntry *entry;
+
+  g_mutex_lock (cache->lock);
+  entry = g_hash_table_lookup (cache->directories, dir);
+  if (entry)
+    g_vfs_ftp_dir_cache_entry_ref (entry);
+  g_mutex_unlock (cache->lock);
+  if (entry && entry->stamp < stamp)
+    g_vfs_ftp_dir_cache_entry_unref (entry);
+  else if (entry)
+    return entry;
+
+  if (g_vfs_ftp_task_send (task,
+		           G_VFS_FTP_PASS_550,
+			   "CWD %s", g_vfs_ftp_file_get_ftp_path (dir)) == 550)
+    {
+      g_set_error_literal (&task->error, 
+		           G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY,
+			   _("The file is not a directory"));
+    }
+  g_vfs_ftp_task_open_data_connection (task);
+
+  g_vfs_ftp_task_send (task,
+		       G_VFS_FTP_PASS_100 | G_VFS_FTP_FAIL_200,
+                       "%s", cache->funcs->command);
+  if (g_vfs_ftp_task_is_in_error (task))
+    return NULL;
+
+  entry = g_vfs_ftp_dir_cache_entry_new (stamp);
+  cache->funcs->process (g_io_stream_get_input_stream (g_vfs_ftp_connection_get_data_stream (task->conn)),
+                         dir,
+                         entry,
+                         task->cancellable,
+                         &task->error);
+  g_vfs_ftp_task_close_data_connection (task);
+  g_vfs_ftp_task_receive (task, 0, NULL);
+  if (g_vfs_ftp_task_is_in_error (task))
+    {
+      g_vfs_ftp_dir_cache_entry_unref (entry);
+      return NULL;
+    }
+  g_mutex_lock (cache->lock);
+  g_hash_table_insert (cache->directories, 
+                       g_vfs_ftp_file_copy (dir), 
+                       g_vfs_ftp_dir_cache_entry_ref (entry));
+  g_mutex_unlock (cache->lock);
+  return entry;
+}
+
+static GFileInfo *
+g_vfs_ftp_dir_cache_resolve_symlink (GVfsFtpDirCache *  cache,
+                                     GVfsFtpTask *      task,
+                                     const GVfsFtpFile *file,
+                                     GFileInfo *        original,
+                                     guint              stamp)
+{
+  GVfsFtpDirCacheEntry *entry;
+  static const char *copy_attributes[] = {
+    G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK,
+    G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
+    G_FILE_ATTRIBUTE_STANDARD_NAME,
+    G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
+    G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME,
+    G_FILE_ATTRIBUTE_STANDARD_COPY_NAME,
+    G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET
+  };
+  GFileInfo *info, *result;
+  GVfsFtpFile *tmp, *link;
+  guint i, lookups = 0;
+
+  if (!g_file_info_get_is_symlink (original) || 
+      g_vfs_ftp_task_is_in_error (task))
+    return original;
+
+  info = g_object_ref (original);
+  link = g_vfs_ftp_file_copy (file);
+  do 
+    {
+      /* This must not happen, as we use one of our own GFileInfos */
+      g_assert (g_file_info_get_symlink_target (info) != NULL);
+      tmp = link;
+      link = cache->funcs->resolve_symlink (task, tmp, g_file_info_get_symlink_target (info));
+      g_vfs_ftp_file_free (tmp);
+      g_object_unref (info);
+      if (link == NULL)
+        {
+          g_vfs_ftp_task_clear_error (task);
+          return original;
+        }
+      tmp = g_vfs_ftp_file_new_parent (link);
+      entry = g_vfs_ftp_dir_cache_lookup_entry (cache, task, tmp, stamp);
+      g_vfs_ftp_file_free (tmp);
+      if (entry == NULL)
+        {
+          g_vfs_ftp_file_free (link);
+          /* clear the (potential) error here, dangling symlinks etc should not cause errors */
+          g_vfs_ftp_task_clear_error (task);
+          return original;
+        }
+      info = g_hash_table_lookup (entry->files, link);
+      if (info == NULL)
+        {
+          g_vfs_ftp_dir_cache_entry_unref (entry);
+          g_vfs_ftp_file_free (link);
+          return original;
+        }
+      else
+        {
+          g_object_ref (info);
+          g_vfs_ftp_dir_cache_entry_unref (entry);
+        }
+    }
+  while (g_file_info_get_is_symlink (info) && lookups++ < 8);
+
+  g_vfs_ftp_file_free (link);
+  if (g_file_info_get_is_symlink (info))
+    {
+      /* too many recursions */
+      g_object_unref (info);
+      return original;
+    }
+
+  result = g_file_info_dup (info);
+  g_object_unref (info);
+  for (i = 0; i < G_N_ELEMENTS (copy_attributes); i++)
+    {
+      GFileAttributeType type;
+      gpointer value;
+
+      if (!g_file_info_get_attribute_data (original,
+					   copy_attributes[i],
+					   &type,
+					   &value,
+					   NULL))
+	continue;
+      
+      g_file_info_set_attribute (result,
+	                         copy_attributes[i],
+				 type,
+				 value);
+    }
+  g_object_unref (original);
+
+  return result;
+}
+
+GFileInfo *
+g_vfs_ftp_dir_cache_lookup_file (GVfsFtpDirCache *  cache,
+                                 GVfsFtpTask *      task,
+                                 const GVfsFtpFile *file,
+                                 gboolean           resolve_symlinks)
+{
+  GVfsFtpDirCacheEntry *entry;
+  GVfsFtpFile *dir;
+  GFileInfo *info;
+
+  g_return_val_if_fail (cache != NULL, NULL);
+  g_return_val_if_fail (task != NULL, NULL);
+  g_return_val_if_fail (file != NULL, NULL);
+
+  if (g_vfs_ftp_task_is_in_error (task))
+    return NULL;
+
+  if (g_vfs_ftp_file_is_root (file))
+    return g_object_ref (cache->root);
+
+  dir = g_vfs_ftp_file_new_parent (file);
+  entry = g_vfs_ftp_dir_cache_lookup_entry (cache, task, dir, 0);
+  g_vfs_ftp_file_free (dir);
+  if (entry == NULL)
+    {
+      g_vfs_ftp_dir_cache_entry_unref (entry);
+      return NULL;
+    }
+
+  info = g_hash_table_lookup (entry->files, file);
+  if (info != NULL)
+    {
+      g_object_ref (info);
+      if (resolve_symlinks)
+        info = g_vfs_ftp_dir_cache_resolve_symlink (cache, task, file, info, 0);
+    }
+
+  g_vfs_ftp_dir_cache_entry_unref (entry);
+  return info;
+}
+
+GList *
+g_vfs_ftp_dir_cache_lookup_dir (GVfsFtpDirCache *  cache,
+                                GVfsFtpTask *      task,
+                                const GVfsFtpFile *dir,
+                                gboolean           flush,
+                                gboolean           resolve_symlinks)
+{
+  GVfsFtpDirCacheEntry *entry;
+  GHashTableIter iter;
+  gpointer file, info;
+  guint stamp;
+  GList *result = NULL;
+
+  g_return_val_if_fail (cache != NULL, NULL);
+  g_return_val_if_fail (task != NULL, NULL);
+  g_return_val_if_fail (dir != NULL, NULL);
+
+  if (g_vfs_ftp_task_is_in_error (task))
+    return NULL;
+
+  if (flush)
+    {
+      g_mutex_lock (cache->lock);
+      g_assert (cache->stamp != G_MAXUINT);
+      stamp = ++cache->stamp;
+      g_mutex_unlock (cache->lock);
+    }
+  else
+    stamp = 0;
+
+  entry = g_vfs_ftp_dir_cache_lookup_entry (cache, task, dir, stamp);
+  if (entry == NULL)
+    return NULL;
+
+  g_hash_table_iter_init (&iter, entry->files);
+  while (g_hash_table_iter_next (&iter, &file, &info))
+    {
+      g_object_ref (info);
+      if (resolve_symlinks)
+        info = g_vfs_ftp_dir_cache_resolve_symlink (cache, task, file, info, stamp);
+      result = g_list_prepend (result, info);
+    }
+  g_vfs_ftp_dir_cache_entry_unref (entry);
+
+  return result;
+}
+
+void
+g_vfs_ftp_dir_cache_purge_dir (GVfsFtpDirCache *  cache,
+                               const GVfsFtpFile *dir)
+{
+  g_return_if_fail (cache != NULL);
+  g_return_if_fail (dir != NULL);
+
+  g_mutex_lock (cache->lock);
+  g_hash_table_remove (cache->directories, dir);
+  g_mutex_unlock (cache->lock);
+}
+
+void
+g_vfs_ftp_dir_cache_purge_file (GVfsFtpDirCache *  cache,
+				const GVfsFtpFile *file)
+{
+  GVfsFtpFile *dir;
+  
+  g_return_if_fail (cache != NULL);
+  g_return_if_fail (file != NULL);
+
+  if (g_vfs_ftp_file_is_root (file))
+    return;
+
+  dir = g_vfs_ftp_file_new_parent (file);
+  g_vfs_ftp_dir_cache_purge_dir (cache, dir);
+  g_vfs_ftp_file_free (dir);
+}
+
+/*** DIR CACHE FUNCS ***/
+
+#include "ParseFTPList.h"
+#include "gvfsdaemonutils.h"
+
+static gboolean
+g_vfs_ftp_dir_cache_funcs_process (GInputStream *        stream,
+                                   const GVfsFtpFile *   dir,
+                                   GVfsFtpDirCacheEntry *entry,
+                                   gboolean              is_unix,
+                                   GCancellable *        cancellable,
+                                   GError **             error)
+{
+  struct list_state state = { 0, };
+  GDataInputStream *data;
+  GFileInfo *info;
+  int type;
+  GVfsFtpFile *file;
+  char *line, *s;
+
+  /* protect against code reorg - in current code, error never is NULL */
+  g_assert (error != NULL);
+  g_assert (*error == NULL);
+
+  data = g_data_input_stream_new (stream);
+  /* we use LF only, because the mozilla code can handle lines ending in CR */
+  g_data_input_stream_set_newline_type (data, G_DATA_STREAM_NEWLINE_TYPE_LF);
+  while ((line = g_data_input_stream_read_line (data, NULL, cancellable, error)))
+    {
+      struct list_result result = { 0, };
+      GTimeVal tv = { 0, 0 };
+
+      type = ParseFTPList (line, &state, &result);
+      if (type != 'd' && type != 'f' && type != 'l')
+        {
+          g_free (line);
+          continue;
+        }
+
+      /* don't list . and .. directories
+       * Let's hope they're not important files on some ftp servers
+       */
+      if (result.fe_fnlen == 1 && 
+          result.fe_fname[0] == '.')
+        {
+          g_free (line);
+          continue;
+        }
+      if (result.fe_fnlen == 2 && 
+          result.fe_fname[0] == '.' &&
+          result.fe_fname[1] == '.')
+        {
+          g_free (line);
+          continue;
+        }
+
+      s = g_strndup (result.fe_fname, result.fe_fnlen);
+      file = g_vfs_ftp_file_new_child  (dir, s, NULL);
+      g_free (s);
+      if (file == NULL)
+        {
+          g_free (line);
+          continue;
+        }
+
+      info = g_file_info_new ();
+
+      s = g_path_get_basename (g_vfs_ftp_file_get_gvfs_path (file));
+      g_file_info_set_name (info, s);
+      g_free (s);
+
+      if (type == 'l')
+        {
+          char *link;
+
+          link = g_strndup (result.fe_lname, result.fe_lnlen);
+          g_file_info_set_symlink_target (info, link);
+          g_file_info_set_is_symlink (info, TRUE);
+          g_free (link);
+        }
+
+      g_file_info_set_size (info, g_ascii_strtoull (result.fe_size, NULL, 10));
+
+      gvfs_file_info_populate_default (info, g_vfs_ftp_file_get_gvfs_path (file),
+                                       type == 'f' ? G_FILE_TYPE_REGULAR :
+                                       type == 'l' ? G_FILE_TYPE_SYMBOLIC_LINK :
+                                       G_FILE_TYPE_DIRECTORY);
+
+      if (unix)
+        g_file_info_set_is_hidden (info, result.fe_fnlen > 0 &&
+                                         result.fe_fname[0] == '.');
+
+      /* Workaround:
+       * result.fetime.tm_year contains actual year instead of offset-from-1900,
+       * which mktime expects.
+       */
+      if (result.fe_time.tm_year >= 1900)
+              result.fe_time.tm_year -= 1900;
+
+      tv.tv_sec = mktime (&result.fe_time);
+      if (tv.tv_sec != -1)
+        g_file_info_set_modification_time (info, &tv);
+
+      g_vfs_ftp_dir_cache_entry_add (entry, file, info);
+      g_free (line);
+    }
+
+  g_object_unref (data);
+  return *error != NULL;
+}
+
+static GVfsFtpFile *
+g_vfs_ftp_dir_cache_funcs_resolve_default (GVfsFtpTask *      task,
+                                           const GVfsFtpFile *file,
+                                           const char *       target)
+{
+  GVfsFtpFile *link;
+  GString *new_path;
+  char *match;
+
+  g_return_val_if_fail (file != NULL, NULL);
+  g_return_val_if_fail (target != NULL, NULL);
+  
+  if (target[0] == '/')
+    return g_vfs_ftp_file_new_from_ftp (task->backend, target);
+
+  new_path = g_string_new (g_vfs_ftp_file_get_ftp_path (file));
+  /* only take directory */
+  match = strrchr (new_path->str, '/');
+  g_string_truncate (new_path, match - new_path->str + 1);
+  g_string_append (new_path, target);
+  g_string_append_c (new_path, '/'); /* slash at end makes code easier */
+  /* cleanup: remove all double slashes */
+  while ((match = strstr (new_path->str, "//")) != NULL)
+    {
+      g_string_erase (new_path, match - new_path->str, 1);
+    }
+  /* cleanup: remove all ".." and the preceeding directory */
+  while ((match = strstr (new_path->str, "/../")) != NULL)
+    {
+      if (match == new_path->str)
+        {
+          g_string_erase (new_path, 0, 3);
+        }
+      else
+        {
+          char *start = match - 1;
+          while (*start != '/')
+            start--;
+          g_string_erase (new_path, start - new_path->str, match - start + 3);
+        }
+    }
+  /* cleanup: remove all "." directories */
+  while ((match = strstr (new_path->str, "/./")) != NULL)
+    g_string_erase (new_path, match - new_path->str, 2);
+  /* remove trailing / */
+  g_string_set_size (new_path, new_path->len - 1);
+
+  link = g_vfs_ftp_file_new_from_ftp (task->backend, new_path->str);
+  g_string_free (new_path, TRUE);
+  return link;
+}
+
+static gboolean
+g_vfs_ftp_dir_cache_funcs_process_unix (GInputStream *        stream,
+                                        const GVfsFtpFile *   dir,
+                                        GVfsFtpDirCacheEntry *entry,
+                                        GCancellable *        cancellable,
+                                        GError **             error)
+{
+  return g_vfs_ftp_dir_cache_funcs_process (stream, dir, entry, TRUE, cancellable, error);
+}
+
+static gboolean
+g_vfs_ftp_dir_cache_funcs_process_default (GInputStream *        stream,
+                                           const GVfsFtpFile *   dir,
+                                           GVfsFtpDirCacheEntry *entry,
+                                           GCancellable *        cancellable,
+                                           GError **             error)
+{
+  return g_vfs_ftp_dir_cache_funcs_process (stream, dir, entry, FALSE, cancellable, error);
+}
+
+const GVfsFtpDirFuncs g_vfs_ftp_dir_cache_funcs_unix = {
+  "LIST -a",
+  g_vfs_ftp_dir_cache_funcs_process_unix,
+  g_vfs_ftp_dir_cache_funcs_resolve_default
+};
+
+const GVfsFtpDirFuncs g_vfs_ftp_dir_cache_funcs_default = {
+  "LIST",
+  g_vfs_ftp_dir_cache_funcs_process_default,
+  g_vfs_ftp_dir_cache_funcs_resolve_default
+};
diff --git a/daemon/gvfsftpdircache.h b/daemon/gvfsftpdircache.h
new file mode 100644
index 0000000..5a1237a
--- /dev/null
+++ b/daemon/gvfsftpdircache.h
@@ -0,0 +1,76 @@
+/* GIO - GLib Input, Output and Streaming Library
+ * 
+ * Copyright (C) 2009 Benjamin Otte <otte gnome org>
+ *
+ * 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: Benjamin Otte <otte gnome org>
+ */
+
+#ifndef __G_VFS_FTP_DIRCACHE_H__
+#define __G_VFS_FTP_DIRCACHE_H__
+
+#include <gvfsftpfile.h>
+#include <gvfsftptask.h>
+
+G_BEGIN_DECLS
+
+
+//typedef struct _GVfsFtpDirCache GVfsFtpDirCache;
+typedef struct _GVfsFtpDirCacheEntry GVfsFtpDirCacheEntry;
+//typedef struct _GVfsFtpDirFuncs GVfsFtpDirFuncs;
+
+struct _GVfsFtpDirFuncs {
+  const char *          command;
+  gboolean              (* process)                             (GInputStream *         stream,
+                                                                 const GVfsFtpFile *    dir,
+                                                                 GVfsFtpDirCacheEntry * entry,
+                                                                 GCancellable *         cancellable,
+                                                                 GError **              error);
+  GVfsFtpFile *         (* resolve_symlink)                     (GVfsFtpTask *          task,
+                                                                 const GVfsFtpFile *    file,
+                                                                 const char *           target);
+};
+
+extern const GVfsFtpDirFuncs g_vfs_ftp_dir_cache_funcs_unix;
+extern const GVfsFtpDirFuncs g_vfs_ftp_dir_cache_funcs_default;
+
+GVfsFtpDirCache *       g_vfs_ftp_dir_cache_new                 (const GVfsFtpDirFuncs *funcs,
+                                                                 GFileInfo *            root);
+void                    g_vfs_ftp_dir_cache_free                (GVfsFtpDirCache *      cache);
+
+GFileInfo *             g_vfs_ftp_dir_cache_lookup_file         (GVfsFtpDirCache *      cache,
+                                                                 GVfsFtpTask *          task,
+                                                                 const GVfsFtpFile *    file,
+                                                                 gboolean               resolve_symlinks);
+GList *                 g_vfs_ftp_dir_cache_lookup_dir          (GVfsFtpDirCache *      cache,
+                                                                 GVfsFtpTask *          task,
+                                                                 const GVfsFtpFile *    dir,
+                                                                 gboolean               flush,
+                                                                 gboolean               resolve_symlinks);
+void                    g_vfs_ftp_dir_cache_purge_file          (GVfsFtpDirCache *      cache,
+                                                                 const GVfsFtpFile *    file);
+void                    g_vfs_ftp_dir_cache_purge_dir           (GVfsFtpDirCache *      cache,
+                                                                 const GVfsFtpFile *    dir);
+
+void                    g_vfs_ftp_dir_cache_entry_add           (GVfsFtpDirCacheEntry * entry,
+                                                                 GVfsFtpFile *          file,
+                                                                 GFileInfo *            info);
+
+
+G_END_DECLS
+
+#endif /* __G_VFS_FTP_DIRCACHE_H__ */
diff --git a/daemon/gvfsftpfile.c b/daemon/gvfsftpfile.c
index f72c0fa..c16cc46 100644
--- a/daemon/gvfsftpfile.c
+++ b/daemon/gvfsftpfile.c
@@ -132,11 +132,11 @@ g_vfs_ftp_file_new_parent (const GVfsFtpFile *file)
 
   g_return_val_if_fail (file != NULL, NULL);
 
+  if (g_vfs_ftp_file_is_root (file))
+    return g_vfs_ftp_file_copy (file);
+
   dirname = g_path_get_dirname (file->gvfs_path);
-  if (dirname[0] == '.' && dirname[1] == 0)
-    dir = g_vfs_ftp_file_copy (file);
-  else
-    dir = g_vfs_ftp_file_new_from_gvfs (file->backend, dirname);
+  dir = g_vfs_ftp_file_new_from_gvfs (file->backend, dirname);
   g_free (dirname);
 
   return dir;
@@ -217,6 +217,22 @@ g_vfs_ftp_file_free (GVfsFtpFile *file)
   g_slice_free (GVfsFtpFile, file);
 }
 
+/**
+ * g_vfs_ftp_file_is_root:
+ * @file: the file to check
+ *
+ * Checks if the given file references the root directory.
+ *
+ * Returns: %TRUE if @file references the root directory
+ **/
+gboolean
+g_vfs_ftp_file_is_root (const GVfsFtpFile *file)
+{
+  g_return_val_if_fail (file != NULL, FALSE);
+
+  return file->gvfs_path[0] == '/' &&
+         file->gvfs_path[1] == 0;
+}
 
 /**
  * g_vfs_ftp_file_get_ftp_path:
diff --git a/daemon/gvfsftpfile.h b/daemon/gvfsftpfile.h
index ac007d8..d924a7f 100644
--- a/daemon/gvfsftpfile.h
+++ b/daemon/gvfsftpfile.h
@@ -41,6 +41,7 @@ GVfsFtpFile *     g_vfs_ftp_file_new_child              (const GVfsFtpFile *
 GVfsFtpFile *     g_vfs_ftp_file_copy                   (const GVfsFtpFile *    file);
 void              g_vfs_ftp_file_free                   (GVfsFtpFile *          file);
 
+gboolean          g_vfs_ftp_file_is_root                (const GVfsFtpFile *    file);
 const char *      g_vfs_ftp_file_get_ftp_path           (const GVfsFtpFile *    file);
 const char *      g_vfs_ftp_file_get_gvfs_path          (const GVfsFtpFile *    file);
 



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]